Firmware Update

Changed mapping behaveiour of seqeuncer blocks.
This commit is contained in:
2026-03-08 22:09:26 +01:00
parent 2108552078
commit 04884bc243
12 changed files with 61393 additions and 177 deletions

View File

@@ -1,11 +1,21 @@
/*
* Example Code Three - Dual Channel Sequencer (COMPLETE)
* - Alle TODOs implementiert
* - VCO Gates, Recording LED, Metronome
* Analoger Audiosynthesizer mit digitaler Steuereinheit
* Firmware-Code für die digitale Einheit
* Autor: Erik Tóth
*/
#include "FIRMWARE_DEF.h"
#include "FIRMWARE.h"
// Calibration table for optimal note accurarcy
const uint16_t NOTE_MV[25] = {
64, 140, 216, 293, 369,
445, 521, 597, 673, 750,
826, 902, 978, 1054, 1131,
1207, 1283, 1359, 1435, 1511,
1588, 1664, 1740, 1816, 1892,
};
#define HLFSTEP(n) NOTE_MV[n]
byte pins_keyboard_row[N_KEYBOARD_ROW] = {PIN_K_R0, PIN_K_R1, PIN_K_R2, PIN_K_R3, PIN_K_R4};
byte pins_keyboard_col[N_KEYBOARD_COL] = {PIN_K_C0, PIN_K_C1, PIN_K_C2, PIN_K_C3, PIN_K_C4};
@@ -14,15 +24,16 @@ Keyboard keyboard(N_KEYBOARD_ROW, N_KEYBOARD_COL, pins_keyboard_row, pins_keyboa
Adafruit_MCP4728 MCP4728;
MCP4728_channel_t cvMap[N_CV_GATES] = {MCP4728_CHANNEL_A, MCP4728_CHANNEL_B};
uint16_t keyToVoltage[N_KEYBOARD_ROW*N_KEYBOARD_COL] = {
0*83, 1*83, 2*83, 3*83, 4*83,
5*83, 6*83, 7*83, 8*83, 9*83,
10*83, 11*83, 12*83, 13*83, 14*83,
15*83, 16*83, 17*83, 18*83, 19*83,
20*83, 21*83, 22*83, 23*83, 24*83
HLFSTEP(0), HLFSTEP(1), HLFSTEP(2), HLFSTEP(3), HLFSTEP(4),
HLFSTEP(5), HLFSTEP(6), HLFSTEP(7), HLFSTEP(8), HLFSTEP(9),
HLFSTEP(10), HLFSTEP(11), HLFSTEP(12), HLFSTEP(13), HLFSTEP(14),
HLFSTEP(15), HLFSTEP(16), HLFSTEP(17), HLFSTEP(18), HLFSTEP(19),
HLFSTEP(20), HLFSTEP(21), HLFSTEP(22), HLFSTEP(23), HLFSTEP(24)
};
CV cv(&MCP4728, &Wire, N_CV_GATES, cvMap, keyToVoltage, N_KEYBOARD_ROW, N_KEYBOARD_COL);
// SB1 -> VCO1 (CV-Channel 0), SB2 -> VCO2 (CV-Channel 1)
SequencerBlock sb1(30000, N_MAX_SEQ_STEPS);
SequencerBlock sb2(30000, N_MAX_SEQ_STEPS);
@@ -43,8 +54,12 @@ const unsigned long DEBOUNCE_DELAY = 50;
static bool seq1_loop_active = false;
static bool seq2_loop_active = false;
static uint16_t last_voltage_ch1 = 0xFFFF;
static uint16_t last_voltage_ch2 = 0xFFFF;
// Separate last-voltage tracking per sequencer
static uint16_t sb1_last_voltage_ch1 = 0xFFFF;
static uint16_t sb1_last_voltage_ch2 = 0xFFFF;
static uint16_t sb2_last_voltage_ch1 = 0xFFFF;
static uint16_t sb2_last_voltage_ch2 = 0xFFFF;
bool readButton(byte pin, ButtonState &state)
{
@@ -123,26 +138,24 @@ void initOutputs()
void handleSequencerButtons()
{
// ===== Sequencer 1 Record Button =====
if(readButton(PIN_SB_1_REC, btn_sb1_rec))
{
if(sb1.isRecording())
{
sb1.stopRecord();
Serial.printf("\n\r[SEQ1] Recording stopped. Steps: %i, Duration: %ims",
Serial.printf("\n\r[SEQ1->VCO1] Recording stopped. Steps: %i, Duration: %ims",
sb1.getStepCount(), sb1.getTotalDuration());
}
else
{
if(sb1.isPlaying()) sb1.stopPlay();
sb1.startRecord();
last_voltage_ch1 = 0xFFFF;
last_voltage_ch2 = 0xFFFF;
Serial.printf("\n\r[SEQ1] Recording started (2 channels)...");
sb1_last_voltage_ch1 = 0xFFFF;
sb1_last_voltage_ch2 = 0xFFFF;
Serial.printf("\n\r[SEQ1->VCO1] Recording started...");
}
}
// ===== Sequencer 1 Play Button =====
if(readButton(PIN_SB_1_PLAY, btn_sb1_play))
{
if(!sb1.isPlaying())
@@ -151,43 +164,41 @@ void handleSequencerButtons()
sb1.setLoop(false);
seq1_loop_active = false;
sb1.startPlay();
Serial.printf("\n\r[SEQ1] Playback started (single)\n\r\tSteps: %i, Duration: %ims",
Serial.printf("\n\r[SEQ1->VCO1] Playback started (single)\n\r\tSteps: %i, Duration: %ims",
sb1.getStepCount(), sb1.getTotalDuration());
}
else if(!seq1_loop_active)
{
sb1.setLoop(true);
seq1_loop_active = true;
Serial.printf("\n\r[SEQ1] Loop activated");
Serial.printf("\n\r[SEQ1->VCO1] Loop activated");
}
else
{
sb1.stopPlay();
seq1_loop_active = false;
Serial.printf("\n\r[SEQ1] Playback stopped");
Serial.printf("\n\r[SEQ1->VCO1] Playback stopped");
}
}
// ===== Sequencer 2 Record Button =====
if(readButton(PIN_SB_2_REC, btn_sb2_rec))
{
if(sb2.isRecording())
{
sb2.stopRecord();
Serial.printf("\n\r[SEQ2] Recording stopped. Steps: %i, Duration: %ims",
Serial.printf("\n\r[SEQ2->VCO2] Recording stopped. Steps: %i, Duration: %ims",
sb2.getStepCount(), sb2.getTotalDuration());
}
else
{
if(sb2.isPlaying()) sb2.stopPlay();
sb2.startRecord();
last_voltage_ch1 = 0xFFFF;
last_voltage_ch2 = 0xFFFF;
Serial.printf("\n\r[SEQ2] Recording started (2 channels)...");
sb2_last_voltage_ch1 = 0xFFFF;
sb2_last_voltage_ch2 = 0xFFFF;
Serial.printf("\n\r[SEQ2->VCO2] Recording started...");
}
}
// ===== Sequencer 2 Play Button =====
if(readButton(PIN_SB_2_PLAY, btn_sb2_play))
{
if(!sb2.isPlaying())
@@ -196,20 +207,20 @@ void handleSequencerButtons()
sb2.setLoop(false);
seq2_loop_active = false;
sb2.startPlay();
Serial.printf("\n\r[SEQ2] Playback started (single)\n\r\tSteps: %i, Duration: %ims",
Serial.printf("\n\r[SEQ2->VCO2] Playback started (single)\n\r\tSteps: %i, Duration: %ims",
sb2.getStepCount(), sb2.getTotalDuration());
}
else if(!seq2_loop_active)
{
sb2.setLoop(true);
seq2_loop_active = true;
Serial.printf("\n\r[SEQ2] Loop activated");
Serial.printf("\n\r[SEQ2->VCO2] Loop activated");
}
else
{
sb2.stopPlay();
seq2_loop_active = false;
Serial.printf("\n\r[SEQ2] Playback stopped");
Serial.printf("\n\r[SEQ2->VCO2] Playback stopped");
}
}
}
@@ -224,17 +235,14 @@ void updateMetronome()
{
unsigned long now = millis();
// BPM von Potentiometer lesen (alle 100ms)
static unsigned long last_bpm_read = 0;
if((now - last_bpm_read) > 100)
{
int adc_value = analogRead(PIN_BPM);
// Map ADC (0-4095) zu BPM (40-240)
current_bpm = map(adc_value, 0, 4095, 40, 240);
last_bpm_read = now;
}
// Metronome Button (Toggle)
if(readButton(PIN_B_METRONOME, btn_metronome))
{
metronome_enabled = !metronome_enabled;
@@ -243,55 +251,41 @@ void updateMetronome()
if(!metronome_enabled)
{
digitalWrite(PIN_L_METRONOME, HIGH); // Active-low: HIGH = OFF
digitalWrite(PIN_L_METRONOME, HIGH);
metronome_led_on = false;
}
}
if(!metronome_enabled) return;
// Berechne Beat-Intervall in ms
unsigned long beat_interval = 60000UL / current_bpm;
// Neue Beat?
if((now - last_beat_time) >= beat_interval)
{
digitalWrite(PIN_L_METRONOME, LOW); // Active-low: LOW = ON
digitalWrite(PIN_L_METRONOME, LOW);
metronome_led_on = true;
last_beat_time = now;
last_pulse_end_time = now + 50; // 50ms Pulse
last_pulse_end_time = now + 50;
}
// Pulse beenden?
if(metronome_led_on && (now >= last_pulse_end_time))
{
digitalWrite(PIN_L_METRONOME, HIGH); // Active-low: HIGH = OFF
digitalWrite(PIN_L_METRONOME, HIGH);
metronome_led_on = false;
}
}
void updateVCOGates(bool cv1_active, bool cv2_active)
{
// PIN_VCO1_EN: HIGH wenn CV1 aktiv (Key mapped to CV-Gate 1)
digitalWrite(PIN_VCO1_EN, cv1_active ? HIGH : LOW);
// PIN_VCO2_EN: HIGH wenn CV2 aktiv (Key mapped to CV-Gate 2)
digitalWrite(PIN_VCO2_EN, cv2_active ? HIGH : LOW);
}
void updateRecordingLED()
{
// PIN_REC: Active-low (LOW = LED ON)
bool any_recording = sb1.isRecording() || sb2.isRecording();
digitalWrite(PIN_REC, any_recording ? LOW : HIGH);
}
void setup()
{
Serial.begin(BAUDRATE);
delay(2000);
Serial.printf("\n\r=== COMPLETE VERSION with TODOs ===");
Serial.printf("\n\r=== DUAL SEQUENCER: SB1->VCO1 | SB2->VCO2 ===");
Serial.printf("\n\rSerial OK!");
keyboard.begin();
@@ -317,45 +311,43 @@ void setup()
sb1.setLoop(false);
sb2.setLoop(false);
Serial.printf("\n\r=== Dual-Channel Sequencer System Started ===");
Serial.printf("\n\rFeatures:");
Serial.printf("\n\r - VCO1/VCO2 Gate Outputs");
Serial.printf("\n\r - Recording LED Indicator");
Serial.printf("\n\r - BPM Metronome (40-240 BPM)");
Serial.printf("\n\r==============================================\n\r");
Serial.printf("\n\r=== System Started ===");
Serial.printf("\n\rMapping:");
Serial.printf("\n\r SB1 -> VCO1 (CV-Ch 0) | SB2 -> VCO2 (CV-Ch 1)");
Serial.printf("\n\rManual fallback:");
Serial.printf("\n\r SB1 playing, SB2 idle -> VCO2 manual (Queue[0])");
Serial.printf("\n\r SB2 playing, SB1 idle -> VCO1 manual (Queue[0])");
Serial.printf("\n\r Both idle -> VCO1=Queue[0], VCO2=Queue[1]");
Serial.printf("\n\r=====================================\n\r");
}
void loop()
{
// ===== DEBUG HEARTBEAT =====
// DEBUG HEARTBEAT
static unsigned long lastDebugPrint = 0;
static unsigned long loopCounter = 0;
loopCounter++;
if(millis() - lastDebugPrint > 5000)
{
Serial.printf("\n\r[HEARTBEAT] Loop: %lu | BPM: %d | Metro: %s",
loopCounter, current_bpm, metronome_enabled ? "ON" : "OFF");
Serial.printf("\n\r[DEBUG] SB1: Rec=%d, Play=%d, Steps=%d",
Serial.printf("\n\r[DEBUG] SB1->VCO1: Rec=%d, Play=%d, Steps=%d",
sb1.isRecording(), sb1.isPlaying(), sb1.getStepCount());
Serial.printf("\n\r[DEBUG] SB2: Rec=%d, Play=%d, Steps=%d",
Serial.printf("\n\r[DEBUG] SB2->VCO2: Rec=%d, Play=%d, Steps=%d",
sb2.isRecording(), sb2.isPlaying(), sb2.getStepCount());
lastDebugPrint = millis();
}
// ===== NON-BLOCKING TIMING =====
// NON-BLOCKING TIMING
static unsigned long lastLoopTime = 0;
unsigned long now = millis();
const unsigned long LOOP_INTERVAL = 10;
if((now - lastLoopTime) < LOOP_INTERVAL)
{
return;
}
if((now - lastLoopTime) < LOOP_INTERVAL) return;
lastLoopTime = now;
// ===== UPDATE FUNCTIONS =====
// UPDATE
keyboard.update();
handleSequencerButtons();
updateMetronome();
@@ -364,97 +356,150 @@ void loop()
sb1.update();
sb2.update();
// KEYBOARD INPUT
int n = keyboard.getQueueLength();
// Aktuelle Spannungen ermitteln
uint16_t voltage_ch1 = 0;
uint16_t voltage_ch2 = 0;
bool cv1_active = false;
bool cv2_active = false;
// Key 0 -> wird als manueller Eingang für den jeweils freien VCO genutzt
uint16_t manual_voltage_0 = 0;
uint16_t manual_voltage_1 = 0;
bool manual_active_0 = false;
bool manual_active_1 = false;
if(n > 0)
{
Key k1 = keyboard.getQueue(0);
if(!isNotKey(k1))
Key k = keyboard.getQueue(0);
if(!isNotKey(k))
{
Serial.printf("\n\r[DEBUG] K1: R%iC%i", k1.row, k1.col);
voltage_ch1 = keyToVoltage[k1.row * N_KEYBOARD_COL + k1.col];
cv1_active = true;
manual_voltage_0 = keyToVoltage[k.row * N_KEYBOARD_COL + k.col];
manual_active_0 = true;
}
}
if(n > 1)
{
Key k2 = keyboard.getQueue(1);
if(!isNotKey(k2))
Key k = keyboard.getQueue(1);
if(!isNotKey(k))
{
Serial.printf("\n\r[DEBUG] K2: R%iC%i", k2.row, k2.col);
voltage_ch2 = keyToVoltage[k2.row * N_KEYBOARD_COL + k2.col];
cv2_active = true;
manual_voltage_1 = keyToVoltage[k.row * N_KEYBOARD_COL + k.col];
manual_active_1 = true;
}
}
// ===== RECORDING =====
// SB1 nimmt immer ch1=manual_voltage_0 / ch2=manual_voltage_1 auf
// (SB1 ist für VCO1 zuständig, nutzt den vollen Keyboard-Input)
if(sb1.isRecording())
{
bool changed = (manual_voltage_0 != sb1_last_voltage_ch1) ||
(manual_voltage_1 != sb1_last_voltage_ch2);
if(changed)
{
sb1.addStep(manual_voltage_0, manual_voltage_1);
sb1_last_voltage_ch1 = manual_voltage_0;
sb1_last_voltage_ch2 = manual_voltage_1;
}
}
// Recording
bool voltageChanged = (voltage_ch1 != last_voltage_ch1) || (voltage_ch2 != last_voltage_ch2);
if(sb1.isRecording() && voltageChanged)
// SB2 nimmt ebenfalls den vollen Keyboard-Input auf
if(sb2.isRecording())
{
sb1.addStep(voltage_ch1, voltage_ch2);
last_voltage_ch1 = voltage_ch1;
last_voltage_ch2 = voltage_ch2;
bool changed = (manual_voltage_0 != sb2_last_voltage_ch1) ||
(manual_voltage_1 != sb2_last_voltage_ch2);
if(changed)
{
sb2.addStep(manual_voltage_0, manual_voltage_1);
sb2_last_voltage_ch1 = manual_voltage_0;
sb2_last_voltage_ch2 = manual_voltage_1;
}
}
if(sb2.isRecording() && voltageChanged)
// ===== CV OUTPUT & VCO GATES =====
//
// SB1 state | SB2 state | VCO1 (ch 0) | VCO2 (ch 1)
// ------------|-------------|---------------------|----------------------
// playing | playing | SB1 seq voltage | SB2 seq voltage
// playing | recording | SB1 seq voltage | live manual Queue[0]
// playing | idle | SB1 seq voltage | live manual Queue[0]
// idle | playing | live manual Queue[0]| SB2 seq voltage
// idle | recording | live manual Queue[0]| live manual Queue[0]
// idle | idle | live manual Queue[0]| live manual Queue[1]
bool sb1_playing = sb1.isPlaying();
bool sb1_recording = sb1.isRecording();
bool sb2_playing = sb2.isPlaying();
bool sb2_recording = sb2.isRecording();
uint16_t out_vco1 = 0;
uint16_t out_vco2 = 0;
bool gate_vco1 = false;
bool gate_vco2 = false;
// VCO1
if(sb1_playing)
{
sb2.addStep(voltage_ch1, voltage_ch2);
last_voltage_ch1 = voltage_ch1;
last_voltage_ch2 = voltage_ch2;
// SB1 Sequenz läuft -> Sequenz-Ausgabe
out_vco1 = sb1.getCurrentVoltageCh1();
gate_vco1 = sb1.isCurrentStepActive();
}
// CV-Ausgabe & VCO Gates
if(sb1.isPlaying())
else if(sb1_recording)
{
uint16_t seq_v1 = sb1.getCurrentVoltageCh1();
uint16_t seq_v2 = sb1.getCurrentVoltageCh2();
cv.setVoltage(0, seq_v1);
cv.setVoltage(1, seq_v2);
// KORREKT: Nutze isCurrentStepActive() statt Spannung > 0
// Da 0V eine gültige Note sein kann!
bool gate_active = sb1.isCurrentStepActive();
updateVCOGates(gate_active, gate_active);
}
else if(sb2.isPlaying())
{
uint16_t seq_v1 = sb2.getCurrentVoltageCh1();
uint16_t seq_v2 = sb2.getCurrentVoltageCh2();
cv.setVoltage(0, seq_v1);
cv.setVoltage(1, seq_v2);
bool gate_active = sb2.isCurrentStepActive();
updateVCOGates(gate_active, gate_active);
// SB1 nimmt auf -> Live-Ausgabe damit man hört was man spielt
out_vco1 = manual_voltage_0;
gate_vco1 = manual_active_0;
}
else
{
// Live-Modus: cv1_active/cv2_active basieren auf tatsächlich gedrückten Tasten
cv.setVoltage(0, voltage_ch1);
cv.setVoltage(1, voltage_ch2);
updateVCOGates(cv1_active, cv2_active);
// SB1 idle -> manuell
out_vco1 = manual_voltage_0;
gate_vco1 = manual_active_0;
}
// VCO2
if(sb2_playing)
{
// SB2 Sequenz läuft -> Sequenz-Ausgabe
out_vco2 = sb2.getCurrentVoltageCh1();
gate_vco2 = sb2.isCurrentStepActive();
}
else if(sb2_recording)
{
// SB2 nimmt auf -> Live-Ausgabe damit man hört was man spielt
out_vco2 = manual_voltage_0;
gate_vco2 = manual_active_0;
gate_vco1 = false;
}
else if(sb1_playing)
{
// SB1 läuft, SB2 idle -> VCO2 manuell mit Queue[0]
out_vco2 = manual_voltage_0;
gate_vco2 = manual_active_0;
}
else
{
// Beide idle -> VCO2 bekommt Queue[1]
out_vco2 = manual_voltage_1;
gate_vco2 = manual_active_1;
}
// Time-Limit Check
cv.setVoltage(0, out_vco1); // CH_A -> VCO1
cv.setVoltage(1, out_vco2); // CH_B -> VCO2
digitalWrite(PIN_VCO1_EN, gate_vco1 ? HIGH : LOW);
digitalWrite(PIN_VCO2_EN, gate_vco2 ? HIGH : LOW);
// TIME-LIMIT CHECK
if(sb1.isRecording() && sb1.timeLimitReached())
{
sb1.stopRecord();
Serial.printf("\n\r[SEQ1] Time limit reached! Recording stopped.");
Serial.printf("\n\r[SEQ1] Final: Steps: %i, Duration: %ims",
Serial.printf("\n\r[SEQ1->VCO1] Time limit reached! Recording stopped.");
Serial.printf("\n\r[SEQ1->VCO1] Final: Steps: %i, Duration: %ims",
sb1.getStepCount(), sb1.getTotalDuration());
}
if(sb2.isRecording() && sb2.timeLimitReached())
{
sb2.stopRecord();
Serial.printf("\n\r[SEQ2] Time limit reached! Recording stopped.");
Serial.printf("\n\r[SEQ2] Final: Steps: %i, Duration: %ims",
Serial.printf("\n\r[SEQ2->VCO2] Time limit reached! Recording stopped.");
Serial.printf("\n\r[SEQ2->VCO2] Final: Steps: %i, Duration: %ims",
sb2.getStepCount(), sb2.getTotalDuration());
}
}