Skip to content
Snippets Groups Projects
Commit ce9ccfaa authored by David Huss's avatar David Huss :speech_balloon:
Browse files

Move code to daisyy-software repo

parent d6662048
No related branches found
No related tags found
No related merge requests found
# 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).
#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
#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
#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();
}
#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
#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
#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
#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
#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
#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
File deleted
#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
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment