diff --git a/README.md b/README.md index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..bacab1a29c72756576855193ee23afec135b449e 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,8 @@ +# daisy-looper + + + +# What does the looper do? + +*daisy-looper* is a software that can be installed on the [daisyy hardware](../../README.md) developed in the course *Synthesizer DIY* that took place during winter semester 2023/2024 at HFBK Hamburg. The looper allows you to record audio present at the input and [overdub](https://en.wikipedia.org/wiki/Overdubbing) into up to 5 different virtual "tapes" called *buffers*. The recorded sounds stored in each buffer will be displayed as a waveform and can be played back at different speeds (including reverse speed). + diff --git a/menu.ods b/menu.ods new file mode 100644 index 0000000000000000000000000000000000000000..2b8391b9cf03447b0e7b7cf66da2c734faf8ed40 Binary files /dev/null and b/menu.ods differ diff --git a/src/.~lock.menu.ods# b/src/.~lock.menu.ods# new file mode 100644 index 0000000000000000000000000000000000000000..3e896df534a831a987623e128f85390b249e2585 --- /dev/null +++ b/src/.~lock.menu.ods# @@ -0,0 +1 @@ +,d0,t,02.02.2024 17:44,file:///home/d0/.config/libreoffice/4; \ No newline at end of file diff --git a/src/button_grid.h b/src/button_grid.h new file mode 100644 index 0000000000000000000000000000000000000000..96395e3a441d14019b08c57eaf5f938a884df272 --- /dev/null +++ b/src/button_grid.h @@ -0,0 +1,187 @@ +#ifndef Buttongrid_h +#define Buttongrid_h + +#include "Adafruit_SH110X.h" +#include "Adafruit_GFX.h" +#include "potentiometers.h" +#include "buttons.h" +#include "ui.h" + + +extern Potentiometer pot_1, pot_2, pot_3, pot_4, pot_5, pot_6, pot_7; +extern Button button_1, button_2, button_3, button_4, button_5, button_6; +extern Adafruit_SH1106G display; + +// Represents the different types of GridButtons in the UI +enum ButtonType { + BUTTON_TYPE_SIMPLE, // Simple Button that can be pressed + BUTTON_TYPE_TOGGLE, // Toggles between two values (name needs to have a break "\n") + BUTTON_TYPE_MULTITOGGLE, // Toggles between two or more values (name needs to have a break "\n") + BUTTON_TYPE_ENUM, // Toggles between a group of buttons + BUTTON_TYPE_LAST +}; + +// A Gridbutton is a single button within the Ui-Button Grid +// The name can have multiple lines, delimited by "\n" which +// will be used to represent different states for buttons of +// the type BUTTON_TYPE_TOGGLE or BUTTON_TYPE_MULTITOGGLE +// is_home tells us if this is the home button (and thus should +// be rendered inverted) +class GridButton { + public: + const char* name; + const char* description; + GridButton(const char* name, Button *button, bool is_home=false, ButtonType type=BUTTON_TYPE_SIMPLE, int default_value=0, const char* description="") + : + name(name), + button(button), + is_home(is_home), + type(type), + active(default_value), + description(description) + { + // Count the number of lines in the name + for(int i = 0; name[i] != '\0'; i++) { + if(name[i] == '\n') + ++lines; + } + } + Button *button; + ButtonType type; + bool is_home; + bool should_render_description = false; + int active; + int lines = 0; + + // Go to the next option + void next() { + active++; + if (active > lines) { + active = 0; + } + } + + void renderDescription() { + if (should_render_description) { + display.setTextWrap(true); + display.clearDisplay(); + display.setCursor(0,0); + display.setTextColor(SH110X_WHITE); + display.print(description); + display.setTextWrap(false); + } + } +}; + +// The ButtonGrid is a grid of 2×3 = 6 buttons +class ButtonGrid { + public: + ButtonGrid(int home_mode, const GridButton (&grid_buttons)[6]) + : grid_buttons_{ + grid_buttons[0], + grid_buttons[1], + grid_buttons[2], + grid_buttons[3], + grid_buttons[4], + grid_buttons[5] + }, + ui_mode(home_mode) + {} + GridButton grid_buttons_[6]; + int ui_mode; + + void setup() { + for (int n=0; n<6; n++) { + if (!grid_buttons_[n].is_home) { + // Not a home button, display help on long hold and hide on release + // grid_buttons_[n].button->onLongHold([this, n](){ + // grid_buttons_[n].should_render_description = true; + // }); + // grid_buttons_[n].button->onReleased([this, n](){ + // grid_buttons_[n].should_render_description = false; + // }); + } + } + } + + int homeButtonIndex() { + for (int i=0; i<6; i++) { + if (grid_buttons_[i].is_home) { + return i; + } + } + return -1; + } + + void hideAllDescriptions() { + for (int i=0; i<6; i++) { + grid_buttons_[i].should_render_description = false; + } + } + + void render(int button_enum) { + int width = display.width(); + int height = display.height(); + int box_width = int(width/3.0f); + int box_height= int(height/2.0f); + int i = 0; + // Draw boxes (2 rows, 3 columns) + for (int box_y=0; box_y<2; box_y++) { + for (int box_x=0; box_x<3; box_x++) { + // Get the current buttons name + const char* name = grid_buttons_[i].name; + + // Prepare colors + uint16_t bg_color = SH110X_BLACK; + uint16_t text_color = SH110X_WHITE; + + // Home-Buttons have a inverted color scheme + if (grid_buttons_[i].is_home) { + bg_color = SH110X_WHITE; + text_color = SH110X_BLACK; + } + + // Position variables + uint16_t x = box_x * box_width; + uint16_t y = box_y * box_height; + uint16_t xc = x + box_width/2; + uint16_t yc = y + box_height/2; + + // Fill Background + display.fillRect(x, y, box_width, box_height, bg_color); + + // Render the different button types + if (grid_buttons_[i].type == BUTTON_TYPE_TOGGLE) { + centeredTextMarkMulti(name, xc, yc, text_color, grid_buttons_[i].active, 12); + } else if (grid_buttons_[i].type == BUTTON_TYPE_MULTITOGGLE) { + button_multi(name, xc, yc, text_color, grid_buttons_[i].active, grid_buttons_[i].lines); + } else if (grid_buttons_[i].type == BUTTON_TYPE_ENUM) { + bool active = i == button_enum; + centeredTextMark(name, xc, yc, text_color, active); + } else { + centeredText(name, xc, yc, text_color); + } + // Increase ounter for the index of the button + i++; + } + } + + // Draw divider lines + display.drawFastVLine(box_width, 0, height, SH110X_WHITE); + display.drawFastVLine(box_width*2, 0, height, SH110X_WHITE); + display.drawFastHLine(0, box_height, width, SH110X_WHITE); + + // Render Descriptions on top (hence another loop) + i = 0; + // Draw boxes (2 rows, 3 columns) + for (int box_y=0; box_y<2; box_y++) { + for (int box_x=0; box_x<3; box_x++) { + grid_buttons_[i].renderDescription(); + i++; + } + } + } +}; + + +#endif \ No newline at end of file diff --git a/src/buttons.h b/src/buttons.h new file mode 100644 index 0000000000000000000000000000000000000000..4ab6d9935deb016e332dd482f1080f93688a2b17 --- /dev/null +++ b/src/buttons.h @@ -0,0 +1,165 @@ +#include "wiring_constants.h" +#ifndef Buttons_h +#define Buttons_h + +#include "Arduino.h" +#include "Adafruit_SH110X.h" +#include "Adafruit_GFX.h" +extern Adafruit_SH1106G display; + +#define DURATION_SHORT_PRESS 800 +#define DURATION_VERY_LONG_PRESS 2000 + + + + + +class Button { + int pin; + bool has_been_pressed; + unsigned long press_start; + unsigned long release_start; + std::function<void()> onPressFunction; + std::function<void()> onHoldFunction; + std::function<void()> onLongHoldFunction; + std::function<void()> onVeryLongHoldFunction; + std::function<void()> onLongPressFunction; + std::function<void()> onVeryLongPressFunction; + std::function<void()> onReleasedFunction; + + public: + Button(int pin); + void init(); + void read(); + unsigned long pressed_since(); + unsigned long released_since(); + + void onPress(std::function<void()> f); + void onHold(std::function<void()> f); + void onLongHold(std::function<void()> f); + void onVeryLongHold(std::function<void()> f); + void onLongPress(std::function<void()> f); + void onVeryLongPress(std::function<void()> f); + void onReleased(std::function<void()> f); + + void reset(); +}; + +Button::Button(int pin) { + this->pin = pin; +} + +void Button::init() { + pinMode(this->pin, INPUT_PULLUP); + this->has_been_pressed = false; + this->press_start = 0; + this->release_start = 0; +} + +void Button::read() { + int is_pressed = !digitalRead(this->pin); + + if (is_pressed && this->press_start == 0) { + this->press_start = millis(); + } + if (!is_pressed && this->has_been_pressed && this->release_start == 0) { + this->release_start = millis(); + } + + unsigned long pressed_since = this->pressed_since(); + unsigned long released_since = this->released_since(); + + if (is_pressed) { + // Fire the callback function all the time while this is being pressed + if (this->onHoldFunction) { this->onHoldFunction(); } + + if (this->pressed_since() > 1000) { + if (this->onLongHoldFunction) { this->onLongHoldFunction(); } + } + if (this->pressed_since() > 5000) { + if (this->onVeryLongHoldFunction) { this->onVeryLongHoldFunction(); } + } + // Serial.print("Pressed since "); + // Serial.println(pressed_since); + if ( released_since > 100) { + this->has_been_pressed = false; + } + } else { + // Not pressed. + if (!this->has_been_pressed) { + if (pressed_since > 0 && pressed_since < DURATION_SHORT_PRESS) { + if (this->onPressFunction) { this->onPressFunction(); } + // Serial.print("Short Press (released after "); + // Serial.print(pressed_since); + // Serial.print(", released since "); + // Serial.print(released_since); + } else if (pressed_since > 0 && pressed_since < DURATION_VERY_LONG_PRESS) { + if (this->onLongPressFunction) { this->onLongPressFunction(); } + // Serial.print("Long Press (released after "); + // Serial.print(pressed_since); + // Serial.println(")"); + } else if (pressed_since > 0 && pressed_since >= DURATION_VERY_LONG_PRESS) { + if (this->onVeryLongPressFunction) { this->onVeryLongPressFunction(); } + // Serial.print("Very Long Press (released after "); + // Serial.print(pressed_since); + // Serial.println(")"); + } + this->press_start = 0; + this->has_been_pressed = true; + this->release_start = millis(); + if (this->onReleasedFunction) { this->onReleasedFunction(); } + } + } +} + +unsigned long Button::pressed_since() { + if ( this->press_start == 0) { + return 0; + } + return millis() - this->press_start; +} + +unsigned long Button::released_since() { + if ( this->release_start == 0) { + return 0; + } + return millis() - this->release_start; +} + +void Button::onPress(std::function<void()> f) { + this->onPressFunction = f; +} + +void Button::onHold(std::function<void()> f) { + this->onHoldFunction = f; +} + +void Button::onLongHold(std::function<void()> f) { + this->onLongHoldFunction = f; +} + +void Button::onVeryLongHold(std::function<void()> f) { + this->onVeryLongHoldFunction = f; +} + +void Button::onLongPress(std::function<void()> f) { + this->onLongPressFunction = f; +} + +void Button::onVeryLongPress(std::function<void()> f) { + this->onVeryLongPressFunction = f; +} + +void Button::onReleased(std::function<void()> f) { + this->onReleasedFunction = f; +} + +void Button::reset() { + this->onPressFunction = NULL; + this->onHoldFunction = NULL; + this->onLongPressFunction = NULL; + this->onVeryLongPressFunction = NULL; +} + + +#endif \ No newline at end of file diff --git a/src/daisy-looper.ino b/src/daisy-looper.ino new file mode 100644 index 0000000000000000000000000000000000000000..b19ec1885feda0039f732ee8acb37ce07b295706 --- /dev/null +++ b/src/daisy-looper.ino @@ -0,0 +1,467 @@ +#include "DaisyDuino.h" +#include <Wire.h> +#include <Adafruit_GFX.h> +#include <Adafruit_SH110X.h> +#include <MIDI.h> + +#include "leds.h" +#include "potentiometers.h" +#include "buttons.h" +#include "looper.h" +#include "env_follower.h" +#include "helpers.h" +#include "luts.h" +#include "ui.h" +#include "lfo.h" + +MIDI_CREATE_DEFAULT_INSTANCE(); +#define BUFFER_LENGTH_SECONDS 5 + +#define DEBUGMODE + +static const size_t buffer_length = 48000 * BUFFER_LENGTH_SECONDS; +static float DSY_SDRAM_BSS buffer[buffer_length]; +static float DSY_SDRAM_BSS buffer_b[buffer_length]; +static float DSY_SDRAM_BSS buffer_c[buffer_length]; +static float DSY_SDRAM_BSS buffer_d[buffer_length]; +static float DSY_SDRAM_BSS buffer_e[buffer_length]; + + +// Create instances of audio stuff +atoav::Looper looper_a, looper_b, looper_c, looper_d, looper_e; +static atoav::EnvelopeFollower input_envelope_follower; +DSY_SDRAM_BSS ReverbSc reverb; +static Compressor compressor; +Oscillator lfo; +static SampleHold sample_and_hold; +static WhiteNoise noise; +static Metro tick; +static Easer easer; + +// Initialize Buttons +Button button_1 = Button(D7); +Button button_2 = Button(D8); +Button button_3 = Button(D9); +Button button_4 = Button(D10); +Button button_5 = Button(D13); +Button button_6 = Button(D27); + +// Initialize Potentiometers +Potentiometer pot_1 = Potentiometer(A0); +Potentiometer pot_2 = Potentiometer(A1); +Potentiometer pot_3 = Potentiometer(A3); +Potentiometer pot_4 = Potentiometer(A2); +Potentiometer pot_5 = Potentiometer(A4); +Potentiometer pot_6 = Potentiometer(A5); +Potentiometer pot_7 = Potentiometer(A6); + +// RGB LED R G B +RGBLed rgb_led = RGBLed(A10, A11, A9); + + +// OLED Display +#define SCREEN_WIDTH 128 // OLED display width, in pixels +#define SCREEN_HEIGHT 64 // OLED display height, in pixels +#define OLED_RESET -1 // QT-PY / XIAO +Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); + +// User Interface +Ui ui; + +// Daisy +DaisyHardware hw; + +// Variables for the Audio-Callback +size_t num_channels; +float blocksize; +float drywetmix = 0.0f; +float volume = 1.0f; +float reverbmix = 0.0f; +float lfo_amount = 0.0f; +float pitch_val = 0.5f; +float midi_pitch_offset = 0.0f; + +float reverb_tone = 15000.0f; +float reverb_decay = 0.95f; +float lfo_speed = 8.0f; +int lfo_kind = 0; +float rand_pitch_mod = 0.0f; + +//float pressure = 0.0f; + +// Actual audio-processing is orchestrated here +void AudioCallback(float **in, float **out, size_t size) { + float output1, output2 = 0.0f; + float out1, out2; + reverb.SetFeedback(reverb_decay); + reverb.SetLpFreq(reverb_tone); + lfo.SetFreq(lfo_speed); + tick.SetFreq(lfo_speed); + + // Iterate through the samples in the buffer + for (size_t i = 0; i < size; i++) { + float lfo_value = 0.0f; + switch (lfo_kind) { + case LfoKind::LFO_KIND_TRI: + lfo.SetWaveform(Oscillator::WAVE_TRI); + lfo_value = lfo.Process(); + break; + case LfoKind::LFO_KIND_SQR: + lfo.SetWaveform(Oscillator::WAVE_SQUARE); + lfo_value = lfo.Process(); + break; + default: + break; + } + + tick.SetFreq(0.25f+lfo_speed*49.75f); + uint8_t trig = tick.Process(); + float noise_value = noise.Process(); + + switch (lfo_kind) { + case LfoKind::LFO_KIND_RAND: + easer.setFactor(0.01); + break; + case LfoKind::LFO_KIND_JUMP: + easer.setFactor(1.0); + break; + } + + float rand = easer.Process( + sample_and_hold.Process( + trig, + noise_value * 5.0f, + sample_and_hold.MODE_SAMPLE_HOLD + )); + + if (ui.rec_source == REC_SOURCE_NOISE) { + ui.activeLooper()->Record(noise_value * 0.5f); + } + // When the metro ticks, trigger the envelope to start. + float random_amount = lfo_amount * 2.0; + + if (trig) { + // Random LFO + switch (lfo_kind) { + case LfoKind::LFO_KIND_RAND: + rand_pitch_mod = rand * random_amount * 5.0f; + break; + case LfoKind::LFO_KIND_JUMP: + // Chance + if (drand(0.0f, 1.0f) < lfo_amount) { + ui.activeLooper()->addToPlayhead(rand * random_amount * 8000.0f); + } + break; + default: + break; + } + } + + // Add the LFO to the signal if it is active + switch (lfo_kind) { + case LfoKind::LFO_KIND_TRI: + case LfoKind::LFO_KIND_SQR: + ui.activeLooper()->setPlaybackSpeed(pitch_val + lfo_value * lfo_amount + midi_pitch_offset); + break; + case LfoKind::LFO_KIND_RAND: + ui.activeLooper()->setPlaybackSpeed(pitch_val + rand_pitch_mod + midi_pitch_offset); + break; + default: + ui.activeLooper()->setPlaybackSpeed(pitch_val + midi_pitch_offset); + break; + } + + + float looper_out; + + if (ui.rec_source == REC_SOURCE_PRE) { + ui.activeLooper()->Record(in[1][i]); + } + + if (ui.rec_source == REC_SOURCE_LAST_BUF) { + // FIXME: This might process the previous looper twice later below, add check... + ui.activeLooper()->Record(ui.previousLooper()->Process()); + } + + // Process the input envelope + input_envelope_follower.Process(in[1][i]); + + // Process the Looper + switch (ui.buffer_summing_mode) { + case BUFFER_SUM_MODE_SOLO: + // Only play active looper + looper_out = ui.activeLooper()->Process(); + break; + case BUFFER_SUM_MODE_SUM: + // Sum all loopers + looper_out = looper_a.Process(); + looper_out += looper_b.Process(); + looper_out += looper_c.Process(); + looper_out += looper_d.Process(); + looper_out += looper_e.Process(); + looper_out = looper_out; + break; + case BUFFER_SUM_MODE_RING: + // Sum all loopers and ringmodulate with input + looper_out = looper_a.Process(); + looper_out += looper_b.Process(); + looper_out += looper_c.Process(); + looper_out += looper_d.Process(); + looper_out += looper_e.Process(); + float deadbanded_input; + if (in[1][i] < 0.0f) { + deadbanded_input = min(0.0f, in[1][i] + 0.05f); + } else { + deadbanded_input = max(0.0f, in[1][i] - 0.05f); + } + looper_out *= deadbanded_input*2.0f; + looper_out = looper_out; + break; + } + + // looper_out = pressure * looper_out; + looper_out = saturate(volume * looper_out); + + // Mix the dry/Wet of the looper + output1 = output2 = drywetmix * looper_out + in[1][i] * (1.0f - drywetmix); + + // Compress the signal + compressor.Process(output1); + + // Process reverb + reverb.Process(output1, output1, &out1, &out2); + + // Short decays are silent, so increase level here + float dec_fac = 1.0f + (1.0f - reverb_decay) * 2.0f; + out1 = out1 * dec_fac; + out2 = out2 * dec_fac; + + // Mix reverb with the dry signal depending on the amount dialed + output1 = output1 * (1.0f - reverbmix) + out1 * reverbmix; + output2 = output1 * (1.0f - reverbmix) + out2 * reverbmix; + + // Record the output if needed + if (ui.rec_source == REC_SOURCE_OUT) { + ui.activeLooper()->Record(output1); + } + + out[0][i] = output1; + out[1][i] = output2; + } +} + +// TODO: Add Voice Stealing/Multiple Playheads? +// TODO: Is USB Midi possible? https://github.com/electro-smith/DaisyExamples/blob/master/seed/USB_MIDI/USB_MIDI.cpp + +void handleNoteOn(byte inChannel, byte inNote, byte inVelocity) { + #ifdef DEBUGMODE + Serial.print("[MIDI ON] chn<"); + Serial.print((int) inChannel); + Serial.print("> note<"); + Serial.print((int) inNote); + Serial.print("> velocity<"); + Serial.print((int) inVelocity); + Serial.println(">"); + #endif + // Note Off can come in as Note On w/ 0 Velocity + if (inVelocity == 0.0f) { + midi_pitch_offset = 0.0f; + } + else { + midi_pitch_offset = (int(inNote)-36.0)/12.0f; + } +} + + + +void handleNoteOff(byte inChannel, byte inNote, byte inVelocity) { + #ifdef DEBUGMODE + Serial.print("[MIDI OFF] chn<"); + Serial.print((int) inChannel); + Serial.print("> note<"); + Serial.print((int) inNote); + Serial.print("> velocity<"); + Serial.print((int) inVelocity); + Serial.println(">"); + #endif + midi_pitch_offset = 0.0f; +} + +// void handleAftertouch(byte channel, byte channel_pressure) { +// #ifdef DEBUGMODE +// Serial.print("[MIDI AFTER] chn<"); +// Serial.print((int) channel); +// Serial.print("> pressure<"); +// Serial.print((int) channel_pressure); +// Serial.println(">"); +// #endif +// pressure = float(channel_pressure)/127.0f; +// } + + + +void setup() { + float sample_rate; + // Initialize for Daisy pod at 48kHz + hw = DAISY.init(DAISY_SEED, AUDIO_SR_48K); + DAISY.SetAudioBlockSize(64); + num_channels = hw.num_channels; + sample_rate = DAISY.get_samplerate(); + blocksize = 64.0f; + + // Create a Tick and a noise source for the Sample and Hold + tick.SetFreq(1.0f+lfo_amount*99.0f); + tick.Init(10, sample_rate); + noise.Init(); + + // Initialize Looper with the buffer + looper_a.Init(buffer, buffer_length); + looper_b.Init(buffer_b, buffer_length); + looper_c.Init(buffer_c, buffer_length); + looper_d.Init(buffer_d, buffer_length); + looper_e.Init(buffer_e, buffer_length); + + // Initialize Envelope Follower for the Level LED + input_envelope_follower.Init(sample_rate); + input_envelope_follower.SetAttack(100.0); + input_envelope_follower.SetDecay(1000.0); + + // Initialize Reverb + reverb.Init(sample_rate); + reverb.SetFeedback(reverb_decay); + reverb.SetLpFreq(reverb_tone); + + // Initialize Compressor + compressor.SetThreshold(-64.0f); + compressor.SetRatio(2.0f); + compressor.SetAttack(0.005f); + compressor.SetRelease(0.1250); + + // Initialize the LFO for modulations + lfo.Init(sample_rate); + lfo.SetWaveform(Oscillator::WAVE_TRI); + lfo.SetAmp(1); + lfo.SetFreq(lfo_speed); + + // Easer for the random jumps + easer.setFactor(0.001); + + // Start serial communications + Serial.begin(250000); + + // Initialize Display + display.begin(0x3C, true); + delay(50); + ui.Render(); + + // Initialize the LED + rgb_led.init(); + + // Set the analog read and write resolution to 12 bits + analogReadResolution(12); + + // Setup MIDI handlers + MIDI.setHandleNoteOn(handleNoteOn); + MIDI.setHandleNoteOff(handleNoteOff); + // MIDI.setHandleAfterTouchChannel(handleAftertouch); + + MIDI.begin(MIDI_CHANNEL_OMNI); // Listen to all incoming messages + + // Set Knob names and display functions + pot_1.name = "Start"; + pot_2.name = "Length"; + pot_3.setDisplayMode("Speed", 1000.0f, POT_DISPLAY_MODE_PERCENT); + pot_4.setDisplayMode("Mix", 100.0f, POT_DISPLAY_MODE_PERCENT); + pot_5.setDisplayMode("LFO", 100.0f, POT_DISPLAY_MODE_PERCENT); + pot_6.setDisplayMode("Volume", 400.0f, POT_DISPLAY_MODE_PERCENT); + pot_7.setDisplayMode("Reverb", 100.0f, POT_DISPLAY_MODE_PERCENT); + + // Set Knob Scaling Modes + // pot_3.setBipolar(); + pot_3.setPitch(); + + // Initialize Buttons (callbacks are assigned in the Ui class) + button_1.init(); + button_2.init(); + button_3.init(); + button_4.init(); + button_5.init(); + button_6.init(); + + // Start the audio Callback + DAISY.begin(AudioCallback); +} + +void loop() { + // Read the values from the potentiometers + float p1 = pot_1.read(); + float p2 = pot_2.read(); + float p3 = pot_3.read(); + float p4 = pot_4.read(); + float p5 = pot_5.read(); + float p6 = pot_6.read(); + float p7 = pot_7.read(); + + // Update the UI + ui.update(); + + // Read the buttons + button_1.read(); + button_2.read(); + button_3.read(); + button_4.read(); + button_5.read(); + button_6.read(); + + // Set loop-start and loop-length with the potentiometers + ui.setLoop(p1, p2); + + // Tune the pitch to the ten-fold of the potentiometer 3, + // a bipolar pot, so it returns values from -1.0 to +1.0 + // Pitch should go 10 octaves up/down (10*1.0 = 1000%) + if (!isnan(p3)) {pitch_val = 10.0f * p3; } + + // Set other parameters (from 0.0 to 1.0) + if (!isnan(p4)) { drywetmix = p4; } + + switch (ui.fx_mode) { + case FX_MODE_ALL: + if (!isnan(p5)) { lfo_amount = p5; } + if (!isnan(p6)) { volume = p6 * 4.0f; } + if (!isnan(p7)) { reverbmix = p7; } + break; + case FX_MODE_REVERB: + if (!isnan(p5)) { reverb_tone = 50.0f + p5 * 20000.0f; } + // TODO: Short Reverb Decay times are too silent? + if (!isnan(p6)) { reverb_decay = 0.05f + p6 * 0.94f; } + if (!isnan(p7)) { reverbmix = p7; } + break; + case FX_MODE_LFO: + if (!isnan(p5)) { lfo_kind = p5; } + if (!isnan(p6)) { lfo_speed = (p6 * p6 *p6) * 100.0f; } + if (!isnan(p7)) { lfo_amount = p7; } + break; + case FX_MODE_GRAIN: + if (!isnan(p5)) { ui.activeLooper()->grain_count = 1+int(p5); } + if (!isnan(p6)) { ui.activeLooper()->grain_spread = p6*10.0f; } + if (!isnan(p7)) { ui.activeLooper()->grain_variation = p7; } + break; + } + + // Render the UI (frame rate limited by UI_MAX_FPS in ui.h) + // double start = millis(); + ui.Render(); + // Serial.print("ui Render took "); + // Serial.print(millis()-start); + // Serial.println("ms"); + + // Set the Color and brightness of the RGB LED in 8 bits + rgb_led.setAudioLevelIndicator(input_envelope_follower.getValue()); + + // MIDI + MIDI.read(); +} + + + + diff --git a/src/env_follower.h b/src/env_follower.h new file mode 100644 index 0000000000000000000000000000000000000000..b3a123969be9a41e91c5ed3ab17b8b5f3265ad5d --- /dev/null +++ b/src/env_follower.h @@ -0,0 +1,91 @@ +#include "wiring_constants.h" +#ifndef Env_follower_h +#define Env_follower_h +#include "Arduino.h" + +namespace atoav { + +class SmoothingFilter { + public: + void Init(float smoothing_time_ms, float sample_rate) { + a = exp(-TWO_PI / (smoothing_time_ms * 0.001f * sample_rate)); + b = 1.0f - a; + z = 0.0f; + } + + inline float Process(float in) { + z = (in * b) + (z * a); + return z; + } + + void setSmoothing(float smoothing_time_ms, float sample_rate) { + a = exp(-TWO_PI / (smoothing_time_ms * 0.001f * sample_rate)); + b = 1.0f - a; + } + + private: + float a; + float b; + float z; +}; + + +class EnvelopeFollower { + public: + void Init(float sample_rate) { + sample_rate = sample_rate; + attack = 200.0f; + decay = 4000.0f; + smoothing = 50.0f; + value = 0.0f; + smoothing_filter.Init(smoothing, sample_rate); + } + + void SetAttack(float attack_ms) { + attack = pow(0.01, 1.0 / (attack_ms * sample_rate * 0.001)); + } + + void SetDecay(float decay_ms) { + decay = pow(0.01, 1.0 / (decay_ms * sample_rate * 0.001)); + } + + void SetSmoothing(float smoothing_ms) { + smoothing_filter.setSmoothing(smoothing_ms, sample_rate); + } + + float Process(float in) { + abs_value = smoothing_filter.Process(abs(in)); + if (abs_value > value) { + value = attack * (value - abs_value) + abs_value; + } else { + value = decay * (value - abs_value) + abs_value; + } + return value; + } + + float value; + + float getValue() { + if (value == 0.0f || value == -0.0f) { + return value; + } + return max(0.0f, (3.0f + log10(value)) / 3.0f); + // return value; + } + + + private: + float sample_rate; + float attack; + float decay; + float smoothing; + float abs_value = 0.0f; + SmoothingFilter smoothing_filter; +}; +}; + + + + + +#endif \ No newline at end of file diff --git a/src/helpers.h b/src/helpers.h new file mode 100644 index 0000000000000000000000000000000000000000..2f6601ac9522345ecb78328232270987587998ba --- /dev/null +++ b/src/helpers.h @@ -0,0 +1,158 @@ +#ifndef Helpers_h +#define Helpers_h + +#include "Adafruit_SH110X.h" +#include "Adafruit_GFX.h" +extern Adafruit_SH1106G display; + + + +int centeredText(const char *buf, int x, int y, int color, int lineheight=8) { + int16_t x1, y1; + uint16_t w, h; + char *line_pointer = strchr(buf, '\n'); + display.setTextColor(color); + if (!line_pointer) { + display.getTextBounds(buf, 0, 0, &x1, &y1, &w, &h); //calc width of new string + display.setCursor(x - (w / 2), y - (h / 2)); + display.print(buf); + }else { + char *tmp = strdup(buf); + char* d = strtok(tmp, "\n"); + int line = 0; + while (d != NULL) { + display.getTextBounds(d, 0, 0, &x1, &y1, &w, &h); //calc width of new string + display.setCursor(x - (w / 2), y - (h / 2)-lineheight/2 + (line*lineheight)); + display.print(d); + d = strtok(NULL, ","); + line++; + } + free(tmp); + } + return w; +} + +int centeredTextMark(const char *buf, int x, int y, int color, int underline_line=0, int lineheight=8) { + int16_t x1, y1; + uint16_t w, h; + char *line_pointer = strchr(buf, '\n'); + display.setTextColor(color); + if (!line_pointer) { + display.getTextBounds(buf, 0, 0, &x1, &y1, &w, &h); //calc width of new string + int x_start = x - (w / 2); + int y_start = y - (h / 2); + display.setCursor(x_start, y_start); + if (underline_line == 1) { + display.drawFastHLine(x_start-2, y_start+lineheight, w+4, color); + } + display.print(buf); + }else { + char *tmp = strdup(buf); + char* d = strtok(tmp, "\n"); + int line = 0; + while (d != NULL) { + display.getTextBounds(d, 0, 0, &x1, &y1, &w, &h); //calc width of new string + int x_start = x - (w / 2); + int y_start = y - (h / 2)-lineheight/2 + (line*lineheight); + display.setCursor(x_start, y_start); + display.print(d); + d = strtok(NULL, ","); + if (underline_line == 1) { + display.drawFastHLine(x_start-2, y_start+lineheight, w+4, color); + } + line++; + } + free(tmp); + } + return w; +} + + +int centeredTextMarkMulti(const char *buf, int x, int y, int color, int underline_line=0, int lineheight=8) { + int16_t x1, y1; + uint16_t w, h; + char *line_pointer = strchr(buf, '\n'); + display.setTextColor(color); + if (!line_pointer) { + display.getTextBounds(buf, 0, 0, &x1, &y1, &w, &h); //calc width of new string + int x_start = x - (w / 2); + int y_start = y - (h / 2); + display.setCursor(x_start, y_start); + if (underline_line == 1) { + display.drawFastHLine(x_start-2, y_start, w+4, color); + } + display.print(buf); + }else { + char *tmp = strdup(buf); + char* d = strtok(tmp, "\n"); + int line = 0; + while (d != NULL) { + display.getTextBounds(d, 0, 0, &x1, &y1, &w, &h); //calc width of new string + int x_start = x - (w / 2); + int y_start = y - (h / 2)-lineheight/2 + (line*lineheight); + display.setCursor(x_start, y_start); + display.print(d); + d = strtok(NULL, ","); + if (line == underline_line) { + display.drawFastHLine(x_start-2, y_start+lineheight-3, w+4, color); + } + line++; + } + free(tmp); + } + return w; +} + +int button_multi(const char *buf, int x, int y, int color, int underline_line=0, int lines=0) { + int16_t x1, y1; + uint16_t w, h; + display.setTextColor(color); + char *tmp = strdup(buf); + int line = 0; + char* pch = NULL; + pch = strtok(tmp, "\n"); + + int radius = 2; + int cell_width = 128/3; + int left_x = x - (cell_width/2); + int margin = 10; + int circle_start_x = left_x + margin; + int spacing = (cell_width - margin - margin) / lines; + + while (pch != NULL){ + // Draw Option-Circles + display.drawCircle(circle_start_x + line*spacing, y+7, radius, SH110X_WHITE); + // Only display the active text + if (line == underline_line) { + display.getTextBounds(pch, 0, 0, &x1, &y1, &w, &h); //calc width of new string + int x_start = x - (w / 2); + int y_start = y - (h / 2)-7; + display.setCursor(x_start, y_start); + display.print(pch); + // On the active option, draw a filled circle + display.fillCircle(circle_start_x + line*spacing, y+7, radius, SH110X_WHITE); + } + line++; + pch = strtok(NULL, "\n"); + } + free(tmp); + return w; +} + +float saturate(float x) { + if (x < -3.0f) { + return -1.0f; + } else if (x > 3.0f) { + return 1.0f; + } else { + return x * (27.0f + x * x ) / (27.0f + 9.0f * x * x); + } +} + + +double drand(double minf, double maxf){ + return minf + random(1UL << 31) * (maxf - minf) / (1UL << 31); // use 1ULL<<63 for max double values) +} + + +#endif \ No newline at end of file diff --git a/src/leds.h b/src/leds.h new file mode 100644 index 0000000000000000000000000000000000000000..5c4a8168eed032f26197954220287731a493f39f --- /dev/null +++ b/src/leds.h @@ -0,0 +1,78 @@ +#ifndef Leds_h +#define Leds_h +#include "Arduino.h" +#include "luts.h" + + +// Lookup Curves LED Red b +float red_lut_x[] = {170.0, 173.5, 177.0, 180.5, 184.0, 187.5, 191.0, 194.5, 198.0, 201.5, 205.0, 208.5, 212.0, 215.5, 219.0, 222.5, 240.0, 240.75, 241.5, 242.25, 243.0, 243.75, 244.5, 245.25, 246.0, 246.75, 247.5, 248.25, 249.0, 249.75, 250.5, 251.25, 255}; +float red_lut_y[] = {0.0, 0.0037500000000000007, 0.030000000000000006, 0.10125000000000003, 0.24000000000000005, 0.46875, 0.8100000000000003, 1.2862500000000003, 1.9200000000000004, 2.7337500000000006, 3.75, 4.991250000000002, 6.480000000000002, 8.238750000000001, 10.290000000000003, 12.65625, 30.0, 31.1503125, 32.4525, 34.0584375, 36.12, 38.7890625, 42.2175, 46.5571875, 51.96, 58.5778125, 66.5625, 76.06593750000002, 87.24000000000001, 100.23656250000002, 115.20750000000002, 132.3046875, 255}; +size_t red_lut_len = 33; + +// Lookup Curves LED Green b +float green_lut_x[] = {10.0, 18.5, 27.0, 35.5, 44.0, 52.5, 61.00000000000001, 69.5, 78.0, 86.5, 95.0, 103.50000000000001, 112.00000000000001, 120.5, 129.0, 137.5, 180.0, 183.5, 187.0, 190.5, 194.0, 197.5, 201.0, 204.5, 208.0, 211.5, 215.0, 218.5, 222.0, 225.5, 229.0, 232.5, 250}; +float green_lut_y[] = {0.0, 0.0075000000000000015, 0.06000000000000001, 0.20250000000000007, 0.4800000000000001, 0.9375, 1.6200000000000006, 2.5725000000000007, 3.8400000000000007, 5.467500000000001, 7.5, 9.982500000000003, 12.960000000000004, 16.477500000000003, 20.580000000000005, 25.3125, 60.0, 56.43, 52.92, 49.47, 46.08, 42.75, 39.48, 36.269999999999996, 33.120000000000005, 30.029999999999998, 27.000000000000007, 24.03, 21.119999999999997, 18.269999999999996, 15.480000000000004, 12.750000000000007, 0}; +size_t green_lut_len = 33; + +// Lookup Curves LED Blue b +float blue_lut_x[] = {0.0, 2.0, 4.0, 6.000000000000001, 8.0, 10.0, 12.000000000000002, 14.000000000000002, 16.0, 18.0, 20.0, 22.0, 24.000000000000004, 26.0, 28.000000000000004, 30.0, 40.0, 46.5, 53.0, 59.5, 66.0, 72.5, 79.0, 85.5, 92.0, 98.5, 105.0, 111.5, 118.00000000000001, 124.5, 131.0, 137.5, 170.0, 170.0, 170.0, 170.0, 170.0, 170.0, 170.0, 170.0, 170.0, 170.0, 170.0, 170.0, 170.0, 170.0, 170.0, 170.0}; +float blue_lut_y[] = {0.0, 0.10099999999999998, 0.20799999999999996, 0.327, 0.46399999999999997, 0.6249999999999999, 0.8160000000000001, 1.0430000000000001, 1.312, 1.629, 1.9999999999999998, 2.4310000000000005, 2.928000000000001, 3.497, 4.144000000000001, 4.875, 10.0, 9.025, 8.1, 7.225, 6.4, 5.625, 4.9, 4.225, 3.5999999999999996, 3.0250000000000004, 2.5, 2.0249999999999986, 1.5999999999999996, 1.2250000000000014, 0.9000000000000004, 0.625, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; +size_t blue_lut_len = 48; + + + + + +class RGBLed { + int pin_red; + int pin_green; + int pin_blue; + float level = 0.0f; + Easer easer; + + public: + RGBLed(int pin_red, int pin_green, int pin_blue); + void init(); + void off(); + void setColor(int r, int g, int b); + void setAudioLevelIndicator(float envelope_value); +}; + +RGBLed::RGBLed(int pin_red, int pin_green, int pin_blue) { + this->pin_red = pin_red; + this->pin_green = pin_green; + this->pin_blue = pin_blue; + this->easer.setFactor(0.7); +} + +void RGBLed::init() { + pinMode(this->pin_red, OUTPUT); + pinMode(this->pin_green, OUTPUT); + pinMode(this->pin_blue, OUTPUT); +} + +void RGBLed::off() { + digitalWrite(this->pin_red, LOW); + digitalWrite(this->pin_green, LOW); + digitalWrite(this->pin_blue, LOW); +} + +void RGBLed::setColor(int r, int g, int b) { + r = min(255, max(0, r)); + g = min(255, max(0, g)); + b = min(255, max(0, b)); + analogWrite(this->pin_red, r); + analogWrite(this->pin_green, g); + analogWrite(this->pin_blue, b); +} + +void RGBLed::setAudioLevelIndicator(float envelope_value) { + level = easer.Process(envelope_value); + int brightness = int(min(255.0f, max(0.0f, level * 255))); + int red = get_from_xy_table(red_lut_x, red_lut_y, brightness, red_lut_len); + int green = get_from_xy_table(green_lut_x, green_lut_y, brightness, green_lut_len); + int blue = get_from_xy_table(blue_lut_x, blue_lut_y, brightness, blue_lut_len); + this->setColor(red, green, blue); +} + +#endif \ No newline at end of file diff --git a/src/lfo.h b/src/lfo.h new file mode 100644 index 0000000000000000000000000000000000000000..a91cdb6d496e4ed4b3da4cecc5eeaeb31ca85869 --- /dev/null +++ b/src/lfo.h @@ -0,0 +1,9 @@ +#pragma once + +enum LfoKind { + LFO_KIND_TRI, + LFO_KIND_SQR, + LFO_KIND_RAND, + LFO_KIND_JUMP, + LFO_KIND_NONE, +}; \ No newline at end of file diff --git a/src/looper.h b/src/looper.h new file mode 100644 index 0000000000000000000000000000000000000000..594ae1a14efdbb6f58c40446bedd2e89cee50300 --- /dev/null +++ b/src/looper.h @@ -0,0 +1,468 @@ +#pragma once + +#include "luts.h" + +namespace atoav { + +enum RecPitchMode { + REC_PITCH_MODE_NORMAL, + REC_PITCH_MODE_UNPITCHED, + REC_PITCH_MODE_LAST, +}; + +enum RecStartMode { + REC_START_MODE_BUFFER, + REC_START_MODE_LOOP, + REC_START_MODE_PLAYHEAD, + REC_START_MODE_LAST, +}; + +enum PlaybackState { + PLAYBACK_STATE_STOPPED, + PLAYBACK_STATE_LOOP, + PLAYBACK_STATE_MULTILOOP, + PLAYBACK_STATE_MIDI, + PLAYBACK_STATE_LAST, +}; + +enum RecordingState { + REC_STATE_EMPTY, + REC_STATE_RECORDING, + REC_STATE_OVERDUBBING, + REC_STATE_ERASING, + REC_STATE_NONE, + REC_STATE_LAST, +}; + +// ================================================= +// = H E A D = +// ================================================= + +class Head { + public: + Head(); + void activate(); + void deactivate(); + bool isActive(); + void setPosition(float value); + void reset(); + void setIncrement(float value); + void incrementBy(float value); + void slowDown(); + void reverse(); + void speedUp(); + void update(); + float read(); + float increment = 1.0f; + float variation = 0.0f; + float variation_amount = 0.0f; + float speed_multiplier = 1.0f; + private: + bool active = true; + float position = 0.0f; +}; + +Head::Head() { + variation = random(-0.1f, 0.1f); +} +void Head::activate() { + this->active = true; +} +void Head::deactivate() { + this->active = false; +} +bool Head::isActive() { + return active; +} +void Head::reset() { + this->position = 0.0f; +} +void Head::setPosition(float value) { + this->position = value * speed_multiplier; +} +void Head::setIncrement(float value) { + this->increment = value; +} +void Head::incrementBy(float value) { + this->position += value; +} +void Head::slowDown() { + this->speed_multiplier = max(0.0f, this->speed_multiplier - 0.0025f); +} +void Head::reverse() { + this->speed_multiplier = -abs(this->speed_multiplier ); +} +void Head::speedUp() { + this->speed_multiplier = 1.0f; +} +void Head::update() { + this->position += (this->increment + (variation * variation_amount)) * speed_multiplier; +} +float Head::read() { + return this->position; +} + + +// ================================================= +// = L O O P E R = +// ================================================= + +class Looper { + public: + Looper(); + void Init(float *buf, size_t buf_size); + void SetRecord(); + void SetOverdub(); + void SetErase(); + void SetStopWriting(); + bool isWriting(); + void ResetRecHead(); + void SetLoop(float loop_start_time, float loop_length_time); + void Record(float in); + float Process(); + float* getBuffer(); + size_t getBufferLength(); + void setRecPitchMode(RecPitchMode mode); + void setRecStartMode(RecStartMode mode); + float GetPlayhead(); + float* GetPlayheads(); + uint8_t GetPlayheadCount(); + float GetRecHead(); + bool toggleRecMode(); + void setRecModeFull(); + void setRecModeLoop(); + void setRecModeFullShot(); + void setRecModeLoopShot(); + void setPlaybackSpeed(float increment); + void addToPlayhead(float value); + void slowDown(); + void reverse(); + void restart(); + void speedUp(); + float loop_start_f = 0.0f; + float loop_length_f = 1.0f; + uint8_t grain_count = 8; + float grain_spread = 2.0f; + float grain_variation = 0.0f; + RecPitchMode rec_pitch_mode = REC_PITCH_MODE_NORMAL; + RecStartMode rec_start_mode = REC_START_MODE_BUFFER; + PlaybackState playback_state = PLAYBACK_STATE_LOOP; + RecordingState recording_state = REC_STATE_EMPTY; + + private: + static const size_t kFadeLength = 200; + static const size_t kMinLoopLength = 2 * kFadeLength; + + float* buffer; + size_t buffer_length = 0; + + Head playheads[9]; + Head rec_head; + + size_t loop_start = 0; + size_t loop_length = 48000; + + bool stop_after_recording = false; + bool stay_within_loop = true; + +}; + + +Looper::Looper() {} + +void Looper::Init(float *buf, size_t buf_size) { + buffer = buf; + buffer_length = buf_size; + memset(buffer, 0, sizeof(float) * buffer_length); +} + +void Looper::SetRecord() { + recording_state = REC_STATE_RECORDING; + ResetRecHead(); + rec_head.activate(); +} + +void Looper::SetOverdub() { + recording_state = REC_STATE_OVERDUBBING; + ResetRecHead(); + rec_head.activate(); +} + +void Looper::SetErase() { + recording_state = REC_STATE_ERASING; + ResetRecHead(); + rec_head.activate(); +} + +void Looper::SetStopWriting() { + recording_state = REC_STATE_NONE; + rec_head.deactivate(); +} + +bool Looper::isWriting() { + return recording_state == REC_STATE_RECORDING + || recording_state == REC_STATE_OVERDUBBING + || recording_state == REC_STATE_ERASING; +} + +void Looper::ResetRecHead() { + if (isWriting()) { + switch (rec_start_mode) { + case REC_START_MODE_LOOP: + rec_head.setPosition(loop_start % buffer_length); + break; + case REC_START_MODE_BUFFER: + rec_head.setPosition(0.0f); + break; + case REC_START_MODE_PLAYHEAD: + rec_head.setPosition(fmod(loop_start + playheads[0].read(), float(buffer_length))); + break; + } + } +} + +void Looper::SetLoop(float loop_start_time, float loop_length_time) { + if (!isnan(loop_start_time)) { + loop_start_f = loop_start_time; + loop_start = static_cast<size_t>(loop_start_time * (buffer_length - 1)); + } + if (!isnan(loop_length_time)) { + loop_length_f = loop_length_time; + loop_length = max(kMinLoopLength, static_cast<size_t>(loop_length_time * buffer_length)); + } +} + +void Looper::Record(float in) { + // Overwrite/Add/Erase the buffer depending on the mode + switch (recording_state) { + case REC_STATE_RECORDING: + buffer[int(rec_head.read())] = in; + break; + case REC_STATE_OVERDUBBING: + buffer[int(rec_head.read())] += in; + break; + case REC_STATE_ERASING: + buffer[int(rec_head.read())] = 0.0f; + break; + } + + // Advance the recording head if needed + if (isWriting()) { + // Set Recording head increment depending on the mode + switch (rec_pitch_mode) { + case REC_PITCH_MODE_NORMAL: + rec_head.setIncrement(1.0f); + break; + case REC_PITCH_MODE_UNPITCHED: + rec_head.setIncrement(playheads[0].increment); + break; + } + // Increment recording head + rec_head.update(); + + // Limit the position of the rec head depending on the active mode + if (!stop_after_recording) { + if (!stay_within_loop) { + // record into whole buffer + rec_head.setPosition(fmod(rec_head.read(), float(buffer_length))); + } else { + // Limit rec head to stay inside the loop + rec_head.setPosition(fmod(rec_head.read(), float(loop_start + loop_length))); + rec_head.setPosition(max(float(loop_start), rec_head.read())); + } + } else { + // Stop at end (either end of buffer or end of loop) + if (!stay_within_loop) { + if (rec_head.read() > buffer_length) { SetStopWriting(); } + } else { + if (rec_head.read() > loop_start + loop_length) { SetStopWriting(); } + } + } + + // Ensure the Rec-Head is never without bounds, even when running backwards + if (rec_head.read() > buffer_length) { + rec_head.setPosition(0.0f); + } else if (rec_head.read() < 0) { + rec_head.setPosition(buffer_length); + } + } +} + +float Looper::Process() { + // Early return if buffer is empty or not playing to save performance + if (recording_state == REC_STATE_EMPTY + || playback_state == PLAYBACK_STATE_STOPPED) { + return 0; + } + + // Deactivate all playheads except first if playback state is Loop + switch (playback_state) { + case PLAYBACK_STATE_LOOP: + playheads[0].activate(); + for (size_t i=1; i<9; i++) { + playheads[i].deactivate(); + } + break; + case PLAYBACK_STATE_MULTILOOP: + for (size_t i=0; i<9; i++) { + playheads[i].activate(); + } + break; + case PLAYBACK_STATE_STOPPED: + for (size_t i=0; i<9; i++) { + playheads[i].deactivate(); + } + break; + } + + double mix = 0.0; + + for (size_t i=0; i<grain_count-1; i++) { + // Skip inactive playheads + if (!playheads[i].isActive()) continue; + + // Ensure we are actually inside the buffer + int from_start = int(playheads[i].read()) % loop_length; + int play_pos = loop_start + from_start; + + float vol = 1.0f; + if (from_start <= kFadeLength) { + vol = from_start / float(kFadeLength); + } + int from_end = abs(int(loop_length) - from_start); + if (from_end <= kFadeLength) { + vol = from_end / float(kFadeLength); + } + + // Read from the buffer + mix += buffer[play_pos] * vol; + + // Advance the playhead + playheads[i].update(); + + // Ensure the playhead stays within bounds of the loop + float pos = playheads[i].read(); + if (pos >= loop_length || pos <= 0.0f) { + playheads[i].setPosition(fmod(pos, float(loop_length))); + } + } + return mix; +} + +float Looper::GetPlayhead() { + return float(int(playheads[0].read()) % loop_length) / float(buffer_length); +} + +float* Looper::GetPlayheads() { + static float playhead_positions[9]; + for (size_t i=0; i<9; i++) { + playhead_positions[i] = float(int(playheads[i].read()) % loop_length) / float(buffer_length); + } + return playhead_positions; +} + +uint8_t Looper::GetPlayheadCount() { + return grain_count; +} + +float Looper::GetRecHead() { + return float(rec_head.read()) / float(buffer_length); +} + +bool Looper::toggleRecMode() { + stay_within_loop = !stay_within_loop; + return stay_within_loop; +} + +void Looper::setRecModeFull() { + stay_within_loop = false; + stop_after_recording = false; +} + +void Looper::setRecModeLoop() { + stay_within_loop = true; + stop_after_recording = false; +} + +void Looper::setRecModeFullShot() { + stay_within_loop = false; + stop_after_recording = true; +} + +void Looper::setRecModeLoopShot() { + stay_within_loop = true; + stop_after_recording = true; +} + +void Looper::setPlaybackSpeed(float increment) { + switch (playback_state) { + case PLAYBACK_STATE_LOOP: + playheads[0].setIncrement(increment); + break; + case PLAYBACK_STATE_MULTILOOP: + playheads[0].setIncrement(increment); + for (size_t i=1; i<9; i++) { + playheads[i].variation_amount = grain_variation; + playheads[i].setIncrement(increment + increment*grain_spread); + } + break; + } +} + +void Looper::addToPlayhead(float value) { + switch (playback_state) { + case PLAYBACK_STATE_LOOP: + playheads[0].incrementBy(value); + break; + case PLAYBACK_STATE_MULTILOOP: + playheads[0].incrementBy(value); + for (size_t i=1; i<9; i++) { + playheads[i].incrementBy(value + value/(1+i)); + } + break; + } +} + +void Looper::slowDown() { + for (size_t i=0; i<9; i++) { + playheads[i].slowDown(); + } +} + +void Looper::reverse() { + for (size_t i=0; i<9; i++) { + playheads[i].reverse(); + } +} + +void Looper::restart() { + for (size_t i=0; i<9; i++) { + playheads[i].reset(); + } +} + +void Looper::speedUp() { + for (size_t i=0; i<9; i++) { + playheads[i].speedUp(); + } +} + +float* Looper::getBuffer() { + return buffer; +} + +size_t Looper::getBufferLength() { + return buffer_length; +} + +void Looper::setRecPitchMode(RecPitchMode mode) { + rec_pitch_mode = mode; +} + +void Looper::setRecStartMode(RecStartMode mode) { + rec_start_mode = mode; +} + + + +}; // namespace atoav \ No newline at end of file diff --git a/src/luts.h b/src/luts.h new file mode 100644 index 0000000000000000000000000000000000000000..b332ed6da58ed786d58f0a815e43280de8186beb --- /dev/null +++ b/src/luts.h @@ -0,0 +1,47 @@ +#ifndef LUTs_h +#define LUTs_h + +#include <MultiMap.h> + +// Lookup Table for Pitch Knob +float pitch_knob_lookup_x[] = {0.0, 0.0025, 0.0175, 0.0225, 0.0375, 0.0425, 0.057499999999999996, 0.0625, 0.0775, 0.0825, 0.0975, 0.10250000000000001, 0.1175, 0.1225, 0.1375, 0.14250000000000002, 0.1575, 0.1625, 0.1975, 0.2025, 0.2475, 0.2525, 0.2975, 0.3025, 0.3775, 0.3825, 0.4175, 0.4225, 0.4575, 0.4625, 0.4975, 0.5025, 0.5375000000000001, 0.5425, 0.5775000000000001, 0.5825, 0.6175, 0.6224999999999999, 0.6975, 0.7024999999999999, 0.7475, 0.7525, 0.7975000000000001, 0.8025, 0.8375, 0.8424999999999999, 0.8575, 0.8624999999999999, 0.8775000000000001, 0.8825, 0.8975000000000001, 0.9025, 0.9175000000000001, 0.9225, 0.9375, 0.9424999999999999, 0.9575, 0.9624999999999999, 0.9775, 0.9824999999999999, 0.9975, 1.0}; +float pitch_knob_lookup_y[] = {-1.0, -1.0, -0.9, -0.9, -0.8, -0.8, -0.7, -0.7, -0.6, -0.6, -0.5, -0.5, -0.4, -0.4, -0.3, -0.3, -0.2, -0.2, -0.1, -0.1, -0.05, -0.05, -0.025, -0.025, -0.0125, -0.0125, -0.00625, -0.00625, -0.003125, -0.003125, 0.0, 0.0, 0.003125, 0.003125, 0.00625, 0.00625, 0.0125, 0.0125, 0.025, 0.025, 0.05, 0.05, 0.1, 0.1, 0.2, 0.2, 0.3, 0.3, 0.4, 0.4, 0.5, 0.5, 0.6, 0.6, 0.7, 0.7, 0.8, 0.8, 0.9, 0.9, 1.0, 1.0}; +size_t pitch_knob_lookup_length = 62; + + +class Easer { + float output = 0.0f; + float delta = 0.0f; + float easing = 0.1f; + public: + Easer(); + + float Process(float input) { + delta = input - output; + output += delta * easing; + return output; + } + + void setFactor(float factor) { + easing = min(max(0.00001f, factor), 1.0f); + } +}; + +Easer::Easer() { + +}; + +float lerp(float a, float b, float f) { + f = min(1.0f, max(0.0f, f)); + + if (f == 0.0) { return a; } + else if (f == 1.0f) { return b; } + else { return a * (1.0f-f) + b * f; } +} + +float get_from_xy_table(float* xtable, float* ytable, float f, size_t length) { + return multiMap<float>(f, xtable, ytable, length); +} + + +#endif \ No newline at end of file diff --git a/src/potentiometers.h b/src/potentiometers.h new file mode 100644 index 0000000000000000000000000000000000000000..055264c4d6e2de012b6d3dbbb39f072120a5487a --- /dev/null +++ b/src/potentiometers.h @@ -0,0 +1,257 @@ +#include "WCharacter.h" +#include "wiring_analog.h" +#include <stdint.h> +#include <limits> +#ifndef Potentiometers_h +#define Potentiometers_h +#include "Arduino.h" +#include "luts.h" +#include "helpers.h" + +#include "Adafruit_SH110X.h" +#include "Adafruit_GFX.h" +extern Adafruit_SH1106G display; + +// The length of the moving average filter that smooths the +// controls. Higher number is smoother, but less responsive +// and needs more memory. +#define POT_MOVING_AVERAGE_SIZE 2 + +// Length of the Textbuffer for floats in the UI +#define UI_TEXTBUFFER_LENGTH 6 + +// Modes +enum PotMode { + POT_MODE_LIN, + POT_MODE_BIP, + POT_MODE_PITCH, + POT_MODE_SWITCH, + POT_MODE_LAST +}; + +// Display Modes +enum PotDisplayMode { + POT_DISPLAY_MODE_DEFAULT, + POT_DISPLAY_MODE_PITCH, + POT_DISPLAY_MODE_PERCENT, + POT_DISPLAY_MODE_SWITCH, + POT_DISPLAY_MODE_SWITCH_NUMBERS, + POT_DISPLAY_MODE_LAST +}; + +typedef void (*callback_function)(void); + +class Potentiometer { + int pin; + int readings[POT_MOVING_AVERAGE_SIZE]; + PotMode mode = POT_MODE_LIN; + PotDisplayMode display_mode = POT_DISPLAY_MODE_DEFAULT; + float last_reading, last_normalized_reading = 0.0f; + float display_scale = 1.0f; + callback_function onChangeFunction; + Easer easer; + + public: + Potentiometer(int pin); + void init(); + void setLinear(); + void setPitch(); + void setSwitch(); + float read(); + void setOnChange(callback_function f); + void renderUi(); + void setDisplayMode(const char *name, float display_scale, PotDisplayMode display_mode); + const char *name; + double last_displayed = 0.0; + bool should_display = false; + bool display_value_changes = false; + bool last_was_nan = false; + uint8_t switch_positions; + uint8_t switch_offset = 0; + const char* const switch_labels[4] = {"TRI", "SQR", "RAND", "JUMP"}; +}; + +Potentiometer::Potentiometer(int pin) { + this->pin = pin; +} + +void Potentiometer::init() { + analogReadResolution(12); + easer.setFactor(0.001); +} + +void Potentiometer::setLinear() { + this->mode = POT_MODE_LIN; +} + +void Potentiometer::setPitch() { + this->mode = POT_MODE_PITCH; +} + +void Potentiometer::setSwitch() { + this->mode = POT_MODE_SWITCH; +} + +float Potentiometer::read() { + int reading = analogRead(this->pin); + // Shift all readings in the buffer over by one position, deleting the oldest + // and adding the newest + for (int i=0; i<POT_MOVING_AVERAGE_SIZE; i++) { + int next = i+1; + if (next < POT_MOVING_AVERAGE_SIZE) { + (this->readings)[i] = (this->readings)[next]; + } + } + (this->readings)[POT_MOVING_AVERAGE_SIZE-1] = reading; + + // Get the average of the last readings + reading = 0; + for (int i=0; i<POT_MOVING_AVERAGE_SIZE; i++) { + reading += (this->readings)[i]; + } + reading = reading / POT_MOVING_AVERAGE_SIZE; + + // Convert the last reading to a float and return + float current_reading = easer.Process(reading / 4096.0f); + float normalized_reading = current_reading; + + // Depending on the Mode + switch (this->mode) { + case POT_MODE_PITCH: + current_reading = get_from_xy_table(pitch_knob_lookup_x, pitch_knob_lookup_y, current_reading, pitch_knob_lookup_length); + break; + case POT_MODE_SWITCH: + current_reading = int(current_reading * switch_positions); + break; + } + + bool changed = abs(normalized_reading - this->last_normalized_reading) > 0.002; + + // If the difference to the last reading is big enough assume the knob has been touched + if (this->last_normalized_reading && changed) { + if (display_value_changes) { + last_displayed = millis(); + should_display = true; + } + if (this->onChangeFunction) { this->onChangeFunction(); } + } + + if (this->last_normalized_reading && !changed) { + // if (!last_was_nan) { + // Serial.print(this->name); + // Serial.println(" returned NaN"); + // } + last_was_nan = true; + return std::numeric_limits<float>::quiet_NaN(); + } + last_was_nan = false; + this->last_reading = current_reading; + this->last_normalized_reading = normalized_reading; + return current_reading; +} + +void Potentiometer::setOnChange(callback_function f) { + this->onChangeFunction = f; +} + +void Potentiometer::setDisplayMode(const char *name, float display_scale, PotDisplayMode display_mode) { + this->display_value_changes = true; + this->name = name; + this->display_scale = display_scale; + this->display_mode = display_mode; +} + +void Potentiometer::renderUi() { + double now = millis(); + if (this->should_display) { + int x_margin = 28; + int y_margin = 13; + int x_center = display.width()/2; + int y_center = display.height()/2; + // Render a rectangle Backdrop for the text + display.fillRect(3+x_margin, 3+y_margin, display.width()-x_margin*2, display.height()-y_margin*2, SH110X_WHITE); + display.fillRect(x_margin, y_margin, display.width()-x_margin*2, display.height()-y_margin*2, SH110X_WHITE); + display.fillRect(x_margin+1, y_margin+1, display.width()-(x_margin+1)*2, display.height()-(y_margin+1)*2, SH110X_BLACK); + + + // Render the name of the parameter (e.g. "Pitch") + centeredText(this->name, x_center, y_center-4, SH110X_WHITE); + + // Choose how many digits to display depending on the mode + int digits = 2; + if (this->display_mode == POT_DISPLAY_MODE_PERCENT) { digits = 0; } + // Allocate a buffer for the float and convert it into characters + char value_buffer[UI_TEXTBUFFER_LENGTH]; // Buffer big enough for 7-character float + dtostrf(this->last_reading*display_scale, 6, digits, value_buffer); // Leave room for too large numbers! + + // If we are on a bipolar pot display an indicator if we are in the center + if (this->mode == POT_MODE_BIP && this->last_reading > -0.0001 && this->last_reading < 0.0001) { + display.fillTriangle(x_center, y_center+10, x_center+3, y_center+15, x_center-3, y_center+15, SH110X_WHITE); + } + + // If we are on a pitch pot display an indicator if we are in the the right steps + if (this->mode == POT_MODE_PITCH) { + float reading_mod = fmod(abs(this->last_reading), 0.05f); + if (reading_mod > 0.999f || reading_mod < 0.001f) { + display.fillTriangle(x_center, y_center+10, x_center+3, y_center+15, x_center-3, y_center+15, SH110X_WHITE); + } + } + + // The float value may contain some empty whitespace characters, remove them by + // first figuring out which the first actual character is + int nonwhite = 0; + for (int i=0; i<UI_TEXTBUFFER_LENGTH; i++) { + if (value_buffer[i] == ' ') { + nonwhite++; + } + } + + // Create a new buffer that can hold everything + char text_buffer[UI_TEXTBUFFER_LENGTH+6]; + + // Copy all non-white characters over + for (int i = 0; i<UI_TEXTBUFFER_LENGTH; i++) { + text_buffer[i] = value_buffer[i+int(nonwhite)]; + } + + // Figure out where the last character (\0) in our new buffer is + int last = UI_TEXTBUFFER_LENGTH+6; + for (int i=16; i>0; i--) { + if (text_buffer[i] == '\0') { + last = i; + } + } + + // Add units depending on the display mode : ) + if (this->display_mode == POT_DISPLAY_MODE_PERCENT) { + text_buffer[last] = ' '; + text_buffer[last+1] = '%'; + text_buffer[last+2] = '\0'; + } else if (this->display_mode == POT_DISPLAY_MODE_PITCH) { + text_buffer[last] = ' '; + text_buffer[last+1] = 'S'; + text_buffer[last+2] = 'e'; + text_buffer[last+3] = 'm'; + text_buffer[last+4] = 'i'; + text_buffer[last+5] = '\0'; + } + + // Render that new text + if (this->display_mode == POT_DISPLAY_MODE_SWITCH) { + centeredText(switch_labels[int(last_reading+switch_offset)], x_center, y_center+4, SH110X_WHITE); + } else if (this->display_mode == POT_DISPLAY_MODE_SWITCH_NUMBERS) { + sprintf(text_buffer, "%d", int(last_reading)); + centeredText(text_buffer, x_center, y_center+4, SH110X_WHITE); + } else { + centeredText(text_buffer, x_center, y_center+4, SH110X_WHITE); + } + } + + // Show this for 700 ms after it has been last touched + if ((now - this->last_displayed) > 700.0) { + this->should_display = false; + } +} + + +#endif \ No newline at end of file diff --git a/src/ui.h b/src/ui.h new file mode 100644 index 0000000000000000000000000000000000000000..030f2661b16dce56fe0b4c8d2a6348f97cbfdfc6 --- /dev/null +++ b/src/ui.h @@ -0,0 +1,884 @@ +#include <stdint.h> +#include "WSerial.h" +#include "usbd_def.h" +#ifndef Ui_h +#define Ui_h + +#include "Adafruit_SH110X.h" +#include "Adafruit_GFX.h" +#include "potentiometers.h" +#include "buttons.h" +#include "looper.h" +#include "button_grid.h" + +#define UI_MAX_FPS 10 +#define WAVEFORM_OVERSAMPLING 2 +#define WAVEFORM_LIN true + +extern Potentiometer pot_1, pot_2, pot_3, pot_4, pot_5, pot_6, pot_7; +extern Button button_1, button_2, button_3, button_4, button_5, button_6; +extern Adafruit_SH1106G display; +extern atoav::Looper looper_a, looper_b, looper_c, looper_d, looper_e; + +// Should the splash-screen be shown on boot? +bool show_splash = false; + +// Represents the possible states of the UI +enum UiMode { + UI_MODE_SPLASH, // A splash screen that is shown on startup + UI_MODE_DEFAULT, // Default screen: Show Waveform and Parameters + UI_MODE_REC_MENU, // ButtonGrid Menu: Recording Settings + UI_MODE_PLAY_MENU, // ButtonGrid Menu: Playback Settings + UI_MODE_TRIGGER_MENU, // ButtonGrid Menu: Trigger Settings + UI_MODE_FX_MENU, // ButtonGrid Menu: FX Settings + UI_MODE_BUFFER_MENU, // ButtonGrid Menu: Buffer Settings + UI_MODE_LAST +}; + +// Represents possible recording modes +enum RecMode { + REC_MODE_FULL, // Record into full buffer (looping back to start) + REC_MODE_LOOP, // Limit recording to the loop + REC_MODE_FULL_SHOT, // Record into full buffer, stop at the end + REC_MODE_LAST +}; + +// Represents possible recording sources +enum RecSource { + REC_SOURCE_PRE, // Record Incoming audio + REC_SOURCE_LAST_BUF, // Record Last selected Buffer + REC_SOURCE_OUT, // Record the buffer output + REC_SOURCE_NOISE, // Record Noise + REC_SOURCE_LAST +}; + +// Represents possible playback modes +enum PlayMode { + PLAY_MODE_DRUNK, // Drunken Walk + PLAY_MODE_WINDOW, // Sliding window + PLAY_MODE_LOOP, // Loop + PLAY_MODE_GRAIN, // Granular ? + PLAY_MODE_ONESHOT, // Play it once + PLAY_MODE_LAST +}; + +// Represents possible recording states +enum RecordingState { + REC_STATE_NOT_RECORDING, // Not recording + REC_STATE_RECORDING, // Recording (replace what is in the buffer) + REC_STATE_OVERDUBBING, // Overdubbing (mix recorded values with the existing samples) + REC_STATE_LAST +}; + +enum ActiveBuffer { + ACTIVE_BUFFER_A, + ACTIVE_BUFFER_B, + ACTIVE_BUFFER_C, + ACTIVE_BUFFER_D, + ACTIVE_BUFFER_E, + ACTIVE_BUFFER_LAST, +}; + +enum BufferSummingMode { + BUFFER_SUM_MODE_SOLO, + BUFFER_SUM_MODE_SUM, + BUFFER_SUM_MODE_RING, + BUFFER_SUM_MODE_LAST, +}; + +enum FXMode { + FX_MODE_ALL, + FX_MODE_REVERB, + FX_MODE_NONE, + FX_MODE_LFO, + FX_MODE_GRAIN, + FX_MODE_FILTER, + FX_MODE_LAST, +}; + +// The Ui is _the_ coordinating class for the whole interaction. +// The default mode +// Note Descriptions get a space of 21 chars and 8 lines +class Ui { + public: + Ui() : button_grids { + ButtonGrid((int) UI_MODE_REC_MENU, { + GridButton("REC\nMENU", &button_1, true), + GridButton("MOM\nTOGGLE", &button_2, false, BUTTON_TYPE_TOGGLE, 0), + GridButton("PRE\nLAST\nOUT\nNOISE", &button_3, false, BUTTON_TYPE_MULTITOGGLE, 0), + GridButton("FULL\nLOOP\nSHOT", &button_4, false, BUTTON_TYPE_MULTITOGGLE, 1), + GridButton("NORMAL\nUNPTCH", &button_5, false, BUTTON_TYPE_MULTITOGGLE, 0), + GridButton("START\nLOOPST\nPLAYHD", &button_6, false, BUTTON_TYPE_MULTITOGGLE, 0), + }), + ButtonGrid((int) UI_MODE_PLAY_MENU, { + GridButton("STOP\nLOOP\nMULTI\nMIDI", &button_1, false, BUTTON_TYPE_MULTITOGGLE, 1), + GridButton("PLAY\nMENU", &button_2, true), + GridButton("ACTIVE\nSUM\nRING", &button_3, false, BUTTON_TYPE_MULTITOGGLE, 0), + GridButton("RE\nSTART", &button_4, false), + GridButton("SLOW\nDOWN", &button_5, false), + GridButton("REV\nERSE", &button_6, false), + }), + ButtonGrid((int) UI_MODE_TRIGGER_MENU, { + GridButton("MIDI\nTRIG.", &button_1, false), + GridButton("MIDI\nUNMUTE", &button_2, false), + GridButton("TRIG.\nMENU", &button_3, true), + GridButton("MANUAL\nTRIG.", &button_4, false), + GridButton("MANUAL\nUNMUTE", &button_5, false), + GridButton("AUTO", &button_6, false), + }), + ButtonGrid((int) UI_MODE_FX_MENU, { + GridButton("ALL", &button_1, false, BUTTON_TYPE_ENUM, 1), + GridButton("REVERB", &button_2, false, BUTTON_TYPE_ENUM, 0), + GridButton("FX\nMENU", &button_3, true), + GridButton("LFO", &button_4, false, BUTTON_TYPE_ENUM, 0), + GridButton("GRAIN", &button_5, false, BUTTON_TYPE_ENUM, 0), + GridButton("-", &button_6, false, BUTTON_TYPE_ENUM, 0), + }), + ButtonGrid((int) UI_MODE_BUFFER_MENU, { + GridButton("A", &button_1, false, BUTTON_TYPE_ENUM, 1), + GridButton("B", &button_2, false, BUTTON_TYPE_ENUM, 0), + GridButton("C", &button_3, false, BUTTON_TYPE_ENUM, 0), + GridButton("D", &button_4, false, BUTTON_TYPE_ENUM, 0), + GridButton("E", &button_5, false, BUTTON_TYPE_ENUM, 0), + GridButton("BUFFER\nMENU", &button_6, true), + }), + } {}; + + // Store the Button Grids declared above (make sure the lenght matches!) + ButtonGrid button_grids[5]; + + // Stores the current Ui Mode + UiMode ui_mode = UI_MODE_SPLASH; + + // Default Recording Mode + RecMode rec_mode = REC_MODE_LOOP; + + // Default Recording Source + RecSource rec_source = REC_SOURCE_PRE; + + // Default active buffer + ActiveBuffer active_buffer = ACTIVE_BUFFER_A; + ActiveBuffer previous_buffer = ACTIVE_BUFFER_A; + + // Default active summing mode + BufferSummingMode buffer_summing_mode = BUFFER_SUM_MODE_SOLO; + + FXMode fx_mode = FX_MODE_ALL; + + // Render the UI + // Except for the splash screen this is expected to be called + // repeatedly in a loop + void Render() { + double now = millis(); + // Serial.println(1000.0/UI_MAX_FPS); + if ((now - last_render) > (1000.0/UI_MAX_FPS)) { + switch (ui_mode) { + case UI_MODE_SPLASH: + renderSplash(); + break; + case UI_MODE_DEFAULT: + renderDefault(); + break; + case UI_MODE_REC_MENU: + renderGrid(0, rec_mode); + break; + case UI_MODE_PLAY_MENU: + renderGrid(1); + break; + case UI_MODE_TRIGGER_MENU: + renderGrid(2); + break; + case UI_MODE_FX_MENU: + renderGrid(3, fx_mode); + break; + case UI_MODE_BUFFER_MENU: + renderGrid(4, active_buffer); + break; + } + last_render = now; + } + } + + // Helper method to render a certain button grid + void renderGrid(size_t num, int button_enum=0) { + display.clearDisplay(); + button_grids[num].render(button_enum); + display.display(); + } + + // Renders a splash screen (runs once) + void renderSplash() { + display.setTextSize(1); + + // Splash rendering is now done, go to next UI Mode + setMode(UI_MODE_DEFAULT); + } + + // Helper method to reset the controls + void resetControls() { + button_1.reset(); + button_2.reset(); + button_3.reset(); + button_4.reset(); + button_5.reset(); + button_6.reset(); + } + + Button* setupButtonGrid(int n) { + // Find the index of the home button + int home_button_index = button_grids[n].homeButtonIndex(); + + // Create a pointer to the hoime button + Button* home_button = button_grids[n].grid_buttons_[home_button_index].button; + + // Reset the controls + resetControls(); + + // Setup the button grid + button_grids[n].setup(); + + // Return to default mode on release + home_button->onReleased([this, n](){ + this->setMode(UI_MODE_DEFAULT); + this->button_grids[n].hideAllDescriptions(); + activeLooper()->speedUp(); + }); + + // Return pointer to the home button + return home_button; + } + + // Setup the Recording Menu + void setupRecMenu() { + // Only run once when the ui_mode changed + if (ui_mode == UI_MODE_REC_MENU && last_ui_mode != UI_MODE_REC_MENU) { + int n = 0; + + // Setup button Grid + Button* home_button = setupButtonGrid(n); + + // Toggle between momentary and toggle recording modes + button_2.onPress([this, n](){ + rec_button_momentary = !rec_button_momentary; + button_grids[n].grid_buttons_[1].active = !rec_button_momentary; + }); + + // Set Recording Source (Pre/Post/Out/Noise) + button_3.onPress([this, n](){ + button_grids[n].grid_buttons_[2].next(); + rec_source = (RecSource) button_grids[n].grid_buttons_[2].active; + }); + + // Switch Recording modes (Full/Loop/Oneshot) + button_4.onPress([this, n](){ + button_grids[n].grid_buttons_[3].next(); + // Button.active returns number according to mode, we cast it to a RecMode enum + rec_mode = (RecMode) button_grids[n].grid_buttons_[3].active; + switch (rec_mode) { + case REC_MODE_FULL: + looper_a.setRecModeFull(); + looper_b.setRecModeFull(); + looper_c.setRecModeFull(); + looper_d.setRecModeFull(); + looper_e.setRecModeFull(); + break; + case REC_MODE_LOOP: + looper_a.setRecModeLoop(); + looper_b.setRecModeLoop(); + looper_c.setRecModeLoop(); + looper_d.setRecModeLoop(); + looper_e.setRecModeLoop(); + break; + case REC_MODE_FULL_SHOT: + looper_a.setRecModeFullShot(); + looper_b.setRecModeFullShot(); + looper_c.setRecModeFullShot(); + looper_d.setRecModeFullShot(); + looper_e.setRecModeFullShot(); + break; + } + }); + + // Set Recording Pitch mode (Normal/Pitched/Unpitched) + button_5.onPress([this, n](){ + button_grids[n].grid_buttons_[4].next(); + looper_a.setRecPitchMode((atoav::RecPitchMode) button_grids[n].grid_buttons_[4].active); + looper_b.setRecPitchMode((atoav::RecPitchMode) button_grids[n].grid_buttons_[4].active); + looper_c.setRecPitchMode((atoav::RecPitchMode) button_grids[n].grid_buttons_[4].active); + looper_d.setRecPitchMode((atoav::RecPitchMode) button_grids[n].grid_buttons_[4].active); + looper_e.setRecPitchMode((atoav::RecPitchMode) button_grids[n].grid_buttons_[4].active); + }); + + // Set Recording Start Option (Buffer Start/Loop Start/Playhead) + button_6.onPress([this, n](){ + button_grids[n].grid_buttons_[5].next(); + looper_a.setRecStartMode((atoav::RecStartMode) button_grids[n].grid_buttons_[5].active); + looper_b.setRecStartMode((atoav::RecStartMode) button_grids[n].grid_buttons_[5].active); + looper_c.setRecStartMode((atoav::RecStartMode) button_grids[n].grid_buttons_[5].active); + looper_d.setRecStartMode((atoav::RecStartMode) button_grids[n].grid_buttons_[5].active); + looper_e.setRecStartMode((atoav::RecStartMode) button_grids[n].grid_buttons_[5].active); + }); + + // Store the last ui mode, for the check on top + last_ui_mode = ui_mode; + } + } + + // Setup the Buffer Menu + void setupBufferMenu() { + // Only run once when the ui_mode changed + if (ui_mode == UI_MODE_BUFFER_MENU && last_ui_mode != UI_MODE_BUFFER_MENU) { + int n = 4; + + // Setup button Grid + Button* home_button = setupButtonGrid(n); + + button_1.onPress([this, n](){ + previous_buffer = active_buffer; + active_buffer = ACTIVE_BUFFER_A; + waveform_cache_dirty = true; + }); + button_2.onPress([this, n](){ + previous_buffer = active_buffer; + active_buffer = ACTIVE_BUFFER_B; + waveform_cache_dirty = true; + }); + button_3.onPress([this, n](){ + previous_buffer = active_buffer; + active_buffer = ACTIVE_BUFFER_C; + waveform_cache_dirty = true; + }); + button_4.onPress([this, n](){ + previous_buffer = active_buffer; + active_buffer = ACTIVE_BUFFER_D; + waveform_cache_dirty = true; + }); + button_5.onPress([this, n](){ + previous_buffer = active_buffer; + active_buffer = ACTIVE_BUFFER_E; + waveform_cache_dirty = true; + }); + + // Store the last ui mode, for the check on top + last_ui_mode = ui_mode; + } + } + + // Setup the Play Menu + void setupPlayMenu() { + // Only run once when the ui_mode changed + if (ui_mode == UI_MODE_PLAY_MENU && last_ui_mode != UI_MODE_PLAY_MENU) { + int n = 1; + + // Ensure the UI showes the play mode of the active looper + button_grids[n].grid_buttons_[0].active = (int) activeLooper()->playback_state ; + + // Setup button Grid + Button* home_button = setupButtonGrid(n); + + // Change the way in which buffers are summed + button_3.onPress([this, n](){ + button_grids[n].grid_buttons_[2].next(); + buffer_summing_mode = (BufferSummingMode) button_grids[n].grid_buttons_[2].active; + }); + + // Change playback state (mode) of the current looper + button_1.onPress([this, n](){ + button_grids[n].grid_buttons_[0].next(); + activeLooper()->playback_state = (atoav::PlaybackState) (button_grids[n].grid_buttons_[0].active); + }); + + // Restart + button_4.onPress([this, n](){ + activeLooper()->restart(); + }); + + // DJ-style slow-down effect + button_5.onHold([this, n](){ + activeLooper()->slowDown(); + }); + button_5.onReleased([this, n](){ + activeLooper()->speedUp(); + }); + + button_6.onHold([this, n](){ + activeLooper()->reverse(); + }); + button_6.onReleased([this, n](){ + activeLooper()->speedUp(); + }); + + // Store the last ui mode, for the check on top + last_ui_mode = ui_mode; + } + } + + // Setup the FX Menu + void setupFXMenu() { + // Only run once when the ui_mode changed + if (ui_mode == UI_MODE_FX_MENU && last_ui_mode != UI_MODE_FX_MENU) { + int n = 3; + + // Ensure the UI showes the play mode of the active looper + // button_grids[n].grid_buttons_[0].active = (int) fx_mode; + + // Setup button Grid + Button* home_button = setupButtonGrid(n); + + // Select the active Effect (All) + button_1.onPress([this, n](){ + fx_mode = FX_MODE_ALL; + pot_5.setDisplayMode("LFO", 100.0f, POT_DISPLAY_MODE_PERCENT); + pot_5.setLinear(); + pot_6.setDisplayMode("Volume", 400.0f, POT_DISPLAY_MODE_PERCENT); + pot_7.setDisplayMode("Reverb", 100.0f, POT_DISPLAY_MODE_PERCENT); + }); + + // Select the active Effect (Reverb) + button_2.onPress([this, n](){ + fx_mode = FX_MODE_REVERB; + pot_5.setDisplayMode("Rev. Tone", 100.0f, POT_DISPLAY_MODE_PERCENT); + pot_5.setLinear(); + pot_6.setDisplayMode("Rev. Decay", 100.0f, POT_DISPLAY_MODE_PERCENT); + pot_7.setDisplayMode("Reverb Mix", 100.0f, POT_DISPLAY_MODE_PERCENT); + }); + + // Select the active Effect (LFO) + button_4.onPress([this, n](){ + fx_mode = FX_MODE_LFO; + pot_5.setDisplayMode("LFO Mode", 100.0f, POT_DISPLAY_MODE_SWITCH); + pot_5.setSwitch(); + pot_5.switch_positions = 4; + pot_5.switch_offset = 0; + pot_6.setDisplayMode("LFO Speed", 100.0f, POT_DISPLAY_MODE_PERCENT); + pot_7.setDisplayMode("LFO Amount", 100.0f, POT_DISPLAY_MODE_PERCENT); + }); + + // Select the active Effect (GRAIN) + button_5.onPress([this, n](){ + fx_mode = FX_MODE_GRAIN; + pot_5.setDisplayMode("Grain Num", 100.0f, POT_DISPLAY_MODE_SWITCH_NUMBERS); + pot_5.setSwitch(); + pot_5.switch_positions = 8; + pot_6.setDisplayMode("Grn. Spread", 100.0f, POT_DISPLAY_MODE_PERCENT); + pot_7.setDisplayMode("Grain Var.", 100.0f, POT_DISPLAY_MODE_PERCENT); + }); + + // Select the active Effect (FILTER) + button_6.onPress([this, n](){ + fx_mode = FX_MODE_FILTER; + pot_5.setDisplayMode("Lowpass", 100.0f, POT_DISPLAY_MODE_PERCENT); + pot_6.setDisplayMode("Highpass", 100.0f, POT_DISPLAY_MODE_PERCENT); + pot_7.setDisplayMode("Resonance", 100.0f, POT_DISPLAY_MODE_PERCENT); + }); + + // Store the last ui mode, for the check on top + last_ui_mode = ui_mode; + } + } + + // Setup the default (waveform) screen + void setupDefault() { + // Only run once on mode change + if (ui_mode == UI_MODE_DEFAULT && last_ui_mode != UI_MODE_DEFAULT) { + // Reset controls + resetControls(); + + // Set up the initial recording mode + switch (rec_mode) { + case REC_MODE_FULL: + looper_a.setRecModeFull(); + looper_b.setRecModeFull(); + looper_c.setRecModeFull(); + looper_d.setRecModeFull(); + looper_e.setRecModeFull(); + break; + case REC_MODE_LOOP: + looper_a.setRecModeLoop(); + looper_b.setRecModeLoop(); + looper_c.setRecModeLoop(); + looper_d.setRecModeLoop(); + looper_e.setRecModeLoop(); + break; + case REC_MODE_FULL_SHOT: + looper_a.setRecModeFullShot(); + looper_b.setRecModeFullShot(); + looper_c.setRecModeFullShot(); + looper_d.setRecModeFullShot(); + looper_e.setRecModeFullShot(); + break; + }; + + // Setup Button functions (these should enter the ButtonGrid Menus) + button_1.onHold([this](){ this->setMode(UI_MODE_REC_MENU); }); + button_2.onHold([this](){ this->setMode(UI_MODE_PLAY_MENU); }); + button_3.onHold([this](){ this->setMode(UI_MODE_FX_MENU); }); + button_6.onHold([this](){ this->setMode(UI_MODE_BUFFER_MENU); }); + + // Set the recording/overdub buttons to toggle or momentary + // depending on the value of the option + if (rec_button_momentary) { + button_4.onHold([this](){ this->activateRecording(); }); + button_5.onHold([this](){ this->activateOverdub(); }); + button_4.onReleased([this](){ this->stopRecording(); }); + button_5.onReleased([this](){ this->stopRecording(); }); + } else { + button_4.onReleased([this](){ this->toggleRecording(); }); + button_5.onReleased([this](){ this->toggleOverdub(); }); + } + + // Store the last ui mode, for the check on top + last_ui_mode = ui_mode; + } + } + + // Render the default screen (waveform) + void renderDefault() { + // Store the current time and check how long ago the last frame was + // in ms + + // Clear the display + display.clearDisplay(); + + // Waveform should be maximum screen-heigh + int wave_height = display.height() * 1.0f; + // Ensure that when stepping from left to right we fit the waveform on the screen + int step = activeLooper()->getBufferLength() / (display.width() * WAVEFORM_OVERSAMPLING); + // Helper variable for the bottom of the screen + int bottom = display.height()-1; + + // Render the waveform by iterating through the samples (oversampled by a factor + // defined on top of this file). Average the samples for each pixel of the 128 px + // wide screen and cache the resulting heights so we only have to recalculate when + // the waveform changes + for (int i=0; i<display.width()*WAVEFORM_OVERSAMPLING; i+=WAVEFORM_OVERSAMPLING) { + uint16_t x = int(i / WAVEFORM_OVERSAMPLING); + // Only recalculate if the cahce is dirty, else use cache + if (waveform_cache_dirty) { + float sig = 0.0f; + float scale = 1.0f; + if (!WAVEFORM_LIN) { + scale = 10.0f; + } + // Step through the buffer and sum the absolute values + for (int s=0; s<WAVEFORM_OVERSAMPLING; s++) { + float abs_sig = activeLooper()->getBuffer()[step*i]; + abs_sig = abs(abs_sig) * scale; + sig += abs_sig; + } + // We oversampled so divide here + sig = sig / float(WAVEFORM_OVERSAMPLING); + + if (!WAVEFORM_LIN) { + // Volume is logarithmic (hiding silent noises) + if (sig != 0.0f) { + sig = log10(sig); + } + } + waveform_cache[x] = int(sig * wave_height); + } + + // Draw the vertical lines from bottom up, depending on the level of the + // calulcated wave on this point of the screen + display.drawFastVLine(x, bottom, -waveform_cache[x], SH110X_WHITE); + + } + // Draw one horizontal line on bottom + display.drawFastHLine(0, bottom, display.width(), SH110X_WHITE); + + // Cache is now marked as clean + waveform_cache_dirty = false; + + // Draw Indicator for loop start + int x_start_loop = int(activeLooper()->loop_start_f * display.width()); + display.drawLine(x_start_loop, 0, x_start_loop, bottom, SH110X_WHITE); + display.fillTriangle(x_start_loop, 6, x_start_loop, 0, x_start_loop+3, 0, SH110X_WHITE); + + // Draw Indicator for Loop End + int x_loop_length = int(activeLooper()->loop_length_f * display.width()); + int x_loop_end = (x_start_loop + x_loop_length) % display.width(); + display.drawLine(x_loop_end, 0, x_loop_end, bottom, SH110X_WHITE); + display.fillTriangle(x_loop_end, 6, x_loop_end-3, 0, x_loop_end, 0, SH110X_WHITE); + + // Draw connecting line for start and end + if (x_loop_end >= x_start_loop) { + display.drawLine(x_start_loop, 0, x_loop_end, 0, SH110X_WHITE); + } else { + display.drawLine(x_start_loop, 0, display.width(), 0, SH110X_WHITE); + display.drawLine(0, 0, x_loop_end, 0, SH110X_WHITE); + } + + // Draw Playhead + switch (activeLooper()->playback_state) { + case atoav::PLAYBACK_STATE_LOOP: + { + int x_playhead = int(activeLooper()->GetPlayhead() * display.width()) + x_start_loop; + display.drawFastVLine(x_playhead, 6, 24, SH110X_WHITE); + break; + } + case atoav::PLAYBACK_STATE_MULTILOOP: + { + float* playheads = activeLooper()->GetPlayheads(); + uint8_t count = activeLooper()->GetPlayheadCount(); + int x_playhead = 0; + for (size_t i=0; i<count-1; i++) { + x_playhead = int(playheads[i] * display.width()) + x_start_loop; + int h = 6 + i*3; + display.drawFastVLine(x_playhead, h, 3, SH110X_WHITE); + } + break; + } + case atoav::PLAYBACK_STATE_MIDI: + { + int x_playhead = int(activeLooper()->GetPlayhead() * display.width()) + x_start_loop; + display.drawFastVLine(x_playhead, 6, 24, SH110X_WHITE); + break; + } + } + + + // Draw Recording Indicator and Recording Head + if (recording_state == REC_STATE_RECORDING) { + // Draw Rec Head + int x_rec_head = int(activeLooper()->GetRecHead() * display.width()); + display.drawFastVLine(x_rec_head, 10, bottom, SH110X_WHITE); + display.fillCircle(x_rec_head, 10, 3, SH110X_WHITE); + // Record sign + display.fillRect(0, 0, 13, 13, SH110X_WHITE); + display.fillRect(2, 2, 12, 12, SH110X_WHITE); + display.fillRect(1, 1, 11, 11, SH110X_BLACK); + display.fillCircle(6, 6, 3, SH110X_WHITE); + } + + // Draw Overdub Indicator and Recording Head + if (recording_state == REC_STATE_OVERDUBBING) { + // Draw Rec Head + int x_rec_head = int(activeLooper()->GetRecHead() * display.width()); + display.drawFastVLine(x_rec_head, 10, bottom, SH110X_WHITE); + display.fillCircle(x_rec_head, 10, 3, SH110X_WHITE); + + // Overdub sign (a "plus") + display.fillRect(0, 0, 13, 13, SH110X_WHITE); + display.fillRect(2, 2, 12, 12, SH110X_WHITE); + display.fillRect(1, 1, 11, 11, SH110X_BLACK); + display.drawLine(6, 2, 6, 10, SH110X_WHITE); + display.drawLine(2, 6, 10, 6, SH110X_WHITE); + } + + // Render potentiometer UIs in case a knob is changed + pot_1.renderUi(); + pot_2.renderUi(); + pot_3.renderUi(); + pot_4.renderUi(); + pot_5.renderUi(); + pot_6.renderUi(); + pot_7.renderUi(); + + // Display all the things done above + display.display(); + } + + // Activate recording and set the waveform cache to dirty + void activateRecording() { + if (recording_state != REC_STATE_RECORDING) { + activeLooper()->SetRecord(); + waveform_cache_dirty = true; + recording_state = REC_STATE_RECORDING; + } + } + + // Toggle recording + void toggleRecording() { + switch (recording_state) { + case REC_STATE_NOT_RECORDING: + activateRecording(); + break; + case REC_STATE_RECORDING: + stopRecording(); + break; + case REC_STATE_OVERDUBBING: + activateRecording(); + break; + } + } + + // Activates overdubbing + void activateOverdub() { + if (recording_state != REC_STATE_OVERDUBBING) { + waveform_cache_dirty = true; + recording_state = REC_STATE_OVERDUBBING; + activeLooper()->SetOverdub(); + } + } + + // Stop the recording + void stopRecording() { + if (recording_state != REC_STATE_NOT_RECORDING) { + recording_state = REC_STATE_NOT_RECORDING; + activeLooper()->SetStopWriting(); + } + } + + // Toggle overdub off and on + void toggleOverdub() { + switch (recording_state) { + case REC_STATE_NOT_RECORDING: + activateOverdub(); + break; + case REC_STATE_OVERDUBBING: + stopRecording(); + break; + case REC_STATE_RECORDING: + activateOverdub(); + break; + } + } + + // Reset the recording state (mark waveform cahce dirty) + void resetRecordingState() { + if (recording_state == REC_STATE_RECORDING || recording_state == REC_STATE_OVERDUBBING) { + waveform_cache_dirty = true; + } + } + + // Set the mode of the UI (and thus change the screen) + void setMode(UiMode mode) { + if (last_ui_mode == mode) { return; } + last_ui_mode = ui_mode; + ui_mode = mode; + switch (ui_mode) { + case UI_MODE_SPLASH: + break; + case UI_MODE_DEFAULT: + break; + case UI_MODE_REC_MENU: + this->button_grids[0].hideAllDescriptions(); + break; + case UI_MODE_PLAY_MENU: + this->button_grids[1].hideAllDescriptions(); + break; + case UI_MODE_TRIGGER_MENU: + this->button_grids[2].hideAllDescriptions(); + break; + case UI_MODE_FX_MENU: + this->button_grids[3].hideAllDescriptions(); + break; + case UI_MODE_BUFFER_MENU: + this->button_grids[4].hideAllDescriptions(); + break; + } + } + + + // Update the Ui variables (expected to run repeatedly) + void update() { + resetRecordingState(); + + switch (ui_mode) { + case UI_MODE_SPLASH: + break; + case UI_MODE_DEFAULT: + setupDefault(); + break; + case UI_MODE_REC_MENU: + setupRecMenu(); + break; + case UI_MODE_PLAY_MENU: + setupPlayMenu(); + break; + case UI_MODE_TRIGGER_MENU: + break; + case UI_MODE_FX_MENU: + setupFXMenu(); + break; + case UI_MODE_BUFFER_MENU: + setupBufferMenu(); + break; + } + } + + // Returns a pointer to the currently active looper + atoav::Looper * activeLooper() { + switch(active_buffer) { + case ACTIVE_BUFFER_A: { + atoav::Looper * ptr = &looper_a; + return ptr; + break; + } + case ACTIVE_BUFFER_B: { + atoav::Looper * ptr = &looper_b; + return ptr; + break; + } + case ACTIVE_BUFFER_C: { + atoav::Looper * ptr = &looper_c; + return ptr; + break; + } + case ACTIVE_BUFFER_D: { + atoav::Looper * ptr = &looper_d; + return ptr; + break; + } + case ACTIVE_BUFFER_E: { + atoav::Looper * ptr = &looper_e; + return ptr; + break; + } + } + // Unreachable, but makes the compiler shut up + atoav::Looper * ptr = &looper_a; + return ptr; + } + + // Returns a pointer to the currently active looper + atoav::Looper * previousLooper() { + switch(previous_buffer) { + case ACTIVE_BUFFER_A: { + atoav::Looper * ptr = &looper_a; + return ptr; + break; + } + case ACTIVE_BUFFER_B: { + atoav::Looper * ptr = &looper_b; + return ptr; + break; + } + case ACTIVE_BUFFER_C: { + atoav::Looper * ptr = &looper_c; + return ptr; + break; + } + case ACTIVE_BUFFER_D: { + atoav::Looper * ptr = &looper_d; + return ptr; + break; + } + case ACTIVE_BUFFER_E: { + atoav::Looper * ptr = &looper_e; + return ptr; + break; + } + } + // Unreachable, but makes the compiler shut up + atoav::Looper * ptr = &looper_a; + return ptr; + } + + // Set the Looper start/length to a given value + void setLoop(float start, float length) { + activeLooper()->SetLoop(start, length); + } + + private: + double last_render = 0.0; + uint16_t waveform_cache[128] = {0}; + bool waveform_cache_dirty = true; + RecordingState recording_state = REC_STATE_NOT_RECORDING; + UiMode last_ui_mode = UI_MODE_LAST; + bool rec_button_momentary = true; +}; + + + +#endif \ No newline at end of file