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