diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt new file mode 100644 index 0000000..25c6260 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt @@ -0,0 +1,18 @@ +/* + * 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.instruments.effect + +class EffectParameterDescription( + val min: Float, + val max: Float, + val initialValue: Float, + val text: (EffectParameter) -> String, +) \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt new file mode 100644 index 0000000..25c6260 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt @@ -0,0 +1,18 @@ +/* + * 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.instruments.effect + +class EffectParameterDescription( + val min: Float, + val max: Float, + val initialValue: Float, + val text: (EffectParameter) -> String, +) \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt new file mode 100644 index 0000000..8af39ea --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -0,0 +1,41 @@ +/* + * 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.instruments.effect + +import com.lukas.music.util.format + +enum class EffectType( + val title: String, + val parameterDescriptions: Array +) { + LowPass("low pass filter", + arrayOf( + EffectParameterDescription(-1f, 3f, 1f) { + "cutoff: ${it.value.format(1)} octaves" + } + )), + Noise("noise", + arrayOf( + EffectParameterDescription(0f, 1f, 0f) { + "unused" + } + ) + ) + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt new file mode 100644 index 0000000..25c6260 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt @@ -0,0 +1,18 @@ +/* + * 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.instruments.effect + +class EffectParameterDescription( + val min: Float, + val max: Float, + val initialValue: Float, + val text: (EffectParameter) -> String, +) \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt new file mode 100644 index 0000000..8af39ea --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -0,0 +1,41 @@ +/* + * 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.instruments.effect + +import com.lukas.music.util.format + +enum class EffectType( + val title: String, + val parameterDescriptions: Array +) { + LowPass("low pass filter", + arrayOf( + EffectParameterDescription(-1f, 3f, 1f) { + "cutoff: ${it.value.format(1)} octaves" + } + )), + Noise("noise", + arrayOf( + EffectParameterDescription(0f, 1f, 0f) { + "unused" + } + ) + ) + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Scale.kt b/app/src/main/java/com/lukas/music/song/Scale.kt deleted file mode 100644 index 8e08034..0000000 --- a/app/src/main/java/com/lukas/music/song/Scale.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song - -import com.lukas.music.song.chords.ChordType - -enum class Scale(val identifier: String, val steps: Array, val chordTypes: Array) { - MAJOR( - "major", - arrayOf(0, 2, 4, 5, 7, 9, 11, 12), - arrayOf( - ChordType.MAJOR, - ChordType.MINOR, - ChordType.MINOR, - ChordType.MAJOR, - ChordType.MAJOR, - ChordType.MINOR, - ChordType.DIMINISHED - ) - ) -} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt new file mode 100644 index 0000000..25c6260 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt @@ -0,0 +1,18 @@ +/* + * 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.instruments.effect + +class EffectParameterDescription( + val min: Float, + val max: Float, + val initialValue: Float, + val text: (EffectParameter) -> String, +) \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt new file mode 100644 index 0000000..8af39ea --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -0,0 +1,41 @@ +/* + * 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.instruments.effect + +import com.lukas.music.util.format + +enum class EffectType( + val title: String, + val parameterDescriptions: Array +) { + LowPass("low pass filter", + arrayOf( + EffectParameterDescription(-1f, 3f, 1f) { + "cutoff: ${it.value.format(1)} octaves" + } + )), + Noise("noise", + arrayOf( + EffectParameterDescription(0f, 1f, 0f) { + "unused" + } + ) + ) + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Scale.kt b/app/src/main/java/com/lukas/music/song/Scale.kt deleted file mode 100644 index 8e08034..0000000 --- a/app/src/main/java/com/lukas/music/song/Scale.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song - -import com.lukas.music.song.chords.ChordType - -enum class Scale(val identifier: String, val steps: Array, val chordTypes: Array) { - MAJOR( - "major", - arrayOf(0, 2, 4, 5, 7, 9, 11, 12), - arrayOf( - ChordType.MAJOR, - ChordType.MINOR, - ChordType.MINOR, - ChordType.MAJOR, - ChordType.MAJOR, - ChordType.MINOR, - ChordType.DIMINISHED - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt new file mode 100644 index 0000000..4c6a0d9 --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -0,0 +1,33 @@ +/* + * 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.song + +import com.lukas.music.song.chords.ChordType + +enum class ScaleType( + val identifier: String, + val steps: Array, + val chordTypes: Array +) { + MAJOR( + "major", + arrayOf(0, 2, 4, 5, 7, 9, 11, 12), + arrayOf( + ChordType.MAJOR, + ChordType.MINOR, + ChordType.MINOR, + ChordType.MAJOR, + ChordType.MAJOR, + ChordType.MINOR, + ChordType.DIMINISHED + ) + ) +} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt new file mode 100644 index 0000000..25c6260 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt @@ -0,0 +1,18 @@ +/* + * 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.instruments.effect + +class EffectParameterDescription( + val min: Float, + val max: Float, + val initialValue: Float, + val text: (EffectParameter) -> String, +) \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt new file mode 100644 index 0000000..8af39ea --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -0,0 +1,41 @@ +/* + * 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.instruments.effect + +import com.lukas.music.util.format + +enum class EffectType( + val title: String, + val parameterDescriptions: Array +) { + LowPass("low pass filter", + arrayOf( + EffectParameterDescription(-1f, 3f, 1f) { + "cutoff: ${it.value.format(1)} octaves" + } + )), + Noise("noise", + arrayOf( + EffectParameterDescription(0f, 1f, 0f) { + "unused" + } + ) + ) + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Scale.kt b/app/src/main/java/com/lukas/music/song/Scale.kt deleted file mode 100644 index 8e08034..0000000 --- a/app/src/main/java/com/lukas/music/song/Scale.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song - -import com.lukas.music.song.chords.ChordType - -enum class Scale(val identifier: String, val steps: Array, val chordTypes: Array) { - MAJOR( - "major", - arrayOf(0, 2, 4, 5, 7, 9, 11, 12), - arrayOf( - ChordType.MAJOR, - ChordType.MINOR, - ChordType.MINOR, - ChordType.MAJOR, - ChordType.MAJOR, - ChordType.MINOR, - ChordType.DIMINISHED - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt new file mode 100644 index 0000000..4c6a0d9 --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -0,0 +1,33 @@ +/* + * 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.song + +import com.lukas.music.song.chords.ChordType + +enum class ScaleType( + val identifier: String, + val steps: Array, + val chordTypes: Array +) { + MAJOR( + "major", + arrayOf(0, 2, 4, 5, 7, 9, 11, 12), + arrayOf( + ChordType.MAJOR, + ChordType.MINOR, + ChordType.MINOR, + ChordType.MAJOR, + ChordType.MAJOR, + ChordType.MINOR, + ChordType.DIMINISHED + ) + ) +} \ 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 fc5421b..5046664 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -14,11 +14,13 @@ import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note import com.lukas.music.util.Cycle +import com.lukas.music.util.MetaCycle class Song( root: Note, - val beats: Int -) : Cycle(beats) { + val beats: Int, + val subBeats: Int, +) : MetaCycle>() { val chordProgression = ChordProgression() var soloInstrument: Instrument? = null set(value) { @@ -46,7 +48,11 @@ init { for (i in 0 until beats) { - this += i + val cycle = Cycle() + for (j in 0 until subBeats) { + cycle += j + } + this += cycle } wraparoundListeners += { chordProgression.step() @@ -54,24 +60,25 @@ } } - override fun step(): Int { + override fun step(): Cycle? { super.step() - val chord = chordProgression.currentItem?.currentItem ?: return index + val chord = chordProgression.currentItem?.currentItem ?: return currentItem val chordNotes = chord.getNotes(root) soloInstrument?.let { - it.voice.step(root, chordNotes, index) + it.voice.step(root, chordNotes, index, currentItem!!.index) } ?: run { for (instrument in Instrument.instruments) { - instrument.voice.step(root, chordNotes, index) + instrument.voice.step(root, chordNotes, index, currentItem!!.index) } } - return index + return currentItem } companion object { var currentSong = Song( Note.NOTES[69], - 4 + 4, + 2, ) } } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt new file mode 100644 index 0000000..25c6260 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt @@ -0,0 +1,18 @@ +/* + * 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.instruments.effect + +class EffectParameterDescription( + val min: Float, + val max: Float, + val initialValue: Float, + val text: (EffectParameter) -> String, +) \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt new file mode 100644 index 0000000..8af39ea --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -0,0 +1,41 @@ +/* + * 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.instruments.effect + +import com.lukas.music.util.format + +enum class EffectType( + val title: String, + val parameterDescriptions: Array +) { + LowPass("low pass filter", + arrayOf( + EffectParameterDescription(-1f, 3f, 1f) { + "cutoff: ${it.value.format(1)} octaves" + } + )), + Noise("noise", + arrayOf( + EffectParameterDescription(0f, 1f, 0f) { + "unused" + } + ) + ) + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Scale.kt b/app/src/main/java/com/lukas/music/song/Scale.kt deleted file mode 100644 index 8e08034..0000000 --- a/app/src/main/java/com/lukas/music/song/Scale.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song - -import com.lukas.music.song.chords.ChordType - -enum class Scale(val identifier: String, val steps: Array, val chordTypes: Array) { - MAJOR( - "major", - arrayOf(0, 2, 4, 5, 7, 9, 11, 12), - arrayOf( - ChordType.MAJOR, - ChordType.MINOR, - ChordType.MINOR, - ChordType.MAJOR, - ChordType.MAJOR, - ChordType.MINOR, - ChordType.DIMINISHED - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt new file mode 100644 index 0000000..4c6a0d9 --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -0,0 +1,33 @@ +/* + * 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.song + +import com.lukas.music.song.chords.ChordType + +enum class ScaleType( + val identifier: String, + val steps: Array, + val chordTypes: Array +) { + MAJOR( + "major", + arrayOf(0, 2, 4, 5, 7, 9, 11, 12), + arrayOf( + ChordType.MAJOR, + ChordType.MINOR, + ChordType.MINOR, + ChordType.MAJOR, + ChordType.MAJOR, + ChordType.MINOR, + ChordType.DIMINISHED + ) + ) +} \ 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 fc5421b..5046664 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -14,11 +14,13 @@ import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note import com.lukas.music.util.Cycle +import com.lukas.music.util.MetaCycle class Song( root: Note, - val beats: Int -) : Cycle(beats) { + val beats: Int, + val subBeats: Int, +) : MetaCycle>() { val chordProgression = ChordProgression() var soloInstrument: Instrument? = null set(value) { @@ -46,7 +48,11 @@ init { for (i in 0 until beats) { - this += i + val cycle = Cycle() + for (j in 0 until subBeats) { + cycle += j + } + this += cycle } wraparoundListeners += { chordProgression.step() @@ -54,24 +60,25 @@ } } - override fun step(): Int { + override fun step(): Cycle? { super.step() - val chord = chordProgression.currentItem?.currentItem ?: return index + val chord = chordProgression.currentItem?.currentItem ?: return currentItem val chordNotes = chord.getNotes(root) soloInstrument?.let { - it.voice.step(root, chordNotes, index) + it.voice.step(root, chordNotes, index, currentItem!!.index) } ?: run { for (instrument in Instrument.instruments) { - instrument.voice.step(root, chordNotes, index) + instrument.voice.step(root, chordNotes, index, currentItem!!.index) } } - return index + return currentItem } companion object { var currentSong = Song( Note.NOTES[69], - 4 + 4, + 2, ) } } \ No newline at end of file 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 deleted file mode 100644 index 4706068..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class BassVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - arrayOf(true), - arrayOf(false), - arrayOf(true), - arrayOf(false) - ) - - 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 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt new file mode 100644 index 0000000..25c6260 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt @@ -0,0 +1,18 @@ +/* + * 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.instruments.effect + +class EffectParameterDescription( + val min: Float, + val max: Float, + val initialValue: Float, + val text: (EffectParameter) -> String, +) \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt new file mode 100644 index 0000000..8af39ea --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -0,0 +1,41 @@ +/* + * 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.instruments.effect + +import com.lukas.music.util.format + +enum class EffectType( + val title: String, + val parameterDescriptions: Array +) { + LowPass("low pass filter", + arrayOf( + EffectParameterDescription(-1f, 3f, 1f) { + "cutoff: ${it.value.format(1)} octaves" + } + )), + Noise("noise", + arrayOf( + EffectParameterDescription(0f, 1f, 0f) { + "unused" + } + ) + ) + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Scale.kt b/app/src/main/java/com/lukas/music/song/Scale.kt deleted file mode 100644 index 8e08034..0000000 --- a/app/src/main/java/com/lukas/music/song/Scale.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song - -import com.lukas.music.song.chords.ChordType - -enum class Scale(val identifier: String, val steps: Array, val chordTypes: Array) { - MAJOR( - "major", - arrayOf(0, 2, 4, 5, 7, 9, 11, 12), - arrayOf( - ChordType.MAJOR, - ChordType.MINOR, - ChordType.MINOR, - ChordType.MAJOR, - ChordType.MAJOR, - ChordType.MINOR, - ChordType.DIMINISHED - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt new file mode 100644 index 0000000..4c6a0d9 --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -0,0 +1,33 @@ +/* + * 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.song + +import com.lukas.music.song.chords.ChordType + +enum class ScaleType( + val identifier: String, + val steps: Array, + val chordTypes: Array +) { + MAJOR( + "major", + arrayOf(0, 2, 4, 5, 7, 9, 11, 12), + arrayOf( + ChordType.MAJOR, + ChordType.MINOR, + ChordType.MINOR, + ChordType.MAJOR, + ChordType.MAJOR, + ChordType.MINOR, + ChordType.DIMINISHED + ) + ) +} \ 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 fc5421b..5046664 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -14,11 +14,13 @@ import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note import com.lukas.music.util.Cycle +import com.lukas.music.util.MetaCycle class Song( root: Note, - val beats: Int -) : Cycle(beats) { + val beats: Int, + val subBeats: Int, +) : MetaCycle>() { val chordProgression = ChordProgression() var soloInstrument: Instrument? = null set(value) { @@ -46,7 +48,11 @@ init { for (i in 0 until beats) { - this += i + val cycle = Cycle() + for (j in 0 until subBeats) { + cycle += j + } + this += cycle } wraparoundListeners += { chordProgression.step() @@ -54,24 +60,25 @@ } } - override fun step(): Int { + override fun step(): Cycle? { super.step() - val chord = chordProgression.currentItem?.currentItem ?: return index + val chord = chordProgression.currentItem?.currentItem ?: return currentItem val chordNotes = chord.getNotes(root) soloInstrument?.let { - it.voice.step(root, chordNotes, index) + it.voice.step(root, chordNotes, index, currentItem!!.index) } ?: run { for (instrument in Instrument.instruments) { - instrument.voice.step(root, chordNotes, index) + instrument.voice.step(root, chordNotes, index, currentItem!!.index) } } - return index + return currentItem } companion object { var currentSong = Song( Note.NOTES[69], - 4 + 4, + 2, ) } } \ No newline at end of file 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 deleted file mode 100644 index 4706068..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class BassVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - arrayOf(true), - arrayOf(false), - arrayOf(true), - arrayOf(false) - ) - - 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 deleted file mode 100644 index ab7117f..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class ChordVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - Array(3) { false }, - Array(3) { true }, - Array(3) { false }, - Array(3) { true }, - ) - override val noteCount: Int = 3 - - 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 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt new file mode 100644 index 0000000..25c6260 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt @@ -0,0 +1,18 @@ +/* + * 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.instruments.effect + +class EffectParameterDescription( + val min: Float, + val max: Float, + val initialValue: Float, + val text: (EffectParameter) -> String, +) \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt new file mode 100644 index 0000000..8af39ea --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -0,0 +1,41 @@ +/* + * 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.instruments.effect + +import com.lukas.music.util.format + +enum class EffectType( + val title: String, + val parameterDescriptions: Array +) { + LowPass("low pass filter", + arrayOf( + EffectParameterDescription(-1f, 3f, 1f) { + "cutoff: ${it.value.format(1)} octaves" + } + )), + Noise("noise", + arrayOf( + EffectParameterDescription(0f, 1f, 0f) { + "unused" + } + ) + ) + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Scale.kt b/app/src/main/java/com/lukas/music/song/Scale.kt deleted file mode 100644 index 8e08034..0000000 --- a/app/src/main/java/com/lukas/music/song/Scale.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song - -import com.lukas.music.song.chords.ChordType - -enum class Scale(val identifier: String, val steps: Array, val chordTypes: Array) { - MAJOR( - "major", - arrayOf(0, 2, 4, 5, 7, 9, 11, 12), - arrayOf( - ChordType.MAJOR, - ChordType.MINOR, - ChordType.MINOR, - ChordType.MAJOR, - ChordType.MAJOR, - ChordType.MINOR, - ChordType.DIMINISHED - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt new file mode 100644 index 0000000..4c6a0d9 --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -0,0 +1,33 @@ +/* + * 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.song + +import com.lukas.music.song.chords.ChordType + +enum class ScaleType( + val identifier: String, + val steps: Array, + val chordTypes: Array +) { + MAJOR( + "major", + arrayOf(0, 2, 4, 5, 7, 9, 11, 12), + arrayOf( + ChordType.MAJOR, + ChordType.MINOR, + ChordType.MINOR, + ChordType.MAJOR, + ChordType.MAJOR, + ChordType.MINOR, + ChordType.DIMINISHED + ) + ) +} \ 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 fc5421b..5046664 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -14,11 +14,13 @@ import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note import com.lukas.music.util.Cycle +import com.lukas.music.util.MetaCycle class Song( root: Note, - val beats: Int -) : Cycle(beats) { + val beats: Int, + val subBeats: Int, +) : MetaCycle>() { val chordProgression = ChordProgression() var soloInstrument: Instrument? = null set(value) { @@ -46,7 +48,11 @@ init { for (i in 0 until beats) { - this += i + val cycle = Cycle() + for (j in 0 until subBeats) { + cycle += j + } + this += cycle } wraparoundListeners += { chordProgression.step() @@ -54,24 +60,25 @@ } } - override fun step(): Int { + override fun step(): Cycle? { super.step() - val chord = chordProgression.currentItem?.currentItem ?: return index + val chord = chordProgression.currentItem?.currentItem ?: return currentItem val chordNotes = chord.getNotes(root) soloInstrument?.let { - it.voice.step(root, chordNotes, index) + it.voice.step(root, chordNotes, index, currentItem!!.index) } ?: run { for (instrument in Instrument.instruments) { - instrument.voice.step(root, chordNotes, index) + instrument.voice.step(root, chordNotes, index, currentItem!!.index) } } - return index + return currentItem } companion object { var currentSong = Song( Note.NOTES[69], - 4 + 4, + 2, ) } } \ No newline at end of file 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 deleted file mode 100644 index 4706068..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class BassVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - arrayOf(true), - arrayOf(false), - arrayOf(true), - arrayOf(false) - ) - - 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 deleted file mode 100644 index ab7117f..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class ChordVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - Array(3) { false }, - Array(3) { true }, - Array(3) { false }, - Array(3) { true }, - ) - override val noteCount: Int = 3 - - 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 4f56c2a..139be78 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 @@ -11,37 +11,41 @@ package com.lukas.music.song.voice import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -import kotlin.reflect.KClass -abstract class Voice(val instrument: Instrument) { - abstract var noteActive: Array> - abstract val noteCount: Int +class Voice(val instrument: Instrument) { + var type: VoiceType = VoiceType.Bass + set(value) { + field = value + noteActive = + Array(Song.currentSong.beats * Song.currentSong.subBeats) { Array(value.noteCount) { false } } + } + var restrikeNotes = false + lateinit var noteActive: Array> - abstract fun getNotes(root: Note, chordNotes: Array): Array + var octaveOffset = 0 - fun step(root: Note, chordNotes: Array, beat: Int) { + init { + type = type + } + + fun step(root: Note, chordNotes: Array, beat: Int, subBeat: Int) { if (instrument.muted) { return } - val activeNotes = noteActive[beat] - val notes = getNotes(root, chordNotes) + val beatIndex = beat * Song.currentSong.subBeats + subBeat + val activeNotes = noteActive[beatIndex] + val notes = type.getNotes(root, chordNotes) for ((index, active) in activeNotes.withIndex()) { - val note = notes[index] + val note = notes[index] + 12 * octaveOffset if (!active) { instrument.stopNote(note) continue } - instrument.startNote(note) + if (restrikeNotes || !instrument.isPlaying(note)) { + 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 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt new file mode 100644 index 0000000..25c6260 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt @@ -0,0 +1,18 @@ +/* + * 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.instruments.effect + +class EffectParameterDescription( + val min: Float, + val max: Float, + val initialValue: Float, + val text: (EffectParameter) -> String, +) \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt new file mode 100644 index 0000000..8af39ea --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -0,0 +1,41 @@ +/* + * 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.instruments.effect + +import com.lukas.music.util.format + +enum class EffectType( + val title: String, + val parameterDescriptions: Array +) { + LowPass("low pass filter", + arrayOf( + EffectParameterDescription(-1f, 3f, 1f) { + "cutoff: ${it.value.format(1)} octaves" + } + )), + Noise("noise", + arrayOf( + EffectParameterDescription(0f, 1f, 0f) { + "unused" + } + ) + ) + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Scale.kt b/app/src/main/java/com/lukas/music/song/Scale.kt deleted file mode 100644 index 8e08034..0000000 --- a/app/src/main/java/com/lukas/music/song/Scale.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song - -import com.lukas.music.song.chords.ChordType - -enum class Scale(val identifier: String, val steps: Array, val chordTypes: Array) { - MAJOR( - "major", - arrayOf(0, 2, 4, 5, 7, 9, 11, 12), - arrayOf( - ChordType.MAJOR, - ChordType.MINOR, - ChordType.MINOR, - ChordType.MAJOR, - ChordType.MAJOR, - ChordType.MINOR, - ChordType.DIMINISHED - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt new file mode 100644 index 0000000..4c6a0d9 --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -0,0 +1,33 @@ +/* + * 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.song + +import com.lukas.music.song.chords.ChordType + +enum class ScaleType( + val identifier: String, + val steps: Array, + val chordTypes: Array +) { + MAJOR( + "major", + arrayOf(0, 2, 4, 5, 7, 9, 11, 12), + arrayOf( + ChordType.MAJOR, + ChordType.MINOR, + ChordType.MINOR, + ChordType.MAJOR, + ChordType.MAJOR, + ChordType.MINOR, + ChordType.DIMINISHED + ) + ) +} \ 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 fc5421b..5046664 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -14,11 +14,13 @@ import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note import com.lukas.music.util.Cycle +import com.lukas.music.util.MetaCycle class Song( root: Note, - val beats: Int -) : Cycle(beats) { + val beats: Int, + val subBeats: Int, +) : MetaCycle>() { val chordProgression = ChordProgression() var soloInstrument: Instrument? = null set(value) { @@ -46,7 +48,11 @@ init { for (i in 0 until beats) { - this += i + val cycle = Cycle() + for (j in 0 until subBeats) { + cycle += j + } + this += cycle } wraparoundListeners += { chordProgression.step() @@ -54,24 +60,25 @@ } } - override fun step(): Int { + override fun step(): Cycle? { super.step() - val chord = chordProgression.currentItem?.currentItem ?: return index + val chord = chordProgression.currentItem?.currentItem ?: return currentItem val chordNotes = chord.getNotes(root) soloInstrument?.let { - it.voice.step(root, chordNotes, index) + it.voice.step(root, chordNotes, index, currentItem!!.index) } ?: run { for (instrument in Instrument.instruments) { - instrument.voice.step(root, chordNotes, index) + instrument.voice.step(root, chordNotes, index, currentItem!!.index) } } - return index + return currentItem } companion object { var currentSong = Song( Note.NOTES[69], - 4 + 4, + 2, ) } } \ No newline at end of file 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 deleted file mode 100644 index 4706068..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class BassVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - arrayOf(true), - arrayOf(false), - arrayOf(true), - arrayOf(false) - ) - - 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 deleted file mode 100644 index ab7117f..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class ChordVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - Array(3) { false }, - Array(3) { true }, - Array(3) { false }, - Array(3) { true }, - ) - override val noteCount: Int = 3 - - 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 4f56c2a..139be78 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 @@ -11,37 +11,41 @@ package com.lukas.music.song.voice import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -import kotlin.reflect.KClass -abstract class Voice(val instrument: Instrument) { - abstract var noteActive: Array> - abstract val noteCount: Int +class Voice(val instrument: Instrument) { + var type: VoiceType = VoiceType.Bass + set(value) { + field = value + noteActive = + Array(Song.currentSong.beats * Song.currentSong.subBeats) { Array(value.noteCount) { false } } + } + var restrikeNotes = false + lateinit var noteActive: Array> - abstract fun getNotes(root: Note, chordNotes: Array): Array + var octaveOffset = 0 - fun step(root: Note, chordNotes: Array, beat: Int) { + init { + type = type + } + + fun step(root: Note, chordNotes: Array, beat: Int, subBeat: Int) { if (instrument.muted) { return } - val activeNotes = noteActive[beat] - val notes = getNotes(root, chordNotes) + val beatIndex = beat * Song.currentSong.subBeats + subBeat + val activeNotes = noteActive[beatIndex] + val notes = type.getNotes(root, chordNotes) for ((index, active) in activeNotes.withIndex()) { - val note = notes[index] + val note = notes[index] + 12 * octaveOffset if (!active) { instrument.stopNote(note) continue } - instrument.startNote(note) + if (restrikeNotes || !instrument.isPlaying(note)) { + 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/song/voice/VoiceType.kt b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt new file mode 100644 index 0000000..e06761a --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -0,0 +1,36 @@ +/* + * 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.song.voice + +import com.lukas.music.song.ScaleType +import com.lukas.music.song.note.Note +import com.lukas.music.util.transform + +enum class VoiceType( + val title: String, + val noteCount: Int, + val getNotes: (Note, Array) -> Array +) { + Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), + Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + Scale("Scale notes", 8, { root, _ -> ScaleType.MAJOR.steps.transform { root + it } }), + Root("Root note", 1, { root, _ -> arrayOf(root) }), + RootRelative("Song root relative", 12, { root, _ -> Array(12) { root + it } }), + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt new file mode 100644 index 0000000..25c6260 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt @@ -0,0 +1,18 @@ +/* + * 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.instruments.effect + +class EffectParameterDescription( + val min: Float, + val max: Float, + val initialValue: Float, + val text: (EffectParameter) -> String, +) \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt new file mode 100644 index 0000000..8af39ea --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -0,0 +1,41 @@ +/* + * 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.instruments.effect + +import com.lukas.music.util.format + +enum class EffectType( + val title: String, + val parameterDescriptions: Array +) { + LowPass("low pass filter", + arrayOf( + EffectParameterDescription(-1f, 3f, 1f) { + "cutoff: ${it.value.format(1)} octaves" + } + )), + Noise("noise", + arrayOf( + EffectParameterDescription(0f, 1f, 0f) { + "unused" + } + ) + ) + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Scale.kt b/app/src/main/java/com/lukas/music/song/Scale.kt deleted file mode 100644 index 8e08034..0000000 --- a/app/src/main/java/com/lukas/music/song/Scale.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song - -import com.lukas.music.song.chords.ChordType - -enum class Scale(val identifier: String, val steps: Array, val chordTypes: Array) { - MAJOR( - "major", - arrayOf(0, 2, 4, 5, 7, 9, 11, 12), - arrayOf( - ChordType.MAJOR, - ChordType.MINOR, - ChordType.MINOR, - ChordType.MAJOR, - ChordType.MAJOR, - ChordType.MINOR, - ChordType.DIMINISHED - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt new file mode 100644 index 0000000..4c6a0d9 --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -0,0 +1,33 @@ +/* + * 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.song + +import com.lukas.music.song.chords.ChordType + +enum class ScaleType( + val identifier: String, + val steps: Array, + val chordTypes: Array +) { + MAJOR( + "major", + arrayOf(0, 2, 4, 5, 7, 9, 11, 12), + arrayOf( + ChordType.MAJOR, + ChordType.MINOR, + ChordType.MINOR, + ChordType.MAJOR, + ChordType.MAJOR, + ChordType.MINOR, + ChordType.DIMINISHED + ) + ) +} \ 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 fc5421b..5046664 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -14,11 +14,13 @@ import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note import com.lukas.music.util.Cycle +import com.lukas.music.util.MetaCycle class Song( root: Note, - val beats: Int -) : Cycle(beats) { + val beats: Int, + val subBeats: Int, +) : MetaCycle>() { val chordProgression = ChordProgression() var soloInstrument: Instrument? = null set(value) { @@ -46,7 +48,11 @@ init { for (i in 0 until beats) { - this += i + val cycle = Cycle() + for (j in 0 until subBeats) { + cycle += j + } + this += cycle } wraparoundListeners += { chordProgression.step() @@ -54,24 +60,25 @@ } } - override fun step(): Int { + override fun step(): Cycle? { super.step() - val chord = chordProgression.currentItem?.currentItem ?: return index + val chord = chordProgression.currentItem?.currentItem ?: return currentItem val chordNotes = chord.getNotes(root) soloInstrument?.let { - it.voice.step(root, chordNotes, index) + it.voice.step(root, chordNotes, index, currentItem!!.index) } ?: run { for (instrument in Instrument.instruments) { - instrument.voice.step(root, chordNotes, index) + instrument.voice.step(root, chordNotes, index, currentItem!!.index) } } - return index + return currentItem } companion object { var currentSong = Song( Note.NOTES[69], - 4 + 4, + 2, ) } } \ No newline at end of file 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 deleted file mode 100644 index 4706068..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class BassVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - arrayOf(true), - arrayOf(false), - arrayOf(true), - arrayOf(false) - ) - - 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 deleted file mode 100644 index ab7117f..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class ChordVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - Array(3) { false }, - Array(3) { true }, - Array(3) { false }, - Array(3) { true }, - ) - override val noteCount: Int = 3 - - 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 4f56c2a..139be78 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 @@ -11,37 +11,41 @@ package com.lukas.music.song.voice import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -import kotlin.reflect.KClass -abstract class Voice(val instrument: Instrument) { - abstract var noteActive: Array> - abstract val noteCount: Int +class Voice(val instrument: Instrument) { + var type: VoiceType = VoiceType.Bass + set(value) { + field = value + noteActive = + Array(Song.currentSong.beats * Song.currentSong.subBeats) { Array(value.noteCount) { false } } + } + var restrikeNotes = false + lateinit var noteActive: Array> - abstract fun getNotes(root: Note, chordNotes: Array): Array + var octaveOffset = 0 - fun step(root: Note, chordNotes: Array, beat: Int) { + init { + type = type + } + + fun step(root: Note, chordNotes: Array, beat: Int, subBeat: Int) { if (instrument.muted) { return } - val activeNotes = noteActive[beat] - val notes = getNotes(root, chordNotes) + val beatIndex = beat * Song.currentSong.subBeats + subBeat + val activeNotes = noteActive[beatIndex] + val notes = type.getNotes(root, chordNotes) for ((index, active) in activeNotes.withIndex()) { - val note = notes[index] + val note = notes[index] + 12 * octaveOffset if (!active) { instrument.stopNote(note) continue } - instrument.startNote(note) + if (restrikeNotes || !instrument.isPlaying(note)) { + 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/song/voice/VoiceType.kt b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt new file mode 100644 index 0000000..e06761a --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -0,0 +1,36 @@ +/* + * 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.song.voice + +import com.lukas.music.song.ScaleType +import com.lukas.music.song.note.Note +import com.lukas.music.util.transform + +enum class VoiceType( + val title: String, + val noteCount: Int, + val getNotes: (Note, Array) -> Array +) { + Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), + Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + Scale("Scale notes", 8, { root, _ -> ScaleType.MAJOR.steps.transform { root + it } }), + Root("Root note", 1, { root, _ -> arrayOf(root) }), + RootRelative("Song root relative", 12, { root, _ -> Array(12) { root + it } }), + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ 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 bd7a3d9..6c3bae3 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 @@ -16,14 +16,14 @@ import android.view.ViewGroup import androidx.fragment.app.DialogFragment import com.lukas.music.databinding.FragmentEditChordBinding -import com.lukas.music.song.Scale +import com.lukas.music.song.ScaleType import com.lukas.music.song.Song 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, private val songFragment: SongFragment) : +class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : DialogFragment() { lateinit var binding: FragmentEditChordBinding @@ -42,12 +42,12 @@ private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { - Array(Scale.MAJOR.steps.size) { (Song.currentSong.root + Scale.MAJOR.steps[it]).noteName.toString() } + Array(ScaleType.MAJOR.steps.size) { (Song.currentSong.root + ScaleType.MAJOR.steps[it]).noteName.toString() } } else Interval.IntervalName.NAMES binding.pitchSpinner.setup(pitches, chord.interval.name.ordinal) { - chord.note = Scale.MAJOR.steps[it] + chord.note = ScaleType.MAJOR.steps[it] if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } songFragment.updateChords() } @@ -60,11 +60,11 @@ } binding.typeSpinner.setup( values, - if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 + if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 else chord.chordType.ordinal + 1 ) { if (it == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } else { chord.chordType = ChordType.VALUES[it - 1] } diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt new file mode 100644 index 0000000..25c6260 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt @@ -0,0 +1,18 @@ +/* + * 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.instruments.effect + +class EffectParameterDescription( + val min: Float, + val max: Float, + val initialValue: Float, + val text: (EffectParameter) -> String, +) \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt new file mode 100644 index 0000000..8af39ea --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -0,0 +1,41 @@ +/* + * 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.instruments.effect + +import com.lukas.music.util.format + +enum class EffectType( + val title: String, + val parameterDescriptions: Array +) { + LowPass("low pass filter", + arrayOf( + EffectParameterDescription(-1f, 3f, 1f) { + "cutoff: ${it.value.format(1)} octaves" + } + )), + Noise("noise", + arrayOf( + EffectParameterDescription(0f, 1f, 0f) { + "unused" + } + ) + ) + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Scale.kt b/app/src/main/java/com/lukas/music/song/Scale.kt deleted file mode 100644 index 8e08034..0000000 --- a/app/src/main/java/com/lukas/music/song/Scale.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song - -import com.lukas.music.song.chords.ChordType - -enum class Scale(val identifier: String, val steps: Array, val chordTypes: Array) { - MAJOR( - "major", - arrayOf(0, 2, 4, 5, 7, 9, 11, 12), - arrayOf( - ChordType.MAJOR, - ChordType.MINOR, - ChordType.MINOR, - ChordType.MAJOR, - ChordType.MAJOR, - ChordType.MINOR, - ChordType.DIMINISHED - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt new file mode 100644 index 0000000..4c6a0d9 --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -0,0 +1,33 @@ +/* + * 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.song + +import com.lukas.music.song.chords.ChordType + +enum class ScaleType( + val identifier: String, + val steps: Array, + val chordTypes: Array +) { + MAJOR( + "major", + arrayOf(0, 2, 4, 5, 7, 9, 11, 12), + arrayOf( + ChordType.MAJOR, + ChordType.MINOR, + ChordType.MINOR, + ChordType.MAJOR, + ChordType.MAJOR, + ChordType.MINOR, + ChordType.DIMINISHED + ) + ) +} \ 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 fc5421b..5046664 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -14,11 +14,13 @@ import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note import com.lukas.music.util.Cycle +import com.lukas.music.util.MetaCycle class Song( root: Note, - val beats: Int -) : Cycle(beats) { + val beats: Int, + val subBeats: Int, +) : MetaCycle>() { val chordProgression = ChordProgression() var soloInstrument: Instrument? = null set(value) { @@ -46,7 +48,11 @@ init { for (i in 0 until beats) { - this += i + val cycle = Cycle() + for (j in 0 until subBeats) { + cycle += j + } + this += cycle } wraparoundListeners += { chordProgression.step() @@ -54,24 +60,25 @@ } } - override fun step(): Int { + override fun step(): Cycle? { super.step() - val chord = chordProgression.currentItem?.currentItem ?: return index + val chord = chordProgression.currentItem?.currentItem ?: return currentItem val chordNotes = chord.getNotes(root) soloInstrument?.let { - it.voice.step(root, chordNotes, index) + it.voice.step(root, chordNotes, index, currentItem!!.index) } ?: run { for (instrument in Instrument.instruments) { - instrument.voice.step(root, chordNotes, index) + instrument.voice.step(root, chordNotes, index, currentItem!!.index) } } - return index + return currentItem } companion object { var currentSong = Song( Note.NOTES[69], - 4 + 4, + 2, ) } } \ No newline at end of file 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 deleted file mode 100644 index 4706068..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class BassVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - arrayOf(true), - arrayOf(false), - arrayOf(true), - arrayOf(false) - ) - - 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 deleted file mode 100644 index ab7117f..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class ChordVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - Array(3) { false }, - Array(3) { true }, - Array(3) { false }, - Array(3) { true }, - ) - override val noteCount: Int = 3 - - 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 4f56c2a..139be78 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 @@ -11,37 +11,41 @@ package com.lukas.music.song.voice import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -import kotlin.reflect.KClass -abstract class Voice(val instrument: Instrument) { - abstract var noteActive: Array> - abstract val noteCount: Int +class Voice(val instrument: Instrument) { + var type: VoiceType = VoiceType.Bass + set(value) { + field = value + noteActive = + Array(Song.currentSong.beats * Song.currentSong.subBeats) { Array(value.noteCount) { false } } + } + var restrikeNotes = false + lateinit var noteActive: Array> - abstract fun getNotes(root: Note, chordNotes: Array): Array + var octaveOffset = 0 - fun step(root: Note, chordNotes: Array, beat: Int) { + init { + type = type + } + + fun step(root: Note, chordNotes: Array, beat: Int, subBeat: Int) { if (instrument.muted) { return } - val activeNotes = noteActive[beat] - val notes = getNotes(root, chordNotes) + val beatIndex = beat * Song.currentSong.subBeats + subBeat + val activeNotes = noteActive[beatIndex] + val notes = type.getNotes(root, chordNotes) for ((index, active) in activeNotes.withIndex()) { - val note = notes[index] + val note = notes[index] + 12 * octaveOffset if (!active) { instrument.stopNote(note) continue } - instrument.startNote(note) + if (restrikeNotes || !instrument.isPlaying(note)) { + 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/song/voice/VoiceType.kt b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt new file mode 100644 index 0000000..e06761a --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -0,0 +1,36 @@ +/* + * 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.song.voice + +import com.lukas.music.song.ScaleType +import com.lukas.music.song.note.Note +import com.lukas.music.util.transform + +enum class VoiceType( + val title: String, + val noteCount: Int, + val getNotes: (Note, Array) -> Array +) { + Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), + Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + Scale("Scale notes", 8, { root, _ -> ScaleType.MAJOR.steps.transform { root + it } }), + Root("Root note", 1, { root, _ -> arrayOf(root) }), + RootRelative("Song root relative", 12, { root, _ -> Array(12) { root + it } }), + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ 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 bd7a3d9..6c3bae3 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 @@ -16,14 +16,14 @@ import android.view.ViewGroup import androidx.fragment.app.DialogFragment import com.lukas.music.databinding.FragmentEditChordBinding -import com.lukas.music.song.Scale +import com.lukas.music.song.ScaleType import com.lukas.music.song.Song 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, private val songFragment: SongFragment) : +class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : DialogFragment() { lateinit var binding: FragmentEditChordBinding @@ -42,12 +42,12 @@ private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { - Array(Scale.MAJOR.steps.size) { (Song.currentSong.root + Scale.MAJOR.steps[it]).noteName.toString() } + Array(ScaleType.MAJOR.steps.size) { (Song.currentSong.root + ScaleType.MAJOR.steps[it]).noteName.toString() } } else Interval.IntervalName.NAMES binding.pitchSpinner.setup(pitches, chord.interval.name.ordinal) { - chord.note = Scale.MAJOR.steps[it] + chord.note = ScaleType.MAJOR.steps[it] if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } songFragment.updateChords() } @@ -60,11 +60,11 @@ } binding.typeSpinner.setup( values, - if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 + if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 else chord.chordType.ordinal + 1 ) { if (it == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } else { chord.chordType = ChordType.VALUES[it - 1] } diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt new file mode 100644 index 0000000..6e359e4 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt @@ -0,0 +1,38 @@ +/* + * 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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEffectsBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.util.EasyDialogFragment + +class EditEffectsFragment(private val instrument: Instrument) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEffectsBinding.inflate(inflater) + for (effect in instrument.effects) { + val effectEditor = EffectFragment(effect) + childFragmentManager.beginTransaction().add(binding.effectsDisplay.id, effectEditor) + .commit() + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt new file mode 100644 index 0000000..25c6260 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt @@ -0,0 +1,18 @@ +/* + * 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.instruments.effect + +class EffectParameterDescription( + val min: Float, + val max: Float, + val initialValue: Float, + val text: (EffectParameter) -> String, +) \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt new file mode 100644 index 0000000..8af39ea --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -0,0 +1,41 @@ +/* + * 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.instruments.effect + +import com.lukas.music.util.format + +enum class EffectType( + val title: String, + val parameterDescriptions: Array +) { + LowPass("low pass filter", + arrayOf( + EffectParameterDescription(-1f, 3f, 1f) { + "cutoff: ${it.value.format(1)} octaves" + } + )), + Noise("noise", + arrayOf( + EffectParameterDescription(0f, 1f, 0f) { + "unused" + } + ) + ) + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Scale.kt b/app/src/main/java/com/lukas/music/song/Scale.kt deleted file mode 100644 index 8e08034..0000000 --- a/app/src/main/java/com/lukas/music/song/Scale.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song - -import com.lukas.music.song.chords.ChordType - -enum class Scale(val identifier: String, val steps: Array, val chordTypes: Array) { - MAJOR( - "major", - arrayOf(0, 2, 4, 5, 7, 9, 11, 12), - arrayOf( - ChordType.MAJOR, - ChordType.MINOR, - ChordType.MINOR, - ChordType.MAJOR, - ChordType.MAJOR, - ChordType.MINOR, - ChordType.DIMINISHED - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt new file mode 100644 index 0000000..4c6a0d9 --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -0,0 +1,33 @@ +/* + * 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.song + +import com.lukas.music.song.chords.ChordType + +enum class ScaleType( + val identifier: String, + val steps: Array, + val chordTypes: Array +) { + MAJOR( + "major", + arrayOf(0, 2, 4, 5, 7, 9, 11, 12), + arrayOf( + ChordType.MAJOR, + ChordType.MINOR, + ChordType.MINOR, + ChordType.MAJOR, + ChordType.MAJOR, + ChordType.MINOR, + ChordType.DIMINISHED + ) + ) +} \ 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 fc5421b..5046664 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -14,11 +14,13 @@ import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note import com.lukas.music.util.Cycle +import com.lukas.music.util.MetaCycle class Song( root: Note, - val beats: Int -) : Cycle(beats) { + val beats: Int, + val subBeats: Int, +) : MetaCycle>() { val chordProgression = ChordProgression() var soloInstrument: Instrument? = null set(value) { @@ -46,7 +48,11 @@ init { for (i in 0 until beats) { - this += i + val cycle = Cycle() + for (j in 0 until subBeats) { + cycle += j + } + this += cycle } wraparoundListeners += { chordProgression.step() @@ -54,24 +60,25 @@ } } - override fun step(): Int { + override fun step(): Cycle? { super.step() - val chord = chordProgression.currentItem?.currentItem ?: return index + val chord = chordProgression.currentItem?.currentItem ?: return currentItem val chordNotes = chord.getNotes(root) soloInstrument?.let { - it.voice.step(root, chordNotes, index) + it.voice.step(root, chordNotes, index, currentItem!!.index) } ?: run { for (instrument in Instrument.instruments) { - instrument.voice.step(root, chordNotes, index) + instrument.voice.step(root, chordNotes, index, currentItem!!.index) } } - return index + return currentItem } companion object { var currentSong = Song( Note.NOTES[69], - 4 + 4, + 2, ) } } \ No newline at end of file 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 deleted file mode 100644 index 4706068..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class BassVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - arrayOf(true), - arrayOf(false), - arrayOf(true), - arrayOf(false) - ) - - 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 deleted file mode 100644 index ab7117f..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class ChordVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - Array(3) { false }, - Array(3) { true }, - Array(3) { false }, - Array(3) { true }, - ) - override val noteCount: Int = 3 - - 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 4f56c2a..139be78 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 @@ -11,37 +11,41 @@ package com.lukas.music.song.voice import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -import kotlin.reflect.KClass -abstract class Voice(val instrument: Instrument) { - abstract var noteActive: Array> - abstract val noteCount: Int +class Voice(val instrument: Instrument) { + var type: VoiceType = VoiceType.Bass + set(value) { + field = value + noteActive = + Array(Song.currentSong.beats * Song.currentSong.subBeats) { Array(value.noteCount) { false } } + } + var restrikeNotes = false + lateinit var noteActive: Array> - abstract fun getNotes(root: Note, chordNotes: Array): Array + var octaveOffset = 0 - fun step(root: Note, chordNotes: Array, beat: Int) { + init { + type = type + } + + fun step(root: Note, chordNotes: Array, beat: Int, subBeat: Int) { if (instrument.muted) { return } - val activeNotes = noteActive[beat] - val notes = getNotes(root, chordNotes) + val beatIndex = beat * Song.currentSong.subBeats + subBeat + val activeNotes = noteActive[beatIndex] + val notes = type.getNotes(root, chordNotes) for ((index, active) in activeNotes.withIndex()) { - val note = notes[index] + val note = notes[index] + 12 * octaveOffset if (!active) { instrument.stopNote(note) continue } - instrument.startNote(note) + if (restrikeNotes || !instrument.isPlaying(note)) { + 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/song/voice/VoiceType.kt b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt new file mode 100644 index 0000000..e06761a --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -0,0 +1,36 @@ +/* + * 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.song.voice + +import com.lukas.music.song.ScaleType +import com.lukas.music.song.note.Note +import com.lukas.music.util.transform + +enum class VoiceType( + val title: String, + val noteCount: Int, + val getNotes: (Note, Array) -> Array +) { + Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), + Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + Scale("Scale notes", 8, { root, _ -> ScaleType.MAJOR.steps.transform { root + it } }), + Root("Root note", 1, { root, _ -> arrayOf(root) }), + RootRelative("Song root relative", 12, { root, _ -> Array(12) { root + it } }), + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ 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 bd7a3d9..6c3bae3 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 @@ -16,14 +16,14 @@ import android.view.ViewGroup import androidx.fragment.app.DialogFragment import com.lukas.music.databinding.FragmentEditChordBinding -import com.lukas.music.song.Scale +import com.lukas.music.song.ScaleType import com.lukas.music.song.Song 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, private val songFragment: SongFragment) : +class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : DialogFragment() { lateinit var binding: FragmentEditChordBinding @@ -42,12 +42,12 @@ private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { - Array(Scale.MAJOR.steps.size) { (Song.currentSong.root + Scale.MAJOR.steps[it]).noteName.toString() } + Array(ScaleType.MAJOR.steps.size) { (Song.currentSong.root + ScaleType.MAJOR.steps[it]).noteName.toString() } } else Interval.IntervalName.NAMES binding.pitchSpinner.setup(pitches, chord.interval.name.ordinal) { - chord.note = Scale.MAJOR.steps[it] + chord.note = ScaleType.MAJOR.steps[it] if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } songFragment.updateChords() } @@ -60,11 +60,11 @@ } binding.typeSpinner.setup( values, - if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 + if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 else chord.chordType.ordinal + 1 ) { if (it == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } else { chord.chordType = ChordType.VALUES[it - 1] } diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt new file mode 100644 index 0000000..6e359e4 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt @@ -0,0 +1,38 @@ +/* + * 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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEffectsBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.util.EasyDialogFragment + +class EditEffectsFragment(private val instrument: Instrument) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEffectsBinding.inflate(inflater) + for (effect in instrument.effects) { + val effectEditor = EffectFragment(effect) + childFragmentManager.beginTransaction().add(binding.effectsDisplay.id, effectEditor) + .commit() + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.kt new file mode 100644 index 0000000..605c9e5 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.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.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEnvelopeBinding +import com.lukas.music.instruments.Envelope +import com.lukas.music.util.EasyDialogFragment +import com.lukas.music.util.smartSetup + +class EditEnvelopeFragment(private val envelope: Envelope) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEnvelopeBinding.inflate(inflater) + binding.attackSeek.smartSetup(5, 200, envelope::attack) { + binding.attackText.text = "Attack: $it ms" + } + binding.delaySeek.smartSetup(5, 200, envelope::delay) { + binding.delayText.text = "Delay: $it ms" + } + binding.sustainSeek.smartSetup(0, 100, envelope::sustain) { + binding.sustainText.text = "Sustain: $it%" + } + binding.releaseSeek.smartSetup(5, 200, envelope::release) { + binding.releaseText.text = "Release: $it ms" + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt new file mode 100644 index 0000000..25c6260 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt @@ -0,0 +1,18 @@ +/* + * 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.instruments.effect + +class EffectParameterDescription( + val min: Float, + val max: Float, + val initialValue: Float, + val text: (EffectParameter) -> String, +) \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt new file mode 100644 index 0000000..8af39ea --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -0,0 +1,41 @@ +/* + * 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.instruments.effect + +import com.lukas.music.util.format + +enum class EffectType( + val title: String, + val parameterDescriptions: Array +) { + LowPass("low pass filter", + arrayOf( + EffectParameterDescription(-1f, 3f, 1f) { + "cutoff: ${it.value.format(1)} octaves" + } + )), + Noise("noise", + arrayOf( + EffectParameterDescription(0f, 1f, 0f) { + "unused" + } + ) + ) + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Scale.kt b/app/src/main/java/com/lukas/music/song/Scale.kt deleted file mode 100644 index 8e08034..0000000 --- a/app/src/main/java/com/lukas/music/song/Scale.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song - -import com.lukas.music.song.chords.ChordType - -enum class Scale(val identifier: String, val steps: Array, val chordTypes: Array) { - MAJOR( - "major", - arrayOf(0, 2, 4, 5, 7, 9, 11, 12), - arrayOf( - ChordType.MAJOR, - ChordType.MINOR, - ChordType.MINOR, - ChordType.MAJOR, - ChordType.MAJOR, - ChordType.MINOR, - ChordType.DIMINISHED - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt new file mode 100644 index 0000000..4c6a0d9 --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -0,0 +1,33 @@ +/* + * 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.song + +import com.lukas.music.song.chords.ChordType + +enum class ScaleType( + val identifier: String, + val steps: Array, + val chordTypes: Array +) { + MAJOR( + "major", + arrayOf(0, 2, 4, 5, 7, 9, 11, 12), + arrayOf( + ChordType.MAJOR, + ChordType.MINOR, + ChordType.MINOR, + ChordType.MAJOR, + ChordType.MAJOR, + ChordType.MINOR, + ChordType.DIMINISHED + ) + ) +} \ 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 fc5421b..5046664 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -14,11 +14,13 @@ import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note import com.lukas.music.util.Cycle +import com.lukas.music.util.MetaCycle class Song( root: Note, - val beats: Int -) : Cycle(beats) { + val beats: Int, + val subBeats: Int, +) : MetaCycle>() { val chordProgression = ChordProgression() var soloInstrument: Instrument? = null set(value) { @@ -46,7 +48,11 @@ init { for (i in 0 until beats) { - this += i + val cycle = Cycle() + for (j in 0 until subBeats) { + cycle += j + } + this += cycle } wraparoundListeners += { chordProgression.step() @@ -54,24 +60,25 @@ } } - override fun step(): Int { + override fun step(): Cycle? { super.step() - val chord = chordProgression.currentItem?.currentItem ?: return index + val chord = chordProgression.currentItem?.currentItem ?: return currentItem val chordNotes = chord.getNotes(root) soloInstrument?.let { - it.voice.step(root, chordNotes, index) + it.voice.step(root, chordNotes, index, currentItem!!.index) } ?: run { for (instrument in Instrument.instruments) { - instrument.voice.step(root, chordNotes, index) + instrument.voice.step(root, chordNotes, index, currentItem!!.index) } } - return index + return currentItem } companion object { var currentSong = Song( Note.NOTES[69], - 4 + 4, + 2, ) } } \ No newline at end of file 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 deleted file mode 100644 index 4706068..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class BassVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - arrayOf(true), - arrayOf(false), - arrayOf(true), - arrayOf(false) - ) - - 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 deleted file mode 100644 index ab7117f..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class ChordVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - Array(3) { false }, - Array(3) { true }, - Array(3) { false }, - Array(3) { true }, - ) - override val noteCount: Int = 3 - - 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 4f56c2a..139be78 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 @@ -11,37 +11,41 @@ package com.lukas.music.song.voice import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -import kotlin.reflect.KClass -abstract class Voice(val instrument: Instrument) { - abstract var noteActive: Array> - abstract val noteCount: Int +class Voice(val instrument: Instrument) { + var type: VoiceType = VoiceType.Bass + set(value) { + field = value + noteActive = + Array(Song.currentSong.beats * Song.currentSong.subBeats) { Array(value.noteCount) { false } } + } + var restrikeNotes = false + lateinit var noteActive: Array> - abstract fun getNotes(root: Note, chordNotes: Array): Array + var octaveOffset = 0 - fun step(root: Note, chordNotes: Array, beat: Int) { + init { + type = type + } + + fun step(root: Note, chordNotes: Array, beat: Int, subBeat: Int) { if (instrument.muted) { return } - val activeNotes = noteActive[beat] - val notes = getNotes(root, chordNotes) + val beatIndex = beat * Song.currentSong.subBeats + subBeat + val activeNotes = noteActive[beatIndex] + val notes = type.getNotes(root, chordNotes) for ((index, active) in activeNotes.withIndex()) { - val note = notes[index] + val note = notes[index] + 12 * octaveOffset if (!active) { instrument.stopNote(note) continue } - instrument.startNote(note) + if (restrikeNotes || !instrument.isPlaying(note)) { + 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/song/voice/VoiceType.kt b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt new file mode 100644 index 0000000..e06761a --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -0,0 +1,36 @@ +/* + * 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.song.voice + +import com.lukas.music.song.ScaleType +import com.lukas.music.song.note.Note +import com.lukas.music.util.transform + +enum class VoiceType( + val title: String, + val noteCount: Int, + val getNotes: (Note, Array) -> Array +) { + Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), + Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + Scale("Scale notes", 8, { root, _ -> ScaleType.MAJOR.steps.transform { root + it } }), + Root("Root note", 1, { root, _ -> arrayOf(root) }), + RootRelative("Song root relative", 12, { root, _ -> Array(12) { root + it } }), + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ 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 bd7a3d9..6c3bae3 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 @@ -16,14 +16,14 @@ import android.view.ViewGroup import androidx.fragment.app.DialogFragment import com.lukas.music.databinding.FragmentEditChordBinding -import com.lukas.music.song.Scale +import com.lukas.music.song.ScaleType import com.lukas.music.song.Song 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, private val songFragment: SongFragment) : +class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : DialogFragment() { lateinit var binding: FragmentEditChordBinding @@ -42,12 +42,12 @@ private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { - Array(Scale.MAJOR.steps.size) { (Song.currentSong.root + Scale.MAJOR.steps[it]).noteName.toString() } + Array(ScaleType.MAJOR.steps.size) { (Song.currentSong.root + ScaleType.MAJOR.steps[it]).noteName.toString() } } else Interval.IntervalName.NAMES binding.pitchSpinner.setup(pitches, chord.interval.name.ordinal) { - chord.note = Scale.MAJOR.steps[it] + chord.note = ScaleType.MAJOR.steps[it] if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } songFragment.updateChords() } @@ -60,11 +60,11 @@ } binding.typeSpinner.setup( values, - if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 + if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 else chord.chordType.ordinal + 1 ) { if (it == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } else { chord.chordType = ChordType.VALUES[it - 1] } diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt new file mode 100644 index 0000000..6e359e4 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt @@ -0,0 +1,38 @@ +/* + * 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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEffectsBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.util.EasyDialogFragment + +class EditEffectsFragment(private val instrument: Instrument) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEffectsBinding.inflate(inflater) + for (effect in instrument.effects) { + val effectEditor = EffectFragment(effect) + childFragmentManager.beginTransaction().add(binding.effectsDisplay.id, effectEditor) + .commit() + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.kt new file mode 100644 index 0000000..605c9e5 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.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.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEnvelopeBinding +import com.lukas.music.instruments.Envelope +import com.lukas.music.util.EasyDialogFragment +import com.lukas.music.util.smartSetup + +class EditEnvelopeFragment(private val envelope: Envelope) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEnvelopeBinding.inflate(inflater) + binding.attackSeek.smartSetup(5, 200, envelope::attack) { + binding.attackText.text = "Attack: $it ms" + } + binding.delaySeek.smartSetup(5, 200, envelope::delay) { + binding.delayText.text = "Delay: $it ms" + } + binding.sustainSeek.smartSetup(0, 100, envelope::sustain) { + binding.sustainText.text = "Sustain: $it%" + } + binding.releaseSeek.smartSetup(5, 200, envelope::release) { + binding.releaseText.text = "Release: $it ms" + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ 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 index 0020ae8..c416df7 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt @@ -16,22 +16,19 @@ 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.song.voice.VoiceType import com.lukas.music.ui.adapters.InstrumentViewHolder +import com.lukas.music.util.EasyDialogFragment 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 - +) : EasyDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -49,33 +46,23 @@ } }) binding.waveformSelection.smartSetup(Waveform.VALUES, instrument::waveform) - binding.volumeSeek.setup(0, 100, 30) { + binding.volumeSeek.setup(0, 100, (instrument.volume * 100f).toInt()) { 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.voiceSelection.smartSetup(VoiceType.VALUES, instrument.voice::type) binding.editVoiceButton.setOnClickListener { EditVoiceFragment(instrument.voice).showNow(childFragmentManager, "") } + binding.editEnvelopeButton.setOnClickListener { + EditEnvelopeFragment(instrument.envelope).showNow(childFragmentManager, "") + } + binding.editEffectsButton.setOnClickListener { + EditEffectsFragment(instrument).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 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt new file mode 100644 index 0000000..25c6260 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt @@ -0,0 +1,18 @@ +/* + * 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.instruments.effect + +class EffectParameterDescription( + val min: Float, + val max: Float, + val initialValue: Float, + val text: (EffectParameter) -> String, +) \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt new file mode 100644 index 0000000..8af39ea --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -0,0 +1,41 @@ +/* + * 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.instruments.effect + +import com.lukas.music.util.format + +enum class EffectType( + val title: String, + val parameterDescriptions: Array +) { + LowPass("low pass filter", + arrayOf( + EffectParameterDescription(-1f, 3f, 1f) { + "cutoff: ${it.value.format(1)} octaves" + } + )), + Noise("noise", + arrayOf( + EffectParameterDescription(0f, 1f, 0f) { + "unused" + } + ) + ) + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Scale.kt b/app/src/main/java/com/lukas/music/song/Scale.kt deleted file mode 100644 index 8e08034..0000000 --- a/app/src/main/java/com/lukas/music/song/Scale.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song - -import com.lukas.music.song.chords.ChordType - -enum class Scale(val identifier: String, val steps: Array, val chordTypes: Array) { - MAJOR( - "major", - arrayOf(0, 2, 4, 5, 7, 9, 11, 12), - arrayOf( - ChordType.MAJOR, - ChordType.MINOR, - ChordType.MINOR, - ChordType.MAJOR, - ChordType.MAJOR, - ChordType.MINOR, - ChordType.DIMINISHED - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt new file mode 100644 index 0000000..4c6a0d9 --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -0,0 +1,33 @@ +/* + * 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.song + +import com.lukas.music.song.chords.ChordType + +enum class ScaleType( + val identifier: String, + val steps: Array, + val chordTypes: Array +) { + MAJOR( + "major", + arrayOf(0, 2, 4, 5, 7, 9, 11, 12), + arrayOf( + ChordType.MAJOR, + ChordType.MINOR, + ChordType.MINOR, + ChordType.MAJOR, + ChordType.MAJOR, + ChordType.MINOR, + ChordType.DIMINISHED + ) + ) +} \ 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 fc5421b..5046664 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -14,11 +14,13 @@ import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note import com.lukas.music.util.Cycle +import com.lukas.music.util.MetaCycle class Song( root: Note, - val beats: Int -) : Cycle(beats) { + val beats: Int, + val subBeats: Int, +) : MetaCycle>() { val chordProgression = ChordProgression() var soloInstrument: Instrument? = null set(value) { @@ -46,7 +48,11 @@ init { for (i in 0 until beats) { - this += i + val cycle = Cycle() + for (j in 0 until subBeats) { + cycle += j + } + this += cycle } wraparoundListeners += { chordProgression.step() @@ -54,24 +60,25 @@ } } - override fun step(): Int { + override fun step(): Cycle? { super.step() - val chord = chordProgression.currentItem?.currentItem ?: return index + val chord = chordProgression.currentItem?.currentItem ?: return currentItem val chordNotes = chord.getNotes(root) soloInstrument?.let { - it.voice.step(root, chordNotes, index) + it.voice.step(root, chordNotes, index, currentItem!!.index) } ?: run { for (instrument in Instrument.instruments) { - instrument.voice.step(root, chordNotes, index) + instrument.voice.step(root, chordNotes, index, currentItem!!.index) } } - return index + return currentItem } companion object { var currentSong = Song( Note.NOTES[69], - 4 + 4, + 2, ) } } \ No newline at end of file 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 deleted file mode 100644 index 4706068..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class BassVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - arrayOf(true), - arrayOf(false), - arrayOf(true), - arrayOf(false) - ) - - 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 deleted file mode 100644 index ab7117f..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class ChordVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - Array(3) { false }, - Array(3) { true }, - Array(3) { false }, - Array(3) { true }, - ) - override val noteCount: Int = 3 - - 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 4f56c2a..139be78 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 @@ -11,37 +11,41 @@ package com.lukas.music.song.voice import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -import kotlin.reflect.KClass -abstract class Voice(val instrument: Instrument) { - abstract var noteActive: Array> - abstract val noteCount: Int +class Voice(val instrument: Instrument) { + var type: VoiceType = VoiceType.Bass + set(value) { + field = value + noteActive = + Array(Song.currentSong.beats * Song.currentSong.subBeats) { Array(value.noteCount) { false } } + } + var restrikeNotes = false + lateinit var noteActive: Array> - abstract fun getNotes(root: Note, chordNotes: Array): Array + var octaveOffset = 0 - fun step(root: Note, chordNotes: Array, beat: Int) { + init { + type = type + } + + fun step(root: Note, chordNotes: Array, beat: Int, subBeat: Int) { if (instrument.muted) { return } - val activeNotes = noteActive[beat] - val notes = getNotes(root, chordNotes) + val beatIndex = beat * Song.currentSong.subBeats + subBeat + val activeNotes = noteActive[beatIndex] + val notes = type.getNotes(root, chordNotes) for ((index, active) in activeNotes.withIndex()) { - val note = notes[index] + val note = notes[index] + 12 * octaveOffset if (!active) { instrument.stopNote(note) continue } - instrument.startNote(note) + if (restrikeNotes || !instrument.isPlaying(note)) { + 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/song/voice/VoiceType.kt b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt new file mode 100644 index 0000000..e06761a --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -0,0 +1,36 @@ +/* + * 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.song.voice + +import com.lukas.music.song.ScaleType +import com.lukas.music.song.note.Note +import com.lukas.music.util.transform + +enum class VoiceType( + val title: String, + val noteCount: Int, + val getNotes: (Note, Array) -> Array +) { + Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), + Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + Scale("Scale notes", 8, { root, _ -> ScaleType.MAJOR.steps.transform { root + it } }), + Root("Root note", 1, { root, _ -> arrayOf(root) }), + RootRelative("Song root relative", 12, { root, _ -> Array(12) { root + it } }), + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ 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 bd7a3d9..6c3bae3 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 @@ -16,14 +16,14 @@ import android.view.ViewGroup import androidx.fragment.app.DialogFragment import com.lukas.music.databinding.FragmentEditChordBinding -import com.lukas.music.song.Scale +import com.lukas.music.song.ScaleType import com.lukas.music.song.Song 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, private val songFragment: SongFragment) : +class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : DialogFragment() { lateinit var binding: FragmentEditChordBinding @@ -42,12 +42,12 @@ private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { - Array(Scale.MAJOR.steps.size) { (Song.currentSong.root + Scale.MAJOR.steps[it]).noteName.toString() } + Array(ScaleType.MAJOR.steps.size) { (Song.currentSong.root + ScaleType.MAJOR.steps[it]).noteName.toString() } } else Interval.IntervalName.NAMES binding.pitchSpinner.setup(pitches, chord.interval.name.ordinal) { - chord.note = Scale.MAJOR.steps[it] + chord.note = ScaleType.MAJOR.steps[it] if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } songFragment.updateChords() } @@ -60,11 +60,11 @@ } binding.typeSpinner.setup( values, - if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 + if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 else chord.chordType.ordinal + 1 ) { if (it == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } else { chord.chordType = ChordType.VALUES[it - 1] } diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt new file mode 100644 index 0000000..6e359e4 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt @@ -0,0 +1,38 @@ +/* + * 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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEffectsBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.util.EasyDialogFragment + +class EditEffectsFragment(private val instrument: Instrument) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEffectsBinding.inflate(inflater) + for (effect in instrument.effects) { + val effectEditor = EffectFragment(effect) + childFragmentManager.beginTransaction().add(binding.effectsDisplay.id, effectEditor) + .commit() + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.kt new file mode 100644 index 0000000..605c9e5 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.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.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEnvelopeBinding +import com.lukas.music.instruments.Envelope +import com.lukas.music.util.EasyDialogFragment +import com.lukas.music.util.smartSetup + +class EditEnvelopeFragment(private val envelope: Envelope) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEnvelopeBinding.inflate(inflater) + binding.attackSeek.smartSetup(5, 200, envelope::attack) { + binding.attackText.text = "Attack: $it ms" + } + binding.delaySeek.smartSetup(5, 200, envelope::delay) { + binding.delayText.text = "Delay: $it ms" + } + binding.sustainSeek.smartSetup(0, 100, envelope::sustain) { + binding.sustainText.text = "Sustain: $it%" + } + binding.releaseSeek.smartSetup(5, 200, envelope::release) { + binding.releaseText.text = "Release: $it ms" + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ 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 index 0020ae8..c416df7 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt @@ -16,22 +16,19 @@ 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.song.voice.VoiceType import com.lukas.music.ui.adapters.InstrumentViewHolder +import com.lukas.music.util.EasyDialogFragment 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 - +) : EasyDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -49,33 +46,23 @@ } }) binding.waveformSelection.smartSetup(Waveform.VALUES, instrument::waveform) - binding.volumeSeek.setup(0, 100, 30) { + binding.volumeSeek.setup(0, 100, (instrument.volume * 100f).toInt()) { 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.voiceSelection.smartSetup(VoiceType.VALUES, instrument.voice::type) binding.editVoiceButton.setOnClickListener { EditVoiceFragment(instrument.voice).showNow(childFragmentManager, "") } + binding.editEnvelopeButton.setOnClickListener { + EditEnvelopeFragment(instrument.envelope).showNow(childFragmentManager, "") + } + binding.editEffectsButton.setOnClickListener { + EditEffectsFragment(instrument).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/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditVoiceFragment.kt new file mode 100644 index 0000000..eb56238 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditVoiceFragment.kt @@ -0,0 +1,76 @@ +/* + * 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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import com.google.android.material.button.MaterialButton +import com.lukas.music.R +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.EasyDialogFragment +import com.lukas.music.util.setup +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + binding.restrikeButton.setupToggle(voice::restrikeNotes, R.color.green) + for (row in voice.type.noteCount - 1 downTo 0) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats * Song.currentSong.subBeats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle( + ArrayProperty(voice.noteActive[column], row), + R.color.blue, + inactiveColor = if (column % Song.currentSong.subBeats == 0) R.color.gray_0x50 else R.color.gray_0x70 + ) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.octaveSeekBar.setup(-4, 4, voice.octaveOffset) { + voice.octaveOffset = it + binding.octaveText.text = "octave = $it" + } + 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.MATCH_PARENT + ) + } + + 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 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt new file mode 100644 index 0000000..25c6260 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt @@ -0,0 +1,18 @@ +/* + * 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.instruments.effect + +class EffectParameterDescription( + val min: Float, + val max: Float, + val initialValue: Float, + val text: (EffectParameter) -> String, +) \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt new file mode 100644 index 0000000..8af39ea --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -0,0 +1,41 @@ +/* + * 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.instruments.effect + +import com.lukas.music.util.format + +enum class EffectType( + val title: String, + val parameterDescriptions: Array +) { + LowPass("low pass filter", + arrayOf( + EffectParameterDescription(-1f, 3f, 1f) { + "cutoff: ${it.value.format(1)} octaves" + } + )), + Noise("noise", + arrayOf( + EffectParameterDescription(0f, 1f, 0f) { + "unused" + } + ) + ) + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Scale.kt b/app/src/main/java/com/lukas/music/song/Scale.kt deleted file mode 100644 index 8e08034..0000000 --- a/app/src/main/java/com/lukas/music/song/Scale.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song - -import com.lukas.music.song.chords.ChordType - -enum class Scale(val identifier: String, val steps: Array, val chordTypes: Array) { - MAJOR( - "major", - arrayOf(0, 2, 4, 5, 7, 9, 11, 12), - arrayOf( - ChordType.MAJOR, - ChordType.MINOR, - ChordType.MINOR, - ChordType.MAJOR, - ChordType.MAJOR, - ChordType.MINOR, - ChordType.DIMINISHED - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt new file mode 100644 index 0000000..4c6a0d9 --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -0,0 +1,33 @@ +/* + * 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.song + +import com.lukas.music.song.chords.ChordType + +enum class ScaleType( + val identifier: String, + val steps: Array, + val chordTypes: Array +) { + MAJOR( + "major", + arrayOf(0, 2, 4, 5, 7, 9, 11, 12), + arrayOf( + ChordType.MAJOR, + ChordType.MINOR, + ChordType.MINOR, + ChordType.MAJOR, + ChordType.MAJOR, + ChordType.MINOR, + ChordType.DIMINISHED + ) + ) +} \ 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 fc5421b..5046664 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -14,11 +14,13 @@ import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note import com.lukas.music.util.Cycle +import com.lukas.music.util.MetaCycle class Song( root: Note, - val beats: Int -) : Cycle(beats) { + val beats: Int, + val subBeats: Int, +) : MetaCycle>() { val chordProgression = ChordProgression() var soloInstrument: Instrument? = null set(value) { @@ -46,7 +48,11 @@ init { for (i in 0 until beats) { - this += i + val cycle = Cycle() + for (j in 0 until subBeats) { + cycle += j + } + this += cycle } wraparoundListeners += { chordProgression.step() @@ -54,24 +60,25 @@ } } - override fun step(): Int { + override fun step(): Cycle? { super.step() - val chord = chordProgression.currentItem?.currentItem ?: return index + val chord = chordProgression.currentItem?.currentItem ?: return currentItem val chordNotes = chord.getNotes(root) soloInstrument?.let { - it.voice.step(root, chordNotes, index) + it.voice.step(root, chordNotes, index, currentItem!!.index) } ?: run { for (instrument in Instrument.instruments) { - instrument.voice.step(root, chordNotes, index) + instrument.voice.step(root, chordNotes, index, currentItem!!.index) } } - return index + return currentItem } companion object { var currentSong = Song( Note.NOTES[69], - 4 + 4, + 2, ) } } \ No newline at end of file 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 deleted file mode 100644 index 4706068..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class BassVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - arrayOf(true), - arrayOf(false), - arrayOf(true), - arrayOf(false) - ) - - 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 deleted file mode 100644 index ab7117f..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class ChordVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - Array(3) { false }, - Array(3) { true }, - Array(3) { false }, - Array(3) { true }, - ) - override val noteCount: Int = 3 - - 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 4f56c2a..139be78 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 @@ -11,37 +11,41 @@ package com.lukas.music.song.voice import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -import kotlin.reflect.KClass -abstract class Voice(val instrument: Instrument) { - abstract var noteActive: Array> - abstract val noteCount: Int +class Voice(val instrument: Instrument) { + var type: VoiceType = VoiceType.Bass + set(value) { + field = value + noteActive = + Array(Song.currentSong.beats * Song.currentSong.subBeats) { Array(value.noteCount) { false } } + } + var restrikeNotes = false + lateinit var noteActive: Array> - abstract fun getNotes(root: Note, chordNotes: Array): Array + var octaveOffset = 0 - fun step(root: Note, chordNotes: Array, beat: Int) { + init { + type = type + } + + fun step(root: Note, chordNotes: Array, beat: Int, subBeat: Int) { if (instrument.muted) { return } - val activeNotes = noteActive[beat] - val notes = getNotes(root, chordNotes) + val beatIndex = beat * Song.currentSong.subBeats + subBeat + val activeNotes = noteActive[beatIndex] + val notes = type.getNotes(root, chordNotes) for ((index, active) in activeNotes.withIndex()) { - val note = notes[index] + val note = notes[index] + 12 * octaveOffset if (!active) { instrument.stopNote(note) continue } - instrument.startNote(note) + if (restrikeNotes || !instrument.isPlaying(note)) { + 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/song/voice/VoiceType.kt b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt new file mode 100644 index 0000000..e06761a --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -0,0 +1,36 @@ +/* + * 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.song.voice + +import com.lukas.music.song.ScaleType +import com.lukas.music.song.note.Note +import com.lukas.music.util.transform + +enum class VoiceType( + val title: String, + val noteCount: Int, + val getNotes: (Note, Array) -> Array +) { + Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), + Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + Scale("Scale notes", 8, { root, _ -> ScaleType.MAJOR.steps.transform { root + it } }), + Root("Root note", 1, { root, _ -> arrayOf(root) }), + RootRelative("Song root relative", 12, { root, _ -> Array(12) { root + it } }), + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ 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 bd7a3d9..6c3bae3 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 @@ -16,14 +16,14 @@ import android.view.ViewGroup import androidx.fragment.app.DialogFragment import com.lukas.music.databinding.FragmentEditChordBinding -import com.lukas.music.song.Scale +import com.lukas.music.song.ScaleType import com.lukas.music.song.Song 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, private val songFragment: SongFragment) : +class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : DialogFragment() { lateinit var binding: FragmentEditChordBinding @@ -42,12 +42,12 @@ private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { - Array(Scale.MAJOR.steps.size) { (Song.currentSong.root + Scale.MAJOR.steps[it]).noteName.toString() } + Array(ScaleType.MAJOR.steps.size) { (Song.currentSong.root + ScaleType.MAJOR.steps[it]).noteName.toString() } } else Interval.IntervalName.NAMES binding.pitchSpinner.setup(pitches, chord.interval.name.ordinal) { - chord.note = Scale.MAJOR.steps[it] + chord.note = ScaleType.MAJOR.steps[it] if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } songFragment.updateChords() } @@ -60,11 +60,11 @@ } binding.typeSpinner.setup( values, - if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 + if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 else chord.chordType.ordinal + 1 ) { if (it == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } else { chord.chordType = ChordType.VALUES[it - 1] } diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt new file mode 100644 index 0000000..6e359e4 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt @@ -0,0 +1,38 @@ +/* + * 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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEffectsBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.util.EasyDialogFragment + +class EditEffectsFragment(private val instrument: Instrument) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEffectsBinding.inflate(inflater) + for (effect in instrument.effects) { + val effectEditor = EffectFragment(effect) + childFragmentManager.beginTransaction().add(binding.effectsDisplay.id, effectEditor) + .commit() + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.kt new file mode 100644 index 0000000..605c9e5 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.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.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEnvelopeBinding +import com.lukas.music.instruments.Envelope +import com.lukas.music.util.EasyDialogFragment +import com.lukas.music.util.smartSetup + +class EditEnvelopeFragment(private val envelope: Envelope) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEnvelopeBinding.inflate(inflater) + binding.attackSeek.smartSetup(5, 200, envelope::attack) { + binding.attackText.text = "Attack: $it ms" + } + binding.delaySeek.smartSetup(5, 200, envelope::delay) { + binding.delayText.text = "Delay: $it ms" + } + binding.sustainSeek.smartSetup(0, 100, envelope::sustain) { + binding.sustainText.text = "Sustain: $it%" + } + binding.releaseSeek.smartSetup(5, 200, envelope::release) { + binding.releaseText.text = "Release: $it ms" + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ 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 index 0020ae8..c416df7 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt @@ -16,22 +16,19 @@ 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.song.voice.VoiceType import com.lukas.music.ui.adapters.InstrumentViewHolder +import com.lukas.music.util.EasyDialogFragment 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 - +) : EasyDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -49,33 +46,23 @@ } }) binding.waveformSelection.smartSetup(Waveform.VALUES, instrument::waveform) - binding.volumeSeek.setup(0, 100, 30) { + binding.volumeSeek.setup(0, 100, (instrument.volume * 100f).toInt()) { 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.voiceSelection.smartSetup(VoiceType.VALUES, instrument.voice::type) binding.editVoiceButton.setOnClickListener { EditVoiceFragment(instrument.voice).showNow(childFragmentManager, "") } + binding.editEnvelopeButton.setOnClickListener { + EditEnvelopeFragment(instrument.envelope).showNow(childFragmentManager, "") + } + binding.editEffectsButton.setOnClickListener { + EditEffectsFragment(instrument).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/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditVoiceFragment.kt new file mode 100644 index 0000000..eb56238 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditVoiceFragment.kt @@ -0,0 +1,76 @@ +/* + * 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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import com.google.android.material.button.MaterialButton +import com.lukas.music.R +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.EasyDialogFragment +import com.lukas.music.util.setup +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + binding.restrikeButton.setupToggle(voice::restrikeNotes, R.color.green) + for (row in voice.type.noteCount - 1 downTo 0) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats * Song.currentSong.subBeats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle( + ArrayProperty(voice.noteActive[column], row), + R.color.blue, + inactiveColor = if (column % Song.currentSong.subBeats == 0) R.color.gray_0x50 else R.color.gray_0x70 + ) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.octaveSeekBar.setup(-4, 4, voice.octaveOffset) { + voice.octaveOffset = it + binding.octaveText.text = "octave = $it" + } + 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.MATCH_PARENT + ) + } + + 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/ui/fragments/EffectFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt new file mode 100644 index 0000000..ebf4cb1 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.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.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.lukas.music.R +import com.lukas.music.databinding.FragmentEffectBinding +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.util.setupToggle +import com.lukas.music.util.smartSetup + +class EffectFragment(private val effect: Effect) : Fragment() { + lateinit var binding: FragmentEffectBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEffectBinding.inflate(inflater) + binding.effectName.text = effect.type.toString() + binding.activeButton.setupToggle(effect::active, R.color.blue) { + binding.activeButton.text = if (it) "ON" else "OFF" + } + binding.activeButton.text = if (effect.active) "ON" else "OFF" + binding.influenceSeekBar.smartSetup(0, 100, effect.influence::percentageValue) { + binding.influenceText.text = effect.influence.description.text(effect.influence) + } + binding.parameter1SeekBar.smartSetup(0, 100, effect.parameters[0]::percentageValue) { + binding.parameter1Text.text = + effect.parameters[0].description.text(effect.parameters[0]) + } + return binding.root + } +} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt new file mode 100644 index 0000000..25c6260 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt @@ -0,0 +1,18 @@ +/* + * 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.instruments.effect + +class EffectParameterDescription( + val min: Float, + val max: Float, + val initialValue: Float, + val text: (EffectParameter) -> String, +) \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt new file mode 100644 index 0000000..8af39ea --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -0,0 +1,41 @@ +/* + * 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.instruments.effect + +import com.lukas.music.util.format + +enum class EffectType( + val title: String, + val parameterDescriptions: Array +) { + LowPass("low pass filter", + arrayOf( + EffectParameterDescription(-1f, 3f, 1f) { + "cutoff: ${it.value.format(1)} octaves" + } + )), + Noise("noise", + arrayOf( + EffectParameterDescription(0f, 1f, 0f) { + "unused" + } + ) + ) + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Scale.kt b/app/src/main/java/com/lukas/music/song/Scale.kt deleted file mode 100644 index 8e08034..0000000 --- a/app/src/main/java/com/lukas/music/song/Scale.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song - -import com.lukas.music.song.chords.ChordType - -enum class Scale(val identifier: String, val steps: Array, val chordTypes: Array) { - MAJOR( - "major", - arrayOf(0, 2, 4, 5, 7, 9, 11, 12), - arrayOf( - ChordType.MAJOR, - ChordType.MINOR, - ChordType.MINOR, - ChordType.MAJOR, - ChordType.MAJOR, - ChordType.MINOR, - ChordType.DIMINISHED - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt new file mode 100644 index 0000000..4c6a0d9 --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -0,0 +1,33 @@ +/* + * 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.song + +import com.lukas.music.song.chords.ChordType + +enum class ScaleType( + val identifier: String, + val steps: Array, + val chordTypes: Array +) { + MAJOR( + "major", + arrayOf(0, 2, 4, 5, 7, 9, 11, 12), + arrayOf( + ChordType.MAJOR, + ChordType.MINOR, + ChordType.MINOR, + ChordType.MAJOR, + ChordType.MAJOR, + ChordType.MINOR, + ChordType.DIMINISHED + ) + ) +} \ 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 fc5421b..5046664 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -14,11 +14,13 @@ import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note import com.lukas.music.util.Cycle +import com.lukas.music.util.MetaCycle class Song( root: Note, - val beats: Int -) : Cycle(beats) { + val beats: Int, + val subBeats: Int, +) : MetaCycle>() { val chordProgression = ChordProgression() var soloInstrument: Instrument? = null set(value) { @@ -46,7 +48,11 @@ init { for (i in 0 until beats) { - this += i + val cycle = Cycle() + for (j in 0 until subBeats) { + cycle += j + } + this += cycle } wraparoundListeners += { chordProgression.step() @@ -54,24 +60,25 @@ } } - override fun step(): Int { + override fun step(): Cycle? { super.step() - val chord = chordProgression.currentItem?.currentItem ?: return index + val chord = chordProgression.currentItem?.currentItem ?: return currentItem val chordNotes = chord.getNotes(root) soloInstrument?.let { - it.voice.step(root, chordNotes, index) + it.voice.step(root, chordNotes, index, currentItem!!.index) } ?: run { for (instrument in Instrument.instruments) { - instrument.voice.step(root, chordNotes, index) + instrument.voice.step(root, chordNotes, index, currentItem!!.index) } } - return index + return currentItem } companion object { var currentSong = Song( Note.NOTES[69], - 4 + 4, + 2, ) } } \ No newline at end of file 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 deleted file mode 100644 index 4706068..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class BassVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - arrayOf(true), - arrayOf(false), - arrayOf(true), - arrayOf(false) - ) - - 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 deleted file mode 100644 index ab7117f..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class ChordVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - Array(3) { false }, - Array(3) { true }, - Array(3) { false }, - Array(3) { true }, - ) - override val noteCount: Int = 3 - - 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 4f56c2a..139be78 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 @@ -11,37 +11,41 @@ package com.lukas.music.song.voice import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -import kotlin.reflect.KClass -abstract class Voice(val instrument: Instrument) { - abstract var noteActive: Array> - abstract val noteCount: Int +class Voice(val instrument: Instrument) { + var type: VoiceType = VoiceType.Bass + set(value) { + field = value + noteActive = + Array(Song.currentSong.beats * Song.currentSong.subBeats) { Array(value.noteCount) { false } } + } + var restrikeNotes = false + lateinit var noteActive: Array> - abstract fun getNotes(root: Note, chordNotes: Array): Array + var octaveOffset = 0 - fun step(root: Note, chordNotes: Array, beat: Int) { + init { + type = type + } + + fun step(root: Note, chordNotes: Array, beat: Int, subBeat: Int) { if (instrument.muted) { return } - val activeNotes = noteActive[beat] - val notes = getNotes(root, chordNotes) + val beatIndex = beat * Song.currentSong.subBeats + subBeat + val activeNotes = noteActive[beatIndex] + val notes = type.getNotes(root, chordNotes) for ((index, active) in activeNotes.withIndex()) { - val note = notes[index] + val note = notes[index] + 12 * octaveOffset if (!active) { instrument.stopNote(note) continue } - instrument.startNote(note) + if (restrikeNotes || !instrument.isPlaying(note)) { + 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/song/voice/VoiceType.kt b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt new file mode 100644 index 0000000..e06761a --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -0,0 +1,36 @@ +/* + * 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.song.voice + +import com.lukas.music.song.ScaleType +import com.lukas.music.song.note.Note +import com.lukas.music.util.transform + +enum class VoiceType( + val title: String, + val noteCount: Int, + val getNotes: (Note, Array) -> Array +) { + Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), + Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + Scale("Scale notes", 8, { root, _ -> ScaleType.MAJOR.steps.transform { root + it } }), + Root("Root note", 1, { root, _ -> arrayOf(root) }), + RootRelative("Song root relative", 12, { root, _ -> Array(12) { root + it } }), + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ 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 bd7a3d9..6c3bae3 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 @@ -16,14 +16,14 @@ import android.view.ViewGroup import androidx.fragment.app.DialogFragment import com.lukas.music.databinding.FragmentEditChordBinding -import com.lukas.music.song.Scale +import com.lukas.music.song.ScaleType import com.lukas.music.song.Song 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, private val songFragment: SongFragment) : +class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : DialogFragment() { lateinit var binding: FragmentEditChordBinding @@ -42,12 +42,12 @@ private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { - Array(Scale.MAJOR.steps.size) { (Song.currentSong.root + Scale.MAJOR.steps[it]).noteName.toString() } + Array(ScaleType.MAJOR.steps.size) { (Song.currentSong.root + ScaleType.MAJOR.steps[it]).noteName.toString() } } else Interval.IntervalName.NAMES binding.pitchSpinner.setup(pitches, chord.interval.name.ordinal) { - chord.note = Scale.MAJOR.steps[it] + chord.note = ScaleType.MAJOR.steps[it] if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } songFragment.updateChords() } @@ -60,11 +60,11 @@ } binding.typeSpinner.setup( values, - if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 + if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 else chord.chordType.ordinal + 1 ) { if (it == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } else { chord.chordType = ChordType.VALUES[it - 1] } diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt new file mode 100644 index 0000000..6e359e4 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt @@ -0,0 +1,38 @@ +/* + * 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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEffectsBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.util.EasyDialogFragment + +class EditEffectsFragment(private val instrument: Instrument) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEffectsBinding.inflate(inflater) + for (effect in instrument.effects) { + val effectEditor = EffectFragment(effect) + childFragmentManager.beginTransaction().add(binding.effectsDisplay.id, effectEditor) + .commit() + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.kt new file mode 100644 index 0000000..605c9e5 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.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.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEnvelopeBinding +import com.lukas.music.instruments.Envelope +import com.lukas.music.util.EasyDialogFragment +import com.lukas.music.util.smartSetup + +class EditEnvelopeFragment(private val envelope: Envelope) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEnvelopeBinding.inflate(inflater) + binding.attackSeek.smartSetup(5, 200, envelope::attack) { + binding.attackText.text = "Attack: $it ms" + } + binding.delaySeek.smartSetup(5, 200, envelope::delay) { + binding.delayText.text = "Delay: $it ms" + } + binding.sustainSeek.smartSetup(0, 100, envelope::sustain) { + binding.sustainText.text = "Sustain: $it%" + } + binding.releaseSeek.smartSetup(5, 200, envelope::release) { + binding.releaseText.text = "Release: $it ms" + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ 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 index 0020ae8..c416df7 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt @@ -16,22 +16,19 @@ 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.song.voice.VoiceType import com.lukas.music.ui.adapters.InstrumentViewHolder +import com.lukas.music.util.EasyDialogFragment 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 - +) : EasyDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -49,33 +46,23 @@ } }) binding.waveformSelection.smartSetup(Waveform.VALUES, instrument::waveform) - binding.volumeSeek.setup(0, 100, 30) { + binding.volumeSeek.setup(0, 100, (instrument.volume * 100f).toInt()) { 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.voiceSelection.smartSetup(VoiceType.VALUES, instrument.voice::type) binding.editVoiceButton.setOnClickListener { EditVoiceFragment(instrument.voice).showNow(childFragmentManager, "") } + binding.editEnvelopeButton.setOnClickListener { + EditEnvelopeFragment(instrument.envelope).showNow(childFragmentManager, "") + } + binding.editEffectsButton.setOnClickListener { + EditEffectsFragment(instrument).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/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditVoiceFragment.kt new file mode 100644 index 0000000..eb56238 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditVoiceFragment.kt @@ -0,0 +1,76 @@ +/* + * 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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import com.google.android.material.button.MaterialButton +import com.lukas.music.R +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.EasyDialogFragment +import com.lukas.music.util.setup +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + binding.restrikeButton.setupToggle(voice::restrikeNotes, R.color.green) + for (row in voice.type.noteCount - 1 downTo 0) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats * Song.currentSong.subBeats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle( + ArrayProperty(voice.noteActive[column], row), + R.color.blue, + inactiveColor = if (column % Song.currentSong.subBeats == 0) R.color.gray_0x50 else R.color.gray_0x70 + ) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.octaveSeekBar.setup(-4, 4, voice.octaveOffset) { + voice.octaveOffset = it + binding.octaveText.text = "octave = $it" + } + 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.MATCH_PARENT + ) + } + + 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/ui/fragments/EffectFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt new file mode 100644 index 0000000..ebf4cb1 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.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.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.lukas.music.R +import com.lukas.music.databinding.FragmentEffectBinding +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.util.setupToggle +import com.lukas.music.util.smartSetup + +class EffectFragment(private val effect: Effect) : Fragment() { + lateinit var binding: FragmentEffectBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEffectBinding.inflate(inflater) + binding.effectName.text = effect.type.toString() + binding.activeButton.setupToggle(effect::active, R.color.blue) { + binding.activeButton.text = if (it) "ON" else "OFF" + } + binding.activeButton.text = if (effect.active) "ON" else "OFF" + binding.influenceSeekBar.smartSetup(0, 100, effect.influence::percentageValue) { + binding.influenceText.text = effect.influence.description.text(effect.influence) + } + binding.parameter1SeekBar.smartSetup(0, 100, effect.parameters[0]::percentageValue) { + binding.parameter1Text.text = + effect.parameters[0].description.text(effect.parameters[0]) + } + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/util/EasyDialogFragment.kt b/app/src/main/java/com/lukas/music/util/EasyDialogFragment.kt new file mode 100644 index 0000000..c93b6e7 --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/EasyDialogFragment.kt @@ -0,0 +1,27 @@ +/* + * 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.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.viewbinding.ViewBinding + +open class EasyDialogFragment : DialogFragment() { + lateinit var binding: T + + 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 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt new file mode 100644 index 0000000..25c6260 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt @@ -0,0 +1,18 @@ +/* + * 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.instruments.effect + +class EffectParameterDescription( + val min: Float, + val max: Float, + val initialValue: Float, + val text: (EffectParameter) -> String, +) \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt new file mode 100644 index 0000000..8af39ea --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -0,0 +1,41 @@ +/* + * 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.instruments.effect + +import com.lukas.music.util.format + +enum class EffectType( + val title: String, + val parameterDescriptions: Array +) { + LowPass("low pass filter", + arrayOf( + EffectParameterDescription(-1f, 3f, 1f) { + "cutoff: ${it.value.format(1)} octaves" + } + )), + Noise("noise", + arrayOf( + EffectParameterDescription(0f, 1f, 0f) { + "unused" + } + ) + ) + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Scale.kt b/app/src/main/java/com/lukas/music/song/Scale.kt deleted file mode 100644 index 8e08034..0000000 --- a/app/src/main/java/com/lukas/music/song/Scale.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song - -import com.lukas.music.song.chords.ChordType - -enum class Scale(val identifier: String, val steps: Array, val chordTypes: Array) { - MAJOR( - "major", - arrayOf(0, 2, 4, 5, 7, 9, 11, 12), - arrayOf( - ChordType.MAJOR, - ChordType.MINOR, - ChordType.MINOR, - ChordType.MAJOR, - ChordType.MAJOR, - ChordType.MINOR, - ChordType.DIMINISHED - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt new file mode 100644 index 0000000..4c6a0d9 --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -0,0 +1,33 @@ +/* + * 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.song + +import com.lukas.music.song.chords.ChordType + +enum class ScaleType( + val identifier: String, + val steps: Array, + val chordTypes: Array +) { + MAJOR( + "major", + arrayOf(0, 2, 4, 5, 7, 9, 11, 12), + arrayOf( + ChordType.MAJOR, + ChordType.MINOR, + ChordType.MINOR, + ChordType.MAJOR, + ChordType.MAJOR, + ChordType.MINOR, + ChordType.DIMINISHED + ) + ) +} \ 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 fc5421b..5046664 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -14,11 +14,13 @@ import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note import com.lukas.music.util.Cycle +import com.lukas.music.util.MetaCycle class Song( root: Note, - val beats: Int -) : Cycle(beats) { + val beats: Int, + val subBeats: Int, +) : MetaCycle>() { val chordProgression = ChordProgression() var soloInstrument: Instrument? = null set(value) { @@ -46,7 +48,11 @@ init { for (i in 0 until beats) { - this += i + val cycle = Cycle() + for (j in 0 until subBeats) { + cycle += j + } + this += cycle } wraparoundListeners += { chordProgression.step() @@ -54,24 +60,25 @@ } } - override fun step(): Int { + override fun step(): Cycle? { super.step() - val chord = chordProgression.currentItem?.currentItem ?: return index + val chord = chordProgression.currentItem?.currentItem ?: return currentItem val chordNotes = chord.getNotes(root) soloInstrument?.let { - it.voice.step(root, chordNotes, index) + it.voice.step(root, chordNotes, index, currentItem!!.index) } ?: run { for (instrument in Instrument.instruments) { - instrument.voice.step(root, chordNotes, index) + instrument.voice.step(root, chordNotes, index, currentItem!!.index) } } - return index + return currentItem } companion object { var currentSong = Song( Note.NOTES[69], - 4 + 4, + 2, ) } } \ No newline at end of file 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 deleted file mode 100644 index 4706068..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class BassVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - arrayOf(true), - arrayOf(false), - arrayOf(true), - arrayOf(false) - ) - - 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 deleted file mode 100644 index ab7117f..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class ChordVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - Array(3) { false }, - Array(3) { true }, - Array(3) { false }, - Array(3) { true }, - ) - override val noteCount: Int = 3 - - 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 4f56c2a..139be78 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 @@ -11,37 +11,41 @@ package com.lukas.music.song.voice import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -import kotlin.reflect.KClass -abstract class Voice(val instrument: Instrument) { - abstract var noteActive: Array> - abstract val noteCount: Int +class Voice(val instrument: Instrument) { + var type: VoiceType = VoiceType.Bass + set(value) { + field = value + noteActive = + Array(Song.currentSong.beats * Song.currentSong.subBeats) { Array(value.noteCount) { false } } + } + var restrikeNotes = false + lateinit var noteActive: Array> - abstract fun getNotes(root: Note, chordNotes: Array): Array + var octaveOffset = 0 - fun step(root: Note, chordNotes: Array, beat: Int) { + init { + type = type + } + + fun step(root: Note, chordNotes: Array, beat: Int, subBeat: Int) { if (instrument.muted) { return } - val activeNotes = noteActive[beat] - val notes = getNotes(root, chordNotes) + val beatIndex = beat * Song.currentSong.subBeats + subBeat + val activeNotes = noteActive[beatIndex] + val notes = type.getNotes(root, chordNotes) for ((index, active) in activeNotes.withIndex()) { - val note = notes[index] + val note = notes[index] + 12 * octaveOffset if (!active) { instrument.stopNote(note) continue } - instrument.startNote(note) + if (restrikeNotes || !instrument.isPlaying(note)) { + 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/song/voice/VoiceType.kt b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt new file mode 100644 index 0000000..e06761a --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -0,0 +1,36 @@ +/* + * 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.song.voice + +import com.lukas.music.song.ScaleType +import com.lukas.music.song.note.Note +import com.lukas.music.util.transform + +enum class VoiceType( + val title: String, + val noteCount: Int, + val getNotes: (Note, Array) -> Array +) { + Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), + Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + Scale("Scale notes", 8, { root, _ -> ScaleType.MAJOR.steps.transform { root + it } }), + Root("Root note", 1, { root, _ -> arrayOf(root) }), + RootRelative("Song root relative", 12, { root, _ -> Array(12) { root + it } }), + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ 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 bd7a3d9..6c3bae3 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 @@ -16,14 +16,14 @@ import android.view.ViewGroup import androidx.fragment.app.DialogFragment import com.lukas.music.databinding.FragmentEditChordBinding -import com.lukas.music.song.Scale +import com.lukas.music.song.ScaleType import com.lukas.music.song.Song 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, private val songFragment: SongFragment) : +class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : DialogFragment() { lateinit var binding: FragmentEditChordBinding @@ -42,12 +42,12 @@ private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { - Array(Scale.MAJOR.steps.size) { (Song.currentSong.root + Scale.MAJOR.steps[it]).noteName.toString() } + Array(ScaleType.MAJOR.steps.size) { (Song.currentSong.root + ScaleType.MAJOR.steps[it]).noteName.toString() } } else Interval.IntervalName.NAMES binding.pitchSpinner.setup(pitches, chord.interval.name.ordinal) { - chord.note = Scale.MAJOR.steps[it] + chord.note = ScaleType.MAJOR.steps[it] if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } songFragment.updateChords() } @@ -60,11 +60,11 @@ } binding.typeSpinner.setup( values, - if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 + if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 else chord.chordType.ordinal + 1 ) { if (it == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } else { chord.chordType = ChordType.VALUES[it - 1] } diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt new file mode 100644 index 0000000..6e359e4 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt @@ -0,0 +1,38 @@ +/* + * 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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEffectsBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.util.EasyDialogFragment + +class EditEffectsFragment(private val instrument: Instrument) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEffectsBinding.inflate(inflater) + for (effect in instrument.effects) { + val effectEditor = EffectFragment(effect) + childFragmentManager.beginTransaction().add(binding.effectsDisplay.id, effectEditor) + .commit() + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.kt new file mode 100644 index 0000000..605c9e5 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.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.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEnvelopeBinding +import com.lukas.music.instruments.Envelope +import com.lukas.music.util.EasyDialogFragment +import com.lukas.music.util.smartSetup + +class EditEnvelopeFragment(private val envelope: Envelope) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEnvelopeBinding.inflate(inflater) + binding.attackSeek.smartSetup(5, 200, envelope::attack) { + binding.attackText.text = "Attack: $it ms" + } + binding.delaySeek.smartSetup(5, 200, envelope::delay) { + binding.delayText.text = "Delay: $it ms" + } + binding.sustainSeek.smartSetup(0, 100, envelope::sustain) { + binding.sustainText.text = "Sustain: $it%" + } + binding.releaseSeek.smartSetup(5, 200, envelope::release) { + binding.releaseText.text = "Release: $it ms" + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ 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 index 0020ae8..c416df7 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt @@ -16,22 +16,19 @@ 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.song.voice.VoiceType import com.lukas.music.ui.adapters.InstrumentViewHolder +import com.lukas.music.util.EasyDialogFragment 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 - +) : EasyDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -49,33 +46,23 @@ } }) binding.waveformSelection.smartSetup(Waveform.VALUES, instrument::waveform) - binding.volumeSeek.setup(0, 100, 30) { + binding.volumeSeek.setup(0, 100, (instrument.volume * 100f).toInt()) { 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.voiceSelection.smartSetup(VoiceType.VALUES, instrument.voice::type) binding.editVoiceButton.setOnClickListener { EditVoiceFragment(instrument.voice).showNow(childFragmentManager, "") } + binding.editEnvelopeButton.setOnClickListener { + EditEnvelopeFragment(instrument.envelope).showNow(childFragmentManager, "") + } + binding.editEffectsButton.setOnClickListener { + EditEffectsFragment(instrument).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/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditVoiceFragment.kt new file mode 100644 index 0000000..eb56238 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditVoiceFragment.kt @@ -0,0 +1,76 @@ +/* + * 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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import com.google.android.material.button.MaterialButton +import com.lukas.music.R +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.EasyDialogFragment +import com.lukas.music.util.setup +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + binding.restrikeButton.setupToggle(voice::restrikeNotes, R.color.green) + for (row in voice.type.noteCount - 1 downTo 0) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats * Song.currentSong.subBeats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle( + ArrayProperty(voice.noteActive[column], row), + R.color.blue, + inactiveColor = if (column % Song.currentSong.subBeats == 0) R.color.gray_0x50 else R.color.gray_0x70 + ) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.octaveSeekBar.setup(-4, 4, voice.octaveOffset) { + voice.octaveOffset = it + binding.octaveText.text = "octave = $it" + } + 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.MATCH_PARENT + ) + } + + 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/ui/fragments/EffectFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt new file mode 100644 index 0000000..ebf4cb1 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.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.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.lukas.music.R +import com.lukas.music.databinding.FragmentEffectBinding +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.util.setupToggle +import com.lukas.music.util.smartSetup + +class EffectFragment(private val effect: Effect) : Fragment() { + lateinit var binding: FragmentEffectBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEffectBinding.inflate(inflater) + binding.effectName.text = effect.type.toString() + binding.activeButton.setupToggle(effect::active, R.color.blue) { + binding.activeButton.text = if (it) "ON" else "OFF" + } + binding.activeButton.text = if (effect.active) "ON" else "OFF" + binding.influenceSeekBar.smartSetup(0, 100, effect.influence::percentageValue) { + binding.influenceText.text = effect.influence.description.text(effect.influence) + } + binding.parameter1SeekBar.smartSetup(0, 100, effect.parameters[0]::percentageValue) { + binding.parameter1Text.text = + effect.parameters[0].description.text(effect.parameters[0]) + } + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/util/EasyDialogFragment.kt b/app/src/main/java/com/lukas/music/util/EasyDialogFragment.kt new file mode 100644 index 0000000..c93b6e7 --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/EasyDialogFragment.kt @@ -0,0 +1,27 @@ +/* + * 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.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.viewbinding.ViewBinding + +open class EasyDialogFragment : DialogFragment() { + lateinit var binding: T + + 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/util/MathUtil.kt b/app/src/main/java/com/lukas/music/util/MathUtil.kt new file mode 100644 index 0000000..166061d --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/MathUtil.kt @@ -0,0 +1,21 @@ +/* + * 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 + +fun Double.format(digits: Int) = "%.${digits}f".format(this) + +fun Float.format(digits: Int) = "%.${digits}f".format(this) + +inline fun Array.transform(callback: (T) -> U): Array { + return Array(this.size) { + callback(this[it]) + } +} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt new file mode 100644 index 0000000..25c6260 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt @@ -0,0 +1,18 @@ +/* + * 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.instruments.effect + +class EffectParameterDescription( + val min: Float, + val max: Float, + val initialValue: Float, + val text: (EffectParameter) -> String, +) \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt new file mode 100644 index 0000000..8af39ea --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -0,0 +1,41 @@ +/* + * 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.instruments.effect + +import com.lukas.music.util.format + +enum class EffectType( + val title: String, + val parameterDescriptions: Array +) { + LowPass("low pass filter", + arrayOf( + EffectParameterDescription(-1f, 3f, 1f) { + "cutoff: ${it.value.format(1)} octaves" + } + )), + Noise("noise", + arrayOf( + EffectParameterDescription(0f, 1f, 0f) { + "unused" + } + ) + ) + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Scale.kt b/app/src/main/java/com/lukas/music/song/Scale.kt deleted file mode 100644 index 8e08034..0000000 --- a/app/src/main/java/com/lukas/music/song/Scale.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song - -import com.lukas.music.song.chords.ChordType - -enum class Scale(val identifier: String, val steps: Array, val chordTypes: Array) { - MAJOR( - "major", - arrayOf(0, 2, 4, 5, 7, 9, 11, 12), - arrayOf( - ChordType.MAJOR, - ChordType.MINOR, - ChordType.MINOR, - ChordType.MAJOR, - ChordType.MAJOR, - ChordType.MINOR, - ChordType.DIMINISHED - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt new file mode 100644 index 0000000..4c6a0d9 --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -0,0 +1,33 @@ +/* + * 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.song + +import com.lukas.music.song.chords.ChordType + +enum class ScaleType( + val identifier: String, + val steps: Array, + val chordTypes: Array +) { + MAJOR( + "major", + arrayOf(0, 2, 4, 5, 7, 9, 11, 12), + arrayOf( + ChordType.MAJOR, + ChordType.MINOR, + ChordType.MINOR, + ChordType.MAJOR, + ChordType.MAJOR, + ChordType.MINOR, + ChordType.DIMINISHED + ) + ) +} \ 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 fc5421b..5046664 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -14,11 +14,13 @@ import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note import com.lukas.music.util.Cycle +import com.lukas.music.util.MetaCycle class Song( root: Note, - val beats: Int -) : Cycle(beats) { + val beats: Int, + val subBeats: Int, +) : MetaCycle>() { val chordProgression = ChordProgression() var soloInstrument: Instrument? = null set(value) { @@ -46,7 +48,11 @@ init { for (i in 0 until beats) { - this += i + val cycle = Cycle() + for (j in 0 until subBeats) { + cycle += j + } + this += cycle } wraparoundListeners += { chordProgression.step() @@ -54,24 +60,25 @@ } } - override fun step(): Int { + override fun step(): Cycle? { super.step() - val chord = chordProgression.currentItem?.currentItem ?: return index + val chord = chordProgression.currentItem?.currentItem ?: return currentItem val chordNotes = chord.getNotes(root) soloInstrument?.let { - it.voice.step(root, chordNotes, index) + it.voice.step(root, chordNotes, index, currentItem!!.index) } ?: run { for (instrument in Instrument.instruments) { - instrument.voice.step(root, chordNotes, index) + instrument.voice.step(root, chordNotes, index, currentItem!!.index) } } - return index + return currentItem } companion object { var currentSong = Song( Note.NOTES[69], - 4 + 4, + 2, ) } } \ No newline at end of file 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 deleted file mode 100644 index 4706068..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class BassVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - arrayOf(true), - arrayOf(false), - arrayOf(true), - arrayOf(false) - ) - - 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 deleted file mode 100644 index ab7117f..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class ChordVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - Array(3) { false }, - Array(3) { true }, - Array(3) { false }, - Array(3) { true }, - ) - override val noteCount: Int = 3 - - 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 4f56c2a..139be78 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 @@ -11,37 +11,41 @@ package com.lukas.music.song.voice import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -import kotlin.reflect.KClass -abstract class Voice(val instrument: Instrument) { - abstract var noteActive: Array> - abstract val noteCount: Int +class Voice(val instrument: Instrument) { + var type: VoiceType = VoiceType.Bass + set(value) { + field = value + noteActive = + Array(Song.currentSong.beats * Song.currentSong.subBeats) { Array(value.noteCount) { false } } + } + var restrikeNotes = false + lateinit var noteActive: Array> - abstract fun getNotes(root: Note, chordNotes: Array): Array + var octaveOffset = 0 - fun step(root: Note, chordNotes: Array, beat: Int) { + init { + type = type + } + + fun step(root: Note, chordNotes: Array, beat: Int, subBeat: Int) { if (instrument.muted) { return } - val activeNotes = noteActive[beat] - val notes = getNotes(root, chordNotes) + val beatIndex = beat * Song.currentSong.subBeats + subBeat + val activeNotes = noteActive[beatIndex] + val notes = type.getNotes(root, chordNotes) for ((index, active) in activeNotes.withIndex()) { - val note = notes[index] + val note = notes[index] + 12 * octaveOffset if (!active) { instrument.stopNote(note) continue } - instrument.startNote(note) + if (restrikeNotes || !instrument.isPlaying(note)) { + 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/song/voice/VoiceType.kt b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt new file mode 100644 index 0000000..e06761a --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -0,0 +1,36 @@ +/* + * 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.song.voice + +import com.lukas.music.song.ScaleType +import com.lukas.music.song.note.Note +import com.lukas.music.util.transform + +enum class VoiceType( + val title: String, + val noteCount: Int, + val getNotes: (Note, Array) -> Array +) { + Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), + Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + Scale("Scale notes", 8, { root, _ -> ScaleType.MAJOR.steps.transform { root + it } }), + Root("Root note", 1, { root, _ -> arrayOf(root) }), + RootRelative("Song root relative", 12, { root, _ -> Array(12) { root + it } }), + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ 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 bd7a3d9..6c3bae3 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 @@ -16,14 +16,14 @@ import android.view.ViewGroup import androidx.fragment.app.DialogFragment import com.lukas.music.databinding.FragmentEditChordBinding -import com.lukas.music.song.Scale +import com.lukas.music.song.ScaleType import com.lukas.music.song.Song 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, private val songFragment: SongFragment) : +class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : DialogFragment() { lateinit var binding: FragmentEditChordBinding @@ -42,12 +42,12 @@ private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { - Array(Scale.MAJOR.steps.size) { (Song.currentSong.root + Scale.MAJOR.steps[it]).noteName.toString() } + Array(ScaleType.MAJOR.steps.size) { (Song.currentSong.root + ScaleType.MAJOR.steps[it]).noteName.toString() } } else Interval.IntervalName.NAMES binding.pitchSpinner.setup(pitches, chord.interval.name.ordinal) { - chord.note = Scale.MAJOR.steps[it] + chord.note = ScaleType.MAJOR.steps[it] if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } songFragment.updateChords() } @@ -60,11 +60,11 @@ } binding.typeSpinner.setup( values, - if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 + if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 else chord.chordType.ordinal + 1 ) { if (it == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } else { chord.chordType = ChordType.VALUES[it - 1] } diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt new file mode 100644 index 0000000..6e359e4 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt @@ -0,0 +1,38 @@ +/* + * 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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEffectsBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.util.EasyDialogFragment + +class EditEffectsFragment(private val instrument: Instrument) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEffectsBinding.inflate(inflater) + for (effect in instrument.effects) { + val effectEditor = EffectFragment(effect) + childFragmentManager.beginTransaction().add(binding.effectsDisplay.id, effectEditor) + .commit() + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.kt new file mode 100644 index 0000000..605c9e5 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.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.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEnvelopeBinding +import com.lukas.music.instruments.Envelope +import com.lukas.music.util.EasyDialogFragment +import com.lukas.music.util.smartSetup + +class EditEnvelopeFragment(private val envelope: Envelope) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEnvelopeBinding.inflate(inflater) + binding.attackSeek.smartSetup(5, 200, envelope::attack) { + binding.attackText.text = "Attack: $it ms" + } + binding.delaySeek.smartSetup(5, 200, envelope::delay) { + binding.delayText.text = "Delay: $it ms" + } + binding.sustainSeek.smartSetup(0, 100, envelope::sustain) { + binding.sustainText.text = "Sustain: $it%" + } + binding.releaseSeek.smartSetup(5, 200, envelope::release) { + binding.releaseText.text = "Release: $it ms" + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ 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 index 0020ae8..c416df7 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt @@ -16,22 +16,19 @@ 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.song.voice.VoiceType import com.lukas.music.ui.adapters.InstrumentViewHolder +import com.lukas.music.util.EasyDialogFragment 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 - +) : EasyDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -49,33 +46,23 @@ } }) binding.waveformSelection.smartSetup(Waveform.VALUES, instrument::waveform) - binding.volumeSeek.setup(0, 100, 30) { + binding.volumeSeek.setup(0, 100, (instrument.volume * 100f).toInt()) { 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.voiceSelection.smartSetup(VoiceType.VALUES, instrument.voice::type) binding.editVoiceButton.setOnClickListener { EditVoiceFragment(instrument.voice).showNow(childFragmentManager, "") } + binding.editEnvelopeButton.setOnClickListener { + EditEnvelopeFragment(instrument.envelope).showNow(childFragmentManager, "") + } + binding.editEffectsButton.setOnClickListener { + EditEffectsFragment(instrument).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/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditVoiceFragment.kt new file mode 100644 index 0000000..eb56238 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditVoiceFragment.kt @@ -0,0 +1,76 @@ +/* + * 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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import com.google.android.material.button.MaterialButton +import com.lukas.music.R +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.EasyDialogFragment +import com.lukas.music.util.setup +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + binding.restrikeButton.setupToggle(voice::restrikeNotes, R.color.green) + for (row in voice.type.noteCount - 1 downTo 0) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats * Song.currentSong.subBeats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle( + ArrayProperty(voice.noteActive[column], row), + R.color.blue, + inactiveColor = if (column % Song.currentSong.subBeats == 0) R.color.gray_0x50 else R.color.gray_0x70 + ) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.octaveSeekBar.setup(-4, 4, voice.octaveOffset) { + voice.octaveOffset = it + binding.octaveText.text = "octave = $it" + } + 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.MATCH_PARENT + ) + } + + 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/ui/fragments/EffectFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt new file mode 100644 index 0000000..ebf4cb1 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.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.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.lukas.music.R +import com.lukas.music.databinding.FragmentEffectBinding +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.util.setupToggle +import com.lukas.music.util.smartSetup + +class EffectFragment(private val effect: Effect) : Fragment() { + lateinit var binding: FragmentEffectBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEffectBinding.inflate(inflater) + binding.effectName.text = effect.type.toString() + binding.activeButton.setupToggle(effect::active, R.color.blue) { + binding.activeButton.text = if (it) "ON" else "OFF" + } + binding.activeButton.text = if (effect.active) "ON" else "OFF" + binding.influenceSeekBar.smartSetup(0, 100, effect.influence::percentageValue) { + binding.influenceText.text = effect.influence.description.text(effect.influence) + } + binding.parameter1SeekBar.smartSetup(0, 100, effect.parameters[0]::percentageValue) { + binding.parameter1Text.text = + effect.parameters[0].description.text(effect.parameters[0]) + } + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/util/EasyDialogFragment.kt b/app/src/main/java/com/lukas/music/util/EasyDialogFragment.kt new file mode 100644 index 0000000..c93b6e7 --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/EasyDialogFragment.kt @@ -0,0 +1,27 @@ +/* + * 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.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.viewbinding.ViewBinding + +open class EasyDialogFragment : DialogFragment() { + lateinit var binding: T + + 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/util/MathUtil.kt b/app/src/main/java/com/lukas/music/util/MathUtil.kt new file mode 100644 index 0000000..166061d --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/MathUtil.kt @@ -0,0 +1,21 @@ +/* + * 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 + +fun Double.format(digits: Int) = "%.${digits}f".format(this) + +fun Float.format(digits: Int) = "%.${digits}f".format(this) + +inline fun Array.transform(callback: (T) -> U): Array { + return Array(this.size) { + callback(this[it]) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/util/UIUtil.kt b/app/src/main/java/com/lukas/music/util/UIUtil.kt index da897c5..b67d0ea 100644 --- a/app/src/main/java/com/lukas/music/util/UIUtil.kt +++ b/app/src/main/java/com/lukas/music/util/UIUtil.kt @@ -36,25 +36,45 @@ override fun onStopTrackingTouch(seekBar: SeekBar) { } }) + if (progress == initialProgress) { + callback(initialProgress) + } this.progress = initialProgress } +fun SeekBar.smartSetup( + min: Int, + max: Int, + target: KMutableProperty0, + callback: (Int) -> Unit +) { + setup(min, max, target.get()) { + target.set(it) + callback(it) + } +} + fun Button.setupToggle( target: KMutableProperty0, activeColor: Int, - callback: (Boolean) -> Unit = {} + inactiveColor: Int = R.color.gray_0x60, + callback: (Boolean) -> Unit = {}, ) { setOnClickListener { target.set(!target.get()) - updateToggle(target, activeColor) + updateToggle(target, activeColor, inactiveColor) callback(target.get()) } - updateToggle(target, activeColor) + updateToggle(target, activeColor, inactiveColor) } -fun Button.updateToggle(target: KMutableProperty0, activeColor: Int) { +fun Button.updateToggle( + target: KMutableProperty0, + activeColor: Int, + inactiveColor: Int = R.color.gray_0x60, +) { setBackgroundColor( - ContextCompat.getColor(context, if (target.get()) activeColor else R.color.gray_0x60) + ContextCompat.getColor(context, if (target.get()) activeColor else inactiveColor) ) } diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt new file mode 100644 index 0000000..25c6260 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt @@ -0,0 +1,18 @@ +/* + * 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.instruments.effect + +class EffectParameterDescription( + val min: Float, + val max: Float, + val initialValue: Float, + val text: (EffectParameter) -> String, +) \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt new file mode 100644 index 0000000..8af39ea --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -0,0 +1,41 @@ +/* + * 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.instruments.effect + +import com.lukas.music.util.format + +enum class EffectType( + val title: String, + val parameterDescriptions: Array +) { + LowPass("low pass filter", + arrayOf( + EffectParameterDescription(-1f, 3f, 1f) { + "cutoff: ${it.value.format(1)} octaves" + } + )), + Noise("noise", + arrayOf( + EffectParameterDescription(0f, 1f, 0f) { + "unused" + } + ) + ) + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Scale.kt b/app/src/main/java/com/lukas/music/song/Scale.kt deleted file mode 100644 index 8e08034..0000000 --- a/app/src/main/java/com/lukas/music/song/Scale.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song - -import com.lukas.music.song.chords.ChordType - -enum class Scale(val identifier: String, val steps: Array, val chordTypes: Array) { - MAJOR( - "major", - arrayOf(0, 2, 4, 5, 7, 9, 11, 12), - arrayOf( - ChordType.MAJOR, - ChordType.MINOR, - ChordType.MINOR, - ChordType.MAJOR, - ChordType.MAJOR, - ChordType.MINOR, - ChordType.DIMINISHED - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt new file mode 100644 index 0000000..4c6a0d9 --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -0,0 +1,33 @@ +/* + * 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.song + +import com.lukas.music.song.chords.ChordType + +enum class ScaleType( + val identifier: String, + val steps: Array, + val chordTypes: Array +) { + MAJOR( + "major", + arrayOf(0, 2, 4, 5, 7, 9, 11, 12), + arrayOf( + ChordType.MAJOR, + ChordType.MINOR, + ChordType.MINOR, + ChordType.MAJOR, + ChordType.MAJOR, + ChordType.MINOR, + ChordType.DIMINISHED + ) + ) +} \ 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 fc5421b..5046664 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -14,11 +14,13 @@ import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note import com.lukas.music.util.Cycle +import com.lukas.music.util.MetaCycle class Song( root: Note, - val beats: Int -) : Cycle(beats) { + val beats: Int, + val subBeats: Int, +) : MetaCycle>() { val chordProgression = ChordProgression() var soloInstrument: Instrument? = null set(value) { @@ -46,7 +48,11 @@ init { for (i in 0 until beats) { - this += i + val cycle = Cycle() + for (j in 0 until subBeats) { + cycle += j + } + this += cycle } wraparoundListeners += { chordProgression.step() @@ -54,24 +60,25 @@ } } - override fun step(): Int { + override fun step(): Cycle? { super.step() - val chord = chordProgression.currentItem?.currentItem ?: return index + val chord = chordProgression.currentItem?.currentItem ?: return currentItem val chordNotes = chord.getNotes(root) soloInstrument?.let { - it.voice.step(root, chordNotes, index) + it.voice.step(root, chordNotes, index, currentItem!!.index) } ?: run { for (instrument in Instrument.instruments) { - instrument.voice.step(root, chordNotes, index) + instrument.voice.step(root, chordNotes, index, currentItem!!.index) } } - return index + return currentItem } companion object { var currentSong = Song( Note.NOTES[69], - 4 + 4, + 2, ) } } \ No newline at end of file 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 deleted file mode 100644 index 4706068..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class BassVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - arrayOf(true), - arrayOf(false), - arrayOf(true), - arrayOf(false) - ) - - 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 deleted file mode 100644 index ab7117f..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class ChordVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - Array(3) { false }, - Array(3) { true }, - Array(3) { false }, - Array(3) { true }, - ) - override val noteCount: Int = 3 - - 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 4f56c2a..139be78 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 @@ -11,37 +11,41 @@ package com.lukas.music.song.voice import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -import kotlin.reflect.KClass -abstract class Voice(val instrument: Instrument) { - abstract var noteActive: Array> - abstract val noteCount: Int +class Voice(val instrument: Instrument) { + var type: VoiceType = VoiceType.Bass + set(value) { + field = value + noteActive = + Array(Song.currentSong.beats * Song.currentSong.subBeats) { Array(value.noteCount) { false } } + } + var restrikeNotes = false + lateinit var noteActive: Array> - abstract fun getNotes(root: Note, chordNotes: Array): Array + var octaveOffset = 0 - fun step(root: Note, chordNotes: Array, beat: Int) { + init { + type = type + } + + fun step(root: Note, chordNotes: Array, beat: Int, subBeat: Int) { if (instrument.muted) { return } - val activeNotes = noteActive[beat] - val notes = getNotes(root, chordNotes) + val beatIndex = beat * Song.currentSong.subBeats + subBeat + val activeNotes = noteActive[beatIndex] + val notes = type.getNotes(root, chordNotes) for ((index, active) in activeNotes.withIndex()) { - val note = notes[index] + val note = notes[index] + 12 * octaveOffset if (!active) { instrument.stopNote(note) continue } - instrument.startNote(note) + if (restrikeNotes || !instrument.isPlaying(note)) { + 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/song/voice/VoiceType.kt b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt new file mode 100644 index 0000000..e06761a --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -0,0 +1,36 @@ +/* + * 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.song.voice + +import com.lukas.music.song.ScaleType +import com.lukas.music.song.note.Note +import com.lukas.music.util.transform + +enum class VoiceType( + val title: String, + val noteCount: Int, + val getNotes: (Note, Array) -> Array +) { + Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), + Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + Scale("Scale notes", 8, { root, _ -> ScaleType.MAJOR.steps.transform { root + it } }), + Root("Root note", 1, { root, _ -> arrayOf(root) }), + RootRelative("Song root relative", 12, { root, _ -> Array(12) { root + it } }), + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ 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 bd7a3d9..6c3bae3 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 @@ -16,14 +16,14 @@ import android.view.ViewGroup import androidx.fragment.app.DialogFragment import com.lukas.music.databinding.FragmentEditChordBinding -import com.lukas.music.song.Scale +import com.lukas.music.song.ScaleType import com.lukas.music.song.Song 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, private val songFragment: SongFragment) : +class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : DialogFragment() { lateinit var binding: FragmentEditChordBinding @@ -42,12 +42,12 @@ private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { - Array(Scale.MAJOR.steps.size) { (Song.currentSong.root + Scale.MAJOR.steps[it]).noteName.toString() } + Array(ScaleType.MAJOR.steps.size) { (Song.currentSong.root + ScaleType.MAJOR.steps[it]).noteName.toString() } } else Interval.IntervalName.NAMES binding.pitchSpinner.setup(pitches, chord.interval.name.ordinal) { - chord.note = Scale.MAJOR.steps[it] + chord.note = ScaleType.MAJOR.steps[it] if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } songFragment.updateChords() } @@ -60,11 +60,11 @@ } binding.typeSpinner.setup( values, - if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 + if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 else chord.chordType.ordinal + 1 ) { if (it == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } else { chord.chordType = ChordType.VALUES[it - 1] } diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt new file mode 100644 index 0000000..6e359e4 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt @@ -0,0 +1,38 @@ +/* + * 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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEffectsBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.util.EasyDialogFragment + +class EditEffectsFragment(private val instrument: Instrument) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEffectsBinding.inflate(inflater) + for (effect in instrument.effects) { + val effectEditor = EffectFragment(effect) + childFragmentManager.beginTransaction().add(binding.effectsDisplay.id, effectEditor) + .commit() + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.kt new file mode 100644 index 0000000..605c9e5 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.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.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEnvelopeBinding +import com.lukas.music.instruments.Envelope +import com.lukas.music.util.EasyDialogFragment +import com.lukas.music.util.smartSetup + +class EditEnvelopeFragment(private val envelope: Envelope) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEnvelopeBinding.inflate(inflater) + binding.attackSeek.smartSetup(5, 200, envelope::attack) { + binding.attackText.text = "Attack: $it ms" + } + binding.delaySeek.smartSetup(5, 200, envelope::delay) { + binding.delayText.text = "Delay: $it ms" + } + binding.sustainSeek.smartSetup(0, 100, envelope::sustain) { + binding.sustainText.text = "Sustain: $it%" + } + binding.releaseSeek.smartSetup(5, 200, envelope::release) { + binding.releaseText.text = "Release: $it ms" + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ 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 index 0020ae8..c416df7 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt @@ -16,22 +16,19 @@ 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.song.voice.VoiceType import com.lukas.music.ui.adapters.InstrumentViewHolder +import com.lukas.music.util.EasyDialogFragment 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 - +) : EasyDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -49,33 +46,23 @@ } }) binding.waveformSelection.smartSetup(Waveform.VALUES, instrument::waveform) - binding.volumeSeek.setup(0, 100, 30) { + binding.volumeSeek.setup(0, 100, (instrument.volume * 100f).toInt()) { 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.voiceSelection.smartSetup(VoiceType.VALUES, instrument.voice::type) binding.editVoiceButton.setOnClickListener { EditVoiceFragment(instrument.voice).showNow(childFragmentManager, "") } + binding.editEnvelopeButton.setOnClickListener { + EditEnvelopeFragment(instrument.envelope).showNow(childFragmentManager, "") + } + binding.editEffectsButton.setOnClickListener { + EditEffectsFragment(instrument).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/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditVoiceFragment.kt new file mode 100644 index 0000000..eb56238 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditVoiceFragment.kt @@ -0,0 +1,76 @@ +/* + * 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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import com.google.android.material.button.MaterialButton +import com.lukas.music.R +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.EasyDialogFragment +import com.lukas.music.util.setup +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + binding.restrikeButton.setupToggle(voice::restrikeNotes, R.color.green) + for (row in voice.type.noteCount - 1 downTo 0) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats * Song.currentSong.subBeats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle( + ArrayProperty(voice.noteActive[column], row), + R.color.blue, + inactiveColor = if (column % Song.currentSong.subBeats == 0) R.color.gray_0x50 else R.color.gray_0x70 + ) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.octaveSeekBar.setup(-4, 4, voice.octaveOffset) { + voice.octaveOffset = it + binding.octaveText.text = "octave = $it" + } + 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.MATCH_PARENT + ) + } + + 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/ui/fragments/EffectFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt new file mode 100644 index 0000000..ebf4cb1 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.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.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.lukas.music.R +import com.lukas.music.databinding.FragmentEffectBinding +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.util.setupToggle +import com.lukas.music.util.smartSetup + +class EffectFragment(private val effect: Effect) : Fragment() { + lateinit var binding: FragmentEffectBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEffectBinding.inflate(inflater) + binding.effectName.text = effect.type.toString() + binding.activeButton.setupToggle(effect::active, R.color.blue) { + binding.activeButton.text = if (it) "ON" else "OFF" + } + binding.activeButton.text = if (effect.active) "ON" else "OFF" + binding.influenceSeekBar.smartSetup(0, 100, effect.influence::percentageValue) { + binding.influenceText.text = effect.influence.description.text(effect.influence) + } + binding.parameter1SeekBar.smartSetup(0, 100, effect.parameters[0]::percentageValue) { + binding.parameter1Text.text = + effect.parameters[0].description.text(effect.parameters[0]) + } + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/util/EasyDialogFragment.kt b/app/src/main/java/com/lukas/music/util/EasyDialogFragment.kt new file mode 100644 index 0000000..c93b6e7 --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/EasyDialogFragment.kt @@ -0,0 +1,27 @@ +/* + * 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.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.viewbinding.ViewBinding + +open class EasyDialogFragment : DialogFragment() { + lateinit var binding: T + + 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/util/MathUtil.kt b/app/src/main/java/com/lukas/music/util/MathUtil.kt new file mode 100644 index 0000000..166061d --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/MathUtil.kt @@ -0,0 +1,21 @@ +/* + * 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 + +fun Double.format(digits: Int) = "%.${digits}f".format(this) + +fun Float.format(digits: Int) = "%.${digits}f".format(this) + +inline fun Array.transform(callback: (T) -> U): Array { + return Array(this.size) { + callback(this[it]) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/util/UIUtil.kt b/app/src/main/java/com/lukas/music/util/UIUtil.kt index da897c5..b67d0ea 100644 --- a/app/src/main/java/com/lukas/music/util/UIUtil.kt +++ b/app/src/main/java/com/lukas/music/util/UIUtil.kt @@ -36,25 +36,45 @@ override fun onStopTrackingTouch(seekBar: SeekBar) { } }) + if (progress == initialProgress) { + callback(initialProgress) + } this.progress = initialProgress } +fun SeekBar.smartSetup( + min: Int, + max: Int, + target: KMutableProperty0, + callback: (Int) -> Unit +) { + setup(min, max, target.get()) { + target.set(it) + callback(it) + } +} + fun Button.setupToggle( target: KMutableProperty0, activeColor: Int, - callback: (Boolean) -> Unit = {} + inactiveColor: Int = R.color.gray_0x60, + callback: (Boolean) -> Unit = {}, ) { setOnClickListener { target.set(!target.get()) - updateToggle(target, activeColor) + updateToggle(target, activeColor, inactiveColor) callback(target.get()) } - updateToggle(target, activeColor) + updateToggle(target, activeColor, inactiveColor) } -fun Button.updateToggle(target: KMutableProperty0, activeColor: Int) { +fun Button.updateToggle( + target: KMutableProperty0, + activeColor: Int, + inactiveColor: Int = R.color.gray_0x60, +) { setBackgroundColor( - ContextCompat.getColor(context, if (target.get()) activeColor else R.color.gray_0x60) + ContextCompat.getColor(context, if (target.get()) activeColor else inactiveColor) ) } diff --git a/app/src/main/res/layout/fragment_edit_effects.xml b/app/src/main/res/layout/fragment_edit_effects.xml new file mode 100644 index 0000000..46d4648 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_effects.xml @@ -0,0 +1,55 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt new file mode 100644 index 0000000..25c6260 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt @@ -0,0 +1,18 @@ +/* + * 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.instruments.effect + +class EffectParameterDescription( + val min: Float, + val max: Float, + val initialValue: Float, + val text: (EffectParameter) -> String, +) \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt new file mode 100644 index 0000000..8af39ea --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -0,0 +1,41 @@ +/* + * 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.instruments.effect + +import com.lukas.music.util.format + +enum class EffectType( + val title: String, + val parameterDescriptions: Array +) { + LowPass("low pass filter", + arrayOf( + EffectParameterDescription(-1f, 3f, 1f) { + "cutoff: ${it.value.format(1)} octaves" + } + )), + Noise("noise", + arrayOf( + EffectParameterDescription(0f, 1f, 0f) { + "unused" + } + ) + ) + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Scale.kt b/app/src/main/java/com/lukas/music/song/Scale.kt deleted file mode 100644 index 8e08034..0000000 --- a/app/src/main/java/com/lukas/music/song/Scale.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song - -import com.lukas.music.song.chords.ChordType - -enum class Scale(val identifier: String, val steps: Array, val chordTypes: Array) { - MAJOR( - "major", - arrayOf(0, 2, 4, 5, 7, 9, 11, 12), - arrayOf( - ChordType.MAJOR, - ChordType.MINOR, - ChordType.MINOR, - ChordType.MAJOR, - ChordType.MAJOR, - ChordType.MINOR, - ChordType.DIMINISHED - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt new file mode 100644 index 0000000..4c6a0d9 --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -0,0 +1,33 @@ +/* + * 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.song + +import com.lukas.music.song.chords.ChordType + +enum class ScaleType( + val identifier: String, + val steps: Array, + val chordTypes: Array +) { + MAJOR( + "major", + arrayOf(0, 2, 4, 5, 7, 9, 11, 12), + arrayOf( + ChordType.MAJOR, + ChordType.MINOR, + ChordType.MINOR, + ChordType.MAJOR, + ChordType.MAJOR, + ChordType.MINOR, + ChordType.DIMINISHED + ) + ) +} \ 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 fc5421b..5046664 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -14,11 +14,13 @@ import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note import com.lukas.music.util.Cycle +import com.lukas.music.util.MetaCycle class Song( root: Note, - val beats: Int -) : Cycle(beats) { + val beats: Int, + val subBeats: Int, +) : MetaCycle>() { val chordProgression = ChordProgression() var soloInstrument: Instrument? = null set(value) { @@ -46,7 +48,11 @@ init { for (i in 0 until beats) { - this += i + val cycle = Cycle() + for (j in 0 until subBeats) { + cycle += j + } + this += cycle } wraparoundListeners += { chordProgression.step() @@ -54,24 +60,25 @@ } } - override fun step(): Int { + override fun step(): Cycle? { super.step() - val chord = chordProgression.currentItem?.currentItem ?: return index + val chord = chordProgression.currentItem?.currentItem ?: return currentItem val chordNotes = chord.getNotes(root) soloInstrument?.let { - it.voice.step(root, chordNotes, index) + it.voice.step(root, chordNotes, index, currentItem!!.index) } ?: run { for (instrument in Instrument.instruments) { - instrument.voice.step(root, chordNotes, index) + instrument.voice.step(root, chordNotes, index, currentItem!!.index) } } - return index + return currentItem } companion object { var currentSong = Song( Note.NOTES[69], - 4 + 4, + 2, ) } } \ No newline at end of file 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 deleted file mode 100644 index 4706068..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class BassVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - arrayOf(true), - arrayOf(false), - arrayOf(true), - arrayOf(false) - ) - - 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 deleted file mode 100644 index ab7117f..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class ChordVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - Array(3) { false }, - Array(3) { true }, - Array(3) { false }, - Array(3) { true }, - ) - override val noteCount: Int = 3 - - 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 4f56c2a..139be78 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 @@ -11,37 +11,41 @@ package com.lukas.music.song.voice import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -import kotlin.reflect.KClass -abstract class Voice(val instrument: Instrument) { - abstract var noteActive: Array> - abstract val noteCount: Int +class Voice(val instrument: Instrument) { + var type: VoiceType = VoiceType.Bass + set(value) { + field = value + noteActive = + Array(Song.currentSong.beats * Song.currentSong.subBeats) { Array(value.noteCount) { false } } + } + var restrikeNotes = false + lateinit var noteActive: Array> - abstract fun getNotes(root: Note, chordNotes: Array): Array + var octaveOffset = 0 - fun step(root: Note, chordNotes: Array, beat: Int) { + init { + type = type + } + + fun step(root: Note, chordNotes: Array, beat: Int, subBeat: Int) { if (instrument.muted) { return } - val activeNotes = noteActive[beat] - val notes = getNotes(root, chordNotes) + val beatIndex = beat * Song.currentSong.subBeats + subBeat + val activeNotes = noteActive[beatIndex] + val notes = type.getNotes(root, chordNotes) for ((index, active) in activeNotes.withIndex()) { - val note = notes[index] + val note = notes[index] + 12 * octaveOffset if (!active) { instrument.stopNote(note) continue } - instrument.startNote(note) + if (restrikeNotes || !instrument.isPlaying(note)) { + 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/song/voice/VoiceType.kt b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt new file mode 100644 index 0000000..e06761a --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -0,0 +1,36 @@ +/* + * 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.song.voice + +import com.lukas.music.song.ScaleType +import com.lukas.music.song.note.Note +import com.lukas.music.util.transform + +enum class VoiceType( + val title: String, + val noteCount: Int, + val getNotes: (Note, Array) -> Array +) { + Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), + Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + Scale("Scale notes", 8, { root, _ -> ScaleType.MAJOR.steps.transform { root + it } }), + Root("Root note", 1, { root, _ -> arrayOf(root) }), + RootRelative("Song root relative", 12, { root, _ -> Array(12) { root + it } }), + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ 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 bd7a3d9..6c3bae3 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 @@ -16,14 +16,14 @@ import android.view.ViewGroup import androidx.fragment.app.DialogFragment import com.lukas.music.databinding.FragmentEditChordBinding -import com.lukas.music.song.Scale +import com.lukas.music.song.ScaleType import com.lukas.music.song.Song 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, private val songFragment: SongFragment) : +class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : DialogFragment() { lateinit var binding: FragmentEditChordBinding @@ -42,12 +42,12 @@ private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { - Array(Scale.MAJOR.steps.size) { (Song.currentSong.root + Scale.MAJOR.steps[it]).noteName.toString() } + Array(ScaleType.MAJOR.steps.size) { (Song.currentSong.root + ScaleType.MAJOR.steps[it]).noteName.toString() } } else Interval.IntervalName.NAMES binding.pitchSpinner.setup(pitches, chord.interval.name.ordinal) { - chord.note = Scale.MAJOR.steps[it] + chord.note = ScaleType.MAJOR.steps[it] if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } songFragment.updateChords() } @@ -60,11 +60,11 @@ } binding.typeSpinner.setup( values, - if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 + if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 else chord.chordType.ordinal + 1 ) { if (it == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } else { chord.chordType = ChordType.VALUES[it - 1] } diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt new file mode 100644 index 0000000..6e359e4 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt @@ -0,0 +1,38 @@ +/* + * 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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEffectsBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.util.EasyDialogFragment + +class EditEffectsFragment(private val instrument: Instrument) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEffectsBinding.inflate(inflater) + for (effect in instrument.effects) { + val effectEditor = EffectFragment(effect) + childFragmentManager.beginTransaction().add(binding.effectsDisplay.id, effectEditor) + .commit() + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.kt new file mode 100644 index 0000000..605c9e5 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.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.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEnvelopeBinding +import com.lukas.music.instruments.Envelope +import com.lukas.music.util.EasyDialogFragment +import com.lukas.music.util.smartSetup + +class EditEnvelopeFragment(private val envelope: Envelope) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEnvelopeBinding.inflate(inflater) + binding.attackSeek.smartSetup(5, 200, envelope::attack) { + binding.attackText.text = "Attack: $it ms" + } + binding.delaySeek.smartSetup(5, 200, envelope::delay) { + binding.delayText.text = "Delay: $it ms" + } + binding.sustainSeek.smartSetup(0, 100, envelope::sustain) { + binding.sustainText.text = "Sustain: $it%" + } + binding.releaseSeek.smartSetup(5, 200, envelope::release) { + binding.releaseText.text = "Release: $it ms" + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ 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 index 0020ae8..c416df7 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt @@ -16,22 +16,19 @@ 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.song.voice.VoiceType import com.lukas.music.ui.adapters.InstrumentViewHolder +import com.lukas.music.util.EasyDialogFragment 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 - +) : EasyDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -49,33 +46,23 @@ } }) binding.waveformSelection.smartSetup(Waveform.VALUES, instrument::waveform) - binding.volumeSeek.setup(0, 100, 30) { + binding.volumeSeek.setup(0, 100, (instrument.volume * 100f).toInt()) { 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.voiceSelection.smartSetup(VoiceType.VALUES, instrument.voice::type) binding.editVoiceButton.setOnClickListener { EditVoiceFragment(instrument.voice).showNow(childFragmentManager, "") } + binding.editEnvelopeButton.setOnClickListener { + EditEnvelopeFragment(instrument.envelope).showNow(childFragmentManager, "") + } + binding.editEffectsButton.setOnClickListener { + EditEffectsFragment(instrument).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/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditVoiceFragment.kt new file mode 100644 index 0000000..eb56238 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditVoiceFragment.kt @@ -0,0 +1,76 @@ +/* + * 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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import com.google.android.material.button.MaterialButton +import com.lukas.music.R +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.EasyDialogFragment +import com.lukas.music.util.setup +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + binding.restrikeButton.setupToggle(voice::restrikeNotes, R.color.green) + for (row in voice.type.noteCount - 1 downTo 0) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats * Song.currentSong.subBeats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle( + ArrayProperty(voice.noteActive[column], row), + R.color.blue, + inactiveColor = if (column % Song.currentSong.subBeats == 0) R.color.gray_0x50 else R.color.gray_0x70 + ) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.octaveSeekBar.setup(-4, 4, voice.octaveOffset) { + voice.octaveOffset = it + binding.octaveText.text = "octave = $it" + } + 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.MATCH_PARENT + ) + } + + 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/ui/fragments/EffectFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt new file mode 100644 index 0000000..ebf4cb1 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.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.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.lukas.music.R +import com.lukas.music.databinding.FragmentEffectBinding +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.util.setupToggle +import com.lukas.music.util.smartSetup + +class EffectFragment(private val effect: Effect) : Fragment() { + lateinit var binding: FragmentEffectBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEffectBinding.inflate(inflater) + binding.effectName.text = effect.type.toString() + binding.activeButton.setupToggle(effect::active, R.color.blue) { + binding.activeButton.text = if (it) "ON" else "OFF" + } + binding.activeButton.text = if (effect.active) "ON" else "OFF" + binding.influenceSeekBar.smartSetup(0, 100, effect.influence::percentageValue) { + binding.influenceText.text = effect.influence.description.text(effect.influence) + } + binding.parameter1SeekBar.smartSetup(0, 100, effect.parameters[0]::percentageValue) { + binding.parameter1Text.text = + effect.parameters[0].description.text(effect.parameters[0]) + } + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/util/EasyDialogFragment.kt b/app/src/main/java/com/lukas/music/util/EasyDialogFragment.kt new file mode 100644 index 0000000..c93b6e7 --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/EasyDialogFragment.kt @@ -0,0 +1,27 @@ +/* + * 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.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.viewbinding.ViewBinding + +open class EasyDialogFragment : DialogFragment() { + lateinit var binding: T + + 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/util/MathUtil.kt b/app/src/main/java/com/lukas/music/util/MathUtil.kt new file mode 100644 index 0000000..166061d --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/MathUtil.kt @@ -0,0 +1,21 @@ +/* + * 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 + +fun Double.format(digits: Int) = "%.${digits}f".format(this) + +fun Float.format(digits: Int) = "%.${digits}f".format(this) + +inline fun Array.transform(callback: (T) -> U): Array { + return Array(this.size) { + callback(this[it]) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/util/UIUtil.kt b/app/src/main/java/com/lukas/music/util/UIUtil.kt index da897c5..b67d0ea 100644 --- a/app/src/main/java/com/lukas/music/util/UIUtil.kt +++ b/app/src/main/java/com/lukas/music/util/UIUtil.kt @@ -36,25 +36,45 @@ override fun onStopTrackingTouch(seekBar: SeekBar) { } }) + if (progress == initialProgress) { + callback(initialProgress) + } this.progress = initialProgress } +fun SeekBar.smartSetup( + min: Int, + max: Int, + target: KMutableProperty0, + callback: (Int) -> Unit +) { + setup(min, max, target.get()) { + target.set(it) + callback(it) + } +} + fun Button.setupToggle( target: KMutableProperty0, activeColor: Int, - callback: (Boolean) -> Unit = {} + inactiveColor: Int = R.color.gray_0x60, + callback: (Boolean) -> Unit = {}, ) { setOnClickListener { target.set(!target.get()) - updateToggle(target, activeColor) + updateToggle(target, activeColor, inactiveColor) callback(target.get()) } - updateToggle(target, activeColor) + updateToggle(target, activeColor, inactiveColor) } -fun Button.updateToggle(target: KMutableProperty0, activeColor: Int) { +fun Button.updateToggle( + target: KMutableProperty0, + activeColor: Int, + inactiveColor: Int = R.color.gray_0x60, +) { setBackgroundColor( - ContextCompat.getColor(context, if (target.get()) activeColor else R.color.gray_0x60) + ContextCompat.getColor(context, if (target.get()) activeColor else inactiveColor) ) } diff --git a/app/src/main/res/layout/fragment_edit_effects.xml b/app/src/main/res/layout/fragment_edit_effects.xml new file mode 100644 index 0000000..46d4648 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_effects.xml @@ -0,0 +1,55 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_envelope.xml b/app/src/main/res/layout/fragment_edit_envelope.xml new file mode 100644 index 0000000..9e45d05 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_envelope.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt new file mode 100644 index 0000000..25c6260 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt @@ -0,0 +1,18 @@ +/* + * 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.instruments.effect + +class EffectParameterDescription( + val min: Float, + val max: Float, + val initialValue: Float, + val text: (EffectParameter) -> String, +) \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt new file mode 100644 index 0000000..8af39ea --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -0,0 +1,41 @@ +/* + * 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.instruments.effect + +import com.lukas.music.util.format + +enum class EffectType( + val title: String, + val parameterDescriptions: Array +) { + LowPass("low pass filter", + arrayOf( + EffectParameterDescription(-1f, 3f, 1f) { + "cutoff: ${it.value.format(1)} octaves" + } + )), + Noise("noise", + arrayOf( + EffectParameterDescription(0f, 1f, 0f) { + "unused" + } + ) + ) + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Scale.kt b/app/src/main/java/com/lukas/music/song/Scale.kt deleted file mode 100644 index 8e08034..0000000 --- a/app/src/main/java/com/lukas/music/song/Scale.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song - -import com.lukas.music.song.chords.ChordType - -enum class Scale(val identifier: String, val steps: Array, val chordTypes: Array) { - MAJOR( - "major", - arrayOf(0, 2, 4, 5, 7, 9, 11, 12), - arrayOf( - ChordType.MAJOR, - ChordType.MINOR, - ChordType.MINOR, - ChordType.MAJOR, - ChordType.MAJOR, - ChordType.MINOR, - ChordType.DIMINISHED - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt new file mode 100644 index 0000000..4c6a0d9 --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -0,0 +1,33 @@ +/* + * 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.song + +import com.lukas.music.song.chords.ChordType + +enum class ScaleType( + val identifier: String, + val steps: Array, + val chordTypes: Array +) { + MAJOR( + "major", + arrayOf(0, 2, 4, 5, 7, 9, 11, 12), + arrayOf( + ChordType.MAJOR, + ChordType.MINOR, + ChordType.MINOR, + ChordType.MAJOR, + ChordType.MAJOR, + ChordType.MINOR, + ChordType.DIMINISHED + ) + ) +} \ 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 fc5421b..5046664 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -14,11 +14,13 @@ import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note import com.lukas.music.util.Cycle +import com.lukas.music.util.MetaCycle class Song( root: Note, - val beats: Int -) : Cycle(beats) { + val beats: Int, + val subBeats: Int, +) : MetaCycle>() { val chordProgression = ChordProgression() var soloInstrument: Instrument? = null set(value) { @@ -46,7 +48,11 @@ init { for (i in 0 until beats) { - this += i + val cycle = Cycle() + for (j in 0 until subBeats) { + cycle += j + } + this += cycle } wraparoundListeners += { chordProgression.step() @@ -54,24 +60,25 @@ } } - override fun step(): Int { + override fun step(): Cycle? { super.step() - val chord = chordProgression.currentItem?.currentItem ?: return index + val chord = chordProgression.currentItem?.currentItem ?: return currentItem val chordNotes = chord.getNotes(root) soloInstrument?.let { - it.voice.step(root, chordNotes, index) + it.voice.step(root, chordNotes, index, currentItem!!.index) } ?: run { for (instrument in Instrument.instruments) { - instrument.voice.step(root, chordNotes, index) + instrument.voice.step(root, chordNotes, index, currentItem!!.index) } } - return index + return currentItem } companion object { var currentSong = Song( Note.NOTES[69], - 4 + 4, + 2, ) } } \ No newline at end of file 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 deleted file mode 100644 index 4706068..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class BassVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - arrayOf(true), - arrayOf(false), - arrayOf(true), - arrayOf(false) - ) - - 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 deleted file mode 100644 index ab7117f..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class ChordVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - Array(3) { false }, - Array(3) { true }, - Array(3) { false }, - Array(3) { true }, - ) - override val noteCount: Int = 3 - - 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 4f56c2a..139be78 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 @@ -11,37 +11,41 @@ package com.lukas.music.song.voice import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -import kotlin.reflect.KClass -abstract class Voice(val instrument: Instrument) { - abstract var noteActive: Array> - abstract val noteCount: Int +class Voice(val instrument: Instrument) { + var type: VoiceType = VoiceType.Bass + set(value) { + field = value + noteActive = + Array(Song.currentSong.beats * Song.currentSong.subBeats) { Array(value.noteCount) { false } } + } + var restrikeNotes = false + lateinit var noteActive: Array> - abstract fun getNotes(root: Note, chordNotes: Array): Array + var octaveOffset = 0 - fun step(root: Note, chordNotes: Array, beat: Int) { + init { + type = type + } + + fun step(root: Note, chordNotes: Array, beat: Int, subBeat: Int) { if (instrument.muted) { return } - val activeNotes = noteActive[beat] - val notes = getNotes(root, chordNotes) + val beatIndex = beat * Song.currentSong.subBeats + subBeat + val activeNotes = noteActive[beatIndex] + val notes = type.getNotes(root, chordNotes) for ((index, active) in activeNotes.withIndex()) { - val note = notes[index] + val note = notes[index] + 12 * octaveOffset if (!active) { instrument.stopNote(note) continue } - instrument.startNote(note) + if (restrikeNotes || !instrument.isPlaying(note)) { + 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/song/voice/VoiceType.kt b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt new file mode 100644 index 0000000..e06761a --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -0,0 +1,36 @@ +/* + * 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.song.voice + +import com.lukas.music.song.ScaleType +import com.lukas.music.song.note.Note +import com.lukas.music.util.transform + +enum class VoiceType( + val title: String, + val noteCount: Int, + val getNotes: (Note, Array) -> Array +) { + Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), + Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + Scale("Scale notes", 8, { root, _ -> ScaleType.MAJOR.steps.transform { root + it } }), + Root("Root note", 1, { root, _ -> arrayOf(root) }), + RootRelative("Song root relative", 12, { root, _ -> Array(12) { root + it } }), + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ 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 bd7a3d9..6c3bae3 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 @@ -16,14 +16,14 @@ import android.view.ViewGroup import androidx.fragment.app.DialogFragment import com.lukas.music.databinding.FragmentEditChordBinding -import com.lukas.music.song.Scale +import com.lukas.music.song.ScaleType import com.lukas.music.song.Song 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, private val songFragment: SongFragment) : +class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : DialogFragment() { lateinit var binding: FragmentEditChordBinding @@ -42,12 +42,12 @@ private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { - Array(Scale.MAJOR.steps.size) { (Song.currentSong.root + Scale.MAJOR.steps[it]).noteName.toString() } + Array(ScaleType.MAJOR.steps.size) { (Song.currentSong.root + ScaleType.MAJOR.steps[it]).noteName.toString() } } else Interval.IntervalName.NAMES binding.pitchSpinner.setup(pitches, chord.interval.name.ordinal) { - chord.note = Scale.MAJOR.steps[it] + chord.note = ScaleType.MAJOR.steps[it] if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } songFragment.updateChords() } @@ -60,11 +60,11 @@ } binding.typeSpinner.setup( values, - if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 + if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 else chord.chordType.ordinal + 1 ) { if (it == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } else { chord.chordType = ChordType.VALUES[it - 1] } diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt new file mode 100644 index 0000000..6e359e4 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt @@ -0,0 +1,38 @@ +/* + * 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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEffectsBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.util.EasyDialogFragment + +class EditEffectsFragment(private val instrument: Instrument) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEffectsBinding.inflate(inflater) + for (effect in instrument.effects) { + val effectEditor = EffectFragment(effect) + childFragmentManager.beginTransaction().add(binding.effectsDisplay.id, effectEditor) + .commit() + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.kt new file mode 100644 index 0000000..605c9e5 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.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.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEnvelopeBinding +import com.lukas.music.instruments.Envelope +import com.lukas.music.util.EasyDialogFragment +import com.lukas.music.util.smartSetup + +class EditEnvelopeFragment(private val envelope: Envelope) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEnvelopeBinding.inflate(inflater) + binding.attackSeek.smartSetup(5, 200, envelope::attack) { + binding.attackText.text = "Attack: $it ms" + } + binding.delaySeek.smartSetup(5, 200, envelope::delay) { + binding.delayText.text = "Delay: $it ms" + } + binding.sustainSeek.smartSetup(0, 100, envelope::sustain) { + binding.sustainText.text = "Sustain: $it%" + } + binding.releaseSeek.smartSetup(5, 200, envelope::release) { + binding.releaseText.text = "Release: $it ms" + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ 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 index 0020ae8..c416df7 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt @@ -16,22 +16,19 @@ 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.song.voice.VoiceType import com.lukas.music.ui.adapters.InstrumentViewHolder +import com.lukas.music.util.EasyDialogFragment 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 - +) : EasyDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -49,33 +46,23 @@ } }) binding.waveformSelection.smartSetup(Waveform.VALUES, instrument::waveform) - binding.volumeSeek.setup(0, 100, 30) { + binding.volumeSeek.setup(0, 100, (instrument.volume * 100f).toInt()) { 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.voiceSelection.smartSetup(VoiceType.VALUES, instrument.voice::type) binding.editVoiceButton.setOnClickListener { EditVoiceFragment(instrument.voice).showNow(childFragmentManager, "") } + binding.editEnvelopeButton.setOnClickListener { + EditEnvelopeFragment(instrument.envelope).showNow(childFragmentManager, "") + } + binding.editEffectsButton.setOnClickListener { + EditEffectsFragment(instrument).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/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditVoiceFragment.kt new file mode 100644 index 0000000..eb56238 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditVoiceFragment.kt @@ -0,0 +1,76 @@ +/* + * 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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import com.google.android.material.button.MaterialButton +import com.lukas.music.R +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.EasyDialogFragment +import com.lukas.music.util.setup +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + binding.restrikeButton.setupToggle(voice::restrikeNotes, R.color.green) + for (row in voice.type.noteCount - 1 downTo 0) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats * Song.currentSong.subBeats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle( + ArrayProperty(voice.noteActive[column], row), + R.color.blue, + inactiveColor = if (column % Song.currentSong.subBeats == 0) R.color.gray_0x50 else R.color.gray_0x70 + ) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.octaveSeekBar.setup(-4, 4, voice.octaveOffset) { + voice.octaveOffset = it + binding.octaveText.text = "octave = $it" + } + 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.MATCH_PARENT + ) + } + + 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/ui/fragments/EffectFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt new file mode 100644 index 0000000..ebf4cb1 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.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.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.lukas.music.R +import com.lukas.music.databinding.FragmentEffectBinding +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.util.setupToggle +import com.lukas.music.util.smartSetup + +class EffectFragment(private val effect: Effect) : Fragment() { + lateinit var binding: FragmentEffectBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEffectBinding.inflate(inflater) + binding.effectName.text = effect.type.toString() + binding.activeButton.setupToggle(effect::active, R.color.blue) { + binding.activeButton.text = if (it) "ON" else "OFF" + } + binding.activeButton.text = if (effect.active) "ON" else "OFF" + binding.influenceSeekBar.smartSetup(0, 100, effect.influence::percentageValue) { + binding.influenceText.text = effect.influence.description.text(effect.influence) + } + binding.parameter1SeekBar.smartSetup(0, 100, effect.parameters[0]::percentageValue) { + binding.parameter1Text.text = + effect.parameters[0].description.text(effect.parameters[0]) + } + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/util/EasyDialogFragment.kt b/app/src/main/java/com/lukas/music/util/EasyDialogFragment.kt new file mode 100644 index 0000000..c93b6e7 --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/EasyDialogFragment.kt @@ -0,0 +1,27 @@ +/* + * 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.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.viewbinding.ViewBinding + +open class EasyDialogFragment : DialogFragment() { + lateinit var binding: T + + 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/util/MathUtil.kt b/app/src/main/java/com/lukas/music/util/MathUtil.kt new file mode 100644 index 0000000..166061d --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/MathUtil.kt @@ -0,0 +1,21 @@ +/* + * 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 + +fun Double.format(digits: Int) = "%.${digits}f".format(this) + +fun Float.format(digits: Int) = "%.${digits}f".format(this) + +inline fun Array.transform(callback: (T) -> U): Array { + return Array(this.size) { + callback(this[it]) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/util/UIUtil.kt b/app/src/main/java/com/lukas/music/util/UIUtil.kt index da897c5..b67d0ea 100644 --- a/app/src/main/java/com/lukas/music/util/UIUtil.kt +++ b/app/src/main/java/com/lukas/music/util/UIUtil.kt @@ -36,25 +36,45 @@ override fun onStopTrackingTouch(seekBar: SeekBar) { } }) + if (progress == initialProgress) { + callback(initialProgress) + } this.progress = initialProgress } +fun SeekBar.smartSetup( + min: Int, + max: Int, + target: KMutableProperty0, + callback: (Int) -> Unit +) { + setup(min, max, target.get()) { + target.set(it) + callback(it) + } +} + fun Button.setupToggle( target: KMutableProperty0, activeColor: Int, - callback: (Boolean) -> Unit = {} + inactiveColor: Int = R.color.gray_0x60, + callback: (Boolean) -> Unit = {}, ) { setOnClickListener { target.set(!target.get()) - updateToggle(target, activeColor) + updateToggle(target, activeColor, inactiveColor) callback(target.get()) } - updateToggle(target, activeColor) + updateToggle(target, activeColor, inactiveColor) } -fun Button.updateToggle(target: KMutableProperty0, activeColor: Int) { +fun Button.updateToggle( + target: KMutableProperty0, + activeColor: Int, + inactiveColor: Int = R.color.gray_0x60, +) { setBackgroundColor( - ContextCompat.getColor(context, if (target.get()) activeColor else R.color.gray_0x60) + ContextCompat.getColor(context, if (target.get()) activeColor else inactiveColor) ) } diff --git a/app/src/main/res/layout/fragment_edit_effects.xml b/app/src/main/res/layout/fragment_edit_effects.xml new file mode 100644 index 0000000..46d4648 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_effects.xml @@ -0,0 +1,55 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_envelope.xml b/app/src/main/res/layout/fragment_edit_envelope.xml new file mode 100644 index 0000000..9e45d05 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_envelope.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 index b5578fc..eb66f19 100644 --- a/app/src/main/res/layout/fragment_edit_instrument.xml +++ b/app/src/main/res/layout/fragment_edit_instrument.xml @@ -15,7 +15,30 @@ android:layout_height="match_parent" tools:context=".EditInstrumentFragment"> - + + + + + app:layout_constraintTop_toBottomOf="@+id/editEffectsButton" /> + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d886c4..b121fe2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,8 +11,11 @@ + + - + + diff --git a/app/build.gradle b/app/build.gradle index 97ec2a0..9de986d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,4 +65,5 @@ testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.github.alanvan0502:segmented-control-group:v1.0' } \ No newline at end of file diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index 8e98b44..6727405 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -1,7 +1,9 @@ #include "AudioHost.h" +#include "Instrument.h" #include #include #include +#include const uint32_t bufferSize = 2; aaudio_data_callback_result_t dataCallback( diff --git a/app/src/main/cpp/AudioHost.h b/app/src/main/cpp/AudioHost.h index f439fdc..97446ea 100644 --- a/app/src/main/cpp/AudioHost.h +++ b/app/src/main/cpp/AudioHost.h @@ -3,7 +3,8 @@ class AudioHost; -#include "Instrument.h" +class Instrument; + #include #include diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index e7286cb..39bdefe 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,11 +10,16 @@ waveforms/Sine.cpp waveforms/Sawtooth.cpp waveforms/Waveform.cpp + waveforms/Square.cpp + waveforms/Triangle.cpp JavaFunctions.cpp AudioHost.cpp Instrument.cpp effects/Envelope.cpp effects/Processable.cpp + effects/Effect.cpp + effects/LowPass.cpp + effects/Noise.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 3d9396d..7310a37 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -1,12 +1,16 @@ #include "Instrument.h" #include "waveforms/Sawtooth.h" #include "waveforms/Sine.h" +#include "waveforms/Square.h" +#include "waveforms/Triangle.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); + lowPass->host = host; + noise->host = host; } void multiply(float *target, float *modulation, uint32_t size) { @@ -15,22 +19,43 @@ } } +void multiply(float *target, float value, uint32_t size) { + for (uint32_t i = 0; i < size; i++) { + target[i] *= value; + } +} + void add(float *target, float *other, uint32_t size) { for (uint32_t i = 0; i < size; i++) { target[i] += other[i]; } } +void processEffect(float *waveform, uint32_t count, Effect *effect) { + if (effect->influence < 0.01f) { + return; + } + effect->input = waveform; + float *effectOutput = effect->render(count); + multiply(effectOutput, effect->influence, count); + multiply(waveform, 1 - effect->influence, count); + add(waveform, effectOutput, count); +} + void Instrument::render(float *buffer, uint32_t count) { - float *modulation = envelope->render(count); float *waveform = wave->render(count); - multiply(waveform, modulation, count); + processEffect(waveform, count, lowPass); + processEffect(waveform, count, noise); + multiply(waveform, envelope->render(count), count); + multiply(waveform, volume, count); add(buffer, waveform, count); } void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); + lowPass->frequency = frequency; + lowPass->update(); } void Instrument::endNote() { @@ -46,6 +71,11 @@ case SAWTOOTH: wave = new Sawtooth(); break; + case SQUARE: + wave = new Square(); + break; + case TRIANGLE: + wave = new Triangle(); } wave->host = host; delete old; diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index f15d631..077bfe0 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -6,6 +6,8 @@ #include "effects/Envelope.h" #include "waveforms/Waveform.h" #include "AudioHost.h" +#include "effects/LowPass.h" +#include "effects/Noise.h" class Instrument { private: @@ -15,10 +17,16 @@ Envelope *const envelope = new Envelope(); Waveform *wave; + LowPass *lowPass = new LowPass(); + Noise *noise = new Noise(); + float volume = 0; void render(float *buffer, uint32_t count); + void startNote(float frequency); + void endNote(); + void setWaveform(WaveformType waveform); }; diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 5505a35..7ccc60b 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -1,6 +1,7 @@ #include #include #include "AudioHost.h" +#include "Instrument.h" #include #include #include @@ -68,7 +69,7 @@ JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, jfloat volume) { - getInstrument(id)->wave->amplitude = volume; + getInstrument(id)->volume = volume; } JNIEXPORT void JNICALL @@ -76,4 +77,40 @@ listSet(audioHost->instruments->begin(), id, nullptr); delete getInstrument(id); } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_updateEnvelopeParameters(JNIEnv *env, + jobject thiz, jint id, + jfloat attack, + jfloat delay, + jfloat sustain, + jfloat release) { + Instrument *instrument = getInstrument((id)); + Envelope *envelope = instrument->envelope; + envelope->attack = attack; + envelope->delay = delay; + envelope->sustain = sustain; + envelope->release = release; + envelope->update(); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_applyEffectAttributes(JNIEnv *env, jobject thiz, + jint id, + jint effect_number, + jfloat influence, + jfloat parameter1) { + Instrument *instrument = getInstrument(id); + Effect *effect; + switch (effect_number) { + case 0: + effect = instrument->lowPass; + break; + case 1: + effect = instrument->noise; + break; + } + effect->influence = influence; + effect->parameter1 = parameter1; +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.cpp b/app/src/main/cpp/effects/Effect.cpp new file mode 100644 index 0000000..881e413 --- /dev/null +++ b/app/src/main/cpp/effects/Effect.cpp @@ -0,0 +1 @@ +#include "Effect.h" \ No newline at end of file diff --git a/app/src/main/cpp/effects/Effect.h b/app/src/main/cpp/effects/Effect.h new file mode 100644 index 0000000..80ba84c --- /dev/null +++ b/app/src/main/cpp/effects/Effect.h @@ -0,0 +1,19 @@ +#ifndef MUSIC_EFFECT_H +#define MUSIC_EFFECT_H + +#include +#include "Processable.h" +#include "../AudioHost.h" + +class Effect : public Processable { +public: + float parameter1, frequency, influence; + float *input; + AudioHost *host; + + virtual void doRender(uint32_t sampleCount) = 0; + + virtual void update() = 0; +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Envelope.cpp b/app/src/main/cpp/effects/Envelope.cpp index f8dfd61..5400548 100644 --- a/app/src/main/cpp/effects/Envelope.cpp +++ b/app/src/main/cpp/effects/Envelope.cpp @@ -3,6 +3,11 @@ #include "Envelope.h" void Envelope::initialize(AudioHost *host) { + this->host = host; + update(); +} + +void Envelope::update() { attackIncrement = 1 / attack / host->sampleRate; delayIncrement = 1 / delay / host->sampleRate; releaseIncrement = 1 / release / host->sampleRate; diff --git a/app/src/main/cpp/effects/Envelope.h b/app/src/main/cpp/effects/Envelope.h index 7076f8f..dbb5575 100644 --- a/app/src/main/cpp/effects/Envelope.h +++ b/app/src/main/cpp/effects/Envelope.h @@ -16,15 +16,16 @@ EnvelopePhase phase; float attackIncrement, delayIncrement, releaseIncrement; float value = 0; + AudioHost *host; public: - float attack = 0.05, delay = 0.2, sustain = 0.75, release = 1; + float attack = 0.05, delay = 0.05, sustain = 0.7, release = 0.1; void initialize(AudioHost *host); + void update(); + void startNote(); - void endNote(); - void doRender(uint32_t sampleCount); }; diff --git a/app/src/main/cpp/effects/LowPass.cpp b/app/src/main/cpp/effects/LowPass.cpp new file mode 100644 index 0000000..8336798 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.cpp @@ -0,0 +1,18 @@ +#include "../Instrument.h" +#include + +// direct model of a RC-filter, R = 1 Ohm for convenience + +void LowPass::update() { + charge = 0; + inverseCapacitance = 2 * M_PI * frequency * pow(2, parameter1); + capacitance = 1 / inverseCapacitance; + timeStep = 1 / (float) host->sampleRate; +} + +void LowPass::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + charge += (input[i] - charge * inverseCapacitance) * timeStep; + buffer[i] = charge * inverseCapacitance; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/LowPass.h b/app/src/main/cpp/effects/LowPass.h new file mode 100644 index 0000000..a285154 --- /dev/null +++ b/app/src/main/cpp/effects/LowPass.h @@ -0,0 +1,18 @@ +#ifndef MUSIC_LOWPASS_H +#define MUSIC_LOWPASS_H + +#include "Effect.h" + +class LowPass : public Effect { +private: + float charge = 0; + float capacitance = 0; + float inverseCapacitance = 0; + float timeStep = 0; +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + +#endif \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.cpp b/app/src/main/cpp/effects/Noise.cpp new file mode 100644 index 0000000..3ecfd2e --- /dev/null +++ b/app/src/main/cpp/effects/Noise.cpp @@ -0,0 +1,21 @@ +#include "Noise.h" +#include +#include + +void Noise::update() { + srand(time(0)); +} + +const static int q = 15; +const static float c1 = (1 << q) - 1; +const static float c2 = ((int) (c1 / 3)) + 1; +const static float c3 = 1.f / c1; + +void Noise::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float random = ((float) rand() / (float) (RAND_MAX + 1)); + buffer[i] = + (2.f * ((random * c2) + (random * c2) + (random * c2)) - 3.f * (c2 - 1.f)) * c3 / + 10; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Noise.h b/app/src/main/cpp/effects/Noise.h new file mode 100644 index 0000000..287e4ac --- /dev/null +++ b/app/src/main/cpp/effects/Noise.h @@ -0,0 +1,13 @@ +#ifndef MUSIC_NOISE_H +#define MUSIC_NOISE_H + +#include "Effect.h" + +class Noise : public Effect { + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Square.cpp b/app/src/main/cpp/waveforms/Square.cpp new file mode 100644 index 0000000..d136bca --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.cpp @@ -0,0 +1,17 @@ +#include +#include "Square.h" + +void Square::setFrequency(float frequency) { + period = 1 / frequency; + step = 1 / (double) host->sampleRate; +} + +void Square::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = position > period / 2 ? 1 : -1; + position += step; + if (position > period) { + position = 0; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Square.h b/app/src/main/cpp/waveforms/Square.h new file mode 100644 index 0000000..abcc39a --- /dev/null +++ b/app/src/main/cpp/waveforms/Square.h @@ -0,0 +1,15 @@ +#ifndef MUSIC_SQUARE_H +#define MUSIC_SQUARE_H + +#include "Waveform.h" + +class Square : public Waveform { +private: + float position, period, step; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + +#endif diff --git a/app/src/main/cpp/waveforms/Triangle.cpp b/app/src/main/cpp/waveforms/Triangle.cpp new file mode 100644 index 0000000..2ef7d1f --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.cpp @@ -0,0 +1,19 @@ +#include "Triangle.h" + +void Triangle::setFrequency(float frequency) { + step = 4 * frequency / (double) host->sampleRate; +} + +void Triangle::renderWaveform(uint32_t frameCount) { + for (uint32_t i = 0; i < frameCount; i++) { + buffer[i] = value; + value += step; + if (value > 1) { + step *= -1; + value = 1; + } else if (value < -1) { + step *= -1; + value = -1; + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Triangle.h b/app/src/main/cpp/waveforms/Triangle.h new file mode 100644 index 0000000..788dc2d --- /dev/null +++ b/app/src/main/cpp/waveforms/Triangle.h @@ -0,0 +1,17 @@ +#ifndef MUSIC_TRIANGLE_H +#define MUSIC_TRIANGLE_H + + +#include "Waveform.h" + +class Triangle : public Waveform { +private: + float value = 0, step = 0; +public: + void renderWaveform(uint32_t frameCount); + + void setFrequency(float freq); +}; + + +#endif diff --git a/app/src/main/cpp/waveforms/Waveform.cpp b/app/src/main/cpp/waveforms/Waveform.cpp index 64e0991..839ca10 100644 --- a/app/src/main/cpp/waveforms/Waveform.cpp +++ b/app/src/main/cpp/waveforms/Waveform.cpp @@ -2,7 +2,4 @@ void Waveform::doRender(uint32_t sampleCount) { renderWaveform(sampleCount); - for (uint32_t i = 0; i < sampleCount; i++) { - buffer[i] *= amplitude; - } } \ No newline at end of file diff --git a/app/src/main/cpp/waveforms/Waveform.h b/app/src/main/cpp/waveforms/Waveform.h index a70ed39..34e1284 100644 --- a/app/src/main/cpp/waveforms/Waveform.h +++ b/app/src/main/cpp/waveforms/Waveform.h @@ -6,6 +6,8 @@ enum WaveformType { SINE = 0, SAWTOOTH = 1, + SQUARE = 2, + TRIANGLE = 3, }; #include "../effects/Processable.h" @@ -13,7 +15,6 @@ class Waveform : public Processable { public: - float amplitude = 0.0f; AudioHost *host; void doRender(uint32_t sampleCount); diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt deleted file mode 100644 index 56f21e6..0000000 --- a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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/Envelope.kt b/app/src/main/java/com/lukas/music/instruments/Envelope.kt new file mode 100644 index 0000000..6603611 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/Envelope.kt @@ -0,0 +1,37 @@ +/* + * 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.instruments + +class Envelope(val instrument: Instrument) { + var attack: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var delay: Int = 50 + set(value) { + field = value + instrument.updateEnvelope() + } + + var sustain: Int = 70 + set(value) { + field = value + instrument.updateEnvelope() + } + + var release: Int = 100 + set(value) { + field = value + instrument.updateEnvelope() + } +} \ 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 92a896c..b9fa2c1 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,12 +10,18 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.instruments.effect.EffectType import com.lukas.music.song.note.Note -import com.lukas.music.song.voice.BassVoice import com.lukas.music.song.voice.Voice abstract class Instrument(var name: String) { - var voice: Voice = BassVoice(this) + var voice: Voice = Voice(this) + var envelope = Envelope(this) + val effects = Array(EffectType.VALUES.size) { + Effect(EffectType.VALUES[it], this) + } + abstract var waveform: Waveform abstract var volume: Float abstract var muted: Boolean @@ -24,6 +30,9 @@ abstract fun stop() abstract fun stopNote(note: Note) abstract fun destroy() + abstract fun updateEnvelope() + abstract fun updateEffects() + abstract fun isPlaying(note: Note): Boolean companion object { val instruments = mutableListOf() 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 b4c68a4..d24f474 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,10 +10,11 @@ package com.lukas.music.instruments +import com.lukas.music.instruments.effect.Effect import com.lukas.music.song.note.Note class InternalInstrument { - private val id = createInstrument() + val id = createInstrument() var note: Note? = null var waveform: Waveform = Waveform.SINE @@ -67,10 +68,43 @@ destroy(id) } + fun applyEnvelope(envelope: Envelope) { + updateEnvelopeParameters( + id, + envelope.attack.toFloat() / 1000f, + envelope.delay.toFloat() / 1000f, + envelope.sustain.toFloat() / 100f, + envelope.release.toFloat() / 1000f, + ) + } + + fun applyEffectAttributes(effect: Effect) { + applyEffectAttributes( + id, + effect.type.ordinal, + if (effect.active) effect.influence.value else 0f, + effect.parameters[0].value + ) + } + private external fun createInstrument(): Int 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) + private external fun updateEnvelopeParameters( + id: Int, + attack: Float, + delay: Float, + sustain: Float, + release: Float + ) + + private external fun applyEffectAttributes( + id: Int, + effectNumber: Int, + influence: Float, + parameter1: Float + ) } \ 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 2b43524..e631548 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -34,9 +34,6 @@ } override fun startNote(note: Note) { - if (note == internalInstrument.note) { - return - } internalInstrument.startNote(note) } @@ -53,4 +50,16 @@ override fun destroy() { internalInstrument.destroy() } + + override fun updateEnvelope() { + internalInstrument.applyEnvelope(envelope) + } + + override fun updateEffects() { + for (effect in effects) { + internalInstrument.applyEffectAttributes(effect) + } + } + + override fun isPlaying(note: Note): Boolean = internalInstrument.note == note } \ 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 26e4fbb..7beb64c 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -48,10 +48,11 @@ return } if (internalInstruments[index].note == note) { + internalInstruments[index].startNote(note) return } } - throw IllegalStateException("cannot start another note with the current amount of oscillators") + println("cannot start another note with the current amount of oscillators") } override fun stop() { @@ -75,4 +76,27 @@ instrument.destroy() } } + + override fun updateEnvelope() { + for (instrument in internalInstruments) { + instrument.applyEnvelope(envelope) + } + } + + override fun updateEffects() { + for (instrument in internalInstruments) { + for (effect in effects) { + instrument.applyEffectAttributes(effect) + } + } + } + + override fun isPlaying(note: Note): Boolean { + for (instrument in internalInstruments) { + if (instrument.note == note) { + return true + } + } + return false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index 75c1545..b310c72 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -36,6 +36,10 @@ if (this::task.isInitialized) { task.cancel() } - task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + task = timer.schedule( + (60000 / tempo / Song.currentSong.subBeats).toLong(), + (60000 / tempo / Song.currentSong.subBeats).toLong(), + callback + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Waveform.kt b/app/src/main/java/com/lukas/music/instruments/Waveform.kt index 3a3de19..e35f7b8 100644 --- a/app/src/main/java/com/lukas/music/instruments/Waveform.kt +++ b/app/src/main/java/com/lukas/music/instruments/Waveform.kt @@ -13,6 +13,8 @@ enum class Waveform(val id: Int, private val identifier: String) { SINE(0, "sine"), SAWTOOTH(1, "sawtooth"), + SQUARE(2, "square"), + TRIANGLE(3, "triangle"), ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt new file mode 100644 index 0000000..f921b1b --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -0,0 +1,32 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument + +class Effect(val type: EffectType, private val instrument: Instrument) { + val parameters = Array(type.parameterDescriptions.size) { + EffectParameter(type.parameterDescriptions[it], instrument) + } + val influence = EffectParameter(influenceDescription, instrument) + + var active: Boolean = false + set(value) { + field = value + instrument.updateEffects() + } + + companion object { + val influenceDescription = EffectParameterDescription(0.0f, 1.0f, 0.0f) { + "influence: ${it.percentageValue}%" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt new file mode 100644 index 0000000..f54857c --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameter.kt @@ -0,0 +1,30 @@ +/* + * 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.instruments.effect + +import com.lukas.music.instruments.Instrument +import kotlin.math.roundToInt + +class EffectParameter(val description: EffectParameterDescription, val instrument: Instrument) { + var value: Float = description.initialValue + set(value) { + field = value + instrument.updateEffects() + } + + // linear interpolation between description extrema + var percentageValue: Int + get() = ((value - description.min) / (description.max - description.min) * 100).roundToInt() + set(value) { + this.value = + description.min + (description.max - description.min) * (value.toFloat() / 100f) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt new file mode 100644 index 0000000..25c6260 --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectParameterDescription.kt @@ -0,0 +1,18 @@ +/* + * 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.instruments.effect + +class EffectParameterDescription( + val min: Float, + val max: Float, + val initialValue: Float, + val text: (EffectParameter) -> String, +) \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt new file mode 100644 index 0000000..8af39ea --- /dev/null +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -0,0 +1,41 @@ +/* + * 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.instruments.effect + +import com.lukas.music.util.format + +enum class EffectType( + val title: String, + val parameterDescriptions: Array +) { + LowPass("low pass filter", + arrayOf( + EffectParameterDescription(-1f, 3f, 1f) { + "cutoff: ${it.value.format(1)} octaves" + } + )), + Noise("noise", + arrayOf( + EffectParameterDescription(0f, 1f, 0f) { + "unused" + } + ) + ) + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Scale.kt b/app/src/main/java/com/lukas/music/song/Scale.kt deleted file mode 100644 index 8e08034..0000000 --- a/app/src/main/java/com/lukas/music/song/Scale.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song - -import com.lukas.music.song.chords.ChordType - -enum class Scale(val identifier: String, val steps: Array, val chordTypes: Array) { - MAJOR( - "major", - arrayOf(0, 2, 4, 5, 7, 9, 11, 12), - arrayOf( - ChordType.MAJOR, - ChordType.MINOR, - ChordType.MINOR, - ChordType.MAJOR, - ChordType.MAJOR, - ChordType.MINOR, - ChordType.DIMINISHED - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt new file mode 100644 index 0000000..4c6a0d9 --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -0,0 +1,33 @@ +/* + * 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.song + +import com.lukas.music.song.chords.ChordType + +enum class ScaleType( + val identifier: String, + val steps: Array, + val chordTypes: Array +) { + MAJOR( + "major", + arrayOf(0, 2, 4, 5, 7, 9, 11, 12), + arrayOf( + ChordType.MAJOR, + ChordType.MINOR, + ChordType.MINOR, + ChordType.MAJOR, + ChordType.MAJOR, + ChordType.MINOR, + ChordType.DIMINISHED + ) + ) +} \ 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 fc5421b..5046664 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -14,11 +14,13 @@ import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note import com.lukas.music.util.Cycle +import com.lukas.music.util.MetaCycle class Song( root: Note, - val beats: Int -) : Cycle(beats) { + val beats: Int, + val subBeats: Int, +) : MetaCycle>() { val chordProgression = ChordProgression() var soloInstrument: Instrument? = null set(value) { @@ -46,7 +48,11 @@ init { for (i in 0 until beats) { - this += i + val cycle = Cycle() + for (j in 0 until subBeats) { + cycle += j + } + this += cycle } wraparoundListeners += { chordProgression.step() @@ -54,24 +60,25 @@ } } - override fun step(): Int { + override fun step(): Cycle? { super.step() - val chord = chordProgression.currentItem?.currentItem ?: return index + val chord = chordProgression.currentItem?.currentItem ?: return currentItem val chordNotes = chord.getNotes(root) soloInstrument?.let { - it.voice.step(root, chordNotes, index) + it.voice.step(root, chordNotes, index, currentItem!!.index) } ?: run { for (instrument in Instrument.instruments) { - instrument.voice.step(root, chordNotes, index) + instrument.voice.step(root, chordNotes, index, currentItem!!.index) } } - return index + return currentItem } companion object { var currentSong = Song( Note.NOTES[69], - 4 + 4, + 2, ) } } \ No newline at end of file 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 deleted file mode 100644 index 4706068..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class BassVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - arrayOf(true), - arrayOf(false), - arrayOf(true), - arrayOf(false) - ) - - 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 deleted file mode 100644 index ab7117f..0000000 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.song.voice - -import com.lukas.music.instruments.Instrument -import com.lukas.music.song.note.Note - -class ChordVoice(instrument: Instrument) : Voice(instrument) { - override var noteActive: Array> = arrayOf( - Array(3) { false }, - Array(3) { true }, - Array(3) { false }, - Array(3) { true }, - ) - override val noteCount: Int = 3 - - 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 4f56c2a..139be78 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 @@ -11,37 +11,41 @@ package com.lukas.music.song.voice import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -import kotlin.reflect.KClass -abstract class Voice(val instrument: Instrument) { - abstract var noteActive: Array> - abstract val noteCount: Int +class Voice(val instrument: Instrument) { + var type: VoiceType = VoiceType.Bass + set(value) { + field = value + noteActive = + Array(Song.currentSong.beats * Song.currentSong.subBeats) { Array(value.noteCount) { false } } + } + var restrikeNotes = false + lateinit var noteActive: Array> - abstract fun getNotes(root: Note, chordNotes: Array): Array + var octaveOffset = 0 - fun step(root: Note, chordNotes: Array, beat: Int) { + init { + type = type + } + + fun step(root: Note, chordNotes: Array, beat: Int, subBeat: Int) { if (instrument.muted) { return } - val activeNotes = noteActive[beat] - val notes = getNotes(root, chordNotes) + val beatIndex = beat * Song.currentSong.subBeats + subBeat + val activeNotes = noteActive[beatIndex] + val notes = type.getNotes(root, chordNotes) for ((index, active) in activeNotes.withIndex()) { - val note = notes[index] + val note = notes[index] + 12 * octaveOffset if (!active) { instrument.stopNote(note) continue } - instrument.startNote(note) + if (restrikeNotes || !instrument.isPlaying(note)) { + 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/song/voice/VoiceType.kt b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt new file mode 100644 index 0000000..e06761a --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -0,0 +1,36 @@ +/* + * 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.song.voice + +import com.lukas.music.song.ScaleType +import com.lukas.music.song.note.Note +import com.lukas.music.util.transform + +enum class VoiceType( + val title: String, + val noteCount: Int, + val getNotes: (Note, Array) -> Array +) { + Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), + Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + Scale("Scale notes", 8, { root, _ -> ScaleType.MAJOR.steps.transform { root + it } }), + Root("Root note", 1, { root, _ -> arrayOf(root) }), + RootRelative("Song root relative", 12, { root, _ -> Array(12) { root + it } }), + ; + + override fun toString(): String { + return title + } + + companion object { + val VALUES = values() + } +} \ 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 bd7a3d9..6c3bae3 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 @@ -16,14 +16,14 @@ import android.view.ViewGroup import androidx.fragment.app.DialogFragment import com.lukas.music.databinding.FragmentEditChordBinding -import com.lukas.music.song.Scale +import com.lukas.music.song.ScaleType import com.lukas.music.song.Song 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, private val songFragment: SongFragment) : +class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : DialogFragment() { lateinit var binding: FragmentEditChordBinding @@ -42,12 +42,12 @@ private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { - Array(Scale.MAJOR.steps.size) { (Song.currentSong.root + Scale.MAJOR.steps[it]).noteName.toString() } + Array(ScaleType.MAJOR.steps.size) { (Song.currentSong.root + ScaleType.MAJOR.steps[it]).noteName.toString() } } else Interval.IntervalName.NAMES binding.pitchSpinner.setup(pitches, chord.interval.name.ordinal) { - chord.note = Scale.MAJOR.steps[it] + chord.note = ScaleType.MAJOR.steps[it] if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } songFragment.updateChords() } @@ -60,11 +60,11 @@ } binding.typeSpinner.setup( values, - if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 + if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 else chord.chordType.ordinal + 1 ) { if (it == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] } else { chord.chordType = ChordType.VALUES[it - 1] } diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt new file mode 100644 index 0000000..6e359e4 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt @@ -0,0 +1,38 @@ +/* + * 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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEffectsBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.util.EasyDialogFragment + +class EditEffectsFragment(private val instrument: Instrument) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEffectsBinding.inflate(inflater) + for (effect in instrument.effects) { + val effectEditor = EffectFragment(effect) + childFragmentManager.beginTransaction().add(binding.effectsDisplay.id, effectEditor) + .commit() + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.kt new file mode 100644 index 0000000..605c9e5 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEnvelopeFragment.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.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.lukas.music.databinding.FragmentEditEnvelopeBinding +import com.lukas.music.instruments.Envelope +import com.lukas.music.util.EasyDialogFragment +import com.lukas.music.util.smartSetup + +class EditEnvelopeFragment(private val envelope: Envelope) : + EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditEnvelopeBinding.inflate(inflater) + binding.attackSeek.smartSetup(5, 200, envelope::attack) { + binding.attackText.text = "Attack: $it ms" + } + binding.delaySeek.smartSetup(5, 200, envelope::delay) { + binding.delayText.text = "Delay: $it ms" + } + binding.sustainSeek.smartSetup(0, 100, envelope::sustain) { + binding.sustainText.text = "Sustain: $it%" + } + binding.releaseSeek.smartSetup(5, 200, envelope::release) { + binding.releaseText.text = "Release: $it ms" + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } +} \ 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 index 0020ae8..c416df7 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt @@ -16,22 +16,19 @@ 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.song.voice.VoiceType import com.lukas.music.ui.adapters.InstrumentViewHolder +import com.lukas.music.util.EasyDialogFragment 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 - +) : EasyDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -49,33 +46,23 @@ } }) binding.waveformSelection.smartSetup(Waveform.VALUES, instrument::waveform) - binding.volumeSeek.setup(0, 100, 30) { + binding.volumeSeek.setup(0, 100, (instrument.volume * 100f).toInt()) { 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.voiceSelection.smartSetup(VoiceType.VALUES, instrument.voice::type) binding.editVoiceButton.setOnClickListener { EditVoiceFragment(instrument.voice).showNow(childFragmentManager, "") } + binding.editEnvelopeButton.setOnClickListener { + EditEnvelopeFragment(instrument.envelope).showNow(childFragmentManager, "") + } + binding.editEffectsButton.setOnClickListener { + EditEffectsFragment(instrument).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/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditVoiceFragment.kt new file mode 100644 index 0000000..eb56238 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditVoiceFragment.kt @@ -0,0 +1,76 @@ +/* + * 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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import com.google.android.material.button.MaterialButton +import com.lukas.music.R +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.EasyDialogFragment +import com.lukas.music.util.setup +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : EasyDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + binding.restrikeButton.setupToggle(voice::restrikeNotes, R.color.green) + for (row in voice.type.noteCount - 1 downTo 0) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats * Song.currentSong.subBeats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle( + ArrayProperty(voice.noteActive[column], row), + R.color.blue, + inactiveColor = if (column % Song.currentSong.subBeats == 0) R.color.gray_0x50 else R.color.gray_0x70 + ) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.octaveSeekBar.setup(-4, 4, voice.octaveOffset) { + voice.octaveOffset = it + binding.octaveText.text = "octave = $it" + } + 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.MATCH_PARENT + ) + } + + 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/ui/fragments/EffectFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt new file mode 100644 index 0000000..ebf4cb1 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.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.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.lukas.music.R +import com.lukas.music.databinding.FragmentEffectBinding +import com.lukas.music.instruments.effect.Effect +import com.lukas.music.util.setupToggle +import com.lukas.music.util.smartSetup + +class EffectFragment(private val effect: Effect) : Fragment() { + lateinit var binding: FragmentEffectBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEffectBinding.inflate(inflater) + binding.effectName.text = effect.type.toString() + binding.activeButton.setupToggle(effect::active, R.color.blue) { + binding.activeButton.text = if (it) "ON" else "OFF" + } + binding.activeButton.text = if (effect.active) "ON" else "OFF" + binding.influenceSeekBar.smartSetup(0, 100, effect.influence::percentageValue) { + binding.influenceText.text = effect.influence.description.text(effect.influence) + } + binding.parameter1SeekBar.smartSetup(0, 100, effect.parameters[0]::percentageValue) { + binding.parameter1Text.text = + effect.parameters[0].description.text(effect.parameters[0]) + } + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/util/EasyDialogFragment.kt b/app/src/main/java/com/lukas/music/util/EasyDialogFragment.kt new file mode 100644 index 0000000..c93b6e7 --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/EasyDialogFragment.kt @@ -0,0 +1,27 @@ +/* + * 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.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.viewbinding.ViewBinding + +open class EasyDialogFragment : DialogFragment() { + lateinit var binding: T + + 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/util/MathUtil.kt b/app/src/main/java/com/lukas/music/util/MathUtil.kt new file mode 100644 index 0000000..166061d --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/MathUtil.kt @@ -0,0 +1,21 @@ +/* + * 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 + +fun Double.format(digits: Int) = "%.${digits}f".format(this) + +fun Float.format(digits: Int) = "%.${digits}f".format(this) + +inline fun Array.transform(callback: (T) -> U): Array { + return Array(this.size) { + callback(this[it]) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/util/UIUtil.kt b/app/src/main/java/com/lukas/music/util/UIUtil.kt index da897c5..b67d0ea 100644 --- a/app/src/main/java/com/lukas/music/util/UIUtil.kt +++ b/app/src/main/java/com/lukas/music/util/UIUtil.kt @@ -36,25 +36,45 @@ override fun onStopTrackingTouch(seekBar: SeekBar) { } }) + if (progress == initialProgress) { + callback(initialProgress) + } this.progress = initialProgress } +fun SeekBar.smartSetup( + min: Int, + max: Int, + target: KMutableProperty0, + callback: (Int) -> Unit +) { + setup(min, max, target.get()) { + target.set(it) + callback(it) + } +} + fun Button.setupToggle( target: KMutableProperty0, activeColor: Int, - callback: (Boolean) -> Unit = {} + inactiveColor: Int = R.color.gray_0x60, + callback: (Boolean) -> Unit = {}, ) { setOnClickListener { target.set(!target.get()) - updateToggle(target, activeColor) + updateToggle(target, activeColor, inactiveColor) callback(target.get()) } - updateToggle(target, activeColor) + updateToggle(target, activeColor, inactiveColor) } -fun Button.updateToggle(target: KMutableProperty0, activeColor: Int) { +fun Button.updateToggle( + target: KMutableProperty0, + activeColor: Int, + inactiveColor: Int = R.color.gray_0x60, +) { setBackgroundColor( - ContextCompat.getColor(context, if (target.get()) activeColor else R.color.gray_0x60) + ContextCompat.getColor(context, if (target.get()) activeColor else inactiveColor) ) } diff --git a/app/src/main/res/layout/fragment_edit_effects.xml b/app/src/main/res/layout/fragment_edit_effects.xml new file mode 100644 index 0000000..46d4648 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_effects.xml @@ -0,0 +1,55 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_envelope.xml b/app/src/main/res/layout/fragment_edit_envelope.xml new file mode 100644 index 0000000..9e45d05 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_envelope.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 index b5578fc..eb66f19 100644 --- a/app/src/main/res/layout/fragment_edit_instrument.xml +++ b/app/src/main/res/layout/fragment_edit_instrument.xml @@ -15,7 +15,30 @@ android:layout_height="match_parent" tools:context=".EditInstrumentFragment"> - + + + + + app:layout_constraintTop_toBottomOf="@+id/editEffectsButton" /> + + + + \ 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 index b29b2c7..2225ed0 100644 --- a/app/src/main/res/layout/fragment_edit_voice.xml +++ b/app/src/main/res/layout/fragment_edit_voice.xml @@ -14,46 +14,87 @@ android:id="@+id/frameLayout2" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".EditVoiceFragment"> + tools:context=".ui.fragments.EditVoiceFragment"> - - - + app:layout_constraintTop_toTopOf="@+id/restrikeButton" /> + app:layout_constraintStart_toStartOf="parent" /> +