diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) companion object { val instruments = mutableListOf() diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ 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 7beb64c..7f10ff2 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -10,11 +10,12 @@ package com.lukas.music.instruments +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note class PolyInstrument(name: String) : Instrument(name) { - private val internalInstruments = Array(3) { InternalInstrument() } - private val playing = Array(3) { false } + private val internalInstruments = Array(Chord.NOTE_COUNT) { InternalInstrument() } + private val playing = Array(Chord.NOTE_COUNT) { false } override var waveform: Waveform = Waveform.SINE set(value) { @@ -86,7 +87,7 @@ override fun updateEffects() { for (instrument in internalInstruments) { for (effect in effects) { - instrument.applyEffectAttributes(effect) + instrument.applyEffectAttributes(this, effect) } } } @@ -99,4 +100,10 @@ } return false } + + override fun moveEffects(from: Int, to: Int) { + for (instrument in internalInstruments) { + instrument.moveEffects(from, to) + } + } } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ 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 7beb64c..7f10ff2 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -10,11 +10,12 @@ package com.lukas.music.instruments +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note class PolyInstrument(name: String) : Instrument(name) { - private val internalInstruments = Array(3) { InternalInstrument() } - private val playing = Array(3) { false } + private val internalInstruments = Array(Chord.NOTE_COUNT) { InternalInstrument() } + private val playing = Array(Chord.NOTE_COUNT) { false } override var waveform: Waveform = Waveform.SINE set(value) { @@ -86,7 +87,7 @@ override fun updateEffects() { for (instrument in internalInstruments) { for (effect in effects) { - instrument.applyEffectAttributes(effect) + instrument.applyEffectAttributes(this, effect) } } } @@ -99,4 +100,10 @@ } return false } + + override fun moveEffects(from: Int, to: Int) { + for (instrument in internalInstruments) { + instrument.moveEffects(from, to) + } + } } \ No newline at end of file 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 index f921b1b..f659a8c 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -14,7 +14,9 @@ class Effect(val type: EffectType, private val instrument: Instrument) { val parameters = Array(type.parameterDescriptions.size) { - EffectParameter(type.parameterDescriptions[it], instrument) + type.parameterDescriptions[it]?.let { parameterDescription -> + EffectParameter(parameterDescription, instrument) + } } val influence = EffectParameter(influenceDescription, instrument) diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ 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 7beb64c..7f10ff2 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -10,11 +10,12 @@ package com.lukas.music.instruments +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note class PolyInstrument(name: String) : Instrument(name) { - private val internalInstruments = Array(3) { InternalInstrument() } - private val playing = Array(3) { false } + private val internalInstruments = Array(Chord.NOTE_COUNT) { InternalInstrument() } + private val playing = Array(Chord.NOTE_COUNT) { false } override var waveform: Waveform = Waveform.SINE set(value) { @@ -86,7 +87,7 @@ override fun updateEffects() { for (instrument in internalInstruments) { for (effect in effects) { - instrument.applyEffectAttributes(effect) + instrument.applyEffectAttributes(this, effect) } } } @@ -99,4 +100,10 @@ } return false } + + override fun moveEffects(from: Int, to: Int) { + for (instrument in internalInstruments) { + instrument.moveEffects(from, to) + } + } } \ No newline at end of file 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 index f921b1b..f659a8c 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -14,7 +14,9 @@ class Effect(val type: EffectType, private val instrument: Instrument) { val parameters = Array(type.parameterDescriptions.size) { - EffectParameter(type.parameterDescriptions[it], instrument) + type.parameterDescriptions[it]?.let { parameterDescription -> + EffectParameter(parameterDescription, instrument) + } } val influence = EffectParameter(influenceDescription, instrument) 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 index 8af39ea..8b90ec5 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -13,8 +13,8 @@ import com.lukas.music.util.format enum class EffectType( - val title: String, - val parameterDescriptions: Array + private val title: String, + val parameterDescriptions: Array ) { LowPass("low pass filter", arrayOf( @@ -22,13 +22,17 @@ "cutoff: ${it.value.format(1)} octaves" } )), - Noise("noise", + Noise( + "noise", arrayOf( - EffectParameterDescription(0f, 1f, 0f) { - "unused" - } + null ) - ) + ), + Distortion("distortion", arrayOf( + EffectParameterDescription(1f, 4f, 1f) { + "strength: ${it.value.format(1)}x" + } + )) ; override fun toString(): String { diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ 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 7beb64c..7f10ff2 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -10,11 +10,12 @@ package com.lukas.music.instruments +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note class PolyInstrument(name: String) : Instrument(name) { - private val internalInstruments = Array(3) { InternalInstrument() } - private val playing = Array(3) { false } + private val internalInstruments = Array(Chord.NOTE_COUNT) { InternalInstrument() } + private val playing = Array(Chord.NOTE_COUNT) { false } override var waveform: Waveform = Waveform.SINE set(value) { @@ -86,7 +87,7 @@ override fun updateEffects() { for (instrument in internalInstruments) { for (effect in effects) { - instrument.applyEffectAttributes(effect) + instrument.applyEffectAttributes(this, effect) } } } @@ -99,4 +100,10 @@ } return false } + + override fun moveEffects(from: Int, to: Int) { + for (instrument in internalInstruments) { + instrument.moveEffects(from, to) + } + } } \ No newline at end of file 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 index f921b1b..f659a8c 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -14,7 +14,9 @@ class Effect(val type: EffectType, private val instrument: Instrument) { val parameters = Array(type.parameterDescriptions.size) { - EffectParameter(type.parameterDescriptions[it], instrument) + type.parameterDescriptions[it]?.let { parameterDescription -> + EffectParameter(parameterDescription, instrument) + } } val influence = EffectParameter(influenceDescription, instrument) 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 index 8af39ea..8b90ec5 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -13,8 +13,8 @@ import com.lukas.music.util.format enum class EffectType( - val title: String, - val parameterDescriptions: Array + private val title: String, + val parameterDescriptions: Array ) { LowPass("low pass filter", arrayOf( @@ -22,13 +22,17 @@ "cutoff: ${it.value.format(1)} octaves" } )), - Noise("noise", + Noise( + "noise", arrayOf( - EffectParameterDescription(0f, 1f, 0f) { - "unused" - } + null ) - ) + ), + Distortion("distortion", arrayOf( + EffectParameterDescription(1f, 4f, 1f) { + "strength: ${it.value.format(1)}x" + } + )) ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt index 4c6a0d9..7807cb9 100644 --- a/app/src/main/java/com/lukas/music/song/ScaleType.kt +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -10,24 +10,12 @@ 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 - ) + arrayOf(0, 2, 4, 5, 7, 9, 11), ) } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ 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 7beb64c..7f10ff2 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -10,11 +10,12 @@ package com.lukas.music.instruments +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note class PolyInstrument(name: String) : Instrument(name) { - private val internalInstruments = Array(3) { InternalInstrument() } - private val playing = Array(3) { false } + private val internalInstruments = Array(Chord.NOTE_COUNT) { InternalInstrument() } + private val playing = Array(Chord.NOTE_COUNT) { false } override var waveform: Waveform = Waveform.SINE set(value) { @@ -86,7 +87,7 @@ override fun updateEffects() { for (instrument in internalInstruments) { for (effect in effects) { - instrument.applyEffectAttributes(effect) + instrument.applyEffectAttributes(this, effect) } } } @@ -99,4 +100,10 @@ } return false } + + override fun moveEffects(from: Int, to: Int) { + for (instrument in internalInstruments) { + instrument.moveEffects(from, to) + } + } } \ No newline at end of file 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 index f921b1b..f659a8c 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -14,7 +14,9 @@ class Effect(val type: EffectType, private val instrument: Instrument) { val parameters = Array(type.parameterDescriptions.size) { - EffectParameter(type.parameterDescriptions[it], instrument) + type.parameterDescriptions[it]?.let { parameterDescription -> + EffectParameter(parameterDescription, instrument) + } } val influence = EffectParameter(influenceDescription, instrument) 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 index 8af39ea..8b90ec5 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -13,8 +13,8 @@ import com.lukas.music.util.format enum class EffectType( - val title: String, - val parameterDescriptions: Array + private val title: String, + val parameterDescriptions: Array ) { LowPass("low pass filter", arrayOf( @@ -22,13 +22,17 @@ "cutoff: ${it.value.format(1)} octaves" } )), - Noise("noise", + Noise( + "noise", arrayOf( - EffectParameterDescription(0f, 1f, 0f) { - "unused" - } + null ) - ) + ), + Distortion("distortion", arrayOf( + EffectParameterDescription(1f, 4f, 1f) { + "strength: ${it.value.format(1)}x" + } + )) ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt index 4c6a0d9..7807cb9 100644 --- a/app/src/main/java/com/lukas/music/song/ScaleType.kt +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -10,24 +10,12 @@ 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 - ) + arrayOf(0, 2, 4, 5, 7, 9, 11), ) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Accidental.kt b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt new file mode 100644 index 0000000..688ae4e --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or(at your option) any later version. + * + * This program is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 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.chords + +enum class Accidental(val id: String, val short: String, val distance: Int) { + Flat("\u266D", "b", -1), + None("\u266E", "", 0), + Sharp("\u266F", "#", 1), + ; + + override fun toString(): String { + return id + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ 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 7beb64c..7f10ff2 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -10,11 +10,12 @@ package com.lukas.music.instruments +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note class PolyInstrument(name: String) : Instrument(name) { - private val internalInstruments = Array(3) { InternalInstrument() } - private val playing = Array(3) { false } + private val internalInstruments = Array(Chord.NOTE_COUNT) { InternalInstrument() } + private val playing = Array(Chord.NOTE_COUNT) { false } override var waveform: Waveform = Waveform.SINE set(value) { @@ -86,7 +87,7 @@ override fun updateEffects() { for (instrument in internalInstruments) { for (effect in effects) { - instrument.applyEffectAttributes(effect) + instrument.applyEffectAttributes(this, effect) } } } @@ -99,4 +100,10 @@ } return false } + + override fun moveEffects(from: Int, to: Int) { + for (instrument in internalInstruments) { + instrument.moveEffects(from, to) + } + } } \ No newline at end of file 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 index f921b1b..f659a8c 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -14,7 +14,9 @@ class Effect(val type: EffectType, private val instrument: Instrument) { val parameters = Array(type.parameterDescriptions.size) { - EffectParameter(type.parameterDescriptions[it], instrument) + type.parameterDescriptions[it]?.let { parameterDescription -> + EffectParameter(parameterDescription, instrument) + } } val influence = EffectParameter(influenceDescription, instrument) 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 index 8af39ea..8b90ec5 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -13,8 +13,8 @@ import com.lukas.music.util.format enum class EffectType( - val title: String, - val parameterDescriptions: Array + private val title: String, + val parameterDescriptions: Array ) { LowPass("low pass filter", arrayOf( @@ -22,13 +22,17 @@ "cutoff: ${it.value.format(1)} octaves" } )), - Noise("noise", + Noise( + "noise", arrayOf( - EffectParameterDescription(0f, 1f, 0f) { - "unused" - } + null ) - ) + ), + Distortion("distortion", arrayOf( + EffectParameterDescription(1f, 4f, 1f) { + "strength: ${it.value.format(1)}x" + } + )) ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt index 4c6a0d9..7807cb9 100644 --- a/app/src/main/java/com/lukas/music/song/ScaleType.kt +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -10,24 +10,12 @@ 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 - ) + arrayOf(0, 2, 4, 5, 7, 9, 11), ) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Accidental.kt b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt new file mode 100644 index 0000000..688ae4e --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or(at your option) any later version. + * + * This program is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 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.chords + +enum class Accidental(val id: String, val short: String, val distance: Int) { + Flat("\u266D", "b", -1), + None("\u266E", "", 0), + Sharp("\u266F", "#", 1), + ; + + override fun toString(): String { + return id + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index 4400dae..85e530b 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -10,14 +10,19 @@ package com.lukas.music.song.chords +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -class Chord(note: Int, var chordType: ChordType) { - var note: Int = note +class Chord { + var accidental = Accidental.None + val accidentals: Array = arrayOf(Accidental.None, Accidental.None, null, null) + + var note: Int = 0 set(value) { field = value interval = Interval(value) } + var interval = Interval(note) set(value) { field = value @@ -27,19 +32,76 @@ } fun getNotes(root: Note): Array { - return Array(chordType.notes.size) { root + note + chordType.notes[it] } + val result = Array(NOTE_COUNT) { root } + var resultIndex = 0 + var accidentalIndex = 0 + var octave = 0 + while (resultIndex < NOTE_COUNT) { + if (accidentalIndex == 0) { + result[resultIndex] = root + note + 12 * octave + accidental.distance + resultIndex++ + } else if (accidentals[accidentalIndex - 1] != null) { + result[resultIndex] = root + note + when (accidentalIndex) { + 1 -> 4 + 2 -> 7 + 3 -> 10 + 4 -> 14 + else -> 0 + } + accidentals[accidentalIndex - 1]!!.distance + 12 * octave + accidental.distance + resultIndex++ + } + accidentalIndex++ + if (accidentalIndex > accidentals.size) { + octave++ + accidentalIndex = 0 + } + } + return result } override fun toString(): String { - return chordType.transform(interval.toString()) + return toString(false, Song.currentSong.root) } fun toString(displayChordNames: Boolean, root: Note): String { - val base = if (displayChordNames) { - (root + note).noteName.toString() + var result = if (displayChordNames) { + (root + note + accidental.distance).noteName.toString() } else { interval.toString() } - return chordType.transform(base) + accidentals[0]?.let { + result += when (it) { + Accidental.Flat -> "-" + Accidental.Sharp -> "sus4" + else -> "" + } + } + accidentals[1]?.let { + if (accidentals[0] != null && it == Accidental.None) { + return@let + } + result += it.short + "5" + } + result = result.replace("-b5", "0") + result = result.replace("(?=[A-G])#5".toRegex(), "+") + accidentals[2]?.let { + result += when (it) { + Accidental.Sharp -> " maj7" + Accidental.None -> " 7" + Accidental.Flat -> " 6" + } + } + accidentals[3]?.let { + result += when (it) { + Accidental.Sharp -> " maj9" + Accidental.None -> " 9" + Accidental.Flat -> " b9" + } + } + return result + } + + companion object { + const val NOTE_COUNT = 5 } } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ 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 7beb64c..7f10ff2 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -10,11 +10,12 @@ package com.lukas.music.instruments +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note class PolyInstrument(name: String) : Instrument(name) { - private val internalInstruments = Array(3) { InternalInstrument() } - private val playing = Array(3) { false } + private val internalInstruments = Array(Chord.NOTE_COUNT) { InternalInstrument() } + private val playing = Array(Chord.NOTE_COUNT) { false } override var waveform: Waveform = Waveform.SINE set(value) { @@ -86,7 +87,7 @@ override fun updateEffects() { for (instrument in internalInstruments) { for (effect in effects) { - instrument.applyEffectAttributes(effect) + instrument.applyEffectAttributes(this, effect) } } } @@ -99,4 +100,10 @@ } return false } + + override fun moveEffects(from: Int, to: Int) { + for (instrument in internalInstruments) { + instrument.moveEffects(from, to) + } + } } \ No newline at end of file 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 index f921b1b..f659a8c 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -14,7 +14,9 @@ class Effect(val type: EffectType, private val instrument: Instrument) { val parameters = Array(type.parameterDescriptions.size) { - EffectParameter(type.parameterDescriptions[it], instrument) + type.parameterDescriptions[it]?.let { parameterDescription -> + EffectParameter(parameterDescription, instrument) + } } val influence = EffectParameter(influenceDescription, instrument) 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 index 8af39ea..8b90ec5 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -13,8 +13,8 @@ import com.lukas.music.util.format enum class EffectType( - val title: String, - val parameterDescriptions: Array + private val title: String, + val parameterDescriptions: Array ) { LowPass("low pass filter", arrayOf( @@ -22,13 +22,17 @@ "cutoff: ${it.value.format(1)} octaves" } )), - Noise("noise", + Noise( + "noise", arrayOf( - EffectParameterDescription(0f, 1f, 0f) { - "unused" - } + null ) - ) + ), + Distortion("distortion", arrayOf( + EffectParameterDescription(1f, 4f, 1f) { + "strength: ${it.value.format(1)}x" + } + )) ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt index 4c6a0d9..7807cb9 100644 --- a/app/src/main/java/com/lukas/music/song/ScaleType.kt +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -10,24 +10,12 @@ 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 - ) + arrayOf(0, 2, 4, 5, 7, 9, 11), ) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Accidental.kt b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt new file mode 100644 index 0000000..688ae4e --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or(at your option) any later version. + * + * This program is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 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.chords + +enum class Accidental(val id: String, val short: String, val distance: Int) { + Flat("\u266D", "b", -1), + None("\u266E", "", 0), + Sharp("\u266F", "#", 1), + ; + + override fun toString(): String { + return id + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index 4400dae..85e530b 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -10,14 +10,19 @@ package com.lukas.music.song.chords +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -class Chord(note: Int, var chordType: ChordType) { - var note: Int = note +class Chord { + var accidental = Accidental.None + val accidentals: Array = arrayOf(Accidental.None, Accidental.None, null, null) + + var note: Int = 0 set(value) { field = value interval = Interval(value) } + var interval = Interval(note) set(value) { field = value @@ -27,19 +32,76 @@ } fun getNotes(root: Note): Array { - return Array(chordType.notes.size) { root + note + chordType.notes[it] } + val result = Array(NOTE_COUNT) { root } + var resultIndex = 0 + var accidentalIndex = 0 + var octave = 0 + while (resultIndex < NOTE_COUNT) { + if (accidentalIndex == 0) { + result[resultIndex] = root + note + 12 * octave + accidental.distance + resultIndex++ + } else if (accidentals[accidentalIndex - 1] != null) { + result[resultIndex] = root + note + when (accidentalIndex) { + 1 -> 4 + 2 -> 7 + 3 -> 10 + 4 -> 14 + else -> 0 + } + accidentals[accidentalIndex - 1]!!.distance + 12 * octave + accidental.distance + resultIndex++ + } + accidentalIndex++ + if (accidentalIndex > accidentals.size) { + octave++ + accidentalIndex = 0 + } + } + return result } override fun toString(): String { - return chordType.transform(interval.toString()) + return toString(false, Song.currentSong.root) } fun toString(displayChordNames: Boolean, root: Note): String { - val base = if (displayChordNames) { - (root + note).noteName.toString() + var result = if (displayChordNames) { + (root + note + accidental.distance).noteName.toString() } else { interval.toString() } - return chordType.transform(base) + accidentals[0]?.let { + result += when (it) { + Accidental.Flat -> "-" + Accidental.Sharp -> "sus4" + else -> "" + } + } + accidentals[1]?.let { + if (accidentals[0] != null && it == Accidental.None) { + return@let + } + result += it.short + "5" + } + result = result.replace("-b5", "0") + result = result.replace("(?=[A-G])#5".toRegex(), "+") + accidentals[2]?.let { + result += when (it) { + Accidental.Sharp -> " maj7" + Accidental.None -> " 7" + Accidental.Flat -> " 6" + } + } + accidentals[3]?.let { + result += when (it) { + Accidental.Sharp -> " maj9" + Accidental.None -> " 9" + Accidental.Flat -> " b9" + } + } + return result + } + + companion object { + const val NOTE_COUNT = 5 } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt b/app/src/main/java/com/lukas/music/song/chords/ChordType.kt deleted file mode 100644 index 1fe4b40..0000000 --- a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt +++ /dev/null @@ -1,30 +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.chords - -enum class ChordType( - val notes: Array, - private val asString: String, - val transform: (String) -> String -) { - MAJOR(arrayOf(0, 4, 7), "major", { it.uppercase() }), - MINOR(arrayOf(0, 3, 7), "minor", { it.lowercase() }), - DIMINISHED(arrayOf(0, 3, 6), "diminished", { it.lowercase() + "0" }), - ; - - override fun toString(): String { - return asString - } - - companion object { - val VALUES = values() - } -} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ 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 7beb64c..7f10ff2 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -10,11 +10,12 @@ package com.lukas.music.instruments +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note class PolyInstrument(name: String) : Instrument(name) { - private val internalInstruments = Array(3) { InternalInstrument() } - private val playing = Array(3) { false } + private val internalInstruments = Array(Chord.NOTE_COUNT) { InternalInstrument() } + private val playing = Array(Chord.NOTE_COUNT) { false } override var waveform: Waveform = Waveform.SINE set(value) { @@ -86,7 +87,7 @@ override fun updateEffects() { for (instrument in internalInstruments) { for (effect in effects) { - instrument.applyEffectAttributes(effect) + instrument.applyEffectAttributes(this, effect) } } } @@ -99,4 +100,10 @@ } return false } + + override fun moveEffects(from: Int, to: Int) { + for (instrument in internalInstruments) { + instrument.moveEffects(from, to) + } + } } \ No newline at end of file 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 index f921b1b..f659a8c 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -14,7 +14,9 @@ class Effect(val type: EffectType, private val instrument: Instrument) { val parameters = Array(type.parameterDescriptions.size) { - EffectParameter(type.parameterDescriptions[it], instrument) + type.parameterDescriptions[it]?.let { parameterDescription -> + EffectParameter(parameterDescription, instrument) + } } val influence = EffectParameter(influenceDescription, instrument) 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 index 8af39ea..8b90ec5 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -13,8 +13,8 @@ import com.lukas.music.util.format enum class EffectType( - val title: String, - val parameterDescriptions: Array + private val title: String, + val parameterDescriptions: Array ) { LowPass("low pass filter", arrayOf( @@ -22,13 +22,17 @@ "cutoff: ${it.value.format(1)} octaves" } )), - Noise("noise", + Noise( + "noise", arrayOf( - EffectParameterDescription(0f, 1f, 0f) { - "unused" - } + null ) - ) + ), + Distortion("distortion", arrayOf( + EffectParameterDescription(1f, 4f, 1f) { + "strength: ${it.value.format(1)}x" + } + )) ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt index 4c6a0d9..7807cb9 100644 --- a/app/src/main/java/com/lukas/music/song/ScaleType.kt +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -10,24 +10,12 @@ 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 - ) + arrayOf(0, 2, 4, 5, 7, 9, 11), ) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Accidental.kt b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt new file mode 100644 index 0000000..688ae4e --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or(at your option) any later version. + * + * This program is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 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.chords + +enum class Accidental(val id: String, val short: String, val distance: Int) { + Flat("\u266D", "b", -1), + None("\u266E", "", 0), + Sharp("\u266F", "#", 1), + ; + + override fun toString(): String { + return id + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index 4400dae..85e530b 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -10,14 +10,19 @@ package com.lukas.music.song.chords +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -class Chord(note: Int, var chordType: ChordType) { - var note: Int = note +class Chord { + var accidental = Accidental.None + val accidentals: Array = arrayOf(Accidental.None, Accidental.None, null, null) + + var note: Int = 0 set(value) { field = value interval = Interval(value) } + var interval = Interval(note) set(value) { field = value @@ -27,19 +32,76 @@ } fun getNotes(root: Note): Array { - return Array(chordType.notes.size) { root + note + chordType.notes[it] } + val result = Array(NOTE_COUNT) { root } + var resultIndex = 0 + var accidentalIndex = 0 + var octave = 0 + while (resultIndex < NOTE_COUNT) { + if (accidentalIndex == 0) { + result[resultIndex] = root + note + 12 * octave + accidental.distance + resultIndex++ + } else if (accidentals[accidentalIndex - 1] != null) { + result[resultIndex] = root + note + when (accidentalIndex) { + 1 -> 4 + 2 -> 7 + 3 -> 10 + 4 -> 14 + else -> 0 + } + accidentals[accidentalIndex - 1]!!.distance + 12 * octave + accidental.distance + resultIndex++ + } + accidentalIndex++ + if (accidentalIndex > accidentals.size) { + octave++ + accidentalIndex = 0 + } + } + return result } override fun toString(): String { - return chordType.transform(interval.toString()) + return toString(false, Song.currentSong.root) } fun toString(displayChordNames: Boolean, root: Note): String { - val base = if (displayChordNames) { - (root + note).noteName.toString() + var result = if (displayChordNames) { + (root + note + accidental.distance).noteName.toString() } else { interval.toString() } - return chordType.transform(base) + accidentals[0]?.let { + result += when (it) { + Accidental.Flat -> "-" + Accidental.Sharp -> "sus4" + else -> "" + } + } + accidentals[1]?.let { + if (accidentals[0] != null && it == Accidental.None) { + return@let + } + result += it.short + "5" + } + result = result.replace("-b5", "0") + result = result.replace("(?=[A-G])#5".toRegex(), "+") + accidentals[2]?.let { + result += when (it) { + Accidental.Sharp -> " maj7" + Accidental.None -> " 7" + Accidental.Flat -> " 6" + } + } + accidentals[3]?.let { + result += when (it) { + Accidental.Sharp -> " maj9" + Accidental.None -> " 9" + Accidental.Flat -> " b9" + } + } + return result + } + + companion object { + const val NOTE_COUNT = 5 } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt b/app/src/main/java/com/lukas/music/song/chords/ChordType.kt deleted file mode 100644 index 1fe4b40..0000000 --- a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt +++ /dev/null @@ -1,30 +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.chords - -enum class ChordType( - val notes: Array, - private val asString: String, - val transform: (String) -> String -) { - MAJOR(arrayOf(0, 4, 7), "major", { it.uppercase() }), - MINOR(arrayOf(0, 3, 7), "minor", { it.lowercase() }), - DIMINISHED(arrayOf(0, 3, 6), "diminished", { it.lowercase() + "0" }), - ; - - override fun toString(): String { - return asString - } - - companion object { - val VALUES = values() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt index 1cadb06..f4bcbc7 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt @@ -15,7 +15,7 @@ class Phrase : Cycle() { init { for (i in 0 until 4) { - this += Chord(0, ChordType.MAJOR) + this += Chord() } } } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ 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 7beb64c..7f10ff2 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -10,11 +10,12 @@ package com.lukas.music.instruments +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note class PolyInstrument(name: String) : Instrument(name) { - private val internalInstruments = Array(3) { InternalInstrument() } - private val playing = Array(3) { false } + private val internalInstruments = Array(Chord.NOTE_COUNT) { InternalInstrument() } + private val playing = Array(Chord.NOTE_COUNT) { false } override var waveform: Waveform = Waveform.SINE set(value) { @@ -86,7 +87,7 @@ override fun updateEffects() { for (instrument in internalInstruments) { for (effect in effects) { - instrument.applyEffectAttributes(effect) + instrument.applyEffectAttributes(this, effect) } } } @@ -99,4 +100,10 @@ } return false } + + override fun moveEffects(from: Int, to: Int) { + for (instrument in internalInstruments) { + instrument.moveEffects(from, to) + } + } } \ No newline at end of file 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 index f921b1b..f659a8c 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -14,7 +14,9 @@ class Effect(val type: EffectType, private val instrument: Instrument) { val parameters = Array(type.parameterDescriptions.size) { - EffectParameter(type.parameterDescriptions[it], instrument) + type.parameterDescriptions[it]?.let { parameterDescription -> + EffectParameter(parameterDescription, instrument) + } } val influence = EffectParameter(influenceDescription, instrument) 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 index 8af39ea..8b90ec5 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -13,8 +13,8 @@ import com.lukas.music.util.format enum class EffectType( - val title: String, - val parameterDescriptions: Array + private val title: String, + val parameterDescriptions: Array ) { LowPass("low pass filter", arrayOf( @@ -22,13 +22,17 @@ "cutoff: ${it.value.format(1)} octaves" } )), - Noise("noise", + Noise( + "noise", arrayOf( - EffectParameterDescription(0f, 1f, 0f) { - "unused" - } + null ) - ) + ), + Distortion("distortion", arrayOf( + EffectParameterDescription(1f, 4f, 1f) { + "strength: ${it.value.format(1)}x" + } + )) ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt index 4c6a0d9..7807cb9 100644 --- a/app/src/main/java/com/lukas/music/song/ScaleType.kt +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -10,24 +10,12 @@ 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 - ) + arrayOf(0, 2, 4, 5, 7, 9, 11), ) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Accidental.kt b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt new file mode 100644 index 0000000..688ae4e --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or(at your option) any later version. + * + * This program is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 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.chords + +enum class Accidental(val id: String, val short: String, val distance: Int) { + Flat("\u266D", "b", -1), + None("\u266E", "", 0), + Sharp("\u266F", "#", 1), + ; + + override fun toString(): String { + return id + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index 4400dae..85e530b 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -10,14 +10,19 @@ package com.lukas.music.song.chords +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -class Chord(note: Int, var chordType: ChordType) { - var note: Int = note +class Chord { + var accidental = Accidental.None + val accidentals: Array = arrayOf(Accidental.None, Accidental.None, null, null) + + var note: Int = 0 set(value) { field = value interval = Interval(value) } + var interval = Interval(note) set(value) { field = value @@ -27,19 +32,76 @@ } fun getNotes(root: Note): Array { - return Array(chordType.notes.size) { root + note + chordType.notes[it] } + val result = Array(NOTE_COUNT) { root } + var resultIndex = 0 + var accidentalIndex = 0 + var octave = 0 + while (resultIndex < NOTE_COUNT) { + if (accidentalIndex == 0) { + result[resultIndex] = root + note + 12 * octave + accidental.distance + resultIndex++ + } else if (accidentals[accidentalIndex - 1] != null) { + result[resultIndex] = root + note + when (accidentalIndex) { + 1 -> 4 + 2 -> 7 + 3 -> 10 + 4 -> 14 + else -> 0 + } + accidentals[accidentalIndex - 1]!!.distance + 12 * octave + accidental.distance + resultIndex++ + } + accidentalIndex++ + if (accidentalIndex > accidentals.size) { + octave++ + accidentalIndex = 0 + } + } + return result } override fun toString(): String { - return chordType.transform(interval.toString()) + return toString(false, Song.currentSong.root) } fun toString(displayChordNames: Boolean, root: Note): String { - val base = if (displayChordNames) { - (root + note).noteName.toString() + var result = if (displayChordNames) { + (root + note + accidental.distance).noteName.toString() } else { interval.toString() } - return chordType.transform(base) + accidentals[0]?.let { + result += when (it) { + Accidental.Flat -> "-" + Accidental.Sharp -> "sus4" + else -> "" + } + } + accidentals[1]?.let { + if (accidentals[0] != null && it == Accidental.None) { + return@let + } + result += it.short + "5" + } + result = result.replace("-b5", "0") + result = result.replace("(?=[A-G])#5".toRegex(), "+") + accidentals[2]?.let { + result += when (it) { + Accidental.Sharp -> " maj7" + Accidental.None -> " 7" + Accidental.Flat -> " 6" + } + } + accidentals[3]?.let { + result += when (it) { + Accidental.Sharp -> " maj9" + Accidental.None -> " 9" + Accidental.Flat -> " b9" + } + } + return result + } + + companion object { + const val NOTE_COUNT = 5 } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt b/app/src/main/java/com/lukas/music/song/chords/ChordType.kt deleted file mode 100644 index 1fe4b40..0000000 --- a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt +++ /dev/null @@ -1,30 +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.chords - -enum class ChordType( - val notes: Array, - private val asString: String, - val transform: (String) -> String -) { - MAJOR(arrayOf(0, 4, 7), "major", { it.uppercase() }), - MINOR(arrayOf(0, 3, 7), "minor", { it.lowercase() }), - DIMINISHED(arrayOf(0, 3, 6), "diminished", { it.lowercase() + "0" }), - ; - - override fun toString(): String { - return asString - } - - companion object { - val VALUES = values() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt index 1cadb06..f4bcbc7 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt @@ -15,7 +15,7 @@ class Phrase : Cycle() { init { for (i in 0 until 4) { - this += Chord(0, ChordType.MAJOR) + this += Chord() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index 17bfb2b..f17e322 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -12,7 +12,7 @@ import kotlin.math.pow -class Note(private val id: Int) { +class Note(val id: Int) { val noteName = NoteName.VALUES[id % 12] val octave = id / 12 - 1 val frequency = 440 * 2.0.pow((id - 69) / 12.0) @@ -28,6 +28,8 @@ return this + (-other) } + operator fun minus(other: Note): Int = id - other.id + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ 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 7beb64c..7f10ff2 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -10,11 +10,12 @@ package com.lukas.music.instruments +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note class PolyInstrument(name: String) : Instrument(name) { - private val internalInstruments = Array(3) { InternalInstrument() } - private val playing = Array(3) { false } + private val internalInstruments = Array(Chord.NOTE_COUNT) { InternalInstrument() } + private val playing = Array(Chord.NOTE_COUNT) { false } override var waveform: Waveform = Waveform.SINE set(value) { @@ -86,7 +87,7 @@ override fun updateEffects() { for (instrument in internalInstruments) { for (effect in effects) { - instrument.applyEffectAttributes(effect) + instrument.applyEffectAttributes(this, effect) } } } @@ -99,4 +100,10 @@ } return false } + + override fun moveEffects(from: Int, to: Int) { + for (instrument in internalInstruments) { + instrument.moveEffects(from, to) + } + } } \ No newline at end of file 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 index f921b1b..f659a8c 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -14,7 +14,9 @@ class Effect(val type: EffectType, private val instrument: Instrument) { val parameters = Array(type.parameterDescriptions.size) { - EffectParameter(type.parameterDescriptions[it], instrument) + type.parameterDescriptions[it]?.let { parameterDescription -> + EffectParameter(parameterDescription, instrument) + } } val influence = EffectParameter(influenceDescription, instrument) 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 index 8af39ea..8b90ec5 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -13,8 +13,8 @@ import com.lukas.music.util.format enum class EffectType( - val title: String, - val parameterDescriptions: Array + private val title: String, + val parameterDescriptions: Array ) { LowPass("low pass filter", arrayOf( @@ -22,13 +22,17 @@ "cutoff: ${it.value.format(1)} octaves" } )), - Noise("noise", + Noise( + "noise", arrayOf( - EffectParameterDescription(0f, 1f, 0f) { - "unused" - } + null ) - ) + ), + Distortion("distortion", arrayOf( + EffectParameterDescription(1f, 4f, 1f) { + "strength: ${it.value.format(1)}x" + } + )) ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt index 4c6a0d9..7807cb9 100644 --- a/app/src/main/java/com/lukas/music/song/ScaleType.kt +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -10,24 +10,12 @@ 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 - ) + arrayOf(0, 2, 4, 5, 7, 9, 11), ) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Accidental.kt b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt new file mode 100644 index 0000000..688ae4e --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or(at your option) any later version. + * + * This program is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 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.chords + +enum class Accidental(val id: String, val short: String, val distance: Int) { + Flat("\u266D", "b", -1), + None("\u266E", "", 0), + Sharp("\u266F", "#", 1), + ; + + override fun toString(): String { + return id + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index 4400dae..85e530b 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -10,14 +10,19 @@ package com.lukas.music.song.chords +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -class Chord(note: Int, var chordType: ChordType) { - var note: Int = note +class Chord { + var accidental = Accidental.None + val accidentals: Array = arrayOf(Accidental.None, Accidental.None, null, null) + + var note: Int = 0 set(value) { field = value interval = Interval(value) } + var interval = Interval(note) set(value) { field = value @@ -27,19 +32,76 @@ } fun getNotes(root: Note): Array { - return Array(chordType.notes.size) { root + note + chordType.notes[it] } + val result = Array(NOTE_COUNT) { root } + var resultIndex = 0 + var accidentalIndex = 0 + var octave = 0 + while (resultIndex < NOTE_COUNT) { + if (accidentalIndex == 0) { + result[resultIndex] = root + note + 12 * octave + accidental.distance + resultIndex++ + } else if (accidentals[accidentalIndex - 1] != null) { + result[resultIndex] = root + note + when (accidentalIndex) { + 1 -> 4 + 2 -> 7 + 3 -> 10 + 4 -> 14 + else -> 0 + } + accidentals[accidentalIndex - 1]!!.distance + 12 * octave + accidental.distance + resultIndex++ + } + accidentalIndex++ + if (accidentalIndex > accidentals.size) { + octave++ + accidentalIndex = 0 + } + } + return result } override fun toString(): String { - return chordType.transform(interval.toString()) + return toString(false, Song.currentSong.root) } fun toString(displayChordNames: Boolean, root: Note): String { - val base = if (displayChordNames) { - (root + note).noteName.toString() + var result = if (displayChordNames) { + (root + note + accidental.distance).noteName.toString() } else { interval.toString() } - return chordType.transform(base) + accidentals[0]?.let { + result += when (it) { + Accidental.Flat -> "-" + Accidental.Sharp -> "sus4" + else -> "" + } + } + accidentals[1]?.let { + if (accidentals[0] != null && it == Accidental.None) { + return@let + } + result += it.short + "5" + } + result = result.replace("-b5", "0") + result = result.replace("(?=[A-G])#5".toRegex(), "+") + accidentals[2]?.let { + result += when (it) { + Accidental.Sharp -> " maj7" + Accidental.None -> " 7" + Accidental.Flat -> " 6" + } + } + accidentals[3]?.let { + result += when (it) { + Accidental.Sharp -> " maj9" + Accidental.None -> " 9" + Accidental.Flat -> " b9" + } + } + return result + } + + companion object { + const val NOTE_COUNT = 5 } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt b/app/src/main/java/com/lukas/music/song/chords/ChordType.kt deleted file mode 100644 index 1fe4b40..0000000 --- a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt +++ /dev/null @@ -1,30 +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.chords - -enum class ChordType( - val notes: Array, - private val asString: String, - val transform: (String) -> String -) { - MAJOR(arrayOf(0, 4, 7), "major", { it.uppercase() }), - MINOR(arrayOf(0, 3, 7), "minor", { it.lowercase() }), - DIMINISHED(arrayOf(0, 3, 6), "diminished", { it.lowercase() + "0" }), - ; - - override fun toString(): String { - return asString - } - - companion object { - val VALUES = values() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt index 1cadb06..f4bcbc7 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt @@ -15,7 +15,7 @@ class Phrase : Cycle() { init { for (i in 0 until 4) { - this += Chord(0, ChordType.MAJOR) + this += Chord() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index 17bfb2b..f17e322 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -12,7 +12,7 @@ import kotlin.math.pow -class Note(private val id: Int) { +class Note(val id: Int) { val noteName = NoteName.VALUES[id % 12] val octave = id / 12 - 1 val frequency = 440 * 2.0.pow((id - 69) / 12.0) @@ -28,6 +28,8 @@ return this + (-other) } + operator fun minus(other: Note): Int = id - other.id + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false 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 index e06761a..40b1d14 100644 --- a/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -11,6 +11,7 @@ package com.lukas.music.song.voice import com.lukas.music.song.ScaleType +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note import com.lukas.music.util.transform @@ -20,7 +21,7 @@ val getNotes: (Note, Array) -> Array ) { Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), - Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + ChordVoice("Chord notes", Chord.NOTE_COUNT, { _, 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 } }), diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ 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 7beb64c..7f10ff2 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -10,11 +10,12 @@ package com.lukas.music.instruments +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note class PolyInstrument(name: String) : Instrument(name) { - private val internalInstruments = Array(3) { InternalInstrument() } - private val playing = Array(3) { false } + private val internalInstruments = Array(Chord.NOTE_COUNT) { InternalInstrument() } + private val playing = Array(Chord.NOTE_COUNT) { false } override var waveform: Waveform = Waveform.SINE set(value) { @@ -86,7 +87,7 @@ override fun updateEffects() { for (instrument in internalInstruments) { for (effect in effects) { - instrument.applyEffectAttributes(effect) + instrument.applyEffectAttributes(this, effect) } } } @@ -99,4 +100,10 @@ } return false } + + override fun moveEffects(from: Int, to: Int) { + for (instrument in internalInstruments) { + instrument.moveEffects(from, to) + } + } } \ No newline at end of file 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 index f921b1b..f659a8c 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -14,7 +14,9 @@ class Effect(val type: EffectType, private val instrument: Instrument) { val parameters = Array(type.parameterDescriptions.size) { - EffectParameter(type.parameterDescriptions[it], instrument) + type.parameterDescriptions[it]?.let { parameterDescription -> + EffectParameter(parameterDescription, instrument) + } } val influence = EffectParameter(influenceDescription, instrument) 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 index 8af39ea..8b90ec5 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -13,8 +13,8 @@ import com.lukas.music.util.format enum class EffectType( - val title: String, - val parameterDescriptions: Array + private val title: String, + val parameterDescriptions: Array ) { LowPass("low pass filter", arrayOf( @@ -22,13 +22,17 @@ "cutoff: ${it.value.format(1)} octaves" } )), - Noise("noise", + Noise( + "noise", arrayOf( - EffectParameterDescription(0f, 1f, 0f) { - "unused" - } + null ) - ) + ), + Distortion("distortion", arrayOf( + EffectParameterDescription(1f, 4f, 1f) { + "strength: ${it.value.format(1)}x" + } + )) ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt index 4c6a0d9..7807cb9 100644 --- a/app/src/main/java/com/lukas/music/song/ScaleType.kt +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -10,24 +10,12 @@ 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 - ) + arrayOf(0, 2, 4, 5, 7, 9, 11), ) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Accidental.kt b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt new file mode 100644 index 0000000..688ae4e --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or(at your option) any later version. + * + * This program is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 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.chords + +enum class Accidental(val id: String, val short: String, val distance: Int) { + Flat("\u266D", "b", -1), + None("\u266E", "", 0), + Sharp("\u266F", "#", 1), + ; + + override fun toString(): String { + return id + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index 4400dae..85e530b 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -10,14 +10,19 @@ package com.lukas.music.song.chords +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -class Chord(note: Int, var chordType: ChordType) { - var note: Int = note +class Chord { + var accidental = Accidental.None + val accidentals: Array = arrayOf(Accidental.None, Accidental.None, null, null) + + var note: Int = 0 set(value) { field = value interval = Interval(value) } + var interval = Interval(note) set(value) { field = value @@ -27,19 +32,76 @@ } fun getNotes(root: Note): Array { - return Array(chordType.notes.size) { root + note + chordType.notes[it] } + val result = Array(NOTE_COUNT) { root } + var resultIndex = 0 + var accidentalIndex = 0 + var octave = 0 + while (resultIndex < NOTE_COUNT) { + if (accidentalIndex == 0) { + result[resultIndex] = root + note + 12 * octave + accidental.distance + resultIndex++ + } else if (accidentals[accidentalIndex - 1] != null) { + result[resultIndex] = root + note + when (accidentalIndex) { + 1 -> 4 + 2 -> 7 + 3 -> 10 + 4 -> 14 + else -> 0 + } + accidentals[accidentalIndex - 1]!!.distance + 12 * octave + accidental.distance + resultIndex++ + } + accidentalIndex++ + if (accidentalIndex > accidentals.size) { + octave++ + accidentalIndex = 0 + } + } + return result } override fun toString(): String { - return chordType.transform(interval.toString()) + return toString(false, Song.currentSong.root) } fun toString(displayChordNames: Boolean, root: Note): String { - val base = if (displayChordNames) { - (root + note).noteName.toString() + var result = if (displayChordNames) { + (root + note + accidental.distance).noteName.toString() } else { interval.toString() } - return chordType.transform(base) + accidentals[0]?.let { + result += when (it) { + Accidental.Flat -> "-" + Accidental.Sharp -> "sus4" + else -> "" + } + } + accidentals[1]?.let { + if (accidentals[0] != null && it == Accidental.None) { + return@let + } + result += it.short + "5" + } + result = result.replace("-b5", "0") + result = result.replace("(?=[A-G])#5".toRegex(), "+") + accidentals[2]?.let { + result += when (it) { + Accidental.Sharp -> " maj7" + Accidental.None -> " 7" + Accidental.Flat -> " 6" + } + } + accidentals[3]?.let { + result += when (it) { + Accidental.Sharp -> " maj9" + Accidental.None -> " 9" + Accidental.Flat -> " b9" + } + } + return result + } + + companion object { + const val NOTE_COUNT = 5 } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt b/app/src/main/java/com/lukas/music/song/chords/ChordType.kt deleted file mode 100644 index 1fe4b40..0000000 --- a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt +++ /dev/null @@ -1,30 +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.chords - -enum class ChordType( - val notes: Array, - private val asString: String, - val transform: (String) -> String -) { - MAJOR(arrayOf(0, 4, 7), "major", { it.uppercase() }), - MINOR(arrayOf(0, 3, 7), "minor", { it.lowercase() }), - DIMINISHED(arrayOf(0, 3, 6), "diminished", { it.lowercase() + "0" }), - ; - - override fun toString(): String { - return asString - } - - companion object { - val VALUES = values() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt index 1cadb06..f4bcbc7 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt @@ -15,7 +15,7 @@ class Phrase : Cycle() { init { for (i in 0 until 4) { - this += Chord(0, ChordType.MAJOR) + this += Chord() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index 17bfb2b..f17e322 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -12,7 +12,7 @@ import kotlin.math.pow -class Note(private val id: Int) { +class Note(val id: Int) { val noteName = NoteName.VALUES[id % 12] val octave = id / 12 - 1 val frequency = 440 * 2.0.pow((id - 69) / 12.0) @@ -28,6 +28,8 @@ return this + (-other) } + operator fun minus(other: Note): Int = id - other.id + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false 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 index e06761a..40b1d14 100644 --- a/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -11,6 +11,7 @@ package com.lukas.music.song.voice import com.lukas.music.song.ScaleType +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note import com.lukas.music.util.transform @@ -20,7 +21,7 @@ val getNotes: (Note, Array) -> Array ) { Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), - Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + ChordVoice("Chord notes", Chord.NOTE_COUNT, { _, 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 } }), diff --git a/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt new file mode 100644 index 0000000..730eabd --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.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.ui.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.lukas.music.databinding.FragmentEffectBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.ui.fragments.EditEffectsFragment +import com.lukas.music.ui.fragments.EffectFragment + +class EffectsAdapter(private val parent: EditEffectsFragment, private val instrument: Instrument) : + RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EffectFragment { + val context = parent.context + val inflater = LayoutInflater.from(context) + val binding = FragmentEffectBinding.inflate(inflater, parent, false) + return EffectFragment(binding) + } + + override fun onBindViewHolder(holder: EffectFragment, position: Int) { + holder.setEffect(instrument.effects[position]) + } + + override fun getItemCount(): Int { + return instrument.effects.size + } +} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ 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 7beb64c..7f10ff2 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -10,11 +10,12 @@ package com.lukas.music.instruments +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note class PolyInstrument(name: String) : Instrument(name) { - private val internalInstruments = Array(3) { InternalInstrument() } - private val playing = Array(3) { false } + private val internalInstruments = Array(Chord.NOTE_COUNT) { InternalInstrument() } + private val playing = Array(Chord.NOTE_COUNT) { false } override var waveform: Waveform = Waveform.SINE set(value) { @@ -86,7 +87,7 @@ override fun updateEffects() { for (instrument in internalInstruments) { for (effect in effects) { - instrument.applyEffectAttributes(effect) + instrument.applyEffectAttributes(this, effect) } } } @@ -99,4 +100,10 @@ } return false } + + override fun moveEffects(from: Int, to: Int) { + for (instrument in internalInstruments) { + instrument.moveEffects(from, to) + } + } } \ No newline at end of file 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 index f921b1b..f659a8c 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -14,7 +14,9 @@ class Effect(val type: EffectType, private val instrument: Instrument) { val parameters = Array(type.parameterDescriptions.size) { - EffectParameter(type.parameterDescriptions[it], instrument) + type.parameterDescriptions[it]?.let { parameterDescription -> + EffectParameter(parameterDescription, instrument) + } } val influence = EffectParameter(influenceDescription, instrument) 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 index 8af39ea..8b90ec5 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -13,8 +13,8 @@ import com.lukas.music.util.format enum class EffectType( - val title: String, - val parameterDescriptions: Array + private val title: String, + val parameterDescriptions: Array ) { LowPass("low pass filter", arrayOf( @@ -22,13 +22,17 @@ "cutoff: ${it.value.format(1)} octaves" } )), - Noise("noise", + Noise( + "noise", arrayOf( - EffectParameterDescription(0f, 1f, 0f) { - "unused" - } + null ) - ) + ), + Distortion("distortion", arrayOf( + EffectParameterDescription(1f, 4f, 1f) { + "strength: ${it.value.format(1)}x" + } + )) ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt index 4c6a0d9..7807cb9 100644 --- a/app/src/main/java/com/lukas/music/song/ScaleType.kt +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -10,24 +10,12 @@ 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 - ) + arrayOf(0, 2, 4, 5, 7, 9, 11), ) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Accidental.kt b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt new file mode 100644 index 0000000..688ae4e --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or(at your option) any later version. + * + * This program is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 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.chords + +enum class Accidental(val id: String, val short: String, val distance: Int) { + Flat("\u266D", "b", -1), + None("\u266E", "", 0), + Sharp("\u266F", "#", 1), + ; + + override fun toString(): String { + return id + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index 4400dae..85e530b 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -10,14 +10,19 @@ package com.lukas.music.song.chords +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -class Chord(note: Int, var chordType: ChordType) { - var note: Int = note +class Chord { + var accidental = Accidental.None + val accidentals: Array = arrayOf(Accidental.None, Accidental.None, null, null) + + var note: Int = 0 set(value) { field = value interval = Interval(value) } + var interval = Interval(note) set(value) { field = value @@ -27,19 +32,76 @@ } fun getNotes(root: Note): Array { - return Array(chordType.notes.size) { root + note + chordType.notes[it] } + val result = Array(NOTE_COUNT) { root } + var resultIndex = 0 + var accidentalIndex = 0 + var octave = 0 + while (resultIndex < NOTE_COUNT) { + if (accidentalIndex == 0) { + result[resultIndex] = root + note + 12 * octave + accidental.distance + resultIndex++ + } else if (accidentals[accidentalIndex - 1] != null) { + result[resultIndex] = root + note + when (accidentalIndex) { + 1 -> 4 + 2 -> 7 + 3 -> 10 + 4 -> 14 + else -> 0 + } + accidentals[accidentalIndex - 1]!!.distance + 12 * octave + accidental.distance + resultIndex++ + } + accidentalIndex++ + if (accidentalIndex > accidentals.size) { + octave++ + accidentalIndex = 0 + } + } + return result } override fun toString(): String { - return chordType.transform(interval.toString()) + return toString(false, Song.currentSong.root) } fun toString(displayChordNames: Boolean, root: Note): String { - val base = if (displayChordNames) { - (root + note).noteName.toString() + var result = if (displayChordNames) { + (root + note + accidental.distance).noteName.toString() } else { interval.toString() } - return chordType.transform(base) + accidentals[0]?.let { + result += when (it) { + Accidental.Flat -> "-" + Accidental.Sharp -> "sus4" + else -> "" + } + } + accidentals[1]?.let { + if (accidentals[0] != null && it == Accidental.None) { + return@let + } + result += it.short + "5" + } + result = result.replace("-b5", "0") + result = result.replace("(?=[A-G])#5".toRegex(), "+") + accidentals[2]?.let { + result += when (it) { + Accidental.Sharp -> " maj7" + Accidental.None -> " 7" + Accidental.Flat -> " 6" + } + } + accidentals[3]?.let { + result += when (it) { + Accidental.Sharp -> " maj9" + Accidental.None -> " 9" + Accidental.Flat -> " b9" + } + } + return result + } + + companion object { + const val NOTE_COUNT = 5 } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt b/app/src/main/java/com/lukas/music/song/chords/ChordType.kt deleted file mode 100644 index 1fe4b40..0000000 --- a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt +++ /dev/null @@ -1,30 +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.chords - -enum class ChordType( - val notes: Array, - private val asString: String, - val transform: (String) -> String -) { - MAJOR(arrayOf(0, 4, 7), "major", { it.uppercase() }), - MINOR(arrayOf(0, 3, 7), "minor", { it.lowercase() }), - DIMINISHED(arrayOf(0, 3, 6), "diminished", { it.lowercase() + "0" }), - ; - - override fun toString(): String { - return asString - } - - companion object { - val VALUES = values() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt index 1cadb06..f4bcbc7 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt @@ -15,7 +15,7 @@ class Phrase : Cycle() { init { for (i in 0 until 4) { - this += Chord(0, ChordType.MAJOR) + this += Chord() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index 17bfb2b..f17e322 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -12,7 +12,7 @@ import kotlin.math.pow -class Note(private val id: Int) { +class Note(val id: Int) { val noteName = NoteName.VALUES[id % 12] val octave = id / 12 - 1 val frequency = 440 * 2.0.pow((id - 69) / 12.0) @@ -28,6 +28,8 @@ return this + (-other) } + operator fun minus(other: Note): Int = id - other.id + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false 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 index e06761a..40b1d14 100644 --- a/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -11,6 +11,7 @@ package com.lukas.music.song.voice import com.lukas.music.song.ScaleType +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note import com.lukas.music.util.transform @@ -20,7 +21,7 @@ val getNotes: (Note, Array) -> Array ) { Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), - Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + ChordVoice("Chord notes", Chord.NOTE_COUNT, { _, 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 } }), diff --git a/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt new file mode 100644 index 0000000..730eabd --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.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.ui.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.lukas.music.databinding.FragmentEffectBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.ui.fragments.EditEffectsFragment +import com.lukas.music.ui.fragments.EffectFragment + +class EffectsAdapter(private val parent: EditEffectsFragment, private val instrument: Instrument) : + RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EffectFragment { + val context = parent.context + val inflater = LayoutInflater.from(context) + val binding = FragmentEffectBinding.inflate(inflater, parent, false) + return EffectFragment(binding) + } + + override fun onBindViewHolder(holder: EffectFragment, position: Int) { + holder.setEffect(instrument.effects[position]) + } + + override fun getItemCount(): Int { + return instrument.effects.size + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt index 3e129bc..3d7d1f4 100644 --- a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt @@ -39,7 +39,7 @@ Song.currentSong.soloInstrument = instrument } field = value - binding.soloButton.updateToggle(this::solo, R.color.blue) + binding.soloButton.updateToggle(this.solo, R.color.blue) } var instrument: Instrument? = null diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ 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 7beb64c..7f10ff2 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -10,11 +10,12 @@ package com.lukas.music.instruments +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note class PolyInstrument(name: String) : Instrument(name) { - private val internalInstruments = Array(3) { InternalInstrument() } - private val playing = Array(3) { false } + private val internalInstruments = Array(Chord.NOTE_COUNT) { InternalInstrument() } + private val playing = Array(Chord.NOTE_COUNT) { false } override var waveform: Waveform = Waveform.SINE set(value) { @@ -86,7 +87,7 @@ override fun updateEffects() { for (instrument in internalInstruments) { for (effect in effects) { - instrument.applyEffectAttributes(effect) + instrument.applyEffectAttributes(this, effect) } } } @@ -99,4 +100,10 @@ } return false } + + override fun moveEffects(from: Int, to: Int) { + for (instrument in internalInstruments) { + instrument.moveEffects(from, to) + } + } } \ No newline at end of file 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 index f921b1b..f659a8c 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -14,7 +14,9 @@ class Effect(val type: EffectType, private val instrument: Instrument) { val parameters = Array(type.parameterDescriptions.size) { - EffectParameter(type.parameterDescriptions[it], instrument) + type.parameterDescriptions[it]?.let { parameterDescription -> + EffectParameter(parameterDescription, instrument) + } } val influence = EffectParameter(influenceDescription, instrument) 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 index 8af39ea..8b90ec5 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -13,8 +13,8 @@ import com.lukas.music.util.format enum class EffectType( - val title: String, - val parameterDescriptions: Array + private val title: String, + val parameterDescriptions: Array ) { LowPass("low pass filter", arrayOf( @@ -22,13 +22,17 @@ "cutoff: ${it.value.format(1)} octaves" } )), - Noise("noise", + Noise( + "noise", arrayOf( - EffectParameterDescription(0f, 1f, 0f) { - "unused" - } + null ) - ) + ), + Distortion("distortion", arrayOf( + EffectParameterDescription(1f, 4f, 1f) { + "strength: ${it.value.format(1)}x" + } + )) ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt index 4c6a0d9..7807cb9 100644 --- a/app/src/main/java/com/lukas/music/song/ScaleType.kt +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -10,24 +10,12 @@ 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 - ) + arrayOf(0, 2, 4, 5, 7, 9, 11), ) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Accidental.kt b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt new file mode 100644 index 0000000..688ae4e --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or(at your option) any later version. + * + * This program is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 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.chords + +enum class Accidental(val id: String, val short: String, val distance: Int) { + Flat("\u266D", "b", -1), + None("\u266E", "", 0), + Sharp("\u266F", "#", 1), + ; + + override fun toString(): String { + return id + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index 4400dae..85e530b 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -10,14 +10,19 @@ package com.lukas.music.song.chords +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -class Chord(note: Int, var chordType: ChordType) { - var note: Int = note +class Chord { + var accidental = Accidental.None + val accidentals: Array = arrayOf(Accidental.None, Accidental.None, null, null) + + var note: Int = 0 set(value) { field = value interval = Interval(value) } + var interval = Interval(note) set(value) { field = value @@ -27,19 +32,76 @@ } fun getNotes(root: Note): Array { - return Array(chordType.notes.size) { root + note + chordType.notes[it] } + val result = Array(NOTE_COUNT) { root } + var resultIndex = 0 + var accidentalIndex = 0 + var octave = 0 + while (resultIndex < NOTE_COUNT) { + if (accidentalIndex == 0) { + result[resultIndex] = root + note + 12 * octave + accidental.distance + resultIndex++ + } else if (accidentals[accidentalIndex - 1] != null) { + result[resultIndex] = root + note + when (accidentalIndex) { + 1 -> 4 + 2 -> 7 + 3 -> 10 + 4 -> 14 + else -> 0 + } + accidentals[accidentalIndex - 1]!!.distance + 12 * octave + accidental.distance + resultIndex++ + } + accidentalIndex++ + if (accidentalIndex > accidentals.size) { + octave++ + accidentalIndex = 0 + } + } + return result } override fun toString(): String { - return chordType.transform(interval.toString()) + return toString(false, Song.currentSong.root) } fun toString(displayChordNames: Boolean, root: Note): String { - val base = if (displayChordNames) { - (root + note).noteName.toString() + var result = if (displayChordNames) { + (root + note + accidental.distance).noteName.toString() } else { interval.toString() } - return chordType.transform(base) + accidentals[0]?.let { + result += when (it) { + Accidental.Flat -> "-" + Accidental.Sharp -> "sus4" + else -> "" + } + } + accidentals[1]?.let { + if (accidentals[0] != null && it == Accidental.None) { + return@let + } + result += it.short + "5" + } + result = result.replace("-b5", "0") + result = result.replace("(?=[A-G])#5".toRegex(), "+") + accidentals[2]?.let { + result += when (it) { + Accidental.Sharp -> " maj7" + Accidental.None -> " 7" + Accidental.Flat -> " 6" + } + } + accidentals[3]?.let { + result += when (it) { + Accidental.Sharp -> " maj9" + Accidental.None -> " 9" + Accidental.Flat -> " b9" + } + } + return result + } + + companion object { + const val NOTE_COUNT = 5 } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt b/app/src/main/java/com/lukas/music/song/chords/ChordType.kt deleted file mode 100644 index 1fe4b40..0000000 --- a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt +++ /dev/null @@ -1,30 +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.chords - -enum class ChordType( - val notes: Array, - private val asString: String, - val transform: (String) -> String -) { - MAJOR(arrayOf(0, 4, 7), "major", { it.uppercase() }), - MINOR(arrayOf(0, 3, 7), "minor", { it.lowercase() }), - DIMINISHED(arrayOf(0, 3, 6), "diminished", { it.lowercase() + "0" }), - ; - - override fun toString(): String { - return asString - } - - companion object { - val VALUES = values() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt index 1cadb06..f4bcbc7 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt @@ -15,7 +15,7 @@ class Phrase : Cycle() { init { for (i in 0 until 4) { - this += Chord(0, ChordType.MAJOR) + this += Chord() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index 17bfb2b..f17e322 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -12,7 +12,7 @@ import kotlin.math.pow -class Note(private val id: Int) { +class Note(val id: Int) { val noteName = NoteName.VALUES[id % 12] val octave = id / 12 - 1 val frequency = 440 * 2.0.pow((id - 69) / 12.0) @@ -28,6 +28,8 @@ return this + (-other) } + operator fun minus(other: Note): Int = id - other.id + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false 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 index e06761a..40b1d14 100644 --- a/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -11,6 +11,7 @@ package com.lukas.music.song.voice import com.lukas.music.song.ScaleType +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note import com.lukas.music.util.transform @@ -20,7 +21,7 @@ val getNotes: (Note, Array) -> Array ) { Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), - Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + ChordVoice("Chord notes", Chord.NOTE_COUNT, { _, 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 } }), diff --git a/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt new file mode 100644 index 0000000..730eabd --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.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.ui.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.lukas.music.databinding.FragmentEffectBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.ui.fragments.EditEffectsFragment +import com.lukas.music.ui.fragments.EffectFragment + +class EffectsAdapter(private val parent: EditEffectsFragment, private val instrument: Instrument) : + RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EffectFragment { + val context = parent.context + val inflater = LayoutInflater.from(context) + val binding = FragmentEffectBinding.inflate(inflater, parent, false) + return EffectFragment(binding) + } + + override fun onBindViewHolder(holder: EffectFragment, position: Int) { + holder.setEffect(instrument.effects[position]) + } + + override fun getItemCount(): Int { + return instrument.effects.size + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt index 3e129bc..3d7d1f4 100644 --- a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt @@ -39,7 +39,7 @@ Song.currentSong.soloInstrument = instrument } field = value - binding.soloButton.updateToggle(this::solo, R.color.blue) + binding.soloButton.updateToggle(this.solo, R.color.blue) } var instrument: Instrument? = null 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 6c3bae3..5fc9dfe 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 @@ -14,61 +14,124 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.DialogFragment +import android.widget.TableRow +import android.widget.TextView +import androidx.core.view.children +import com.google.android.material.button.MaterialButton +import com.lukas.music.R import com.lukas.music.databinding.FragmentEditChordBinding import com.lukas.music.song.ScaleType import com.lukas.music.song.Song +import com.lukas.music.song.chords.Accidental 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 +import com.lukas.music.util.* class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : - DialogFragment() { - lateinit var binding: FragmentEditChordBinding + EasyDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentEditChordBinding.inflate(inflater) + Array(Accidental.VALUES.size) { + val button = MaterialButton(binding.root.context) + button.layoutParams = UIUtil.cardLayout + binding.accidentalSelection.addView(button) + return@Array button + }.setupEnumSelection(chord::accidental, Accidental.VALUES, callback = { update() }) setupPitchSpinner() - setupTypeSpinner() + setupEditor() binding.exitButton.setOnClickListener { dismiss() } return binding.root } + private fun update() { + songFragment.updateChords() + binding.chordText.text = chord.toString(true, Song.currentSong.root) + updateEditor() + } + private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { 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 = ScaleType.MAJOR.steps[it] - if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] + if (chord.note == ScaleType.MAJOR.steps[it]) { + update() + return@setup } - songFragment.updateChords() + chord.note = ScaleType.MAJOR.steps[it] + chord.accidental = Accidental.None + chord.accidentals[0] = + Accidental.VALUES[(ScaleType.MAJOR.steps[(it + 2) % ScaleType.MAJOR.steps.size] distance chord.note) - 3] + chord.accidentals[1] = + Accidental.VALUES[(ScaleType.MAJOR.steps[(it + 4) % ScaleType.MAJOR.steps.size] distance chord.note) - 6] + update() } } - private fun setupTypeSpinner() { - val values = mutableListOf("default") - for (chordType in ChordType.VALUES) { - values += chordType.toString() + private fun setupEditor() { + binding.editorGrid.removeAllViews() + val row = TableRow(binding.root.context) + for (description in descriptions) { + val text = TextView(binding.root.context) + text.text = description + text.layoutParams = UIUtil.cardLayout + text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER + row.addView(text) } - binding.typeSpinner.setup( - values, - if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 - else chord.chordType.ordinal + 1 - ) { - if (it == 0) { - chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] - } else { - chord.chordType = ChordType.VALUES[it - 1] + binding.editorGrid.addView(row) + for (accidental in Accidental.VALUES) { + val row = TableRow(binding.root.context) + for (position in 0 until Chord.NOTE_COUNT - 1) { + val button = MaterialButton(binding.root.context) + button.text = accidental.toString() + button.layoutParams = UIUtil.cardLayout + button.updateToggle(chord.accidentals[position] == accidental, R.color.blue) + button.setOnClickListener { + if (chord.accidentals[position] == accidental) { + chord.accidentals[position] = null + } else { + chord.accidentals[position] = accidental + } + update() + } + row.addView(button) } - songFragment.updateChords() + binding.editorGrid.addView(row) } } + + private fun updateEditor() { + for ((index, view) in binding.editorGrid.children.iterator().withIndex()) { + if (index == 0) { + continue + } + view as TableRow + for ((childIndex, childView) in view.children.iterator().withIndex()) { + childView as MaterialButton + childView.updateToggle( + chord.accidentals[childIndex] == Accidental.VALUES[index - 1], + R.color.blue + ) + } + } + } + + companion object { + val descriptions = arrayOf("III", "V", "VII", "IX") + } +} + +infix fun Int.distance(other: Int): Int { + var result = this - other + while (result < 0) { + result += 12 + } + result %= 12 + return result } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ 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 7beb64c..7f10ff2 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -10,11 +10,12 @@ package com.lukas.music.instruments +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note class PolyInstrument(name: String) : Instrument(name) { - private val internalInstruments = Array(3) { InternalInstrument() } - private val playing = Array(3) { false } + private val internalInstruments = Array(Chord.NOTE_COUNT) { InternalInstrument() } + private val playing = Array(Chord.NOTE_COUNT) { false } override var waveform: Waveform = Waveform.SINE set(value) { @@ -86,7 +87,7 @@ override fun updateEffects() { for (instrument in internalInstruments) { for (effect in effects) { - instrument.applyEffectAttributes(effect) + instrument.applyEffectAttributes(this, effect) } } } @@ -99,4 +100,10 @@ } return false } + + override fun moveEffects(from: Int, to: Int) { + for (instrument in internalInstruments) { + instrument.moveEffects(from, to) + } + } } \ No newline at end of file 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 index f921b1b..f659a8c 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -14,7 +14,9 @@ class Effect(val type: EffectType, private val instrument: Instrument) { val parameters = Array(type.parameterDescriptions.size) { - EffectParameter(type.parameterDescriptions[it], instrument) + type.parameterDescriptions[it]?.let { parameterDescription -> + EffectParameter(parameterDescription, instrument) + } } val influence = EffectParameter(influenceDescription, instrument) 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 index 8af39ea..8b90ec5 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -13,8 +13,8 @@ import com.lukas.music.util.format enum class EffectType( - val title: String, - val parameterDescriptions: Array + private val title: String, + val parameterDescriptions: Array ) { LowPass("low pass filter", arrayOf( @@ -22,13 +22,17 @@ "cutoff: ${it.value.format(1)} octaves" } )), - Noise("noise", + Noise( + "noise", arrayOf( - EffectParameterDescription(0f, 1f, 0f) { - "unused" - } + null ) - ) + ), + Distortion("distortion", arrayOf( + EffectParameterDescription(1f, 4f, 1f) { + "strength: ${it.value.format(1)}x" + } + )) ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt index 4c6a0d9..7807cb9 100644 --- a/app/src/main/java/com/lukas/music/song/ScaleType.kt +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -10,24 +10,12 @@ 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 - ) + arrayOf(0, 2, 4, 5, 7, 9, 11), ) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Accidental.kt b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt new file mode 100644 index 0000000..688ae4e --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or(at your option) any later version. + * + * This program is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 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.chords + +enum class Accidental(val id: String, val short: String, val distance: Int) { + Flat("\u266D", "b", -1), + None("\u266E", "", 0), + Sharp("\u266F", "#", 1), + ; + + override fun toString(): String { + return id + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index 4400dae..85e530b 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -10,14 +10,19 @@ package com.lukas.music.song.chords +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -class Chord(note: Int, var chordType: ChordType) { - var note: Int = note +class Chord { + var accidental = Accidental.None + val accidentals: Array = arrayOf(Accidental.None, Accidental.None, null, null) + + var note: Int = 0 set(value) { field = value interval = Interval(value) } + var interval = Interval(note) set(value) { field = value @@ -27,19 +32,76 @@ } fun getNotes(root: Note): Array { - return Array(chordType.notes.size) { root + note + chordType.notes[it] } + val result = Array(NOTE_COUNT) { root } + var resultIndex = 0 + var accidentalIndex = 0 + var octave = 0 + while (resultIndex < NOTE_COUNT) { + if (accidentalIndex == 0) { + result[resultIndex] = root + note + 12 * octave + accidental.distance + resultIndex++ + } else if (accidentals[accidentalIndex - 1] != null) { + result[resultIndex] = root + note + when (accidentalIndex) { + 1 -> 4 + 2 -> 7 + 3 -> 10 + 4 -> 14 + else -> 0 + } + accidentals[accidentalIndex - 1]!!.distance + 12 * octave + accidental.distance + resultIndex++ + } + accidentalIndex++ + if (accidentalIndex > accidentals.size) { + octave++ + accidentalIndex = 0 + } + } + return result } override fun toString(): String { - return chordType.transform(interval.toString()) + return toString(false, Song.currentSong.root) } fun toString(displayChordNames: Boolean, root: Note): String { - val base = if (displayChordNames) { - (root + note).noteName.toString() + var result = if (displayChordNames) { + (root + note + accidental.distance).noteName.toString() } else { interval.toString() } - return chordType.transform(base) + accidentals[0]?.let { + result += when (it) { + Accidental.Flat -> "-" + Accidental.Sharp -> "sus4" + else -> "" + } + } + accidentals[1]?.let { + if (accidentals[0] != null && it == Accidental.None) { + return@let + } + result += it.short + "5" + } + result = result.replace("-b5", "0") + result = result.replace("(?=[A-G])#5".toRegex(), "+") + accidentals[2]?.let { + result += when (it) { + Accidental.Sharp -> " maj7" + Accidental.None -> " 7" + Accidental.Flat -> " 6" + } + } + accidentals[3]?.let { + result += when (it) { + Accidental.Sharp -> " maj9" + Accidental.None -> " 9" + Accidental.Flat -> " b9" + } + } + return result + } + + companion object { + const val NOTE_COUNT = 5 } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt b/app/src/main/java/com/lukas/music/song/chords/ChordType.kt deleted file mode 100644 index 1fe4b40..0000000 --- a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt +++ /dev/null @@ -1,30 +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.chords - -enum class ChordType( - val notes: Array, - private val asString: String, - val transform: (String) -> String -) { - MAJOR(arrayOf(0, 4, 7), "major", { it.uppercase() }), - MINOR(arrayOf(0, 3, 7), "minor", { it.lowercase() }), - DIMINISHED(arrayOf(0, 3, 6), "diminished", { it.lowercase() + "0" }), - ; - - override fun toString(): String { - return asString - } - - companion object { - val VALUES = values() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt index 1cadb06..f4bcbc7 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt @@ -15,7 +15,7 @@ class Phrase : Cycle() { init { for (i in 0 until 4) { - this += Chord(0, ChordType.MAJOR) + this += Chord() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index 17bfb2b..f17e322 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -12,7 +12,7 @@ import kotlin.math.pow -class Note(private val id: Int) { +class Note(val id: Int) { val noteName = NoteName.VALUES[id % 12] val octave = id / 12 - 1 val frequency = 440 * 2.0.pow((id - 69) / 12.0) @@ -28,6 +28,8 @@ return this + (-other) } + operator fun minus(other: Note): Int = id - other.id + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false 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 index e06761a..40b1d14 100644 --- a/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -11,6 +11,7 @@ package com.lukas.music.song.voice import com.lukas.music.song.ScaleType +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note import com.lukas.music.util.transform @@ -20,7 +21,7 @@ val getNotes: (Note, Array) -> Array ) { Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), - Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + ChordVoice("Chord notes", Chord.NOTE_COUNT, { _, 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 } }), diff --git a/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt new file mode 100644 index 0000000..730eabd --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.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.ui.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.lukas.music.databinding.FragmentEffectBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.ui.fragments.EditEffectsFragment +import com.lukas.music.ui.fragments.EffectFragment + +class EffectsAdapter(private val parent: EditEffectsFragment, private val instrument: Instrument) : + RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EffectFragment { + val context = parent.context + val inflater = LayoutInflater.from(context) + val binding = FragmentEffectBinding.inflate(inflater, parent, false) + return EffectFragment(binding) + } + + override fun onBindViewHolder(holder: EffectFragment, position: Int) { + holder.setEffect(instrument.effects[position]) + } + + override fun getItemCount(): Int { + return instrument.effects.size + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt index 3e129bc..3d7d1f4 100644 --- a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt @@ -39,7 +39,7 @@ Song.currentSong.soloInstrument = instrument } field = value - binding.soloButton.updateToggle(this::solo, R.color.blue) + binding.soloButton.updateToggle(this.solo, R.color.blue) } var instrument: Instrument? = null 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 6c3bae3..5fc9dfe 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 @@ -14,61 +14,124 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.DialogFragment +import android.widget.TableRow +import android.widget.TextView +import androidx.core.view.children +import com.google.android.material.button.MaterialButton +import com.lukas.music.R import com.lukas.music.databinding.FragmentEditChordBinding import com.lukas.music.song.ScaleType import com.lukas.music.song.Song +import com.lukas.music.song.chords.Accidental 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 +import com.lukas.music.util.* class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : - DialogFragment() { - lateinit var binding: FragmentEditChordBinding + EasyDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentEditChordBinding.inflate(inflater) + Array(Accidental.VALUES.size) { + val button = MaterialButton(binding.root.context) + button.layoutParams = UIUtil.cardLayout + binding.accidentalSelection.addView(button) + return@Array button + }.setupEnumSelection(chord::accidental, Accidental.VALUES, callback = { update() }) setupPitchSpinner() - setupTypeSpinner() + setupEditor() binding.exitButton.setOnClickListener { dismiss() } return binding.root } + private fun update() { + songFragment.updateChords() + binding.chordText.text = chord.toString(true, Song.currentSong.root) + updateEditor() + } + private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { 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 = ScaleType.MAJOR.steps[it] - if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] + if (chord.note == ScaleType.MAJOR.steps[it]) { + update() + return@setup } - songFragment.updateChords() + chord.note = ScaleType.MAJOR.steps[it] + chord.accidental = Accidental.None + chord.accidentals[0] = + Accidental.VALUES[(ScaleType.MAJOR.steps[(it + 2) % ScaleType.MAJOR.steps.size] distance chord.note) - 3] + chord.accidentals[1] = + Accidental.VALUES[(ScaleType.MAJOR.steps[(it + 4) % ScaleType.MAJOR.steps.size] distance chord.note) - 6] + update() } } - private fun setupTypeSpinner() { - val values = mutableListOf("default") - for (chordType in ChordType.VALUES) { - values += chordType.toString() + private fun setupEditor() { + binding.editorGrid.removeAllViews() + val row = TableRow(binding.root.context) + for (description in descriptions) { + val text = TextView(binding.root.context) + text.text = description + text.layoutParams = UIUtil.cardLayout + text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER + row.addView(text) } - binding.typeSpinner.setup( - values, - if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 - else chord.chordType.ordinal + 1 - ) { - if (it == 0) { - chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] - } else { - chord.chordType = ChordType.VALUES[it - 1] + binding.editorGrid.addView(row) + for (accidental in Accidental.VALUES) { + val row = TableRow(binding.root.context) + for (position in 0 until Chord.NOTE_COUNT - 1) { + val button = MaterialButton(binding.root.context) + button.text = accidental.toString() + button.layoutParams = UIUtil.cardLayout + button.updateToggle(chord.accidentals[position] == accidental, R.color.blue) + button.setOnClickListener { + if (chord.accidentals[position] == accidental) { + chord.accidentals[position] = null + } else { + chord.accidentals[position] = accidental + } + update() + } + row.addView(button) } - songFragment.updateChords() + binding.editorGrid.addView(row) } } + + private fun updateEditor() { + for ((index, view) in binding.editorGrid.children.iterator().withIndex()) { + if (index == 0) { + continue + } + view as TableRow + for ((childIndex, childView) in view.children.iterator().withIndex()) { + childView as MaterialButton + childView.updateToggle( + chord.accidentals[childIndex] == Accidental.VALUES[index - 1], + R.color.blue + ) + } + } + } + + companion object { + val descriptions = arrayOf("III", "V", "VII", "IX") + } +} + +infix fun Int.distance(other: Int): Int { + var result = this - other + while (result < 0) { + result += 12 + } + result %= 12 + return result } \ No newline at end of file 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 index 6e359e4..0d532c2 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt @@ -14,9 +14,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager import com.lukas.music.databinding.FragmentEditEffectsBinding import com.lukas.music.instruments.Instrument +import com.lukas.music.ui.adapters.EffectsAdapter import com.lukas.music.util.EasyDialogFragment +import com.lukas.music.util.makeMoveCallback class EditEffectsFragment(private val instrument: Instrument) : EasyDialogFragment() { @@ -25,11 +29,12 @@ 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.effectsDisplay.adapter = EffectsAdapter(this, instrument) + binding.effectsDisplay.layoutManager = LinearLayoutManager(context) + val helper = ItemTouchHelper(makeMoveCallback(instrument.effects) { from, to -> + instrument.moveEffects(from, to) + }) + helper.attachToRecyclerView(binding.effectsDisplay) binding.closeButton.setOnClickListener { dismiss() } diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ 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 7beb64c..7f10ff2 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -10,11 +10,12 @@ package com.lukas.music.instruments +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note class PolyInstrument(name: String) : Instrument(name) { - private val internalInstruments = Array(3) { InternalInstrument() } - private val playing = Array(3) { false } + private val internalInstruments = Array(Chord.NOTE_COUNT) { InternalInstrument() } + private val playing = Array(Chord.NOTE_COUNT) { false } override var waveform: Waveform = Waveform.SINE set(value) { @@ -86,7 +87,7 @@ override fun updateEffects() { for (instrument in internalInstruments) { for (effect in effects) { - instrument.applyEffectAttributes(effect) + instrument.applyEffectAttributes(this, effect) } } } @@ -99,4 +100,10 @@ } return false } + + override fun moveEffects(from: Int, to: Int) { + for (instrument in internalInstruments) { + instrument.moveEffects(from, to) + } + } } \ No newline at end of file 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 index f921b1b..f659a8c 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -14,7 +14,9 @@ class Effect(val type: EffectType, private val instrument: Instrument) { val parameters = Array(type.parameterDescriptions.size) { - EffectParameter(type.parameterDescriptions[it], instrument) + type.parameterDescriptions[it]?.let { parameterDescription -> + EffectParameter(parameterDescription, instrument) + } } val influence = EffectParameter(influenceDescription, instrument) 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 index 8af39ea..8b90ec5 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -13,8 +13,8 @@ import com.lukas.music.util.format enum class EffectType( - val title: String, - val parameterDescriptions: Array + private val title: String, + val parameterDescriptions: Array ) { LowPass("low pass filter", arrayOf( @@ -22,13 +22,17 @@ "cutoff: ${it.value.format(1)} octaves" } )), - Noise("noise", + Noise( + "noise", arrayOf( - EffectParameterDescription(0f, 1f, 0f) { - "unused" - } + null ) - ) + ), + Distortion("distortion", arrayOf( + EffectParameterDescription(1f, 4f, 1f) { + "strength: ${it.value.format(1)}x" + } + )) ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt index 4c6a0d9..7807cb9 100644 --- a/app/src/main/java/com/lukas/music/song/ScaleType.kt +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -10,24 +10,12 @@ 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 - ) + arrayOf(0, 2, 4, 5, 7, 9, 11), ) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Accidental.kt b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt new file mode 100644 index 0000000..688ae4e --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or(at your option) any later version. + * + * This program is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 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.chords + +enum class Accidental(val id: String, val short: String, val distance: Int) { + Flat("\u266D", "b", -1), + None("\u266E", "", 0), + Sharp("\u266F", "#", 1), + ; + + override fun toString(): String { + return id + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index 4400dae..85e530b 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -10,14 +10,19 @@ package com.lukas.music.song.chords +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -class Chord(note: Int, var chordType: ChordType) { - var note: Int = note +class Chord { + var accidental = Accidental.None + val accidentals: Array = arrayOf(Accidental.None, Accidental.None, null, null) + + var note: Int = 0 set(value) { field = value interval = Interval(value) } + var interval = Interval(note) set(value) { field = value @@ -27,19 +32,76 @@ } fun getNotes(root: Note): Array { - return Array(chordType.notes.size) { root + note + chordType.notes[it] } + val result = Array(NOTE_COUNT) { root } + var resultIndex = 0 + var accidentalIndex = 0 + var octave = 0 + while (resultIndex < NOTE_COUNT) { + if (accidentalIndex == 0) { + result[resultIndex] = root + note + 12 * octave + accidental.distance + resultIndex++ + } else if (accidentals[accidentalIndex - 1] != null) { + result[resultIndex] = root + note + when (accidentalIndex) { + 1 -> 4 + 2 -> 7 + 3 -> 10 + 4 -> 14 + else -> 0 + } + accidentals[accidentalIndex - 1]!!.distance + 12 * octave + accidental.distance + resultIndex++ + } + accidentalIndex++ + if (accidentalIndex > accidentals.size) { + octave++ + accidentalIndex = 0 + } + } + return result } override fun toString(): String { - return chordType.transform(interval.toString()) + return toString(false, Song.currentSong.root) } fun toString(displayChordNames: Boolean, root: Note): String { - val base = if (displayChordNames) { - (root + note).noteName.toString() + var result = if (displayChordNames) { + (root + note + accidental.distance).noteName.toString() } else { interval.toString() } - return chordType.transform(base) + accidentals[0]?.let { + result += when (it) { + Accidental.Flat -> "-" + Accidental.Sharp -> "sus4" + else -> "" + } + } + accidentals[1]?.let { + if (accidentals[0] != null && it == Accidental.None) { + return@let + } + result += it.short + "5" + } + result = result.replace("-b5", "0") + result = result.replace("(?=[A-G])#5".toRegex(), "+") + accidentals[2]?.let { + result += when (it) { + Accidental.Sharp -> " maj7" + Accidental.None -> " 7" + Accidental.Flat -> " 6" + } + } + accidentals[3]?.let { + result += when (it) { + Accidental.Sharp -> " maj9" + Accidental.None -> " 9" + Accidental.Flat -> " b9" + } + } + return result + } + + companion object { + const val NOTE_COUNT = 5 } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt b/app/src/main/java/com/lukas/music/song/chords/ChordType.kt deleted file mode 100644 index 1fe4b40..0000000 --- a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt +++ /dev/null @@ -1,30 +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.chords - -enum class ChordType( - val notes: Array, - private val asString: String, - val transform: (String) -> String -) { - MAJOR(arrayOf(0, 4, 7), "major", { it.uppercase() }), - MINOR(arrayOf(0, 3, 7), "minor", { it.lowercase() }), - DIMINISHED(arrayOf(0, 3, 6), "diminished", { it.lowercase() + "0" }), - ; - - override fun toString(): String { - return asString - } - - companion object { - val VALUES = values() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt index 1cadb06..f4bcbc7 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt @@ -15,7 +15,7 @@ class Phrase : Cycle() { init { for (i in 0 until 4) { - this += Chord(0, ChordType.MAJOR) + this += Chord() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index 17bfb2b..f17e322 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -12,7 +12,7 @@ import kotlin.math.pow -class Note(private val id: Int) { +class Note(val id: Int) { val noteName = NoteName.VALUES[id % 12] val octave = id / 12 - 1 val frequency = 440 * 2.0.pow((id - 69) / 12.0) @@ -28,6 +28,8 @@ return this + (-other) } + operator fun minus(other: Note): Int = id - other.id + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false 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 index e06761a..40b1d14 100644 --- a/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -11,6 +11,7 @@ package com.lukas.music.song.voice import com.lukas.music.song.ScaleType +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note import com.lukas.music.util.transform @@ -20,7 +21,7 @@ val getNotes: (Note, Array) -> Array ) { Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), - Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + ChordVoice("Chord notes", Chord.NOTE_COUNT, { _, 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 } }), diff --git a/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt new file mode 100644 index 0000000..730eabd --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.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.ui.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.lukas.music.databinding.FragmentEffectBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.ui.fragments.EditEffectsFragment +import com.lukas.music.ui.fragments.EffectFragment + +class EffectsAdapter(private val parent: EditEffectsFragment, private val instrument: Instrument) : + RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EffectFragment { + val context = parent.context + val inflater = LayoutInflater.from(context) + val binding = FragmentEffectBinding.inflate(inflater, parent, false) + return EffectFragment(binding) + } + + override fun onBindViewHolder(holder: EffectFragment, position: Int) { + holder.setEffect(instrument.effects[position]) + } + + override fun getItemCount(): Int { + return instrument.effects.size + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt index 3e129bc..3d7d1f4 100644 --- a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt @@ -39,7 +39,7 @@ Song.currentSong.soloInstrument = instrument } field = value - binding.soloButton.updateToggle(this::solo, R.color.blue) + binding.soloButton.updateToggle(this.solo, R.color.blue) } var instrument: Instrument? = null 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 6c3bae3..5fc9dfe 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 @@ -14,61 +14,124 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.DialogFragment +import android.widget.TableRow +import android.widget.TextView +import androidx.core.view.children +import com.google.android.material.button.MaterialButton +import com.lukas.music.R import com.lukas.music.databinding.FragmentEditChordBinding import com.lukas.music.song.ScaleType import com.lukas.music.song.Song +import com.lukas.music.song.chords.Accidental 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 +import com.lukas.music.util.* class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : - DialogFragment() { - lateinit var binding: FragmentEditChordBinding + EasyDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentEditChordBinding.inflate(inflater) + Array(Accidental.VALUES.size) { + val button = MaterialButton(binding.root.context) + button.layoutParams = UIUtil.cardLayout + binding.accidentalSelection.addView(button) + return@Array button + }.setupEnumSelection(chord::accidental, Accidental.VALUES, callback = { update() }) setupPitchSpinner() - setupTypeSpinner() + setupEditor() binding.exitButton.setOnClickListener { dismiss() } return binding.root } + private fun update() { + songFragment.updateChords() + binding.chordText.text = chord.toString(true, Song.currentSong.root) + updateEditor() + } + private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { 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 = ScaleType.MAJOR.steps[it] - if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] + if (chord.note == ScaleType.MAJOR.steps[it]) { + update() + return@setup } - songFragment.updateChords() + chord.note = ScaleType.MAJOR.steps[it] + chord.accidental = Accidental.None + chord.accidentals[0] = + Accidental.VALUES[(ScaleType.MAJOR.steps[(it + 2) % ScaleType.MAJOR.steps.size] distance chord.note) - 3] + chord.accidentals[1] = + Accidental.VALUES[(ScaleType.MAJOR.steps[(it + 4) % ScaleType.MAJOR.steps.size] distance chord.note) - 6] + update() } } - private fun setupTypeSpinner() { - val values = mutableListOf("default") - for (chordType in ChordType.VALUES) { - values += chordType.toString() + private fun setupEditor() { + binding.editorGrid.removeAllViews() + val row = TableRow(binding.root.context) + for (description in descriptions) { + val text = TextView(binding.root.context) + text.text = description + text.layoutParams = UIUtil.cardLayout + text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER + row.addView(text) } - binding.typeSpinner.setup( - values, - if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 - else chord.chordType.ordinal + 1 - ) { - if (it == 0) { - chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] - } else { - chord.chordType = ChordType.VALUES[it - 1] + binding.editorGrid.addView(row) + for (accidental in Accidental.VALUES) { + val row = TableRow(binding.root.context) + for (position in 0 until Chord.NOTE_COUNT - 1) { + val button = MaterialButton(binding.root.context) + button.text = accidental.toString() + button.layoutParams = UIUtil.cardLayout + button.updateToggle(chord.accidentals[position] == accidental, R.color.blue) + button.setOnClickListener { + if (chord.accidentals[position] == accidental) { + chord.accidentals[position] = null + } else { + chord.accidentals[position] = accidental + } + update() + } + row.addView(button) } - songFragment.updateChords() + binding.editorGrid.addView(row) } } + + private fun updateEditor() { + for ((index, view) in binding.editorGrid.children.iterator().withIndex()) { + if (index == 0) { + continue + } + view as TableRow + for ((childIndex, childView) in view.children.iterator().withIndex()) { + childView as MaterialButton + childView.updateToggle( + chord.accidentals[childIndex] == Accidental.VALUES[index - 1], + R.color.blue + ) + } + } + } + + companion object { + val descriptions = arrayOf("III", "V", "VII", "IX") + } +} + +infix fun Int.distance(other: Int): Int { + var result = this - other + while (result < 0) { + result += 12 + } + result %= 12 + return result } \ No newline at end of file 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 index 6e359e4..0d532c2 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt @@ -14,9 +14,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager import com.lukas.music.databinding.FragmentEditEffectsBinding import com.lukas.music.instruments.Instrument +import com.lukas.music.ui.adapters.EffectsAdapter import com.lukas.music.util.EasyDialogFragment +import com.lukas.music.util.makeMoveCallback class EditEffectsFragment(private val instrument: Instrument) : EasyDialogFragment() { @@ -25,11 +29,12 @@ 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.effectsDisplay.adapter = EffectsAdapter(this, instrument) + binding.effectsDisplay.layoutManager = LinearLayoutManager(context) + val helper = ItemTouchHelper(makeMoveCallback(instrument.effects) { from, to -> + instrument.moveEffects(from, to) + }) + helper.attachToRecyclerView(binding.effectsDisplay) binding.closeButton.setOnClickListener { dismiss() } 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 index ebf4cb1..7b13ed6 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt @@ -10,25 +10,18 @@ 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 androidx.recyclerview.widget.RecyclerView 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) +class EffectFragment(val binding: FragmentEffectBinding) : RecyclerView.ViewHolder( + binding.root +) { + fun setEffect(effect: Effect) { binding.effectName.text = effect.type.toString() binding.activeButton.setupToggle(effect::active, R.color.blue) { binding.activeButton.text = if (it) "ON" else "OFF" @@ -37,10 +30,15 @@ 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]) + binding.parameter1SeekBar.visibility = + if (effect.parameters[0] == null) View.GONE else View.VISIBLE + binding.parameter1Text.visibility = + if (effect.parameters[0] == null) View.GONE else View.VISIBLE + effect.parameters[0]?.let { + binding.parameter1SeekBar.smartSetup(0, 100, it::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 b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ 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 7beb64c..7f10ff2 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -10,11 +10,12 @@ package com.lukas.music.instruments +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note class PolyInstrument(name: String) : Instrument(name) { - private val internalInstruments = Array(3) { InternalInstrument() } - private val playing = Array(3) { false } + private val internalInstruments = Array(Chord.NOTE_COUNT) { InternalInstrument() } + private val playing = Array(Chord.NOTE_COUNT) { false } override var waveform: Waveform = Waveform.SINE set(value) { @@ -86,7 +87,7 @@ override fun updateEffects() { for (instrument in internalInstruments) { for (effect in effects) { - instrument.applyEffectAttributes(effect) + instrument.applyEffectAttributes(this, effect) } } } @@ -99,4 +100,10 @@ } return false } + + override fun moveEffects(from: Int, to: Int) { + for (instrument in internalInstruments) { + instrument.moveEffects(from, to) + } + } } \ No newline at end of file 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 index f921b1b..f659a8c 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -14,7 +14,9 @@ class Effect(val type: EffectType, private val instrument: Instrument) { val parameters = Array(type.parameterDescriptions.size) { - EffectParameter(type.parameterDescriptions[it], instrument) + type.parameterDescriptions[it]?.let { parameterDescription -> + EffectParameter(parameterDescription, instrument) + } } val influence = EffectParameter(influenceDescription, instrument) 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 index 8af39ea..8b90ec5 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -13,8 +13,8 @@ import com.lukas.music.util.format enum class EffectType( - val title: String, - val parameterDescriptions: Array + private val title: String, + val parameterDescriptions: Array ) { LowPass("low pass filter", arrayOf( @@ -22,13 +22,17 @@ "cutoff: ${it.value.format(1)} octaves" } )), - Noise("noise", + Noise( + "noise", arrayOf( - EffectParameterDescription(0f, 1f, 0f) { - "unused" - } + null ) - ) + ), + Distortion("distortion", arrayOf( + EffectParameterDescription(1f, 4f, 1f) { + "strength: ${it.value.format(1)}x" + } + )) ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt index 4c6a0d9..7807cb9 100644 --- a/app/src/main/java/com/lukas/music/song/ScaleType.kt +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -10,24 +10,12 @@ 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 - ) + arrayOf(0, 2, 4, 5, 7, 9, 11), ) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Accidental.kt b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt new file mode 100644 index 0000000..688ae4e --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or(at your option) any later version. + * + * This program is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 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.chords + +enum class Accidental(val id: String, val short: String, val distance: Int) { + Flat("\u266D", "b", -1), + None("\u266E", "", 0), + Sharp("\u266F", "#", 1), + ; + + override fun toString(): String { + return id + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index 4400dae..85e530b 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -10,14 +10,19 @@ package com.lukas.music.song.chords +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -class Chord(note: Int, var chordType: ChordType) { - var note: Int = note +class Chord { + var accidental = Accidental.None + val accidentals: Array = arrayOf(Accidental.None, Accidental.None, null, null) + + var note: Int = 0 set(value) { field = value interval = Interval(value) } + var interval = Interval(note) set(value) { field = value @@ -27,19 +32,76 @@ } fun getNotes(root: Note): Array { - return Array(chordType.notes.size) { root + note + chordType.notes[it] } + val result = Array(NOTE_COUNT) { root } + var resultIndex = 0 + var accidentalIndex = 0 + var octave = 0 + while (resultIndex < NOTE_COUNT) { + if (accidentalIndex == 0) { + result[resultIndex] = root + note + 12 * octave + accidental.distance + resultIndex++ + } else if (accidentals[accidentalIndex - 1] != null) { + result[resultIndex] = root + note + when (accidentalIndex) { + 1 -> 4 + 2 -> 7 + 3 -> 10 + 4 -> 14 + else -> 0 + } + accidentals[accidentalIndex - 1]!!.distance + 12 * octave + accidental.distance + resultIndex++ + } + accidentalIndex++ + if (accidentalIndex > accidentals.size) { + octave++ + accidentalIndex = 0 + } + } + return result } override fun toString(): String { - return chordType.transform(interval.toString()) + return toString(false, Song.currentSong.root) } fun toString(displayChordNames: Boolean, root: Note): String { - val base = if (displayChordNames) { - (root + note).noteName.toString() + var result = if (displayChordNames) { + (root + note + accidental.distance).noteName.toString() } else { interval.toString() } - return chordType.transform(base) + accidentals[0]?.let { + result += when (it) { + Accidental.Flat -> "-" + Accidental.Sharp -> "sus4" + else -> "" + } + } + accidentals[1]?.let { + if (accidentals[0] != null && it == Accidental.None) { + return@let + } + result += it.short + "5" + } + result = result.replace("-b5", "0") + result = result.replace("(?=[A-G])#5".toRegex(), "+") + accidentals[2]?.let { + result += when (it) { + Accidental.Sharp -> " maj7" + Accidental.None -> " 7" + Accidental.Flat -> " 6" + } + } + accidentals[3]?.let { + result += when (it) { + Accidental.Sharp -> " maj9" + Accidental.None -> " 9" + Accidental.Flat -> " b9" + } + } + return result + } + + companion object { + const val NOTE_COUNT = 5 } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt b/app/src/main/java/com/lukas/music/song/chords/ChordType.kt deleted file mode 100644 index 1fe4b40..0000000 --- a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt +++ /dev/null @@ -1,30 +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.chords - -enum class ChordType( - val notes: Array, - private val asString: String, - val transform: (String) -> String -) { - MAJOR(arrayOf(0, 4, 7), "major", { it.uppercase() }), - MINOR(arrayOf(0, 3, 7), "minor", { it.lowercase() }), - DIMINISHED(arrayOf(0, 3, 6), "diminished", { it.lowercase() + "0" }), - ; - - override fun toString(): String { - return asString - } - - companion object { - val VALUES = values() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt index 1cadb06..f4bcbc7 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt @@ -15,7 +15,7 @@ class Phrase : Cycle() { init { for (i in 0 until 4) { - this += Chord(0, ChordType.MAJOR) + this += Chord() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index 17bfb2b..f17e322 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -12,7 +12,7 @@ import kotlin.math.pow -class Note(private val id: Int) { +class Note(val id: Int) { val noteName = NoteName.VALUES[id % 12] val octave = id / 12 - 1 val frequency = 440 * 2.0.pow((id - 69) / 12.0) @@ -28,6 +28,8 @@ return this + (-other) } + operator fun minus(other: Note): Int = id - other.id + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false 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 index e06761a..40b1d14 100644 --- a/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -11,6 +11,7 @@ package com.lukas.music.song.voice import com.lukas.music.song.ScaleType +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note import com.lukas.music.util.transform @@ -20,7 +21,7 @@ val getNotes: (Note, Array) -> Array ) { Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), - Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + ChordVoice("Chord notes", Chord.NOTE_COUNT, { _, 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 } }), diff --git a/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt new file mode 100644 index 0000000..730eabd --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.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.ui.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.lukas.music.databinding.FragmentEffectBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.ui.fragments.EditEffectsFragment +import com.lukas.music.ui.fragments.EffectFragment + +class EffectsAdapter(private val parent: EditEffectsFragment, private val instrument: Instrument) : + RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EffectFragment { + val context = parent.context + val inflater = LayoutInflater.from(context) + val binding = FragmentEffectBinding.inflate(inflater, parent, false) + return EffectFragment(binding) + } + + override fun onBindViewHolder(holder: EffectFragment, position: Int) { + holder.setEffect(instrument.effects[position]) + } + + override fun getItemCount(): Int { + return instrument.effects.size + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt index 3e129bc..3d7d1f4 100644 --- a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt @@ -39,7 +39,7 @@ Song.currentSong.soloInstrument = instrument } field = value - binding.soloButton.updateToggle(this::solo, R.color.blue) + binding.soloButton.updateToggle(this.solo, R.color.blue) } var instrument: Instrument? = null 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 6c3bae3..5fc9dfe 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 @@ -14,61 +14,124 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.DialogFragment +import android.widget.TableRow +import android.widget.TextView +import androidx.core.view.children +import com.google.android.material.button.MaterialButton +import com.lukas.music.R import com.lukas.music.databinding.FragmentEditChordBinding import com.lukas.music.song.ScaleType import com.lukas.music.song.Song +import com.lukas.music.song.chords.Accidental 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 +import com.lukas.music.util.* class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : - DialogFragment() { - lateinit var binding: FragmentEditChordBinding + EasyDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentEditChordBinding.inflate(inflater) + Array(Accidental.VALUES.size) { + val button = MaterialButton(binding.root.context) + button.layoutParams = UIUtil.cardLayout + binding.accidentalSelection.addView(button) + return@Array button + }.setupEnumSelection(chord::accidental, Accidental.VALUES, callback = { update() }) setupPitchSpinner() - setupTypeSpinner() + setupEditor() binding.exitButton.setOnClickListener { dismiss() } return binding.root } + private fun update() { + songFragment.updateChords() + binding.chordText.text = chord.toString(true, Song.currentSong.root) + updateEditor() + } + private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { 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 = ScaleType.MAJOR.steps[it] - if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] + if (chord.note == ScaleType.MAJOR.steps[it]) { + update() + return@setup } - songFragment.updateChords() + chord.note = ScaleType.MAJOR.steps[it] + chord.accidental = Accidental.None + chord.accidentals[0] = + Accidental.VALUES[(ScaleType.MAJOR.steps[(it + 2) % ScaleType.MAJOR.steps.size] distance chord.note) - 3] + chord.accidentals[1] = + Accidental.VALUES[(ScaleType.MAJOR.steps[(it + 4) % ScaleType.MAJOR.steps.size] distance chord.note) - 6] + update() } } - private fun setupTypeSpinner() { - val values = mutableListOf("default") - for (chordType in ChordType.VALUES) { - values += chordType.toString() + private fun setupEditor() { + binding.editorGrid.removeAllViews() + val row = TableRow(binding.root.context) + for (description in descriptions) { + val text = TextView(binding.root.context) + text.text = description + text.layoutParams = UIUtil.cardLayout + text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER + row.addView(text) } - binding.typeSpinner.setup( - values, - if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 - else chord.chordType.ordinal + 1 - ) { - if (it == 0) { - chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] - } else { - chord.chordType = ChordType.VALUES[it - 1] + binding.editorGrid.addView(row) + for (accidental in Accidental.VALUES) { + val row = TableRow(binding.root.context) + for (position in 0 until Chord.NOTE_COUNT - 1) { + val button = MaterialButton(binding.root.context) + button.text = accidental.toString() + button.layoutParams = UIUtil.cardLayout + button.updateToggle(chord.accidentals[position] == accidental, R.color.blue) + button.setOnClickListener { + if (chord.accidentals[position] == accidental) { + chord.accidentals[position] = null + } else { + chord.accidentals[position] = accidental + } + update() + } + row.addView(button) } - songFragment.updateChords() + binding.editorGrid.addView(row) } } + + private fun updateEditor() { + for ((index, view) in binding.editorGrid.children.iterator().withIndex()) { + if (index == 0) { + continue + } + view as TableRow + for ((childIndex, childView) in view.children.iterator().withIndex()) { + childView as MaterialButton + childView.updateToggle( + chord.accidentals[childIndex] == Accidental.VALUES[index - 1], + R.color.blue + ) + } + } + } + + companion object { + val descriptions = arrayOf("III", "V", "VII", "IX") + } +} + +infix fun Int.distance(other: Int): Int { + var result = this - other + while (result < 0) { + result += 12 + } + result %= 12 + return result } \ No newline at end of file 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 index 6e359e4..0d532c2 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt @@ -14,9 +14,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager import com.lukas.music.databinding.FragmentEditEffectsBinding import com.lukas.music.instruments.Instrument +import com.lukas.music.ui.adapters.EffectsAdapter import com.lukas.music.util.EasyDialogFragment +import com.lukas.music.util.makeMoveCallback class EditEffectsFragment(private val instrument: Instrument) : EasyDialogFragment() { @@ -25,11 +29,12 @@ 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.effectsDisplay.adapter = EffectsAdapter(this, instrument) + binding.effectsDisplay.layoutManager = LinearLayoutManager(context) + val helper = ItemTouchHelper(makeMoveCallback(instrument.effects) { from, to -> + instrument.moveEffects(from, to) + }) + helper.attachToRecyclerView(binding.effectsDisplay) binding.closeButton.setOnClickListener { dismiss() } 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 index ebf4cb1..7b13ed6 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt @@ -10,25 +10,18 @@ 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 androidx.recyclerview.widget.RecyclerView 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) +class EffectFragment(val binding: FragmentEffectBinding) : RecyclerView.ViewHolder( + binding.root +) { + fun setEffect(effect: Effect) { binding.effectName.text = effect.type.toString() binding.activeButton.setupToggle(effect::active, R.color.blue) { binding.activeButton.text = if (it) "ON" else "OFF" @@ -37,10 +30,15 @@ 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]) + binding.parameter1SeekBar.visibility = + if (effect.parameters[0] == null) View.GONE else View.VISIBLE + binding.parameter1Text.visibility = + if (effect.parameters[0] == null) View.GONE else View.VISIBLE + effect.parameters[0]?.let { + binding.parameter1SeekBar.smartSetup(0, 100, it::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/ui/fragments/InstrumentListFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt index 1d35c10..0b66892 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt @@ -24,6 +24,7 @@ import com.lukas.music.instruments.MonoInstrument import com.lukas.music.instruments.PolyInstrument import com.lukas.music.ui.adapters.InstrumentAdapter +import com.lukas.music.util.makeMoveCallback class InstrumentListFragment : Fragment() { lateinit var binding: FragmentInstrumentListBinding @@ -35,32 +36,7 @@ binding = FragmentInstrumentListBinding.inflate(inflater) binding.recyclerView.adapter = InstrumentAdapter(this) binding.recyclerView.layoutManager = LinearLayoutManager(context) - val callback = object : ItemTouchHelper.SimpleCallback( - ItemTouchHelper.UP or ItemTouchHelper.DOWN, - 0 - ) { - override fun onMove( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean { - val adapter = recyclerView.adapter as InstrumentAdapter - val startPosition = viewHolder.adapterPosition - val endPosition = target.adapterPosition - val instrument = Instrument.instruments[startPosition] - Instrument.instruments.removeAt(startPosition) - if (endPosition < startPosition) { - Instrument.instruments.add(endPosition + 1, instrument) - } else { - Instrument.instruments.add(endPosition - 1, instrument) - } - adapter.notifyItemMoved(startPosition, endPosition) - return true - } - - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} - } - val helper = ItemTouchHelper(callback) + val helper = ItemTouchHelper(makeMoveCallback(Instrument.instruments)) helper.attachToRecyclerView(binding.recyclerView) val builder = AlertDialog.Builder(binding.root.context) diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ 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 7beb64c..7f10ff2 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -10,11 +10,12 @@ package com.lukas.music.instruments +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note class PolyInstrument(name: String) : Instrument(name) { - private val internalInstruments = Array(3) { InternalInstrument() } - private val playing = Array(3) { false } + private val internalInstruments = Array(Chord.NOTE_COUNT) { InternalInstrument() } + private val playing = Array(Chord.NOTE_COUNT) { false } override var waveform: Waveform = Waveform.SINE set(value) { @@ -86,7 +87,7 @@ override fun updateEffects() { for (instrument in internalInstruments) { for (effect in effects) { - instrument.applyEffectAttributes(effect) + instrument.applyEffectAttributes(this, effect) } } } @@ -99,4 +100,10 @@ } return false } + + override fun moveEffects(from: Int, to: Int) { + for (instrument in internalInstruments) { + instrument.moveEffects(from, to) + } + } } \ No newline at end of file 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 index f921b1b..f659a8c 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -14,7 +14,9 @@ class Effect(val type: EffectType, private val instrument: Instrument) { val parameters = Array(type.parameterDescriptions.size) { - EffectParameter(type.parameterDescriptions[it], instrument) + type.parameterDescriptions[it]?.let { parameterDescription -> + EffectParameter(parameterDescription, instrument) + } } val influence = EffectParameter(influenceDescription, instrument) 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 index 8af39ea..8b90ec5 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -13,8 +13,8 @@ import com.lukas.music.util.format enum class EffectType( - val title: String, - val parameterDescriptions: Array + private val title: String, + val parameterDescriptions: Array ) { LowPass("low pass filter", arrayOf( @@ -22,13 +22,17 @@ "cutoff: ${it.value.format(1)} octaves" } )), - Noise("noise", + Noise( + "noise", arrayOf( - EffectParameterDescription(0f, 1f, 0f) { - "unused" - } + null ) - ) + ), + Distortion("distortion", arrayOf( + EffectParameterDescription(1f, 4f, 1f) { + "strength: ${it.value.format(1)}x" + } + )) ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt index 4c6a0d9..7807cb9 100644 --- a/app/src/main/java/com/lukas/music/song/ScaleType.kt +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -10,24 +10,12 @@ 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 - ) + arrayOf(0, 2, 4, 5, 7, 9, 11), ) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Accidental.kt b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt new file mode 100644 index 0000000..688ae4e --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or(at your option) any later version. + * + * This program is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 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.chords + +enum class Accidental(val id: String, val short: String, val distance: Int) { + Flat("\u266D", "b", -1), + None("\u266E", "", 0), + Sharp("\u266F", "#", 1), + ; + + override fun toString(): String { + return id + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index 4400dae..85e530b 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -10,14 +10,19 @@ package com.lukas.music.song.chords +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -class Chord(note: Int, var chordType: ChordType) { - var note: Int = note +class Chord { + var accidental = Accidental.None + val accidentals: Array = arrayOf(Accidental.None, Accidental.None, null, null) + + var note: Int = 0 set(value) { field = value interval = Interval(value) } + var interval = Interval(note) set(value) { field = value @@ -27,19 +32,76 @@ } fun getNotes(root: Note): Array { - return Array(chordType.notes.size) { root + note + chordType.notes[it] } + val result = Array(NOTE_COUNT) { root } + var resultIndex = 0 + var accidentalIndex = 0 + var octave = 0 + while (resultIndex < NOTE_COUNT) { + if (accidentalIndex == 0) { + result[resultIndex] = root + note + 12 * octave + accidental.distance + resultIndex++ + } else if (accidentals[accidentalIndex - 1] != null) { + result[resultIndex] = root + note + when (accidentalIndex) { + 1 -> 4 + 2 -> 7 + 3 -> 10 + 4 -> 14 + else -> 0 + } + accidentals[accidentalIndex - 1]!!.distance + 12 * octave + accidental.distance + resultIndex++ + } + accidentalIndex++ + if (accidentalIndex > accidentals.size) { + octave++ + accidentalIndex = 0 + } + } + return result } override fun toString(): String { - return chordType.transform(interval.toString()) + return toString(false, Song.currentSong.root) } fun toString(displayChordNames: Boolean, root: Note): String { - val base = if (displayChordNames) { - (root + note).noteName.toString() + var result = if (displayChordNames) { + (root + note + accidental.distance).noteName.toString() } else { interval.toString() } - return chordType.transform(base) + accidentals[0]?.let { + result += when (it) { + Accidental.Flat -> "-" + Accidental.Sharp -> "sus4" + else -> "" + } + } + accidentals[1]?.let { + if (accidentals[0] != null && it == Accidental.None) { + return@let + } + result += it.short + "5" + } + result = result.replace("-b5", "0") + result = result.replace("(?=[A-G])#5".toRegex(), "+") + accidentals[2]?.let { + result += when (it) { + Accidental.Sharp -> " maj7" + Accidental.None -> " 7" + Accidental.Flat -> " 6" + } + } + accidentals[3]?.let { + result += when (it) { + Accidental.Sharp -> " maj9" + Accidental.None -> " 9" + Accidental.Flat -> " b9" + } + } + return result + } + + companion object { + const val NOTE_COUNT = 5 } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt b/app/src/main/java/com/lukas/music/song/chords/ChordType.kt deleted file mode 100644 index 1fe4b40..0000000 --- a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt +++ /dev/null @@ -1,30 +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.chords - -enum class ChordType( - val notes: Array, - private val asString: String, - val transform: (String) -> String -) { - MAJOR(arrayOf(0, 4, 7), "major", { it.uppercase() }), - MINOR(arrayOf(0, 3, 7), "minor", { it.lowercase() }), - DIMINISHED(arrayOf(0, 3, 6), "diminished", { it.lowercase() + "0" }), - ; - - override fun toString(): String { - return asString - } - - companion object { - val VALUES = values() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt index 1cadb06..f4bcbc7 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt @@ -15,7 +15,7 @@ class Phrase : Cycle() { init { for (i in 0 until 4) { - this += Chord(0, ChordType.MAJOR) + this += Chord() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index 17bfb2b..f17e322 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -12,7 +12,7 @@ import kotlin.math.pow -class Note(private val id: Int) { +class Note(val id: Int) { val noteName = NoteName.VALUES[id % 12] val octave = id / 12 - 1 val frequency = 440 * 2.0.pow((id - 69) / 12.0) @@ -28,6 +28,8 @@ return this + (-other) } + operator fun minus(other: Note): Int = id - other.id + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false 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 index e06761a..40b1d14 100644 --- a/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -11,6 +11,7 @@ package com.lukas.music.song.voice import com.lukas.music.song.ScaleType +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note import com.lukas.music.util.transform @@ -20,7 +21,7 @@ val getNotes: (Note, Array) -> Array ) { Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), - Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + ChordVoice("Chord notes", Chord.NOTE_COUNT, { _, 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 } }), diff --git a/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt new file mode 100644 index 0000000..730eabd --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.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.ui.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.lukas.music.databinding.FragmentEffectBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.ui.fragments.EditEffectsFragment +import com.lukas.music.ui.fragments.EffectFragment + +class EffectsAdapter(private val parent: EditEffectsFragment, private val instrument: Instrument) : + RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EffectFragment { + val context = parent.context + val inflater = LayoutInflater.from(context) + val binding = FragmentEffectBinding.inflate(inflater, parent, false) + return EffectFragment(binding) + } + + override fun onBindViewHolder(holder: EffectFragment, position: Int) { + holder.setEffect(instrument.effects[position]) + } + + override fun getItemCount(): Int { + return instrument.effects.size + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt index 3e129bc..3d7d1f4 100644 --- a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt @@ -39,7 +39,7 @@ Song.currentSong.soloInstrument = instrument } field = value - binding.soloButton.updateToggle(this::solo, R.color.blue) + binding.soloButton.updateToggle(this.solo, R.color.blue) } var instrument: Instrument? = null 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 6c3bae3..5fc9dfe 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 @@ -14,61 +14,124 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.DialogFragment +import android.widget.TableRow +import android.widget.TextView +import androidx.core.view.children +import com.google.android.material.button.MaterialButton +import com.lukas.music.R import com.lukas.music.databinding.FragmentEditChordBinding import com.lukas.music.song.ScaleType import com.lukas.music.song.Song +import com.lukas.music.song.chords.Accidental 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 +import com.lukas.music.util.* class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : - DialogFragment() { - lateinit var binding: FragmentEditChordBinding + EasyDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentEditChordBinding.inflate(inflater) + Array(Accidental.VALUES.size) { + val button = MaterialButton(binding.root.context) + button.layoutParams = UIUtil.cardLayout + binding.accidentalSelection.addView(button) + return@Array button + }.setupEnumSelection(chord::accidental, Accidental.VALUES, callback = { update() }) setupPitchSpinner() - setupTypeSpinner() + setupEditor() binding.exitButton.setOnClickListener { dismiss() } return binding.root } + private fun update() { + songFragment.updateChords() + binding.chordText.text = chord.toString(true, Song.currentSong.root) + updateEditor() + } + private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { 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 = ScaleType.MAJOR.steps[it] - if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] + if (chord.note == ScaleType.MAJOR.steps[it]) { + update() + return@setup } - songFragment.updateChords() + chord.note = ScaleType.MAJOR.steps[it] + chord.accidental = Accidental.None + chord.accidentals[0] = + Accidental.VALUES[(ScaleType.MAJOR.steps[(it + 2) % ScaleType.MAJOR.steps.size] distance chord.note) - 3] + chord.accidentals[1] = + Accidental.VALUES[(ScaleType.MAJOR.steps[(it + 4) % ScaleType.MAJOR.steps.size] distance chord.note) - 6] + update() } } - private fun setupTypeSpinner() { - val values = mutableListOf("default") - for (chordType in ChordType.VALUES) { - values += chordType.toString() + private fun setupEditor() { + binding.editorGrid.removeAllViews() + val row = TableRow(binding.root.context) + for (description in descriptions) { + val text = TextView(binding.root.context) + text.text = description + text.layoutParams = UIUtil.cardLayout + text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER + row.addView(text) } - binding.typeSpinner.setup( - values, - if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 - else chord.chordType.ordinal + 1 - ) { - if (it == 0) { - chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] - } else { - chord.chordType = ChordType.VALUES[it - 1] + binding.editorGrid.addView(row) + for (accidental in Accidental.VALUES) { + val row = TableRow(binding.root.context) + for (position in 0 until Chord.NOTE_COUNT - 1) { + val button = MaterialButton(binding.root.context) + button.text = accidental.toString() + button.layoutParams = UIUtil.cardLayout + button.updateToggle(chord.accidentals[position] == accidental, R.color.blue) + button.setOnClickListener { + if (chord.accidentals[position] == accidental) { + chord.accidentals[position] = null + } else { + chord.accidentals[position] = accidental + } + update() + } + row.addView(button) } - songFragment.updateChords() + binding.editorGrid.addView(row) } } + + private fun updateEditor() { + for ((index, view) in binding.editorGrid.children.iterator().withIndex()) { + if (index == 0) { + continue + } + view as TableRow + for ((childIndex, childView) in view.children.iterator().withIndex()) { + childView as MaterialButton + childView.updateToggle( + chord.accidentals[childIndex] == Accidental.VALUES[index - 1], + R.color.blue + ) + } + } + } + + companion object { + val descriptions = arrayOf("III", "V", "VII", "IX") + } +} + +infix fun Int.distance(other: Int): Int { + var result = this - other + while (result < 0) { + result += 12 + } + result %= 12 + return result } \ No newline at end of file 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 index 6e359e4..0d532c2 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt @@ -14,9 +14,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager import com.lukas.music.databinding.FragmentEditEffectsBinding import com.lukas.music.instruments.Instrument +import com.lukas.music.ui.adapters.EffectsAdapter import com.lukas.music.util.EasyDialogFragment +import com.lukas.music.util.makeMoveCallback class EditEffectsFragment(private val instrument: Instrument) : EasyDialogFragment() { @@ -25,11 +29,12 @@ 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.effectsDisplay.adapter = EffectsAdapter(this, instrument) + binding.effectsDisplay.layoutManager = LinearLayoutManager(context) + val helper = ItemTouchHelper(makeMoveCallback(instrument.effects) { from, to -> + instrument.moveEffects(from, to) + }) + helper.attachToRecyclerView(binding.effectsDisplay) binding.closeButton.setOnClickListener { dismiss() } 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 index ebf4cb1..7b13ed6 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt @@ -10,25 +10,18 @@ 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 androidx.recyclerview.widget.RecyclerView 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) +class EffectFragment(val binding: FragmentEffectBinding) : RecyclerView.ViewHolder( + binding.root +) { + fun setEffect(effect: Effect) { binding.effectName.text = effect.type.toString() binding.activeButton.setupToggle(effect::active, R.color.blue) { binding.activeButton.text = if (it) "ON" else "OFF" @@ -37,10 +30,15 @@ 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]) + binding.parameter1SeekBar.visibility = + if (effect.parameters[0] == null) View.GONE else View.VISIBLE + binding.parameter1Text.visibility = + if (effect.parameters[0] == null) View.GONE else View.VISIBLE + effect.parameters[0]?.let { + binding.parameter1SeekBar.smartSetup(0, 100, it::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/ui/fragments/InstrumentListFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt index 1d35c10..0b66892 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt @@ -24,6 +24,7 @@ import com.lukas.music.instruments.MonoInstrument import com.lukas.music.instruments.PolyInstrument import com.lukas.music.ui.adapters.InstrumentAdapter +import com.lukas.music.util.makeMoveCallback class InstrumentListFragment : Fragment() { lateinit var binding: FragmentInstrumentListBinding @@ -35,32 +36,7 @@ binding = FragmentInstrumentListBinding.inflate(inflater) binding.recyclerView.adapter = InstrumentAdapter(this) binding.recyclerView.layoutManager = LinearLayoutManager(context) - val callback = object : ItemTouchHelper.SimpleCallback( - ItemTouchHelper.UP or ItemTouchHelper.DOWN, - 0 - ) { - override fun onMove( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean { - val adapter = recyclerView.adapter as InstrumentAdapter - val startPosition = viewHolder.adapterPosition - val endPosition = target.adapterPosition - val instrument = Instrument.instruments[startPosition] - Instrument.instruments.removeAt(startPosition) - if (endPosition < startPosition) { - Instrument.instruments.add(endPosition + 1, instrument) - } else { - Instrument.instruments.add(endPosition - 1, instrument) - } - adapter.notifyItemMoved(startPosition, endPosition) - return true - } - - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} - } - val helper = ItemTouchHelper(callback) + val helper = ItemTouchHelper(makeMoveCallback(Instrument.instruments)) helper.attachToRecyclerView(binding.recyclerView) val builder = AlertDialog.Builder(binding.root.context) diff --git a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt index 0c33019..4b668c4 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt @@ -27,6 +27,7 @@ import com.lukas.music.databinding.FragmentPlayBinding import com.lukas.music.instruments.Rhythm import com.lukas.music.song.Song +import com.lukas.music.util.UIUtil import com.lukas.music.util.setup class PlayFragment : Fragment() { @@ -125,12 +126,12 @@ chordDisplays.clear() for (chord in Song.currentSong.chordProgression.currentItem ?: return) { val card = CardView(binding.root.context) - card.layoutParams = SongFragment.tableRowLayout + card.layoutParams = UIUtil.cardLayout card.radius = 10f card.preventCornerOverlap = false val text = TextView(binding.root.context) text.text = chord.toString(true, Song.currentSong.root) - text.layoutParams = SongFragment.tableRowLayout + text.layoutParams = UIUtil.fillingLayout text.textSize = 20f text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER card.addView(text) diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ 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 7beb64c..7f10ff2 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -10,11 +10,12 @@ package com.lukas.music.instruments +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note class PolyInstrument(name: String) : Instrument(name) { - private val internalInstruments = Array(3) { InternalInstrument() } - private val playing = Array(3) { false } + private val internalInstruments = Array(Chord.NOTE_COUNT) { InternalInstrument() } + private val playing = Array(Chord.NOTE_COUNT) { false } override var waveform: Waveform = Waveform.SINE set(value) { @@ -86,7 +87,7 @@ override fun updateEffects() { for (instrument in internalInstruments) { for (effect in effects) { - instrument.applyEffectAttributes(effect) + instrument.applyEffectAttributes(this, effect) } } } @@ -99,4 +100,10 @@ } return false } + + override fun moveEffects(from: Int, to: Int) { + for (instrument in internalInstruments) { + instrument.moveEffects(from, to) + } + } } \ No newline at end of file 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 index f921b1b..f659a8c 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -14,7 +14,9 @@ class Effect(val type: EffectType, private val instrument: Instrument) { val parameters = Array(type.parameterDescriptions.size) { - EffectParameter(type.parameterDescriptions[it], instrument) + type.parameterDescriptions[it]?.let { parameterDescription -> + EffectParameter(parameterDescription, instrument) + } } val influence = EffectParameter(influenceDescription, instrument) 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 index 8af39ea..8b90ec5 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -13,8 +13,8 @@ import com.lukas.music.util.format enum class EffectType( - val title: String, - val parameterDescriptions: Array + private val title: String, + val parameterDescriptions: Array ) { LowPass("low pass filter", arrayOf( @@ -22,13 +22,17 @@ "cutoff: ${it.value.format(1)} octaves" } )), - Noise("noise", + Noise( + "noise", arrayOf( - EffectParameterDescription(0f, 1f, 0f) { - "unused" - } + null ) - ) + ), + Distortion("distortion", arrayOf( + EffectParameterDescription(1f, 4f, 1f) { + "strength: ${it.value.format(1)}x" + } + )) ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt index 4c6a0d9..7807cb9 100644 --- a/app/src/main/java/com/lukas/music/song/ScaleType.kt +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -10,24 +10,12 @@ 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 - ) + arrayOf(0, 2, 4, 5, 7, 9, 11), ) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Accidental.kt b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt new file mode 100644 index 0000000..688ae4e --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or(at your option) any later version. + * + * This program is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 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.chords + +enum class Accidental(val id: String, val short: String, val distance: Int) { + Flat("\u266D", "b", -1), + None("\u266E", "", 0), + Sharp("\u266F", "#", 1), + ; + + override fun toString(): String { + return id + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index 4400dae..85e530b 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -10,14 +10,19 @@ package com.lukas.music.song.chords +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -class Chord(note: Int, var chordType: ChordType) { - var note: Int = note +class Chord { + var accidental = Accidental.None + val accidentals: Array = arrayOf(Accidental.None, Accidental.None, null, null) + + var note: Int = 0 set(value) { field = value interval = Interval(value) } + var interval = Interval(note) set(value) { field = value @@ -27,19 +32,76 @@ } fun getNotes(root: Note): Array { - return Array(chordType.notes.size) { root + note + chordType.notes[it] } + val result = Array(NOTE_COUNT) { root } + var resultIndex = 0 + var accidentalIndex = 0 + var octave = 0 + while (resultIndex < NOTE_COUNT) { + if (accidentalIndex == 0) { + result[resultIndex] = root + note + 12 * octave + accidental.distance + resultIndex++ + } else if (accidentals[accidentalIndex - 1] != null) { + result[resultIndex] = root + note + when (accidentalIndex) { + 1 -> 4 + 2 -> 7 + 3 -> 10 + 4 -> 14 + else -> 0 + } + accidentals[accidentalIndex - 1]!!.distance + 12 * octave + accidental.distance + resultIndex++ + } + accidentalIndex++ + if (accidentalIndex > accidentals.size) { + octave++ + accidentalIndex = 0 + } + } + return result } override fun toString(): String { - return chordType.transform(interval.toString()) + return toString(false, Song.currentSong.root) } fun toString(displayChordNames: Boolean, root: Note): String { - val base = if (displayChordNames) { - (root + note).noteName.toString() + var result = if (displayChordNames) { + (root + note + accidental.distance).noteName.toString() } else { interval.toString() } - return chordType.transform(base) + accidentals[0]?.let { + result += when (it) { + Accidental.Flat -> "-" + Accidental.Sharp -> "sus4" + else -> "" + } + } + accidentals[1]?.let { + if (accidentals[0] != null && it == Accidental.None) { + return@let + } + result += it.short + "5" + } + result = result.replace("-b5", "0") + result = result.replace("(?=[A-G])#5".toRegex(), "+") + accidentals[2]?.let { + result += when (it) { + Accidental.Sharp -> " maj7" + Accidental.None -> " 7" + Accidental.Flat -> " 6" + } + } + accidentals[3]?.let { + result += when (it) { + Accidental.Sharp -> " maj9" + Accidental.None -> " 9" + Accidental.Flat -> " b9" + } + } + return result + } + + companion object { + const val NOTE_COUNT = 5 } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt b/app/src/main/java/com/lukas/music/song/chords/ChordType.kt deleted file mode 100644 index 1fe4b40..0000000 --- a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt +++ /dev/null @@ -1,30 +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.chords - -enum class ChordType( - val notes: Array, - private val asString: String, - val transform: (String) -> String -) { - MAJOR(arrayOf(0, 4, 7), "major", { it.uppercase() }), - MINOR(arrayOf(0, 3, 7), "minor", { it.lowercase() }), - DIMINISHED(arrayOf(0, 3, 6), "diminished", { it.lowercase() + "0" }), - ; - - override fun toString(): String { - return asString - } - - companion object { - val VALUES = values() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt index 1cadb06..f4bcbc7 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt @@ -15,7 +15,7 @@ class Phrase : Cycle() { init { for (i in 0 until 4) { - this += Chord(0, ChordType.MAJOR) + this += Chord() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index 17bfb2b..f17e322 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -12,7 +12,7 @@ import kotlin.math.pow -class Note(private val id: Int) { +class Note(val id: Int) { val noteName = NoteName.VALUES[id % 12] val octave = id / 12 - 1 val frequency = 440 * 2.0.pow((id - 69) / 12.0) @@ -28,6 +28,8 @@ return this + (-other) } + operator fun minus(other: Note): Int = id - other.id + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false 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 index e06761a..40b1d14 100644 --- a/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -11,6 +11,7 @@ package com.lukas.music.song.voice import com.lukas.music.song.ScaleType +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note import com.lukas.music.util.transform @@ -20,7 +21,7 @@ val getNotes: (Note, Array) -> Array ) { Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), - Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + ChordVoice("Chord notes", Chord.NOTE_COUNT, { _, 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 } }), diff --git a/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt new file mode 100644 index 0000000..730eabd --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.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.ui.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.lukas.music.databinding.FragmentEffectBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.ui.fragments.EditEffectsFragment +import com.lukas.music.ui.fragments.EffectFragment + +class EffectsAdapter(private val parent: EditEffectsFragment, private val instrument: Instrument) : + RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EffectFragment { + val context = parent.context + val inflater = LayoutInflater.from(context) + val binding = FragmentEffectBinding.inflate(inflater, parent, false) + return EffectFragment(binding) + } + + override fun onBindViewHolder(holder: EffectFragment, position: Int) { + holder.setEffect(instrument.effects[position]) + } + + override fun getItemCount(): Int { + return instrument.effects.size + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt index 3e129bc..3d7d1f4 100644 --- a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt @@ -39,7 +39,7 @@ Song.currentSong.soloInstrument = instrument } field = value - binding.soloButton.updateToggle(this::solo, R.color.blue) + binding.soloButton.updateToggle(this.solo, R.color.blue) } var instrument: Instrument? = null 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 6c3bae3..5fc9dfe 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 @@ -14,61 +14,124 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.DialogFragment +import android.widget.TableRow +import android.widget.TextView +import androidx.core.view.children +import com.google.android.material.button.MaterialButton +import com.lukas.music.R import com.lukas.music.databinding.FragmentEditChordBinding import com.lukas.music.song.ScaleType import com.lukas.music.song.Song +import com.lukas.music.song.chords.Accidental 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 +import com.lukas.music.util.* class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : - DialogFragment() { - lateinit var binding: FragmentEditChordBinding + EasyDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentEditChordBinding.inflate(inflater) + Array(Accidental.VALUES.size) { + val button = MaterialButton(binding.root.context) + button.layoutParams = UIUtil.cardLayout + binding.accidentalSelection.addView(button) + return@Array button + }.setupEnumSelection(chord::accidental, Accidental.VALUES, callback = { update() }) setupPitchSpinner() - setupTypeSpinner() + setupEditor() binding.exitButton.setOnClickListener { dismiss() } return binding.root } + private fun update() { + songFragment.updateChords() + binding.chordText.text = chord.toString(true, Song.currentSong.root) + updateEditor() + } + private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { 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 = ScaleType.MAJOR.steps[it] - if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] + if (chord.note == ScaleType.MAJOR.steps[it]) { + update() + return@setup } - songFragment.updateChords() + chord.note = ScaleType.MAJOR.steps[it] + chord.accidental = Accidental.None + chord.accidentals[0] = + Accidental.VALUES[(ScaleType.MAJOR.steps[(it + 2) % ScaleType.MAJOR.steps.size] distance chord.note) - 3] + chord.accidentals[1] = + Accidental.VALUES[(ScaleType.MAJOR.steps[(it + 4) % ScaleType.MAJOR.steps.size] distance chord.note) - 6] + update() } } - private fun setupTypeSpinner() { - val values = mutableListOf("default") - for (chordType in ChordType.VALUES) { - values += chordType.toString() + private fun setupEditor() { + binding.editorGrid.removeAllViews() + val row = TableRow(binding.root.context) + for (description in descriptions) { + val text = TextView(binding.root.context) + text.text = description + text.layoutParams = UIUtil.cardLayout + text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER + row.addView(text) } - binding.typeSpinner.setup( - values, - if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 - else chord.chordType.ordinal + 1 - ) { - if (it == 0) { - chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] - } else { - chord.chordType = ChordType.VALUES[it - 1] + binding.editorGrid.addView(row) + for (accidental in Accidental.VALUES) { + val row = TableRow(binding.root.context) + for (position in 0 until Chord.NOTE_COUNT - 1) { + val button = MaterialButton(binding.root.context) + button.text = accidental.toString() + button.layoutParams = UIUtil.cardLayout + button.updateToggle(chord.accidentals[position] == accidental, R.color.blue) + button.setOnClickListener { + if (chord.accidentals[position] == accidental) { + chord.accidentals[position] = null + } else { + chord.accidentals[position] = accidental + } + update() + } + row.addView(button) } - songFragment.updateChords() + binding.editorGrid.addView(row) } } + + private fun updateEditor() { + for ((index, view) in binding.editorGrid.children.iterator().withIndex()) { + if (index == 0) { + continue + } + view as TableRow + for ((childIndex, childView) in view.children.iterator().withIndex()) { + childView as MaterialButton + childView.updateToggle( + chord.accidentals[childIndex] == Accidental.VALUES[index - 1], + R.color.blue + ) + } + } + } + + companion object { + val descriptions = arrayOf("III", "V", "VII", "IX") + } +} + +infix fun Int.distance(other: Int): Int { + var result = this - other + while (result < 0) { + result += 12 + } + result %= 12 + return result } \ No newline at end of file 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 index 6e359e4..0d532c2 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt @@ -14,9 +14,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager import com.lukas.music.databinding.FragmentEditEffectsBinding import com.lukas.music.instruments.Instrument +import com.lukas.music.ui.adapters.EffectsAdapter import com.lukas.music.util.EasyDialogFragment +import com.lukas.music.util.makeMoveCallback class EditEffectsFragment(private val instrument: Instrument) : EasyDialogFragment() { @@ -25,11 +29,12 @@ 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.effectsDisplay.adapter = EffectsAdapter(this, instrument) + binding.effectsDisplay.layoutManager = LinearLayoutManager(context) + val helper = ItemTouchHelper(makeMoveCallback(instrument.effects) { from, to -> + instrument.moveEffects(from, to) + }) + helper.attachToRecyclerView(binding.effectsDisplay) binding.closeButton.setOnClickListener { dismiss() } 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 index ebf4cb1..7b13ed6 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt @@ -10,25 +10,18 @@ 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 androidx.recyclerview.widget.RecyclerView 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) +class EffectFragment(val binding: FragmentEffectBinding) : RecyclerView.ViewHolder( + binding.root +) { + fun setEffect(effect: Effect) { binding.effectName.text = effect.type.toString() binding.activeButton.setupToggle(effect::active, R.color.blue) { binding.activeButton.text = if (it) "ON" else "OFF" @@ -37,10 +30,15 @@ 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]) + binding.parameter1SeekBar.visibility = + if (effect.parameters[0] == null) View.GONE else View.VISIBLE + binding.parameter1Text.visibility = + if (effect.parameters[0] == null) View.GONE else View.VISIBLE + effect.parameters[0]?.let { + binding.parameter1SeekBar.smartSetup(0, 100, it::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/ui/fragments/InstrumentListFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt index 1d35c10..0b66892 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt @@ -24,6 +24,7 @@ import com.lukas.music.instruments.MonoInstrument import com.lukas.music.instruments.PolyInstrument import com.lukas.music.ui.adapters.InstrumentAdapter +import com.lukas.music.util.makeMoveCallback class InstrumentListFragment : Fragment() { lateinit var binding: FragmentInstrumentListBinding @@ -35,32 +36,7 @@ binding = FragmentInstrumentListBinding.inflate(inflater) binding.recyclerView.adapter = InstrumentAdapter(this) binding.recyclerView.layoutManager = LinearLayoutManager(context) - val callback = object : ItemTouchHelper.SimpleCallback( - ItemTouchHelper.UP or ItemTouchHelper.DOWN, - 0 - ) { - override fun onMove( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean { - val adapter = recyclerView.adapter as InstrumentAdapter - val startPosition = viewHolder.adapterPosition - val endPosition = target.adapterPosition - val instrument = Instrument.instruments[startPosition] - Instrument.instruments.removeAt(startPosition) - if (endPosition < startPosition) { - Instrument.instruments.add(endPosition + 1, instrument) - } else { - Instrument.instruments.add(endPosition - 1, instrument) - } - adapter.notifyItemMoved(startPosition, endPosition) - return true - } - - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} - } - val helper = ItemTouchHelper(callback) + val helper = ItemTouchHelper(makeMoveCallback(Instrument.instruments)) helper.attachToRecyclerView(binding.recyclerView) val builder = AlertDialog.Builder(binding.root.context) diff --git a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt index 0c33019..4b668c4 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt @@ -27,6 +27,7 @@ import com.lukas.music.databinding.FragmentPlayBinding import com.lukas.music.instruments.Rhythm import com.lukas.music.song.Song +import com.lukas.music.util.UIUtil import com.lukas.music.util.setup class PlayFragment : Fragment() { @@ -125,12 +126,12 @@ chordDisplays.clear() for (chord in Song.currentSong.chordProgression.currentItem ?: return) { val card = CardView(binding.root.context) - card.layoutParams = SongFragment.tableRowLayout + card.layoutParams = UIUtil.cardLayout card.radius = 10f card.preventCornerOverlap = false val text = TextView(binding.root.context) text.text = chord.toString(true, Song.currentSong.root) - text.layoutParams = SongFragment.tableRowLayout + text.layoutParams = UIUtil.fillingLayout text.textSize = 20f text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER card.addView(text) diff --git a/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt index 62d314f..afe6137 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt @@ -16,13 +16,13 @@ import android.view.ViewGroup import android.widget.* import androidx.cardview.widget.CardView -import androidx.core.view.setMargins import androidx.fragment.app.Fragment import com.lukas.music.databinding.FragmentSongBinding import com.lukas.music.song.Song import com.lukas.music.song.chords.Phrase import com.lukas.music.song.note.Note import com.lukas.music.song.note.NoteName +import com.lukas.music.util.UIUtil class SongFragment(val playFragment: PlayFragment) : Fragment(), @@ -63,14 +63,14 @@ for (chord in phrase) { val card = CardView(binding.root.context) card.radius = 10f - card.layoutParams = tableRowLayout + card.layoutParams = UIUtil.cardLayout card.setOnClickListener { EditChordFragment(chord, this).showNow(childFragmentManager, "") } val text = TextView(binding.root.context) text.text = chord.toString(displayChordNames, Song.currentSong.root) text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER - text.layoutParams = tableRowLayout + text.layoutParams = UIUtil.fillingLayout text.textSize = 20f card.addView(text) row.addView(card) @@ -81,28 +81,13 @@ updateChords() } button.setImageResource(android.R.drawable.ic_delete) - button.layoutParams = buttonLayout + button.layoutParams = UIUtil.buttonLayout row.addView(button) binding.chords.addView(row) } playFragment.updateChords() } - companion object { - val tableRowLayout = TableRow.LayoutParams( - TableRow.LayoutParams.MATCH_PARENT, - TableRow.LayoutParams.MATCH_PARENT - ) - val buttonLayout = TableRow.LayoutParams( - 0, - TableRow.LayoutParams.WRAP_CONTENT - ) - - init { - tableRowLayout.setMargins(10) - } - } - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { Song.currentSong.root = Note.of(NoteName.VALUES[position], 4) if (displayChordNames) { diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ 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 7beb64c..7f10ff2 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -10,11 +10,12 @@ package com.lukas.music.instruments +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note class PolyInstrument(name: String) : Instrument(name) { - private val internalInstruments = Array(3) { InternalInstrument() } - private val playing = Array(3) { false } + private val internalInstruments = Array(Chord.NOTE_COUNT) { InternalInstrument() } + private val playing = Array(Chord.NOTE_COUNT) { false } override var waveform: Waveform = Waveform.SINE set(value) { @@ -86,7 +87,7 @@ override fun updateEffects() { for (instrument in internalInstruments) { for (effect in effects) { - instrument.applyEffectAttributes(effect) + instrument.applyEffectAttributes(this, effect) } } } @@ -99,4 +100,10 @@ } return false } + + override fun moveEffects(from: Int, to: Int) { + for (instrument in internalInstruments) { + instrument.moveEffects(from, to) + } + } } \ No newline at end of file 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 index f921b1b..f659a8c 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -14,7 +14,9 @@ class Effect(val type: EffectType, private val instrument: Instrument) { val parameters = Array(type.parameterDescriptions.size) { - EffectParameter(type.parameterDescriptions[it], instrument) + type.parameterDescriptions[it]?.let { parameterDescription -> + EffectParameter(parameterDescription, instrument) + } } val influence = EffectParameter(influenceDescription, instrument) 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 index 8af39ea..8b90ec5 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -13,8 +13,8 @@ import com.lukas.music.util.format enum class EffectType( - val title: String, - val parameterDescriptions: Array + private val title: String, + val parameterDescriptions: Array ) { LowPass("low pass filter", arrayOf( @@ -22,13 +22,17 @@ "cutoff: ${it.value.format(1)} octaves" } )), - Noise("noise", + Noise( + "noise", arrayOf( - EffectParameterDescription(0f, 1f, 0f) { - "unused" - } + null ) - ) + ), + Distortion("distortion", arrayOf( + EffectParameterDescription(1f, 4f, 1f) { + "strength: ${it.value.format(1)}x" + } + )) ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt index 4c6a0d9..7807cb9 100644 --- a/app/src/main/java/com/lukas/music/song/ScaleType.kt +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -10,24 +10,12 @@ 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 - ) + arrayOf(0, 2, 4, 5, 7, 9, 11), ) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Accidental.kt b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt new file mode 100644 index 0000000..688ae4e --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or(at your option) any later version. + * + * This program is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 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.chords + +enum class Accidental(val id: String, val short: String, val distance: Int) { + Flat("\u266D", "b", -1), + None("\u266E", "", 0), + Sharp("\u266F", "#", 1), + ; + + override fun toString(): String { + return id + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index 4400dae..85e530b 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -10,14 +10,19 @@ package com.lukas.music.song.chords +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -class Chord(note: Int, var chordType: ChordType) { - var note: Int = note +class Chord { + var accidental = Accidental.None + val accidentals: Array = arrayOf(Accidental.None, Accidental.None, null, null) + + var note: Int = 0 set(value) { field = value interval = Interval(value) } + var interval = Interval(note) set(value) { field = value @@ -27,19 +32,76 @@ } fun getNotes(root: Note): Array { - return Array(chordType.notes.size) { root + note + chordType.notes[it] } + val result = Array(NOTE_COUNT) { root } + var resultIndex = 0 + var accidentalIndex = 0 + var octave = 0 + while (resultIndex < NOTE_COUNT) { + if (accidentalIndex == 0) { + result[resultIndex] = root + note + 12 * octave + accidental.distance + resultIndex++ + } else if (accidentals[accidentalIndex - 1] != null) { + result[resultIndex] = root + note + when (accidentalIndex) { + 1 -> 4 + 2 -> 7 + 3 -> 10 + 4 -> 14 + else -> 0 + } + accidentals[accidentalIndex - 1]!!.distance + 12 * octave + accidental.distance + resultIndex++ + } + accidentalIndex++ + if (accidentalIndex > accidentals.size) { + octave++ + accidentalIndex = 0 + } + } + return result } override fun toString(): String { - return chordType.transform(interval.toString()) + return toString(false, Song.currentSong.root) } fun toString(displayChordNames: Boolean, root: Note): String { - val base = if (displayChordNames) { - (root + note).noteName.toString() + var result = if (displayChordNames) { + (root + note + accidental.distance).noteName.toString() } else { interval.toString() } - return chordType.transform(base) + accidentals[0]?.let { + result += when (it) { + Accidental.Flat -> "-" + Accidental.Sharp -> "sus4" + else -> "" + } + } + accidentals[1]?.let { + if (accidentals[0] != null && it == Accidental.None) { + return@let + } + result += it.short + "5" + } + result = result.replace("-b5", "0") + result = result.replace("(?=[A-G])#5".toRegex(), "+") + accidentals[2]?.let { + result += when (it) { + Accidental.Sharp -> " maj7" + Accidental.None -> " 7" + Accidental.Flat -> " 6" + } + } + accidentals[3]?.let { + result += when (it) { + Accidental.Sharp -> " maj9" + Accidental.None -> " 9" + Accidental.Flat -> " b9" + } + } + return result + } + + companion object { + const val NOTE_COUNT = 5 } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt b/app/src/main/java/com/lukas/music/song/chords/ChordType.kt deleted file mode 100644 index 1fe4b40..0000000 --- a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt +++ /dev/null @@ -1,30 +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.chords - -enum class ChordType( - val notes: Array, - private val asString: String, - val transform: (String) -> String -) { - MAJOR(arrayOf(0, 4, 7), "major", { it.uppercase() }), - MINOR(arrayOf(0, 3, 7), "minor", { it.lowercase() }), - DIMINISHED(arrayOf(0, 3, 6), "diminished", { it.lowercase() + "0" }), - ; - - override fun toString(): String { - return asString - } - - companion object { - val VALUES = values() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt index 1cadb06..f4bcbc7 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt @@ -15,7 +15,7 @@ class Phrase : Cycle() { init { for (i in 0 until 4) { - this += Chord(0, ChordType.MAJOR) + this += Chord() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index 17bfb2b..f17e322 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -12,7 +12,7 @@ import kotlin.math.pow -class Note(private val id: Int) { +class Note(val id: Int) { val noteName = NoteName.VALUES[id % 12] val octave = id / 12 - 1 val frequency = 440 * 2.0.pow((id - 69) / 12.0) @@ -28,6 +28,8 @@ return this + (-other) } + operator fun minus(other: Note): Int = id - other.id + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false 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 index e06761a..40b1d14 100644 --- a/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -11,6 +11,7 @@ package com.lukas.music.song.voice import com.lukas.music.song.ScaleType +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note import com.lukas.music.util.transform @@ -20,7 +21,7 @@ val getNotes: (Note, Array) -> Array ) { Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), - Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + ChordVoice("Chord notes", Chord.NOTE_COUNT, { _, 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 } }), diff --git a/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt new file mode 100644 index 0000000..730eabd --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.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.ui.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.lukas.music.databinding.FragmentEffectBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.ui.fragments.EditEffectsFragment +import com.lukas.music.ui.fragments.EffectFragment + +class EffectsAdapter(private val parent: EditEffectsFragment, private val instrument: Instrument) : + RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EffectFragment { + val context = parent.context + val inflater = LayoutInflater.from(context) + val binding = FragmentEffectBinding.inflate(inflater, parent, false) + return EffectFragment(binding) + } + + override fun onBindViewHolder(holder: EffectFragment, position: Int) { + holder.setEffect(instrument.effects[position]) + } + + override fun getItemCount(): Int { + return instrument.effects.size + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt index 3e129bc..3d7d1f4 100644 --- a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt @@ -39,7 +39,7 @@ Song.currentSong.soloInstrument = instrument } field = value - binding.soloButton.updateToggle(this::solo, R.color.blue) + binding.soloButton.updateToggle(this.solo, R.color.blue) } var instrument: Instrument? = null 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 6c3bae3..5fc9dfe 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 @@ -14,61 +14,124 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.DialogFragment +import android.widget.TableRow +import android.widget.TextView +import androidx.core.view.children +import com.google.android.material.button.MaterialButton +import com.lukas.music.R import com.lukas.music.databinding.FragmentEditChordBinding import com.lukas.music.song.ScaleType import com.lukas.music.song.Song +import com.lukas.music.song.chords.Accidental 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 +import com.lukas.music.util.* class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : - DialogFragment() { - lateinit var binding: FragmentEditChordBinding + EasyDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentEditChordBinding.inflate(inflater) + Array(Accidental.VALUES.size) { + val button = MaterialButton(binding.root.context) + button.layoutParams = UIUtil.cardLayout + binding.accidentalSelection.addView(button) + return@Array button + }.setupEnumSelection(chord::accidental, Accidental.VALUES, callback = { update() }) setupPitchSpinner() - setupTypeSpinner() + setupEditor() binding.exitButton.setOnClickListener { dismiss() } return binding.root } + private fun update() { + songFragment.updateChords() + binding.chordText.text = chord.toString(true, Song.currentSong.root) + updateEditor() + } + private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { 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 = ScaleType.MAJOR.steps[it] - if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] + if (chord.note == ScaleType.MAJOR.steps[it]) { + update() + return@setup } - songFragment.updateChords() + chord.note = ScaleType.MAJOR.steps[it] + chord.accidental = Accidental.None + chord.accidentals[0] = + Accidental.VALUES[(ScaleType.MAJOR.steps[(it + 2) % ScaleType.MAJOR.steps.size] distance chord.note) - 3] + chord.accidentals[1] = + Accidental.VALUES[(ScaleType.MAJOR.steps[(it + 4) % ScaleType.MAJOR.steps.size] distance chord.note) - 6] + update() } } - private fun setupTypeSpinner() { - val values = mutableListOf("default") - for (chordType in ChordType.VALUES) { - values += chordType.toString() + private fun setupEditor() { + binding.editorGrid.removeAllViews() + val row = TableRow(binding.root.context) + for (description in descriptions) { + val text = TextView(binding.root.context) + text.text = description + text.layoutParams = UIUtil.cardLayout + text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER + row.addView(text) } - binding.typeSpinner.setup( - values, - if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 - else chord.chordType.ordinal + 1 - ) { - if (it == 0) { - chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] - } else { - chord.chordType = ChordType.VALUES[it - 1] + binding.editorGrid.addView(row) + for (accidental in Accidental.VALUES) { + val row = TableRow(binding.root.context) + for (position in 0 until Chord.NOTE_COUNT - 1) { + val button = MaterialButton(binding.root.context) + button.text = accidental.toString() + button.layoutParams = UIUtil.cardLayout + button.updateToggle(chord.accidentals[position] == accidental, R.color.blue) + button.setOnClickListener { + if (chord.accidentals[position] == accidental) { + chord.accidentals[position] = null + } else { + chord.accidentals[position] = accidental + } + update() + } + row.addView(button) } - songFragment.updateChords() + binding.editorGrid.addView(row) } } + + private fun updateEditor() { + for ((index, view) in binding.editorGrid.children.iterator().withIndex()) { + if (index == 0) { + continue + } + view as TableRow + for ((childIndex, childView) in view.children.iterator().withIndex()) { + childView as MaterialButton + childView.updateToggle( + chord.accidentals[childIndex] == Accidental.VALUES[index - 1], + R.color.blue + ) + } + } + } + + companion object { + val descriptions = arrayOf("III", "V", "VII", "IX") + } +} + +infix fun Int.distance(other: Int): Int { + var result = this - other + while (result < 0) { + result += 12 + } + result %= 12 + return result } \ No newline at end of file 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 index 6e359e4..0d532c2 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt @@ -14,9 +14,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager import com.lukas.music.databinding.FragmentEditEffectsBinding import com.lukas.music.instruments.Instrument +import com.lukas.music.ui.adapters.EffectsAdapter import com.lukas.music.util.EasyDialogFragment +import com.lukas.music.util.makeMoveCallback class EditEffectsFragment(private val instrument: Instrument) : EasyDialogFragment() { @@ -25,11 +29,12 @@ 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.effectsDisplay.adapter = EffectsAdapter(this, instrument) + binding.effectsDisplay.layoutManager = LinearLayoutManager(context) + val helper = ItemTouchHelper(makeMoveCallback(instrument.effects) { from, to -> + instrument.moveEffects(from, to) + }) + helper.attachToRecyclerView(binding.effectsDisplay) binding.closeButton.setOnClickListener { dismiss() } 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 index ebf4cb1..7b13ed6 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt @@ -10,25 +10,18 @@ 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 androidx.recyclerview.widget.RecyclerView 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) +class EffectFragment(val binding: FragmentEffectBinding) : RecyclerView.ViewHolder( + binding.root +) { + fun setEffect(effect: Effect) { binding.effectName.text = effect.type.toString() binding.activeButton.setupToggle(effect::active, R.color.blue) { binding.activeButton.text = if (it) "ON" else "OFF" @@ -37,10 +30,15 @@ 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]) + binding.parameter1SeekBar.visibility = + if (effect.parameters[0] == null) View.GONE else View.VISIBLE + binding.parameter1Text.visibility = + if (effect.parameters[0] == null) View.GONE else View.VISIBLE + effect.parameters[0]?.let { + binding.parameter1SeekBar.smartSetup(0, 100, it::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/ui/fragments/InstrumentListFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt index 1d35c10..0b66892 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt @@ -24,6 +24,7 @@ import com.lukas.music.instruments.MonoInstrument import com.lukas.music.instruments.PolyInstrument import com.lukas.music.ui.adapters.InstrumentAdapter +import com.lukas.music.util.makeMoveCallback class InstrumentListFragment : Fragment() { lateinit var binding: FragmentInstrumentListBinding @@ -35,32 +36,7 @@ binding = FragmentInstrumentListBinding.inflate(inflater) binding.recyclerView.adapter = InstrumentAdapter(this) binding.recyclerView.layoutManager = LinearLayoutManager(context) - val callback = object : ItemTouchHelper.SimpleCallback( - ItemTouchHelper.UP or ItemTouchHelper.DOWN, - 0 - ) { - override fun onMove( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean { - val adapter = recyclerView.adapter as InstrumentAdapter - val startPosition = viewHolder.adapterPosition - val endPosition = target.adapterPosition - val instrument = Instrument.instruments[startPosition] - Instrument.instruments.removeAt(startPosition) - if (endPosition < startPosition) { - Instrument.instruments.add(endPosition + 1, instrument) - } else { - Instrument.instruments.add(endPosition - 1, instrument) - } - adapter.notifyItemMoved(startPosition, endPosition) - return true - } - - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} - } - val helper = ItemTouchHelper(callback) + val helper = ItemTouchHelper(makeMoveCallback(Instrument.instruments)) helper.attachToRecyclerView(binding.recyclerView) val builder = AlertDialog.Builder(binding.root.context) diff --git a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt index 0c33019..4b668c4 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt @@ -27,6 +27,7 @@ import com.lukas.music.databinding.FragmentPlayBinding import com.lukas.music.instruments.Rhythm import com.lukas.music.song.Song +import com.lukas.music.util.UIUtil import com.lukas.music.util.setup class PlayFragment : Fragment() { @@ -125,12 +126,12 @@ chordDisplays.clear() for (chord in Song.currentSong.chordProgression.currentItem ?: return) { val card = CardView(binding.root.context) - card.layoutParams = SongFragment.tableRowLayout + card.layoutParams = UIUtil.cardLayout card.radius = 10f card.preventCornerOverlap = false val text = TextView(binding.root.context) text.text = chord.toString(true, Song.currentSong.root) - text.layoutParams = SongFragment.tableRowLayout + text.layoutParams = UIUtil.fillingLayout text.textSize = 20f text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER card.addView(text) diff --git a/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt index 62d314f..afe6137 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt @@ -16,13 +16,13 @@ import android.view.ViewGroup import android.widget.* import androidx.cardview.widget.CardView -import androidx.core.view.setMargins import androidx.fragment.app.Fragment import com.lukas.music.databinding.FragmentSongBinding import com.lukas.music.song.Song import com.lukas.music.song.chords.Phrase import com.lukas.music.song.note.Note import com.lukas.music.song.note.NoteName +import com.lukas.music.util.UIUtil class SongFragment(val playFragment: PlayFragment) : Fragment(), @@ -63,14 +63,14 @@ for (chord in phrase) { val card = CardView(binding.root.context) card.radius = 10f - card.layoutParams = tableRowLayout + card.layoutParams = UIUtil.cardLayout card.setOnClickListener { EditChordFragment(chord, this).showNow(childFragmentManager, "") } val text = TextView(binding.root.context) text.text = chord.toString(displayChordNames, Song.currentSong.root) text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER - text.layoutParams = tableRowLayout + text.layoutParams = UIUtil.fillingLayout text.textSize = 20f card.addView(text) row.addView(card) @@ -81,28 +81,13 @@ updateChords() } button.setImageResource(android.R.drawable.ic_delete) - button.layoutParams = buttonLayout + button.layoutParams = UIUtil.buttonLayout row.addView(button) binding.chords.addView(row) } playFragment.updateChords() } - companion object { - val tableRowLayout = TableRow.LayoutParams( - TableRow.LayoutParams.MATCH_PARENT, - TableRow.LayoutParams.MATCH_PARENT - ) - val buttonLayout = TableRow.LayoutParams( - 0, - TableRow.LayoutParams.WRAP_CONTENT - ) - - init { - tableRowLayout.setMargins(10) - } - } - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { Song.currentSong.root = Note.of(NoteName.VALUES[position], 4) if (displayChordNames) { 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 b67d0ea..6ae1bd9 100644 --- a/app/src/main/java/com/lukas/music/util/UIUtil.kt +++ b/app/src/main/java/com/lukas/music/util/UIUtil.kt @@ -13,6 +13,10 @@ import android.view.View import android.widget.* import androidx.core.content.ContextCompat +import androidx.core.view.setMargins +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.button.MaterialButton import com.lukas.music.R import kotlin.reflect.KMutableProperty0 @@ -62,19 +66,19 @@ ) { setOnClickListener { target.set(!target.get()) - updateToggle(target, activeColor, inactiveColor) + updateToggle(target.get(), activeColor, inactiveColor) callback(target.get()) } - updateToggle(target, activeColor, inactiveColor) + updateToggle(target.get(), activeColor, inactiveColor) } fun Button.updateToggle( - target: KMutableProperty0, + value: Boolean, activeColor: Int, inactiveColor: Int = R.color.gray_0x60, ) { setBackgroundColor( - ContextCompat.getColor(context, if (target.get()) activeColor else inactiveColor) + ContextCompat.getColor(context, if (value) activeColor else inactiveColor) ) } @@ -138,4 +142,79 @@ } callback(it) } +} + +fun makeMoveCallback( + list: MutableList, + callback: (Int, Int) -> Unit = { _, _ -> } +): ItemTouchHelper.SimpleCallback { + return object : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, + 0 + ) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val adapter = recyclerView.adapter + val startPosition = viewHolder.adapterPosition + val endPosition = target.adapterPosition + val item = list[startPosition] + list.removeAt(startPosition) + list.add(endPosition, item) + adapter!!.notifyItemMoved(startPosition, endPosition) + callback(startPosition, endPosition) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} + } +} + +object UIUtil { + val cardLayout = TableRow.LayoutParams( + 0, + TableRow.LayoutParams.WRAP_CONTENT, + 1f + ) + val buttonLayout = TableRow.LayoutParams( + 0, TableRow.LayoutParams.MATCH_PARENT, 0.5f + ) + val fillingLayout = TableRow.LayoutParams( + TableRow.LayoutParams.MATCH_PARENT, + TableRow.LayoutParams.MATCH_PARENT + ) + + init { + cardLayout.setMargins(5) + } +} + +fun Array.setupEnumSelection( + target: KMutableProperty0, + values: Array, + activeColor: Int = R.color.blue, + inactiveColor: Int = R.color.gray_0x60, + callback: () -> Unit = {}, +) { + fun update() { + for ((i, currentButton) in withIndex()) { + currentButton.setBackgroundColor( + ContextCompat.getColor( + currentButton.context, + if (target.get() == values[i]) activeColor else inactiveColor + ) + ) + } + callback() + } + for ((i, button) in withIndex()) { + button.text = values[i].toString() + button.setOnClickListener { + target.set(values[i]) + update() + } + } + update() } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ 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 7beb64c..7f10ff2 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -10,11 +10,12 @@ package com.lukas.music.instruments +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note class PolyInstrument(name: String) : Instrument(name) { - private val internalInstruments = Array(3) { InternalInstrument() } - private val playing = Array(3) { false } + private val internalInstruments = Array(Chord.NOTE_COUNT) { InternalInstrument() } + private val playing = Array(Chord.NOTE_COUNT) { false } override var waveform: Waveform = Waveform.SINE set(value) { @@ -86,7 +87,7 @@ override fun updateEffects() { for (instrument in internalInstruments) { for (effect in effects) { - instrument.applyEffectAttributes(effect) + instrument.applyEffectAttributes(this, effect) } } } @@ -99,4 +100,10 @@ } return false } + + override fun moveEffects(from: Int, to: Int) { + for (instrument in internalInstruments) { + instrument.moveEffects(from, to) + } + } } \ No newline at end of file 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 index f921b1b..f659a8c 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -14,7 +14,9 @@ class Effect(val type: EffectType, private val instrument: Instrument) { val parameters = Array(type.parameterDescriptions.size) { - EffectParameter(type.parameterDescriptions[it], instrument) + type.parameterDescriptions[it]?.let { parameterDescription -> + EffectParameter(parameterDescription, instrument) + } } val influence = EffectParameter(influenceDescription, instrument) 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 index 8af39ea..8b90ec5 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -13,8 +13,8 @@ import com.lukas.music.util.format enum class EffectType( - val title: String, - val parameterDescriptions: Array + private val title: String, + val parameterDescriptions: Array ) { LowPass("low pass filter", arrayOf( @@ -22,13 +22,17 @@ "cutoff: ${it.value.format(1)} octaves" } )), - Noise("noise", + Noise( + "noise", arrayOf( - EffectParameterDescription(0f, 1f, 0f) { - "unused" - } + null ) - ) + ), + Distortion("distortion", arrayOf( + EffectParameterDescription(1f, 4f, 1f) { + "strength: ${it.value.format(1)}x" + } + )) ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt index 4c6a0d9..7807cb9 100644 --- a/app/src/main/java/com/lukas/music/song/ScaleType.kt +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -10,24 +10,12 @@ 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 - ) + arrayOf(0, 2, 4, 5, 7, 9, 11), ) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Accidental.kt b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt new file mode 100644 index 0000000..688ae4e --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or(at your option) any later version. + * + * This program is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 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.chords + +enum class Accidental(val id: String, val short: String, val distance: Int) { + Flat("\u266D", "b", -1), + None("\u266E", "", 0), + Sharp("\u266F", "#", 1), + ; + + override fun toString(): String { + return id + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index 4400dae..85e530b 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -10,14 +10,19 @@ package com.lukas.music.song.chords +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -class Chord(note: Int, var chordType: ChordType) { - var note: Int = note +class Chord { + var accidental = Accidental.None + val accidentals: Array = arrayOf(Accidental.None, Accidental.None, null, null) + + var note: Int = 0 set(value) { field = value interval = Interval(value) } + var interval = Interval(note) set(value) { field = value @@ -27,19 +32,76 @@ } fun getNotes(root: Note): Array { - return Array(chordType.notes.size) { root + note + chordType.notes[it] } + val result = Array(NOTE_COUNT) { root } + var resultIndex = 0 + var accidentalIndex = 0 + var octave = 0 + while (resultIndex < NOTE_COUNT) { + if (accidentalIndex == 0) { + result[resultIndex] = root + note + 12 * octave + accidental.distance + resultIndex++ + } else if (accidentals[accidentalIndex - 1] != null) { + result[resultIndex] = root + note + when (accidentalIndex) { + 1 -> 4 + 2 -> 7 + 3 -> 10 + 4 -> 14 + else -> 0 + } + accidentals[accidentalIndex - 1]!!.distance + 12 * octave + accidental.distance + resultIndex++ + } + accidentalIndex++ + if (accidentalIndex > accidentals.size) { + octave++ + accidentalIndex = 0 + } + } + return result } override fun toString(): String { - return chordType.transform(interval.toString()) + return toString(false, Song.currentSong.root) } fun toString(displayChordNames: Boolean, root: Note): String { - val base = if (displayChordNames) { - (root + note).noteName.toString() + var result = if (displayChordNames) { + (root + note + accidental.distance).noteName.toString() } else { interval.toString() } - return chordType.transform(base) + accidentals[0]?.let { + result += when (it) { + Accidental.Flat -> "-" + Accidental.Sharp -> "sus4" + else -> "" + } + } + accidentals[1]?.let { + if (accidentals[0] != null && it == Accidental.None) { + return@let + } + result += it.short + "5" + } + result = result.replace("-b5", "0") + result = result.replace("(?=[A-G])#5".toRegex(), "+") + accidentals[2]?.let { + result += when (it) { + Accidental.Sharp -> " maj7" + Accidental.None -> " 7" + Accidental.Flat -> " 6" + } + } + accidentals[3]?.let { + result += when (it) { + Accidental.Sharp -> " maj9" + Accidental.None -> " 9" + Accidental.Flat -> " b9" + } + } + return result + } + + companion object { + const val NOTE_COUNT = 5 } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt b/app/src/main/java/com/lukas/music/song/chords/ChordType.kt deleted file mode 100644 index 1fe4b40..0000000 --- a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt +++ /dev/null @@ -1,30 +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.chords - -enum class ChordType( - val notes: Array, - private val asString: String, - val transform: (String) -> String -) { - MAJOR(arrayOf(0, 4, 7), "major", { it.uppercase() }), - MINOR(arrayOf(0, 3, 7), "minor", { it.lowercase() }), - DIMINISHED(arrayOf(0, 3, 6), "diminished", { it.lowercase() + "0" }), - ; - - override fun toString(): String { - return asString - } - - companion object { - val VALUES = values() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt index 1cadb06..f4bcbc7 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt @@ -15,7 +15,7 @@ class Phrase : Cycle() { init { for (i in 0 until 4) { - this += Chord(0, ChordType.MAJOR) + this += Chord() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index 17bfb2b..f17e322 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -12,7 +12,7 @@ import kotlin.math.pow -class Note(private val id: Int) { +class Note(val id: Int) { val noteName = NoteName.VALUES[id % 12] val octave = id / 12 - 1 val frequency = 440 * 2.0.pow((id - 69) / 12.0) @@ -28,6 +28,8 @@ return this + (-other) } + operator fun minus(other: Note): Int = id - other.id + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false 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 index e06761a..40b1d14 100644 --- a/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -11,6 +11,7 @@ package com.lukas.music.song.voice import com.lukas.music.song.ScaleType +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note import com.lukas.music.util.transform @@ -20,7 +21,7 @@ val getNotes: (Note, Array) -> Array ) { Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), - Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + ChordVoice("Chord notes", Chord.NOTE_COUNT, { _, 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 } }), diff --git a/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt new file mode 100644 index 0000000..730eabd --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.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.ui.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.lukas.music.databinding.FragmentEffectBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.ui.fragments.EditEffectsFragment +import com.lukas.music.ui.fragments.EffectFragment + +class EffectsAdapter(private val parent: EditEffectsFragment, private val instrument: Instrument) : + RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EffectFragment { + val context = parent.context + val inflater = LayoutInflater.from(context) + val binding = FragmentEffectBinding.inflate(inflater, parent, false) + return EffectFragment(binding) + } + + override fun onBindViewHolder(holder: EffectFragment, position: Int) { + holder.setEffect(instrument.effects[position]) + } + + override fun getItemCount(): Int { + return instrument.effects.size + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt index 3e129bc..3d7d1f4 100644 --- a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt @@ -39,7 +39,7 @@ Song.currentSong.soloInstrument = instrument } field = value - binding.soloButton.updateToggle(this::solo, R.color.blue) + binding.soloButton.updateToggle(this.solo, R.color.blue) } var instrument: Instrument? = null 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 6c3bae3..5fc9dfe 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 @@ -14,61 +14,124 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.DialogFragment +import android.widget.TableRow +import android.widget.TextView +import androidx.core.view.children +import com.google.android.material.button.MaterialButton +import com.lukas.music.R import com.lukas.music.databinding.FragmentEditChordBinding import com.lukas.music.song.ScaleType import com.lukas.music.song.Song +import com.lukas.music.song.chords.Accidental 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 +import com.lukas.music.util.* class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : - DialogFragment() { - lateinit var binding: FragmentEditChordBinding + EasyDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentEditChordBinding.inflate(inflater) + Array(Accidental.VALUES.size) { + val button = MaterialButton(binding.root.context) + button.layoutParams = UIUtil.cardLayout + binding.accidentalSelection.addView(button) + return@Array button + }.setupEnumSelection(chord::accidental, Accidental.VALUES, callback = { update() }) setupPitchSpinner() - setupTypeSpinner() + setupEditor() binding.exitButton.setOnClickListener { dismiss() } return binding.root } + private fun update() { + songFragment.updateChords() + binding.chordText.text = chord.toString(true, Song.currentSong.root) + updateEditor() + } + private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { 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 = ScaleType.MAJOR.steps[it] - if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] + if (chord.note == ScaleType.MAJOR.steps[it]) { + update() + return@setup } - songFragment.updateChords() + chord.note = ScaleType.MAJOR.steps[it] + chord.accidental = Accidental.None + chord.accidentals[0] = + Accidental.VALUES[(ScaleType.MAJOR.steps[(it + 2) % ScaleType.MAJOR.steps.size] distance chord.note) - 3] + chord.accidentals[1] = + Accidental.VALUES[(ScaleType.MAJOR.steps[(it + 4) % ScaleType.MAJOR.steps.size] distance chord.note) - 6] + update() } } - private fun setupTypeSpinner() { - val values = mutableListOf("default") - for (chordType in ChordType.VALUES) { - values += chordType.toString() + private fun setupEditor() { + binding.editorGrid.removeAllViews() + val row = TableRow(binding.root.context) + for (description in descriptions) { + val text = TextView(binding.root.context) + text.text = description + text.layoutParams = UIUtil.cardLayout + text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER + row.addView(text) } - binding.typeSpinner.setup( - values, - if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 - else chord.chordType.ordinal + 1 - ) { - if (it == 0) { - chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] - } else { - chord.chordType = ChordType.VALUES[it - 1] + binding.editorGrid.addView(row) + for (accidental in Accidental.VALUES) { + val row = TableRow(binding.root.context) + for (position in 0 until Chord.NOTE_COUNT - 1) { + val button = MaterialButton(binding.root.context) + button.text = accidental.toString() + button.layoutParams = UIUtil.cardLayout + button.updateToggle(chord.accidentals[position] == accidental, R.color.blue) + button.setOnClickListener { + if (chord.accidentals[position] == accidental) { + chord.accidentals[position] = null + } else { + chord.accidentals[position] = accidental + } + update() + } + row.addView(button) } - songFragment.updateChords() + binding.editorGrid.addView(row) } } + + private fun updateEditor() { + for ((index, view) in binding.editorGrid.children.iterator().withIndex()) { + if (index == 0) { + continue + } + view as TableRow + for ((childIndex, childView) in view.children.iterator().withIndex()) { + childView as MaterialButton + childView.updateToggle( + chord.accidentals[childIndex] == Accidental.VALUES[index - 1], + R.color.blue + ) + } + } + } + + companion object { + val descriptions = arrayOf("III", "V", "VII", "IX") + } +} + +infix fun Int.distance(other: Int): Int { + var result = this - other + while (result < 0) { + result += 12 + } + result %= 12 + return result } \ No newline at end of file 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 index 6e359e4..0d532c2 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt @@ -14,9 +14,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager import com.lukas.music.databinding.FragmentEditEffectsBinding import com.lukas.music.instruments.Instrument +import com.lukas.music.ui.adapters.EffectsAdapter import com.lukas.music.util.EasyDialogFragment +import com.lukas.music.util.makeMoveCallback class EditEffectsFragment(private val instrument: Instrument) : EasyDialogFragment() { @@ -25,11 +29,12 @@ 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.effectsDisplay.adapter = EffectsAdapter(this, instrument) + binding.effectsDisplay.layoutManager = LinearLayoutManager(context) + val helper = ItemTouchHelper(makeMoveCallback(instrument.effects) { from, to -> + instrument.moveEffects(from, to) + }) + helper.attachToRecyclerView(binding.effectsDisplay) binding.closeButton.setOnClickListener { dismiss() } 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 index ebf4cb1..7b13ed6 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt @@ -10,25 +10,18 @@ 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 androidx.recyclerview.widget.RecyclerView 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) +class EffectFragment(val binding: FragmentEffectBinding) : RecyclerView.ViewHolder( + binding.root +) { + fun setEffect(effect: Effect) { binding.effectName.text = effect.type.toString() binding.activeButton.setupToggle(effect::active, R.color.blue) { binding.activeButton.text = if (it) "ON" else "OFF" @@ -37,10 +30,15 @@ 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]) + binding.parameter1SeekBar.visibility = + if (effect.parameters[0] == null) View.GONE else View.VISIBLE + binding.parameter1Text.visibility = + if (effect.parameters[0] == null) View.GONE else View.VISIBLE + effect.parameters[0]?.let { + binding.parameter1SeekBar.smartSetup(0, 100, it::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/ui/fragments/InstrumentListFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt index 1d35c10..0b66892 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt @@ -24,6 +24,7 @@ import com.lukas.music.instruments.MonoInstrument import com.lukas.music.instruments.PolyInstrument import com.lukas.music.ui.adapters.InstrumentAdapter +import com.lukas.music.util.makeMoveCallback class InstrumentListFragment : Fragment() { lateinit var binding: FragmentInstrumentListBinding @@ -35,32 +36,7 @@ binding = FragmentInstrumentListBinding.inflate(inflater) binding.recyclerView.adapter = InstrumentAdapter(this) binding.recyclerView.layoutManager = LinearLayoutManager(context) - val callback = object : ItemTouchHelper.SimpleCallback( - ItemTouchHelper.UP or ItemTouchHelper.DOWN, - 0 - ) { - override fun onMove( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean { - val adapter = recyclerView.adapter as InstrumentAdapter - val startPosition = viewHolder.adapterPosition - val endPosition = target.adapterPosition - val instrument = Instrument.instruments[startPosition] - Instrument.instruments.removeAt(startPosition) - if (endPosition < startPosition) { - Instrument.instruments.add(endPosition + 1, instrument) - } else { - Instrument.instruments.add(endPosition - 1, instrument) - } - adapter.notifyItemMoved(startPosition, endPosition) - return true - } - - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} - } - val helper = ItemTouchHelper(callback) + val helper = ItemTouchHelper(makeMoveCallback(Instrument.instruments)) helper.attachToRecyclerView(binding.recyclerView) val builder = AlertDialog.Builder(binding.root.context) diff --git a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt index 0c33019..4b668c4 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt @@ -27,6 +27,7 @@ import com.lukas.music.databinding.FragmentPlayBinding import com.lukas.music.instruments.Rhythm import com.lukas.music.song.Song +import com.lukas.music.util.UIUtil import com.lukas.music.util.setup class PlayFragment : Fragment() { @@ -125,12 +126,12 @@ chordDisplays.clear() for (chord in Song.currentSong.chordProgression.currentItem ?: return) { val card = CardView(binding.root.context) - card.layoutParams = SongFragment.tableRowLayout + card.layoutParams = UIUtil.cardLayout card.radius = 10f card.preventCornerOverlap = false val text = TextView(binding.root.context) text.text = chord.toString(true, Song.currentSong.root) - text.layoutParams = SongFragment.tableRowLayout + text.layoutParams = UIUtil.fillingLayout text.textSize = 20f text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER card.addView(text) diff --git a/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt index 62d314f..afe6137 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt @@ -16,13 +16,13 @@ import android.view.ViewGroup import android.widget.* import androidx.cardview.widget.CardView -import androidx.core.view.setMargins import androidx.fragment.app.Fragment import com.lukas.music.databinding.FragmentSongBinding import com.lukas.music.song.Song import com.lukas.music.song.chords.Phrase import com.lukas.music.song.note.Note import com.lukas.music.song.note.NoteName +import com.lukas.music.util.UIUtil class SongFragment(val playFragment: PlayFragment) : Fragment(), @@ -63,14 +63,14 @@ for (chord in phrase) { val card = CardView(binding.root.context) card.radius = 10f - card.layoutParams = tableRowLayout + card.layoutParams = UIUtil.cardLayout card.setOnClickListener { EditChordFragment(chord, this).showNow(childFragmentManager, "") } val text = TextView(binding.root.context) text.text = chord.toString(displayChordNames, Song.currentSong.root) text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER - text.layoutParams = tableRowLayout + text.layoutParams = UIUtil.fillingLayout text.textSize = 20f card.addView(text) row.addView(card) @@ -81,28 +81,13 @@ updateChords() } button.setImageResource(android.R.drawable.ic_delete) - button.layoutParams = buttonLayout + button.layoutParams = UIUtil.buttonLayout row.addView(button) binding.chords.addView(row) } playFragment.updateChords() } - companion object { - val tableRowLayout = TableRow.LayoutParams( - TableRow.LayoutParams.MATCH_PARENT, - TableRow.LayoutParams.MATCH_PARENT - ) - val buttonLayout = TableRow.LayoutParams( - 0, - TableRow.LayoutParams.WRAP_CONTENT - ) - - init { - tableRowLayout.setMargins(10) - } - } - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { Song.currentSong.root = Note.of(NoteName.VALUES[position], 4) if (displayChordNames) { 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 b67d0ea..6ae1bd9 100644 --- a/app/src/main/java/com/lukas/music/util/UIUtil.kt +++ b/app/src/main/java/com/lukas/music/util/UIUtil.kt @@ -13,6 +13,10 @@ import android.view.View import android.widget.* import androidx.core.content.ContextCompat +import androidx.core.view.setMargins +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.button.MaterialButton import com.lukas.music.R import kotlin.reflect.KMutableProperty0 @@ -62,19 +66,19 @@ ) { setOnClickListener { target.set(!target.get()) - updateToggle(target, activeColor, inactiveColor) + updateToggle(target.get(), activeColor, inactiveColor) callback(target.get()) } - updateToggle(target, activeColor, inactiveColor) + updateToggle(target.get(), activeColor, inactiveColor) } fun Button.updateToggle( - target: KMutableProperty0, + value: Boolean, activeColor: Int, inactiveColor: Int = R.color.gray_0x60, ) { setBackgroundColor( - ContextCompat.getColor(context, if (target.get()) activeColor else inactiveColor) + ContextCompat.getColor(context, if (value) activeColor else inactiveColor) ) } @@ -138,4 +142,79 @@ } callback(it) } +} + +fun makeMoveCallback( + list: MutableList, + callback: (Int, Int) -> Unit = { _, _ -> } +): ItemTouchHelper.SimpleCallback { + return object : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, + 0 + ) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val adapter = recyclerView.adapter + val startPosition = viewHolder.adapterPosition + val endPosition = target.adapterPosition + val item = list[startPosition] + list.removeAt(startPosition) + list.add(endPosition, item) + adapter!!.notifyItemMoved(startPosition, endPosition) + callback(startPosition, endPosition) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} + } +} + +object UIUtil { + val cardLayout = TableRow.LayoutParams( + 0, + TableRow.LayoutParams.WRAP_CONTENT, + 1f + ) + val buttonLayout = TableRow.LayoutParams( + 0, TableRow.LayoutParams.MATCH_PARENT, 0.5f + ) + val fillingLayout = TableRow.LayoutParams( + TableRow.LayoutParams.MATCH_PARENT, + TableRow.LayoutParams.MATCH_PARENT + ) + + init { + cardLayout.setMargins(5) + } +} + +fun Array.setupEnumSelection( + target: KMutableProperty0, + values: Array, + activeColor: Int = R.color.blue, + inactiveColor: Int = R.color.gray_0x60, + callback: () -> Unit = {}, +) { + fun update() { + for ((i, currentButton) in withIndex()) { + currentButton.setBackgroundColor( + ContextCompat.getColor( + currentButton.context, + if (target.get() == values[i]) activeColor else inactiveColor + ) + ) + } + callback() + } + for ((i, button) in withIndex()) { + button.text = values[i].toString() + button.setOnClickListener { + target.set(values[i]) + update() + } + } + update() } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_chord.xml b/app/src/main/res/layout/fragment_edit_chord.xml index 16fa8f2..8b34d22 100644 --- a/app/src/main/res/layout/fragment_edit_chord.xml +++ b/app/src/main/res/layout/fragment_edit_chord.xml @@ -19,10 +19,10 @@ @@ -37,20 +37,7 @@ android:minHeight="48dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/textView5" - app:layout_constraintTop_toBottomOf="@+id/textView4" /> - - + app:layout_constraintTop_toBottomOf="@+id/accidentalSelection" /> - - + app:layout_constraintTop_toBottomOf="@+id/editorGrid" /> + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ 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 7beb64c..7f10ff2 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -10,11 +10,12 @@ package com.lukas.music.instruments +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note class PolyInstrument(name: String) : Instrument(name) { - private val internalInstruments = Array(3) { InternalInstrument() } - private val playing = Array(3) { false } + private val internalInstruments = Array(Chord.NOTE_COUNT) { InternalInstrument() } + private val playing = Array(Chord.NOTE_COUNT) { false } override var waveform: Waveform = Waveform.SINE set(value) { @@ -86,7 +87,7 @@ override fun updateEffects() { for (instrument in internalInstruments) { for (effect in effects) { - instrument.applyEffectAttributes(effect) + instrument.applyEffectAttributes(this, effect) } } } @@ -99,4 +100,10 @@ } return false } + + override fun moveEffects(from: Int, to: Int) { + for (instrument in internalInstruments) { + instrument.moveEffects(from, to) + } + } } \ No newline at end of file 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 index f921b1b..f659a8c 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -14,7 +14,9 @@ class Effect(val type: EffectType, private val instrument: Instrument) { val parameters = Array(type.parameterDescriptions.size) { - EffectParameter(type.parameterDescriptions[it], instrument) + type.parameterDescriptions[it]?.let { parameterDescription -> + EffectParameter(parameterDescription, instrument) + } } val influence = EffectParameter(influenceDescription, instrument) 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 index 8af39ea..8b90ec5 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -13,8 +13,8 @@ import com.lukas.music.util.format enum class EffectType( - val title: String, - val parameterDescriptions: Array + private val title: String, + val parameterDescriptions: Array ) { LowPass("low pass filter", arrayOf( @@ -22,13 +22,17 @@ "cutoff: ${it.value.format(1)} octaves" } )), - Noise("noise", + Noise( + "noise", arrayOf( - EffectParameterDescription(0f, 1f, 0f) { - "unused" - } + null ) - ) + ), + Distortion("distortion", arrayOf( + EffectParameterDescription(1f, 4f, 1f) { + "strength: ${it.value.format(1)}x" + } + )) ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt index 4c6a0d9..7807cb9 100644 --- a/app/src/main/java/com/lukas/music/song/ScaleType.kt +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -10,24 +10,12 @@ 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 - ) + arrayOf(0, 2, 4, 5, 7, 9, 11), ) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Accidental.kt b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt new file mode 100644 index 0000000..688ae4e --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or(at your option) any later version. + * + * This program is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 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.chords + +enum class Accidental(val id: String, val short: String, val distance: Int) { + Flat("\u266D", "b", -1), + None("\u266E", "", 0), + Sharp("\u266F", "#", 1), + ; + + override fun toString(): String { + return id + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index 4400dae..85e530b 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -10,14 +10,19 @@ package com.lukas.music.song.chords +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -class Chord(note: Int, var chordType: ChordType) { - var note: Int = note +class Chord { + var accidental = Accidental.None + val accidentals: Array = arrayOf(Accidental.None, Accidental.None, null, null) + + var note: Int = 0 set(value) { field = value interval = Interval(value) } + var interval = Interval(note) set(value) { field = value @@ -27,19 +32,76 @@ } fun getNotes(root: Note): Array { - return Array(chordType.notes.size) { root + note + chordType.notes[it] } + val result = Array(NOTE_COUNT) { root } + var resultIndex = 0 + var accidentalIndex = 0 + var octave = 0 + while (resultIndex < NOTE_COUNT) { + if (accidentalIndex == 0) { + result[resultIndex] = root + note + 12 * octave + accidental.distance + resultIndex++ + } else if (accidentals[accidentalIndex - 1] != null) { + result[resultIndex] = root + note + when (accidentalIndex) { + 1 -> 4 + 2 -> 7 + 3 -> 10 + 4 -> 14 + else -> 0 + } + accidentals[accidentalIndex - 1]!!.distance + 12 * octave + accidental.distance + resultIndex++ + } + accidentalIndex++ + if (accidentalIndex > accidentals.size) { + octave++ + accidentalIndex = 0 + } + } + return result } override fun toString(): String { - return chordType.transform(interval.toString()) + return toString(false, Song.currentSong.root) } fun toString(displayChordNames: Boolean, root: Note): String { - val base = if (displayChordNames) { - (root + note).noteName.toString() + var result = if (displayChordNames) { + (root + note + accidental.distance).noteName.toString() } else { interval.toString() } - return chordType.transform(base) + accidentals[0]?.let { + result += when (it) { + Accidental.Flat -> "-" + Accidental.Sharp -> "sus4" + else -> "" + } + } + accidentals[1]?.let { + if (accidentals[0] != null && it == Accidental.None) { + return@let + } + result += it.short + "5" + } + result = result.replace("-b5", "0") + result = result.replace("(?=[A-G])#5".toRegex(), "+") + accidentals[2]?.let { + result += when (it) { + Accidental.Sharp -> " maj7" + Accidental.None -> " 7" + Accidental.Flat -> " 6" + } + } + accidentals[3]?.let { + result += when (it) { + Accidental.Sharp -> " maj9" + Accidental.None -> " 9" + Accidental.Flat -> " b9" + } + } + return result + } + + companion object { + const val NOTE_COUNT = 5 } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt b/app/src/main/java/com/lukas/music/song/chords/ChordType.kt deleted file mode 100644 index 1fe4b40..0000000 --- a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt +++ /dev/null @@ -1,30 +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.chords - -enum class ChordType( - val notes: Array, - private val asString: String, - val transform: (String) -> String -) { - MAJOR(arrayOf(0, 4, 7), "major", { it.uppercase() }), - MINOR(arrayOf(0, 3, 7), "minor", { it.lowercase() }), - DIMINISHED(arrayOf(0, 3, 6), "diminished", { it.lowercase() + "0" }), - ; - - override fun toString(): String { - return asString - } - - companion object { - val VALUES = values() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt index 1cadb06..f4bcbc7 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt @@ -15,7 +15,7 @@ class Phrase : Cycle() { init { for (i in 0 until 4) { - this += Chord(0, ChordType.MAJOR) + this += Chord() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index 17bfb2b..f17e322 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -12,7 +12,7 @@ import kotlin.math.pow -class Note(private val id: Int) { +class Note(val id: Int) { val noteName = NoteName.VALUES[id % 12] val octave = id / 12 - 1 val frequency = 440 * 2.0.pow((id - 69) / 12.0) @@ -28,6 +28,8 @@ return this + (-other) } + operator fun minus(other: Note): Int = id - other.id + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false 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 index e06761a..40b1d14 100644 --- a/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -11,6 +11,7 @@ package com.lukas.music.song.voice import com.lukas.music.song.ScaleType +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note import com.lukas.music.util.transform @@ -20,7 +21,7 @@ val getNotes: (Note, Array) -> Array ) { Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), - Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + ChordVoice("Chord notes", Chord.NOTE_COUNT, { _, 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 } }), diff --git a/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt new file mode 100644 index 0000000..730eabd --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.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.ui.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.lukas.music.databinding.FragmentEffectBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.ui.fragments.EditEffectsFragment +import com.lukas.music.ui.fragments.EffectFragment + +class EffectsAdapter(private val parent: EditEffectsFragment, private val instrument: Instrument) : + RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EffectFragment { + val context = parent.context + val inflater = LayoutInflater.from(context) + val binding = FragmentEffectBinding.inflate(inflater, parent, false) + return EffectFragment(binding) + } + + override fun onBindViewHolder(holder: EffectFragment, position: Int) { + holder.setEffect(instrument.effects[position]) + } + + override fun getItemCount(): Int { + return instrument.effects.size + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt index 3e129bc..3d7d1f4 100644 --- a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt @@ -39,7 +39,7 @@ Song.currentSong.soloInstrument = instrument } field = value - binding.soloButton.updateToggle(this::solo, R.color.blue) + binding.soloButton.updateToggle(this.solo, R.color.blue) } var instrument: Instrument? = null 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 6c3bae3..5fc9dfe 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 @@ -14,61 +14,124 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.DialogFragment +import android.widget.TableRow +import android.widget.TextView +import androidx.core.view.children +import com.google.android.material.button.MaterialButton +import com.lukas.music.R import com.lukas.music.databinding.FragmentEditChordBinding import com.lukas.music.song.ScaleType import com.lukas.music.song.Song +import com.lukas.music.song.chords.Accidental 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 +import com.lukas.music.util.* class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : - DialogFragment() { - lateinit var binding: FragmentEditChordBinding + EasyDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentEditChordBinding.inflate(inflater) + Array(Accidental.VALUES.size) { + val button = MaterialButton(binding.root.context) + button.layoutParams = UIUtil.cardLayout + binding.accidentalSelection.addView(button) + return@Array button + }.setupEnumSelection(chord::accidental, Accidental.VALUES, callback = { update() }) setupPitchSpinner() - setupTypeSpinner() + setupEditor() binding.exitButton.setOnClickListener { dismiss() } return binding.root } + private fun update() { + songFragment.updateChords() + binding.chordText.text = chord.toString(true, Song.currentSong.root) + updateEditor() + } + private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { 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 = ScaleType.MAJOR.steps[it] - if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] + if (chord.note == ScaleType.MAJOR.steps[it]) { + update() + return@setup } - songFragment.updateChords() + chord.note = ScaleType.MAJOR.steps[it] + chord.accidental = Accidental.None + chord.accidentals[0] = + Accidental.VALUES[(ScaleType.MAJOR.steps[(it + 2) % ScaleType.MAJOR.steps.size] distance chord.note) - 3] + chord.accidentals[1] = + Accidental.VALUES[(ScaleType.MAJOR.steps[(it + 4) % ScaleType.MAJOR.steps.size] distance chord.note) - 6] + update() } } - private fun setupTypeSpinner() { - val values = mutableListOf("default") - for (chordType in ChordType.VALUES) { - values += chordType.toString() + private fun setupEditor() { + binding.editorGrid.removeAllViews() + val row = TableRow(binding.root.context) + for (description in descriptions) { + val text = TextView(binding.root.context) + text.text = description + text.layoutParams = UIUtil.cardLayout + text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER + row.addView(text) } - binding.typeSpinner.setup( - values, - if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 - else chord.chordType.ordinal + 1 - ) { - if (it == 0) { - chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] - } else { - chord.chordType = ChordType.VALUES[it - 1] + binding.editorGrid.addView(row) + for (accidental in Accidental.VALUES) { + val row = TableRow(binding.root.context) + for (position in 0 until Chord.NOTE_COUNT - 1) { + val button = MaterialButton(binding.root.context) + button.text = accidental.toString() + button.layoutParams = UIUtil.cardLayout + button.updateToggle(chord.accidentals[position] == accidental, R.color.blue) + button.setOnClickListener { + if (chord.accidentals[position] == accidental) { + chord.accidentals[position] = null + } else { + chord.accidentals[position] = accidental + } + update() + } + row.addView(button) } - songFragment.updateChords() + binding.editorGrid.addView(row) } } + + private fun updateEditor() { + for ((index, view) in binding.editorGrid.children.iterator().withIndex()) { + if (index == 0) { + continue + } + view as TableRow + for ((childIndex, childView) in view.children.iterator().withIndex()) { + childView as MaterialButton + childView.updateToggle( + chord.accidentals[childIndex] == Accidental.VALUES[index - 1], + R.color.blue + ) + } + } + } + + companion object { + val descriptions = arrayOf("III", "V", "VII", "IX") + } +} + +infix fun Int.distance(other: Int): Int { + var result = this - other + while (result < 0) { + result += 12 + } + result %= 12 + return result } \ No newline at end of file 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 index 6e359e4..0d532c2 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt @@ -14,9 +14,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager import com.lukas.music.databinding.FragmentEditEffectsBinding import com.lukas.music.instruments.Instrument +import com.lukas.music.ui.adapters.EffectsAdapter import com.lukas.music.util.EasyDialogFragment +import com.lukas.music.util.makeMoveCallback class EditEffectsFragment(private val instrument: Instrument) : EasyDialogFragment() { @@ -25,11 +29,12 @@ 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.effectsDisplay.adapter = EffectsAdapter(this, instrument) + binding.effectsDisplay.layoutManager = LinearLayoutManager(context) + val helper = ItemTouchHelper(makeMoveCallback(instrument.effects) { from, to -> + instrument.moveEffects(from, to) + }) + helper.attachToRecyclerView(binding.effectsDisplay) binding.closeButton.setOnClickListener { dismiss() } 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 index ebf4cb1..7b13ed6 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt @@ -10,25 +10,18 @@ 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 androidx.recyclerview.widget.RecyclerView 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) +class EffectFragment(val binding: FragmentEffectBinding) : RecyclerView.ViewHolder( + binding.root +) { + fun setEffect(effect: Effect) { binding.effectName.text = effect.type.toString() binding.activeButton.setupToggle(effect::active, R.color.blue) { binding.activeButton.text = if (it) "ON" else "OFF" @@ -37,10 +30,15 @@ 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]) + binding.parameter1SeekBar.visibility = + if (effect.parameters[0] == null) View.GONE else View.VISIBLE + binding.parameter1Text.visibility = + if (effect.parameters[0] == null) View.GONE else View.VISIBLE + effect.parameters[0]?.let { + binding.parameter1SeekBar.smartSetup(0, 100, it::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/ui/fragments/InstrumentListFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt index 1d35c10..0b66892 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt @@ -24,6 +24,7 @@ import com.lukas.music.instruments.MonoInstrument import com.lukas.music.instruments.PolyInstrument import com.lukas.music.ui.adapters.InstrumentAdapter +import com.lukas.music.util.makeMoveCallback class InstrumentListFragment : Fragment() { lateinit var binding: FragmentInstrumentListBinding @@ -35,32 +36,7 @@ binding = FragmentInstrumentListBinding.inflate(inflater) binding.recyclerView.adapter = InstrumentAdapter(this) binding.recyclerView.layoutManager = LinearLayoutManager(context) - val callback = object : ItemTouchHelper.SimpleCallback( - ItemTouchHelper.UP or ItemTouchHelper.DOWN, - 0 - ) { - override fun onMove( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean { - val adapter = recyclerView.adapter as InstrumentAdapter - val startPosition = viewHolder.adapterPosition - val endPosition = target.adapterPosition - val instrument = Instrument.instruments[startPosition] - Instrument.instruments.removeAt(startPosition) - if (endPosition < startPosition) { - Instrument.instruments.add(endPosition + 1, instrument) - } else { - Instrument.instruments.add(endPosition - 1, instrument) - } - adapter.notifyItemMoved(startPosition, endPosition) - return true - } - - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} - } - val helper = ItemTouchHelper(callback) + val helper = ItemTouchHelper(makeMoveCallback(Instrument.instruments)) helper.attachToRecyclerView(binding.recyclerView) val builder = AlertDialog.Builder(binding.root.context) diff --git a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt index 0c33019..4b668c4 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt @@ -27,6 +27,7 @@ import com.lukas.music.databinding.FragmentPlayBinding import com.lukas.music.instruments.Rhythm import com.lukas.music.song.Song +import com.lukas.music.util.UIUtil import com.lukas.music.util.setup class PlayFragment : Fragment() { @@ -125,12 +126,12 @@ chordDisplays.clear() for (chord in Song.currentSong.chordProgression.currentItem ?: return) { val card = CardView(binding.root.context) - card.layoutParams = SongFragment.tableRowLayout + card.layoutParams = UIUtil.cardLayout card.radius = 10f card.preventCornerOverlap = false val text = TextView(binding.root.context) text.text = chord.toString(true, Song.currentSong.root) - text.layoutParams = SongFragment.tableRowLayout + text.layoutParams = UIUtil.fillingLayout text.textSize = 20f text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER card.addView(text) diff --git a/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt index 62d314f..afe6137 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt @@ -16,13 +16,13 @@ import android.view.ViewGroup import android.widget.* import androidx.cardview.widget.CardView -import androidx.core.view.setMargins import androidx.fragment.app.Fragment import com.lukas.music.databinding.FragmentSongBinding import com.lukas.music.song.Song import com.lukas.music.song.chords.Phrase import com.lukas.music.song.note.Note import com.lukas.music.song.note.NoteName +import com.lukas.music.util.UIUtil class SongFragment(val playFragment: PlayFragment) : Fragment(), @@ -63,14 +63,14 @@ for (chord in phrase) { val card = CardView(binding.root.context) card.radius = 10f - card.layoutParams = tableRowLayout + card.layoutParams = UIUtil.cardLayout card.setOnClickListener { EditChordFragment(chord, this).showNow(childFragmentManager, "") } val text = TextView(binding.root.context) text.text = chord.toString(displayChordNames, Song.currentSong.root) text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER - text.layoutParams = tableRowLayout + text.layoutParams = UIUtil.fillingLayout text.textSize = 20f card.addView(text) row.addView(card) @@ -81,28 +81,13 @@ updateChords() } button.setImageResource(android.R.drawable.ic_delete) - button.layoutParams = buttonLayout + button.layoutParams = UIUtil.buttonLayout row.addView(button) binding.chords.addView(row) } playFragment.updateChords() } - companion object { - val tableRowLayout = TableRow.LayoutParams( - TableRow.LayoutParams.MATCH_PARENT, - TableRow.LayoutParams.MATCH_PARENT - ) - val buttonLayout = TableRow.LayoutParams( - 0, - TableRow.LayoutParams.WRAP_CONTENT - ) - - init { - tableRowLayout.setMargins(10) - } - } - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { Song.currentSong.root = Note.of(NoteName.VALUES[position], 4) if (displayChordNames) { 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 b67d0ea..6ae1bd9 100644 --- a/app/src/main/java/com/lukas/music/util/UIUtil.kt +++ b/app/src/main/java/com/lukas/music/util/UIUtil.kt @@ -13,6 +13,10 @@ import android.view.View import android.widget.* import androidx.core.content.ContextCompat +import androidx.core.view.setMargins +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.button.MaterialButton import com.lukas.music.R import kotlin.reflect.KMutableProperty0 @@ -62,19 +66,19 @@ ) { setOnClickListener { target.set(!target.get()) - updateToggle(target, activeColor, inactiveColor) + updateToggle(target.get(), activeColor, inactiveColor) callback(target.get()) } - updateToggle(target, activeColor, inactiveColor) + updateToggle(target.get(), activeColor, inactiveColor) } fun Button.updateToggle( - target: KMutableProperty0, + value: Boolean, activeColor: Int, inactiveColor: Int = R.color.gray_0x60, ) { setBackgroundColor( - ContextCompat.getColor(context, if (target.get()) activeColor else inactiveColor) + ContextCompat.getColor(context, if (value) activeColor else inactiveColor) ) } @@ -138,4 +142,79 @@ } callback(it) } +} + +fun makeMoveCallback( + list: MutableList, + callback: (Int, Int) -> Unit = { _, _ -> } +): ItemTouchHelper.SimpleCallback { + return object : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, + 0 + ) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val adapter = recyclerView.adapter + val startPosition = viewHolder.adapterPosition + val endPosition = target.adapterPosition + val item = list[startPosition] + list.removeAt(startPosition) + list.add(endPosition, item) + adapter!!.notifyItemMoved(startPosition, endPosition) + callback(startPosition, endPosition) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} + } +} + +object UIUtil { + val cardLayout = TableRow.LayoutParams( + 0, + TableRow.LayoutParams.WRAP_CONTENT, + 1f + ) + val buttonLayout = TableRow.LayoutParams( + 0, TableRow.LayoutParams.MATCH_PARENT, 0.5f + ) + val fillingLayout = TableRow.LayoutParams( + TableRow.LayoutParams.MATCH_PARENT, + TableRow.LayoutParams.MATCH_PARENT + ) + + init { + cardLayout.setMargins(5) + } +} + +fun Array.setupEnumSelection( + target: KMutableProperty0, + values: Array, + activeColor: Int = R.color.blue, + inactiveColor: Int = R.color.gray_0x60, + callback: () -> Unit = {}, +) { + fun update() { + for ((i, currentButton) in withIndex()) { + currentButton.setBackgroundColor( + ContextCompat.getColor( + currentButton.context, + if (target.get() == values[i]) activeColor else inactiveColor + ) + ) + } + callback() + } + for ((i, button) in withIndex()) { + button.text = values[i].toString() + button.setOnClickListener { + target.set(values[i]) + update() + } + } + update() } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_chord.xml b/app/src/main/res/layout/fragment_edit_chord.xml index 16fa8f2..8b34d22 100644 --- a/app/src/main/res/layout/fragment_edit_chord.xml +++ b/app/src/main/res/layout/fragment_edit_chord.xml @@ -19,10 +19,10 @@ @@ -37,20 +37,7 @@ android:minHeight="48dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/textView5" - app:layout_constraintTop_toBottomOf="@+id/textView4" /> - - + app:layout_constraintTop_toBottomOf="@+id/accidentalSelection" /> - - + app:layout_constraintTop_toBottomOf="@+id/editorGrid" /> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_effects.xml b/app/src/main/res/layout/fragment_edit_effects.xml index 46d4648..051d66f 100644 --- a/app/src/main/res/layout/fragment_edit_effects.xml +++ b/app/src/main/res/layout/fragment_edit_effects.xml @@ -43,7 +43,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/effectsDisplay" /> - + app:layout_constraintTop_toBottomOf="@+id/textView10"> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b121fe2..9b588fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,7 +10,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..309dd10 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Tiny Music app + +This is an app to easily create backing tracks to play along to certain chords. + +Features: + +- Enter an arbitrary chord progression +- Synthesize sounds on the go +- Preview which chords will be played soon +- Use effects on instruments + +Gplv3+ Licensed \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 39bdefe..4656574 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -20,6 +20,7 @@ effects/Effect.cpp effects/LowPass.cpp effects/Noise.cpp + effects/Distortion.cpp ) find_library( diff --git a/app/src/main/cpp/Instrument.cpp b/app/src/main/cpp/Instrument.cpp index 7310a37..5c91ad6 100644 --- a/app/src/main/cpp/Instrument.cpp +++ b/app/src/main/cpp/Instrument.cpp @@ -3,14 +3,22 @@ #include "waveforms/Sine.h" #include "waveforms/Square.h" #include "waveforms/Triangle.h" +#include "effects/Distortion.h" Instrument::Instrument(AudioHost *host) { this->host = host; wave = new Sine(); wave->host = host; envelope->initialize(host); - lowPass->host = host; + auto *filter = new LowPass(); + filter->host = host; + effects.push_back(filter); + auto *noise = new Noise(); noise->host = host; + effects.push_back(noise); + auto *distortion = new Distortion(); + distortion->host = host; + effects.push_back(distortion); } void multiply(float *target, float *modulation, uint32_t size) { @@ -44,8 +52,9 @@ void Instrument::render(float *buffer, uint32_t count) { float *waveform = wave->render(count); - processEffect(waveform, count, lowPass); - processEffect(waveform, count, noise); + for (auto effect: effects) { + processEffect(waveform, count, effect); + } multiply(waveform, envelope->render(count), count); multiply(waveform, volume, count); add(buffer, waveform, count); @@ -54,8 +63,10 @@ void Instrument::startNote(float frequency) { wave->setFrequency(frequency); envelope->startNote(); - lowPass->frequency = frequency; - lowPass->update(); + for (auto effect: effects) { + effect->frequency = frequency; + effect->update(); + } } void Instrument::endNote() { diff --git a/app/src/main/cpp/Instrument.h b/app/src/main/cpp/Instrument.h index 077bfe0..df45330 100644 --- a/app/src/main/cpp/Instrument.h +++ b/app/src/main/cpp/Instrument.h @@ -17,8 +17,7 @@ Envelope *const envelope = new Envelope(); Waveform *wave; - LowPass *lowPass = new LowPass(); - Noise *noise = new Noise(); + std::list effects; float volume = 0; void render(float *buffer, uint32_t count); diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 7ccc60b..488e5f0 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -101,16 +101,24 @@ 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; - } + auto iterator = instrument->effects.begin(); + std::advance(iterator, effect_number); + auto *effect = *iterator; effect->influence = influence; effect->parameter1 = parameter1; } + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_moveEffects(JNIEnv *env, jobject thiz, jint id, + jint from, jint to) { + Instrument *instrument = getInstrument(id); + auto source = instrument->effects.begin(); + std::advance(source, from); + auto destination = instrument->effects.begin(); + std::advance(destination, to); + if (from < to) { + std::advance(destination, 1); + } + instrument->effects.splice(destination, instrument->effects, source); +} } \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.cpp b/app/src/main/cpp/effects/Distortion.cpp new file mode 100644 index 0000000..5f65da5 --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.cpp @@ -0,0 +1,16 @@ +#include "Distortion.h" + +void Distortion::update() { +} + +void Distortion::doRender(uint32_t sampleCount) { + for (uint32_t i = 0; i < sampleCount; i++) { + float value = input[i] * parameter1; + if (value > 1.f) { + value = 1.f; + } else if (value < -1.f) { + value = -1.f; + } + buffer[i] = value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/effects/Distortion.h b/app/src/main/cpp/effects/Distortion.h new file mode 100644 index 0000000..3edb70c --- /dev/null +++ b/app/src/main/cpp/effects/Distortion.h @@ -0,0 +1,14 @@ +#ifndef MUSIC_DISTORTION_H +#define MUSIC_DISTORTION_H + +#include "Effect.h" + +class Distortion : public Effect { +public: + void update(); + + void doRender(uint32_t sampleCount); +}; + + +#endif 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 b9fa2c1..70d173a 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -18,7 +18,7 @@ abstract class Instrument(var name: String) { var voice: Voice = Voice(this) var envelope = Envelope(this) - val effects = Array(EffectType.VALUES.size) { + val effects = MutableList(EffectType.VALUES.size) { Effect(EffectType.VALUES[it], this) } @@ -33,6 +33,7 @@ abstract fun updateEnvelope() abstract fun updateEffects() abstract fun isPlaying(note: Note): Boolean + abstract fun moveEffects(from: Int, to: Int) 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 d24f474..40f3e97 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -78,15 +78,19 @@ ) } - fun applyEffectAttributes(effect: Effect) { + fun applyEffectAttributes(instrument: Instrument, effect: Effect) { applyEffectAttributes( id, - effect.type.ordinal, + instrument.effects.indexOf(effect), if (effect.active) effect.influence.value else 0f, - effect.parameters[0].value + effect.parameters[0]?.value ?: 0f ) } + fun moveEffects(from: Int, to: Int) { + moveEffects(id, from, to) + } + private external fun createInstrument(): Int private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) @@ -107,4 +111,6 @@ influence: Float, parameter1: Float ) + + private external fun moveEffects(id: Int, from: Int, to: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e631548..b702455 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -33,14 +33,6 @@ internalInstrument.muted = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note) - } - - override fun stop() { - internalInstrument.endNote() - } - override fun stopNote(note: Note) { if (note == internalInstrument.note) { stop() @@ -51,15 +43,15 @@ internalInstrument.destroy() } - override fun updateEnvelope() { - internalInstrument.applyEnvelope(envelope) - } - override fun updateEffects() { for (effect in effects) { - internalInstrument.applyEffectAttributes(effect) + internalInstrument.applyEffectAttributes(this, effect) } } override fun isPlaying(note: Note): Boolean = internalInstrument.note == note + override fun moveEffects(from: Int, to: Int) = internalInstrument.moveEffects(from, to) + override fun updateEnvelope() = internalInstrument.applyEnvelope(envelope) + override fun startNote(note: Note) = internalInstrument.startNote(note) + override fun stop() = internalInstrument.endNote() } \ 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 7beb64c..7f10ff2 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -10,11 +10,12 @@ package com.lukas.music.instruments +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note class PolyInstrument(name: String) : Instrument(name) { - private val internalInstruments = Array(3) { InternalInstrument() } - private val playing = Array(3) { false } + private val internalInstruments = Array(Chord.NOTE_COUNT) { InternalInstrument() } + private val playing = Array(Chord.NOTE_COUNT) { false } override var waveform: Waveform = Waveform.SINE set(value) { @@ -86,7 +87,7 @@ override fun updateEffects() { for (instrument in internalInstruments) { for (effect in effects) { - instrument.applyEffectAttributes(effect) + instrument.applyEffectAttributes(this, effect) } } } @@ -99,4 +100,10 @@ } return false } + + override fun moveEffects(from: Int, to: Int) { + for (instrument in internalInstruments) { + instrument.moveEffects(from, to) + } + } } \ No newline at end of file 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 index f921b1b..f659a8c 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/Effect.kt @@ -14,7 +14,9 @@ class Effect(val type: EffectType, private val instrument: Instrument) { val parameters = Array(type.parameterDescriptions.size) { - EffectParameter(type.parameterDescriptions[it], instrument) + type.parameterDescriptions[it]?.let { parameterDescription -> + EffectParameter(parameterDescription, instrument) + } } val influence = EffectParameter(influenceDescription, instrument) 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 index 8af39ea..8b90ec5 100644 --- a/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt +++ b/app/src/main/java/com/lukas/music/instruments/effect/EffectType.kt @@ -13,8 +13,8 @@ import com.lukas.music.util.format enum class EffectType( - val title: String, - val parameterDescriptions: Array + private val title: String, + val parameterDescriptions: Array ) { LowPass("low pass filter", arrayOf( @@ -22,13 +22,17 @@ "cutoff: ${it.value.format(1)} octaves" } )), - Noise("noise", + Noise( + "noise", arrayOf( - EffectParameterDescription(0f, 1f, 0f) { - "unused" - } + null ) - ) + ), + Distortion("distortion", arrayOf( + EffectParameterDescription(1f, 4f, 1f) { + "strength: ${it.value.format(1)}x" + } + )) ; override fun toString(): String { diff --git a/app/src/main/java/com/lukas/music/song/ScaleType.kt b/app/src/main/java/com/lukas/music/song/ScaleType.kt index 4c6a0d9..7807cb9 100644 --- a/app/src/main/java/com/lukas/music/song/ScaleType.kt +++ b/app/src/main/java/com/lukas/music/song/ScaleType.kt @@ -10,24 +10,12 @@ 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 - ) + arrayOf(0, 2, 4, 5, 7, 9, 11), ) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Accidental.kt b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt new file mode 100644 index 0000000..688ae4e --- /dev/null +++ b/app/src/main/java/com/lukas/music/song/chords/Accidental.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or(at your option) any later version. + * + * This program is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 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.chords + +enum class Accidental(val id: String, val short: String, val distance: Int) { + Flat("\u266D", "b", -1), + None("\u266E", "", 0), + Sharp("\u266F", "#", 1), + ; + + override fun toString(): String { + return id + } + + companion object { + val VALUES = values() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index 4400dae..85e530b 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -10,14 +10,19 @@ package com.lukas.music.song.chords +import com.lukas.music.song.Song import com.lukas.music.song.note.Note -class Chord(note: Int, var chordType: ChordType) { - var note: Int = note +class Chord { + var accidental = Accidental.None + val accidentals: Array = arrayOf(Accidental.None, Accidental.None, null, null) + + var note: Int = 0 set(value) { field = value interval = Interval(value) } + var interval = Interval(note) set(value) { field = value @@ -27,19 +32,76 @@ } fun getNotes(root: Note): Array { - return Array(chordType.notes.size) { root + note + chordType.notes[it] } + val result = Array(NOTE_COUNT) { root } + var resultIndex = 0 + var accidentalIndex = 0 + var octave = 0 + while (resultIndex < NOTE_COUNT) { + if (accidentalIndex == 0) { + result[resultIndex] = root + note + 12 * octave + accidental.distance + resultIndex++ + } else if (accidentals[accidentalIndex - 1] != null) { + result[resultIndex] = root + note + when (accidentalIndex) { + 1 -> 4 + 2 -> 7 + 3 -> 10 + 4 -> 14 + else -> 0 + } + accidentals[accidentalIndex - 1]!!.distance + 12 * octave + accidental.distance + resultIndex++ + } + accidentalIndex++ + if (accidentalIndex > accidentals.size) { + octave++ + accidentalIndex = 0 + } + } + return result } override fun toString(): String { - return chordType.transform(interval.toString()) + return toString(false, Song.currentSong.root) } fun toString(displayChordNames: Boolean, root: Note): String { - val base = if (displayChordNames) { - (root + note).noteName.toString() + var result = if (displayChordNames) { + (root + note + accidental.distance).noteName.toString() } else { interval.toString() } - return chordType.transform(base) + accidentals[0]?.let { + result += when (it) { + Accidental.Flat -> "-" + Accidental.Sharp -> "sus4" + else -> "" + } + } + accidentals[1]?.let { + if (accidentals[0] != null && it == Accidental.None) { + return@let + } + result += it.short + "5" + } + result = result.replace("-b5", "0") + result = result.replace("(?=[A-G])#5".toRegex(), "+") + accidentals[2]?.let { + result += when (it) { + Accidental.Sharp -> " maj7" + Accidental.None -> " 7" + Accidental.Flat -> " 6" + } + } + accidentals[3]?.let { + result += when (it) { + Accidental.Sharp -> " maj9" + Accidental.None -> " 9" + Accidental.Flat -> " b9" + } + } + return result + } + + companion object { + const val NOTE_COUNT = 5 } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt b/app/src/main/java/com/lukas/music/song/chords/ChordType.kt deleted file mode 100644 index 1fe4b40..0000000 --- a/app/src/main/java/com/lukas/music/song/chords/ChordType.kt +++ /dev/null @@ -1,30 +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.chords - -enum class ChordType( - val notes: Array, - private val asString: String, - val transform: (String) -> String -) { - MAJOR(arrayOf(0, 4, 7), "major", { it.uppercase() }), - MINOR(arrayOf(0, 3, 7), "minor", { it.lowercase() }), - DIMINISHED(arrayOf(0, 3, 6), "diminished", { it.lowercase() + "0" }), - ; - - override fun toString(): String { - return asString - } - - companion object { - val VALUES = values() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt index 1cadb06..f4bcbc7 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt @@ -15,7 +15,7 @@ class Phrase : Cycle() { init { for (i in 0 until 4) { - this += Chord(0, ChordType.MAJOR) + this += Chord() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index 17bfb2b..f17e322 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -12,7 +12,7 @@ import kotlin.math.pow -class Note(private val id: Int) { +class Note(val id: Int) { val noteName = NoteName.VALUES[id % 12] val octave = id / 12 - 1 val frequency = 440 * 2.0.pow((id - 69) / 12.0) @@ -28,6 +28,8 @@ return this + (-other) } + operator fun minus(other: Note): Int = id - other.id + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false 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 index e06761a..40b1d14 100644 --- a/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt +++ b/app/src/main/java/com/lukas/music/song/voice/VoiceType.kt @@ -11,6 +11,7 @@ package com.lukas.music.song.voice import com.lukas.music.song.ScaleType +import com.lukas.music.song.chords.Chord import com.lukas.music.song.note.Note import com.lukas.music.util.transform @@ -20,7 +21,7 @@ val getNotes: (Note, Array) -> Array ) { Bass("Bass note", 1, { _, chordNotes -> arrayOf(chordNotes[0]) }), - Chord("Chord notes", 3, { _, chordNotes -> chordNotes }), + ChordVoice("Chord notes", Chord.NOTE_COUNT, { _, 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 } }), diff --git a/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.kt new file mode 100644 index 0000000..730eabd --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/adapters/EffectsAdapter.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.ui.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.lukas.music.databinding.FragmentEffectBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.ui.fragments.EditEffectsFragment +import com.lukas.music.ui.fragments.EffectFragment + +class EffectsAdapter(private val parent: EditEffectsFragment, private val instrument: Instrument) : + RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EffectFragment { + val context = parent.context + val inflater = LayoutInflater.from(context) + val binding = FragmentEffectBinding.inflate(inflater, parent, false) + return EffectFragment(binding) + } + + override fun onBindViewHolder(holder: EffectFragment, position: Int) { + holder.setEffect(instrument.effects[position]) + } + + override fun getItemCount(): Int { + return instrument.effects.size + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt index 3e129bc..3d7d1f4 100644 --- a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt @@ -39,7 +39,7 @@ Song.currentSong.soloInstrument = instrument } field = value - binding.soloButton.updateToggle(this::solo, R.color.blue) + binding.soloButton.updateToggle(this.solo, R.color.blue) } var instrument: Instrument? = null 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 6c3bae3..5fc9dfe 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 @@ -14,61 +14,124 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.DialogFragment +import android.widget.TableRow +import android.widget.TextView +import androidx.core.view.children +import com.google.android.material.button.MaterialButton +import com.lukas.music.R import com.lukas.music.databinding.FragmentEditChordBinding import com.lukas.music.song.ScaleType import com.lukas.music.song.Song +import com.lukas.music.song.chords.Accidental 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 +import com.lukas.music.util.* class EditChordFragment(private val chord: Chord, private val songFragment: SongFragment) : - DialogFragment() { - lateinit var binding: FragmentEditChordBinding + EasyDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = FragmentEditChordBinding.inflate(inflater) + Array(Accidental.VALUES.size) { + val button = MaterialButton(binding.root.context) + button.layoutParams = UIUtil.cardLayout + binding.accidentalSelection.addView(button) + return@Array button + }.setupEnumSelection(chord::accidental, Accidental.VALUES, callback = { update() }) setupPitchSpinner() - setupTypeSpinner() + setupEditor() binding.exitButton.setOnClickListener { dismiss() } return binding.root } + private fun update() { + songFragment.updateChords() + binding.chordText.text = chord.toString(true, Song.currentSong.root) + updateEditor() + } + private fun setupPitchSpinner() { val pitches = if (songFragment.displayChordNames) { 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 = ScaleType.MAJOR.steps[it] - if (binding.typeSpinner.selectedItemPosition == 0) { - chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] + if (chord.note == ScaleType.MAJOR.steps[it]) { + update() + return@setup } - songFragment.updateChords() + chord.note = ScaleType.MAJOR.steps[it] + chord.accidental = Accidental.None + chord.accidentals[0] = + Accidental.VALUES[(ScaleType.MAJOR.steps[(it + 2) % ScaleType.MAJOR.steps.size] distance chord.note) - 3] + chord.accidentals[1] = + Accidental.VALUES[(ScaleType.MAJOR.steps[(it + 4) % ScaleType.MAJOR.steps.size] distance chord.note) - 6] + update() } } - private fun setupTypeSpinner() { - val values = mutableListOf("default") - for (chordType in ChordType.VALUES) { - values += chordType.toString() + private fun setupEditor() { + binding.editorGrid.removeAllViews() + val row = TableRow(binding.root.context) + for (description in descriptions) { + val text = TextView(binding.root.context) + text.text = description + text.layoutParams = UIUtil.cardLayout + text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER + row.addView(text) } - binding.typeSpinner.setup( - values, - if (chord.chordType == ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 - else chord.chordType.ordinal + 1 - ) { - if (it == 0) { - chord.chordType = ScaleType.MAJOR.chordTypes[chord.interval.name.ordinal] - } else { - chord.chordType = ChordType.VALUES[it - 1] + binding.editorGrid.addView(row) + for (accidental in Accidental.VALUES) { + val row = TableRow(binding.root.context) + for (position in 0 until Chord.NOTE_COUNT - 1) { + val button = MaterialButton(binding.root.context) + button.text = accidental.toString() + button.layoutParams = UIUtil.cardLayout + button.updateToggle(chord.accidentals[position] == accidental, R.color.blue) + button.setOnClickListener { + if (chord.accidentals[position] == accidental) { + chord.accidentals[position] = null + } else { + chord.accidentals[position] = accidental + } + update() + } + row.addView(button) } - songFragment.updateChords() + binding.editorGrid.addView(row) } } + + private fun updateEditor() { + for ((index, view) in binding.editorGrid.children.iterator().withIndex()) { + if (index == 0) { + continue + } + view as TableRow + for ((childIndex, childView) in view.children.iterator().withIndex()) { + childView as MaterialButton + childView.updateToggle( + chord.accidentals[childIndex] == Accidental.VALUES[index - 1], + R.color.blue + ) + } + } + } + + companion object { + val descriptions = arrayOf("III", "V", "VII", "IX") + } +} + +infix fun Int.distance(other: Int): Int { + var result = this - other + while (result < 0) { + result += 12 + } + result %= 12 + return result } \ No newline at end of file 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 index 6e359e4..0d532c2 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditEffectsFragment.kt @@ -14,9 +14,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager import com.lukas.music.databinding.FragmentEditEffectsBinding import com.lukas.music.instruments.Instrument +import com.lukas.music.ui.adapters.EffectsAdapter import com.lukas.music.util.EasyDialogFragment +import com.lukas.music.util.makeMoveCallback class EditEffectsFragment(private val instrument: Instrument) : EasyDialogFragment() { @@ -25,11 +29,12 @@ 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.effectsDisplay.adapter = EffectsAdapter(this, instrument) + binding.effectsDisplay.layoutManager = LinearLayoutManager(context) + val helper = ItemTouchHelper(makeMoveCallback(instrument.effects) { from, to -> + instrument.moveEffects(from, to) + }) + helper.attachToRecyclerView(binding.effectsDisplay) binding.closeButton.setOnClickListener { dismiss() } 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 index ebf4cb1..7b13ed6 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EffectFragment.kt @@ -10,25 +10,18 @@ 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 androidx.recyclerview.widget.RecyclerView 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) +class EffectFragment(val binding: FragmentEffectBinding) : RecyclerView.ViewHolder( + binding.root +) { + fun setEffect(effect: Effect) { binding.effectName.text = effect.type.toString() binding.activeButton.setupToggle(effect::active, R.color.blue) { binding.activeButton.text = if (it) "ON" else "OFF" @@ -37,10 +30,15 @@ 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]) + binding.parameter1SeekBar.visibility = + if (effect.parameters[0] == null) View.GONE else View.VISIBLE + binding.parameter1Text.visibility = + if (effect.parameters[0] == null) View.GONE else View.VISIBLE + effect.parameters[0]?.let { + binding.parameter1SeekBar.smartSetup(0, 100, it::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/ui/fragments/InstrumentListFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt index 1d35c10..0b66892 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt @@ -24,6 +24,7 @@ import com.lukas.music.instruments.MonoInstrument import com.lukas.music.instruments.PolyInstrument import com.lukas.music.ui.adapters.InstrumentAdapter +import com.lukas.music.util.makeMoveCallback class InstrumentListFragment : Fragment() { lateinit var binding: FragmentInstrumentListBinding @@ -35,32 +36,7 @@ binding = FragmentInstrumentListBinding.inflate(inflater) binding.recyclerView.adapter = InstrumentAdapter(this) binding.recyclerView.layoutManager = LinearLayoutManager(context) - val callback = object : ItemTouchHelper.SimpleCallback( - ItemTouchHelper.UP or ItemTouchHelper.DOWN, - 0 - ) { - override fun onMove( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean { - val adapter = recyclerView.adapter as InstrumentAdapter - val startPosition = viewHolder.adapterPosition - val endPosition = target.adapterPosition - val instrument = Instrument.instruments[startPosition] - Instrument.instruments.removeAt(startPosition) - if (endPosition < startPosition) { - Instrument.instruments.add(endPosition + 1, instrument) - } else { - Instrument.instruments.add(endPosition - 1, instrument) - } - adapter.notifyItemMoved(startPosition, endPosition) - return true - } - - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} - } - val helper = ItemTouchHelper(callback) + val helper = ItemTouchHelper(makeMoveCallback(Instrument.instruments)) helper.attachToRecyclerView(binding.recyclerView) val builder = AlertDialog.Builder(binding.root.context) diff --git a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt index 0c33019..4b668c4 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt @@ -27,6 +27,7 @@ import com.lukas.music.databinding.FragmentPlayBinding import com.lukas.music.instruments.Rhythm import com.lukas.music.song.Song +import com.lukas.music.util.UIUtil import com.lukas.music.util.setup class PlayFragment : Fragment() { @@ -125,12 +126,12 @@ chordDisplays.clear() for (chord in Song.currentSong.chordProgression.currentItem ?: return) { val card = CardView(binding.root.context) - card.layoutParams = SongFragment.tableRowLayout + card.layoutParams = UIUtil.cardLayout card.radius = 10f card.preventCornerOverlap = false val text = TextView(binding.root.context) text.text = chord.toString(true, Song.currentSong.root) - text.layoutParams = SongFragment.tableRowLayout + text.layoutParams = UIUtil.fillingLayout text.textSize = 20f text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER card.addView(text) diff --git a/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt index 62d314f..afe6137 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt @@ -16,13 +16,13 @@ import android.view.ViewGroup import android.widget.* import androidx.cardview.widget.CardView -import androidx.core.view.setMargins import androidx.fragment.app.Fragment import com.lukas.music.databinding.FragmentSongBinding import com.lukas.music.song.Song import com.lukas.music.song.chords.Phrase import com.lukas.music.song.note.Note import com.lukas.music.song.note.NoteName +import com.lukas.music.util.UIUtil class SongFragment(val playFragment: PlayFragment) : Fragment(), @@ -63,14 +63,14 @@ for (chord in phrase) { val card = CardView(binding.root.context) card.radius = 10f - card.layoutParams = tableRowLayout + card.layoutParams = UIUtil.cardLayout card.setOnClickListener { EditChordFragment(chord, this).showNow(childFragmentManager, "") } val text = TextView(binding.root.context) text.text = chord.toString(displayChordNames, Song.currentSong.root) text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER - text.layoutParams = tableRowLayout + text.layoutParams = UIUtil.fillingLayout text.textSize = 20f card.addView(text) row.addView(card) @@ -81,28 +81,13 @@ updateChords() } button.setImageResource(android.R.drawable.ic_delete) - button.layoutParams = buttonLayout + button.layoutParams = UIUtil.buttonLayout row.addView(button) binding.chords.addView(row) } playFragment.updateChords() } - companion object { - val tableRowLayout = TableRow.LayoutParams( - TableRow.LayoutParams.MATCH_PARENT, - TableRow.LayoutParams.MATCH_PARENT - ) - val buttonLayout = TableRow.LayoutParams( - 0, - TableRow.LayoutParams.WRAP_CONTENT - ) - - init { - tableRowLayout.setMargins(10) - } - } - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { Song.currentSong.root = Note.of(NoteName.VALUES[position], 4) if (displayChordNames) { 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 b67d0ea..6ae1bd9 100644 --- a/app/src/main/java/com/lukas/music/util/UIUtil.kt +++ b/app/src/main/java/com/lukas/music/util/UIUtil.kt @@ -13,6 +13,10 @@ import android.view.View import android.widget.* import androidx.core.content.ContextCompat +import androidx.core.view.setMargins +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.button.MaterialButton import com.lukas.music.R import kotlin.reflect.KMutableProperty0 @@ -62,19 +66,19 @@ ) { setOnClickListener { target.set(!target.get()) - updateToggle(target, activeColor, inactiveColor) + updateToggle(target.get(), activeColor, inactiveColor) callback(target.get()) } - updateToggle(target, activeColor, inactiveColor) + updateToggle(target.get(), activeColor, inactiveColor) } fun Button.updateToggle( - target: KMutableProperty0, + value: Boolean, activeColor: Int, inactiveColor: Int = R.color.gray_0x60, ) { setBackgroundColor( - ContextCompat.getColor(context, if (target.get()) activeColor else inactiveColor) + ContextCompat.getColor(context, if (value) activeColor else inactiveColor) ) } @@ -138,4 +142,79 @@ } callback(it) } +} + +fun makeMoveCallback( + list: MutableList, + callback: (Int, Int) -> Unit = { _, _ -> } +): ItemTouchHelper.SimpleCallback { + return object : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, + 0 + ) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val adapter = recyclerView.adapter + val startPosition = viewHolder.adapterPosition + val endPosition = target.adapterPosition + val item = list[startPosition] + list.removeAt(startPosition) + list.add(endPosition, item) + adapter!!.notifyItemMoved(startPosition, endPosition) + callback(startPosition, endPosition) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} + } +} + +object UIUtil { + val cardLayout = TableRow.LayoutParams( + 0, + TableRow.LayoutParams.WRAP_CONTENT, + 1f + ) + val buttonLayout = TableRow.LayoutParams( + 0, TableRow.LayoutParams.MATCH_PARENT, 0.5f + ) + val fillingLayout = TableRow.LayoutParams( + TableRow.LayoutParams.MATCH_PARENT, + TableRow.LayoutParams.MATCH_PARENT + ) + + init { + cardLayout.setMargins(5) + } +} + +fun Array.setupEnumSelection( + target: KMutableProperty0, + values: Array, + activeColor: Int = R.color.blue, + inactiveColor: Int = R.color.gray_0x60, + callback: () -> Unit = {}, +) { + fun update() { + for ((i, currentButton) in withIndex()) { + currentButton.setBackgroundColor( + ContextCompat.getColor( + currentButton.context, + if (target.get() == values[i]) activeColor else inactiveColor + ) + ) + } + callback() + } + for ((i, button) in withIndex()) { + button.text = values[i].toString() + button.setOnClickListener { + target.set(values[i]) + update() + } + } + update() } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_chord.xml b/app/src/main/res/layout/fragment_edit_chord.xml index 16fa8f2..8b34d22 100644 --- a/app/src/main/res/layout/fragment_edit_chord.xml +++ b/app/src/main/res/layout/fragment_edit_chord.xml @@ -19,10 +19,10 @@ @@ -37,20 +37,7 @@ android:minHeight="48dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/textView5" - app:layout_constraintTop_toBottomOf="@+id/textView4" /> - - + app:layout_constraintTop_toBottomOf="@+id/accidentalSelection" /> - - + app:layout_constraintTop_toBottomOf="@+id/editorGrid" /> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_effects.xml b/app/src/main/res/layout/fragment_edit_effects.xml index 46d4648..051d66f 100644 --- a/app/src/main/res/layout/fragment_edit_effects.xml +++ b/app/src/main/res/layout/fragment_edit_effects.xml @@ -43,7 +43,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/effectsDisplay" /> - + app:layout_constraintTop_toBottomOf="@+id/textView10"> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_effect.xml b/app/src/main/res/layout/fragment_effect.xml index 7f305be..b563832 100644 --- a/app/src/main/res/layout/fragment_effect.xml +++ b/app/src/main/res/layout/fragment_effect.xml @@ -12,7 +12,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent" + android:layout_height="wrap_content" tools:context=".ui.fragments.EffectFragment"> + android:layout_height="match_parent" + android:padding="16dp"> @@ -45,7 +45,6 @@ android:id="@+id/influenceText" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="16dp" android:text="@string/placeholder" app:layout_constraintBottom_toBottomOf="@+id/influenceSeekBar" app:layout_constraintEnd_toEndOf="@+id/parameter1Text" @@ -56,9 +55,6 @@ android:id="@+id/effectName" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="16dp" - android:layout_marginTop="16dp" - android:layout_marginEnd="16dp" android:text="@string/placeholder" android:textSize="16sp" app:layout_constraintEnd_toEndOf="parent" @@ -70,8 +66,6 @@ android:layout_width="0dp" android:layout_height="40dp" android:layout_marginTop="16dp" - android:layout_marginBottom="16dp" - app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@+id/influenceSeekBar" app:layout_constraintStart_toStartOf="@+id/influenceSeekBar" app:layout_constraintTop_toBottomOf="@+id/influenceSeekBar" /> @@ -80,7 +74,6 @@ android:id="@+id/parameter1Text" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="16dp" android:layout_marginEnd="16dp" android:text="@string/placeholder" app:layout_constraintBottom_toBottomOf="@+id/parameter1SeekBar"