Add per-channel CV Input mod configuration (#4)
Each channel can enable CV 1 or CV 2 as an input source for modulation, which can be applied to any of the user-editable parameters. When editing the parameter, cv mod is not applied in the UI so the user can easily see the base value for editing. When not editing, the UI will display the current cv modded value in the UI for the modded parameter. I had originally intended to provide configuration for attenuating and offsetting the cv input per channel, but that introduced a significant amount of memory needed to store several new ints per channel. I may return to add this feature later, but given it's something that can easily be done with other modules between the modulation source and Gravity cv input, I am deprioritizing this feature. Reviewed-on: https://git.pinkduck.xyz/adam/libGravity/pulls/4 Co-authored-by: Adam Wonak <adam.wonak@gmail.com> Co-committed-by: Adam Wonak <adam.wonak@gmail.com>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
docs
|
||||
.vscode
|
||||
.vscode
|
||||
.DS_Store
|
||||
@ -43,6 +43,8 @@ enum ParamsChannelPage {
|
||||
PARAM_CH_PROB,
|
||||
PARAM_CH_DUTY,
|
||||
PARAM_CH_OFFSET,
|
||||
PARAM_CH_CV_SRC,
|
||||
PARAM_CH_CV_DEST,
|
||||
PARAM_CH_LAST,
|
||||
};
|
||||
|
||||
@ -86,6 +88,13 @@ void loop() {
|
||||
// Process change in state of inputs and outputs.
|
||||
gravity.Process();
|
||||
|
||||
// Read CVs and call the update function for each channel.
|
||||
int cv1 = gravity.cv1.Read();
|
||||
int cv2 = gravity.cv2.Read();
|
||||
for (int i = 0; i < OUTPUT_COUNT; i++) {
|
||||
app.channel[i].applyCvMod(cv1, cv2);
|
||||
}
|
||||
|
||||
if (app.refresh_screen) {
|
||||
UpdateDisplay();
|
||||
}
|
||||
@ -96,8 +105,17 @@ void loop() {
|
||||
//
|
||||
|
||||
void HandleIntClockTick(uint32_t tick) {
|
||||
bool refresh = false;
|
||||
for (int i = 0; i < OUTPUT_COUNT; i++) {
|
||||
app.channel[i].processClockTick(tick, gravity.outputs[i]);
|
||||
|
||||
if (app.channel[i].isCvModActive()) {
|
||||
refresh = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!app.editing_param) {
|
||||
app.refresh_screen |= refresh;
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,7 +161,7 @@ void HandleRotate(Direction dir, int val) {
|
||||
if (app.selected_channel == 0) {
|
||||
editMainParameter(val);
|
||||
} else {
|
||||
editChannelParameter(dir, val);
|
||||
editChannelParameter(val);
|
||||
}
|
||||
}
|
||||
app.refresh_screen = true;
|
||||
@ -178,9 +196,9 @@ void editMainParameter(int val) {
|
||||
}
|
||||
}
|
||||
|
||||
void editChannelParameter(Direction dir, int val) {
|
||||
void editChannelParameter(int val) {
|
||||
auto& ch = GetSelectedChannel();
|
||||
switch (static_cast<ParamsChannelPage>(app.selected_param)) {
|
||||
switch (app.selected_param) {
|
||||
case PARAM_CH_MOD:
|
||||
ch.setClockMod(ch.getClockModIndex() + val);
|
||||
break;
|
||||
@ -193,6 +211,18 @@ void editChannelParameter(Direction dir, int val) {
|
||||
case PARAM_CH_OFFSET:
|
||||
ch.setOffset(ch.getOffset() + val);
|
||||
break;
|
||||
case PARAM_CH_CV_SRC: {
|
||||
int source = static_cast<int>(ch.getCvSource());
|
||||
updateSelection(source, val, CV_LAST);
|
||||
ch.setCvSource(static_cast<CvSource>(source));
|
||||
break;
|
||||
}
|
||||
case PARAM_CH_CV_DEST: {
|
||||
int dest = static_cast<int>(ch.getCvDestination());
|
||||
updateSelection(dest, val, CV_DEST_LAST);
|
||||
ch.setCvDestination(static_cast<CvDestination>(dest));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -303,9 +333,13 @@ void DisplayChannelPage() {
|
||||
char mainText[5];
|
||||
const char* subText;
|
||||
|
||||
// When editing a param, just show the base value. When not editing show
|
||||
// the value with cv mod.
|
||||
bool withCvMod = !app.editing_param;
|
||||
|
||||
switch (app.selected_param) {
|
||||
case 0: { // Clock Mod
|
||||
int mod_value = ch.getClockMod();
|
||||
case PARAM_CH_MOD: {
|
||||
int mod_value = ch.getClockMod(withCvMod);
|
||||
if (mod_value > 1) {
|
||||
sprintf(mainText, "/%d", mod_value);
|
||||
subText = "Divide";
|
||||
@ -315,25 +349,68 @@ void DisplayChannelPage() {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 1: // Probability
|
||||
sprintf(mainText, "%d%%", ch.getProbability());
|
||||
case PARAM_CH_PROB:
|
||||
sprintf(mainText, "%d%%", ch.getProbability(withCvMod));
|
||||
subText = "Hit Chance";
|
||||
break;
|
||||
case 2: // Duty Cycle
|
||||
sprintf(mainText, "%d%%", ch.getDutyCycle());
|
||||
case PARAM_CH_DUTY:
|
||||
sprintf(mainText, "%d%%", ch.getDutyCycle(withCvMod));
|
||||
subText = "Pulse Width";
|
||||
break;
|
||||
case 3: // Offset
|
||||
sprintf(mainText, "%d%%", ch.getOffset());
|
||||
case PARAM_CH_OFFSET:
|
||||
sprintf(mainText, "%d%%", ch.getOffset(withCvMod));
|
||||
subText = "Shift Hit";
|
||||
break;
|
||||
case PARAM_CH_CV_SRC: {
|
||||
switch (ch.getCvSource()) {
|
||||
case CV_NONE:
|
||||
sprintf(mainText, "SRC");
|
||||
subText = "None";
|
||||
break;
|
||||
case CV_1:
|
||||
sprintf(mainText, "SRC");
|
||||
subText = "CV 1";
|
||||
break;
|
||||
case CV_2:
|
||||
sprintf(mainText, "SRC");
|
||||
subText = "CV 2";
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PARAM_CH_CV_DEST: {
|
||||
switch (ch.getCvDestination()) {
|
||||
case CV_DEST_NONE:
|
||||
sprintf(mainText, "DEST");
|
||||
subText = "None";
|
||||
break;
|
||||
case CV_DEST_MOD:
|
||||
sprintf(mainText, "DEST");
|
||||
subText = "Clock Mod";
|
||||
break;
|
||||
case CV_DEST_PROB:
|
||||
sprintf(mainText, "DEST");
|
||||
subText = "Probability";
|
||||
break;
|
||||
case CV_DEST_DUTY:
|
||||
sprintf(mainText, "DEST");
|
||||
subText = "Duty Cycle";
|
||||
break;
|
||||
case CV_DEST_OFFSET:
|
||||
sprintf(mainText, "DEST");
|
||||
subText = "Offset";
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
drawCenteredText(mainText, MAIN_TEXT_Y, LARGE_FONT);
|
||||
drawCenteredText(subText, SUB_TEXT_Y, TEXT_FONT);
|
||||
|
||||
// Draw Channel Page menu items
|
||||
const char* menu_items[PARAM_CH_LAST] = {"Mod", "Probability", "Duty Cycle", "Offset"};
|
||||
const char* menu_items[PARAM_CH_LAST] = {
|
||||
"Mod", "Probability", "Duty", "Offset", "CV Source", "CV Dest"};
|
||||
drawMenuItems(menu_items, PARAM_CH_LAST);
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,23 @@
|
||||
#include <Arduino.h>
|
||||
#include <gravity.h>
|
||||
|
||||
// Enums for CV configuration
|
||||
enum CvSource {
|
||||
CV_NONE,
|
||||
CV_1,
|
||||
CV_2,
|
||||
CV_LAST,
|
||||
};
|
||||
|
||||
enum CvDestination {
|
||||
CV_DEST_NONE,
|
||||
CV_DEST_MOD,
|
||||
CV_DEST_PROB,
|
||||
CV_DEST_DUTY,
|
||||
CV_DEST_OFFSET,
|
||||
CV_DEST_LAST,
|
||||
};
|
||||
|
||||
static const int MOD_CHOICE_SIZE = 21;
|
||||
// Negative for multiply, positive for divide.
|
||||
static const int clock_mod[MOD_CHOICE_SIZE] = {-24, -12, -8, -6, -4, -3, -2, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 24, 32, 64, 128};
|
||||
@ -12,85 +29,116 @@ static const int clock_mod_pulses[MOD_CHOICE_SIZE] = {4, 8, 12, 16, 24, 32, 48,
|
||||
|
||||
class Channel {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Channel object with default values.
|
||||
*/
|
||||
Channel() {
|
||||
updatePulses();
|
||||
cvmod_clock_mod_index = base_clock_mod_index;
|
||||
cvmod_probability = base_probability;
|
||||
cvmod_duty_cycle = base_duty_cycle;
|
||||
cvmod_offset = base_offset;
|
||||
}
|
||||
|
||||
// Setters for channel properties
|
||||
// Setters (Set the BASE value)
|
||||
|
||||
void setClockMod(int index) {
|
||||
if (index >= 0 && index < MOD_CHOICE_SIZE) {
|
||||
clock_mod_index = index;
|
||||
updatePulses();
|
||||
}
|
||||
if (index >= 0 && index < MOD_CHOICE_SIZE) base_clock_mod_index = index;
|
||||
}
|
||||
void setProbability(int prob) { base_probability = constrain(prob, 0, 100); }
|
||||
void setDutyCycle(int duty) { base_duty_cycle = constrain(duty, 1, 99); }
|
||||
void setOffset(int off) { base_offset = constrain(off, 0, 100); }
|
||||
void setCvSource(CvSource source) { cv_source = source; }
|
||||
void setCvDestination(CvDestination dest) { cv_destination = dest; }
|
||||
|
||||
void setProbability(int prob) {
|
||||
probability = constrain(prob, 0, 100);
|
||||
}
|
||||
// Getters (Get the BASE value for editing or cv modded value for display)
|
||||
|
||||
void setDutyCycle(int duty) {
|
||||
duty_cycle = constrain(duty, 0, 99);
|
||||
updatePulses();
|
||||
}
|
||||
|
||||
void setOffset(int off) {
|
||||
offset = constrain(off, 0, 99);
|
||||
updatePulses();
|
||||
}
|
||||
|
||||
// Getters for channel properties
|
||||
|
||||
int getProbability() const { return probability; }
|
||||
int getDutyCycle() const { return duty_cycle; }
|
||||
int getOffset() const { return offset; }
|
||||
int getClockMod() const { return clock_mod[clock_mod_index]; }
|
||||
int getProbability(bool withCvMod = false) const { return withCvMod ? cvmod_probability : base_probability; }
|
||||
int getDutyCycle(bool withCvMod = false) const { return withCvMod ? cvmod_duty_cycle : base_duty_cycle; }
|
||||
int getOffset(bool withCvMod = false) const { return withCvMod ? cvmod_offset : base_offset; }
|
||||
int getClockMod(bool withCvMod = false) const { return clock_mod[getClockModIndex(withCvMod)]; }
|
||||
int getClockModIndex(bool withCvMod = false) const { return withCvMod ? cvmod_clock_mod_index : base_clock_mod_index; }
|
||||
uint32_t getDutyCyclePulses() const { return duty_cycle_pulses; }
|
||||
uint32_t getOffsetPulses() const { return offset_pulses; }
|
||||
CvSource getCvSource() { return cv_source; }
|
||||
CvDestination getCvDestination() { return cv_destination; }
|
||||
bool isCvModActive() const { return cv_source != CV_NONE && cv_destination != CV_DEST_NONE; }
|
||||
|
||||
/**
|
||||
* @brief Processes a clock tick and determines if the output should be high or low.
|
||||
* @param tick The current clock tick count.
|
||||
* @param output The output object (or a reference to its state) to be modified.
|
||||
* @param output The output object to be modified.
|
||||
*/
|
||||
void processClockTick(uint32_t tick, DigitalOutput& output) {
|
||||
const uint32_t mod_pulses = clock_mod_pulses[clock_mod_index];
|
||||
// Calculate output duty cycle state using cv modded values to determine pulse counts.
|
||||
const uint32_t mod_pulses = clock_mod_pulses[cvmod_clock_mod_index];
|
||||
const uint32_t duty_pulses = max((long)((mod_pulses * (100L - cvmod_duty_cycle)) / 100L), 1L);
|
||||
const uint32_t offset_pulses = (long)((mod_pulses * (100L - cvmod_offset)) / 100L);
|
||||
|
||||
const uint32_t current_tick_offset = tick + offset_pulses;
|
||||
|
||||
// Duty cycle high check
|
||||
if (current_tick_offset % mod_pulses == 0) {
|
||||
if (probability >= random(0, 100)) {
|
||||
if (cvmod_probability >= random(0, 100)) {
|
||||
output.High();
|
||||
}
|
||||
}
|
||||
|
||||
// Duty cycle low check
|
||||
const uint32_t duty_cycle_end_tick = tick + duty_cycle_pulses + offset_pulses;
|
||||
const uint32_t duty_cycle_end_tick = tick + duty_pulses + offset_pulses;
|
||||
if (duty_cycle_end_tick % mod_pulses == 0) {
|
||||
output.Low();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Recalculates pulse values based on current channel settings.
|
||||
* Should be called whenever mod, duty cycle, or offset changes.
|
||||
*/
|
||||
void updatePulses() {
|
||||
uint32_t mod_pulses = clock_mod_pulses[clock_mod_index];
|
||||
duty_cycle_pulses = max((long)((mod_pulses * (100L - duty_cycle)) / 100L), 1L);
|
||||
offset_pulses = (long)((mod_pulses * (100L - offset)) / 100L);
|
||||
void applyCvMod(int cv1_value, int cv2_value) {
|
||||
if (!isCvModActive()) {
|
||||
// If CV is off, ensure cv modded values match the base values.
|
||||
cvmod_clock_mod_index = base_clock_mod_index;
|
||||
cvmod_probability = base_probability;
|
||||
cvmod_duty_cycle = base_duty_cycle;
|
||||
cvmod_offset = base_offset;
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the CV value for current selected cv source.
|
||||
int value = (cv_source == CV_1) ? cv1_value : cv2_value;
|
||||
|
||||
// Calculate and store cv modded values using bipolar mapping.
|
||||
// Default to base value if not the current CV destination.
|
||||
|
||||
cvmod_clock_mod_index = (cv_destination == CV_DEST_MOD)
|
||||
? constrain(base_clock_mod_index + map(value, -512, 512, -10, 10), 0, MOD_CHOICE_SIZE - 1)
|
||||
: base_clock_mod_index;
|
||||
|
||||
cvmod_probability = (cv_destination == CV_DEST_PROB)
|
||||
? constrain(base_probability + map(value, -512, 512, -50, 50), 0, 100)
|
||||
: base_probability;
|
||||
|
||||
cvmod_duty_cycle = (cv_destination == CV_DEST_DUTY)
|
||||
? constrain(base_duty_cycle + map(value, -512, 512, -50, 50), 1, 99)
|
||||
: base_duty_cycle;
|
||||
|
||||
cvmod_offset = (cv_destination == CV_DEST_OFFSET)
|
||||
? constrain(base_offset + map(value, -512, 512, -50, 50), 0, 99)
|
||||
: base_offset;
|
||||
}
|
||||
|
||||
byte clock_mod_index = 7; // 1x clock mod
|
||||
byte probability = 100;
|
||||
byte duty_cycle = 50;
|
||||
byte offset = 0;
|
||||
private:
|
||||
// User-settable "base" values.
|
||||
byte base_clock_mod_index = 7;
|
||||
byte base_probability = 100;
|
||||
byte base_duty_cycle = 50;
|
||||
byte base_offset = 0;
|
||||
|
||||
// Base value with cv mod applied.
|
||||
byte cvmod_clock_mod_index;
|
||||
byte cvmod_probability;
|
||||
byte cvmod_duty_cycle;
|
||||
byte cvmod_offset;
|
||||
|
||||
int duty_cycle_pulses;
|
||||
int offset_pulses;
|
||||
|
||||
// CV configuration
|
||||
CvSource cv_source = CV_NONE;
|
||||
CvDestination cv_destination = CV_DEST_NONE;
|
||||
};
|
||||
|
||||
#endif // CHANNEL_H
|
||||
@ -19,7 +19,7 @@
|
||||
// Peripheral input pins
|
||||
#define ENCODER_PIN1 17 // A3
|
||||
#define ENCODER_PIN2 4
|
||||
#define ENCODER_SW_PIN 14
|
||||
#define ENCODER_SW_PIN 14 // A0
|
||||
|
||||
// Clock and CV Inputs
|
||||
#define EXT_PIN 2
|
||||
|
||||
Reference in New Issue
Block a user