2 Commits

10 changed files with 367 additions and 140 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,7 @@
.pio
.pio/libdeps
.pio/build/project*
.pio/build/esp32-s3-devkitm-1/*
!.pio/build/esp32-s3-devkitm-1/firmware.bin
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json

View File

@@ -3,7 +3,8 @@
@author: Erik Tóth
@contact: etoth@tsn.at
@date: 2025-10-26
@brief: Header for FIRMWARE.cpp
@updated: 2025-12-06
@brief: Header for FIRMWARE.cpp (FIXED VERSION)
*/
#include <Arduino.h>
#include <Wire.h>
@@ -29,6 +30,7 @@ struct DualVoltageDurationPair
uint16_t voltage_ch1;
uint16_t voltage_ch2;
uint16_t duration;
bool active; // NEU: true wenn Step aktive Noten hat, false für Pausen
};
const Key NOT_A_KEY = {-1, -1};
@@ -116,6 +118,7 @@ class SequencerBlock
uint16_t getStepCount();
uint16_t getCurrentVoltageCh1();
uint16_t getCurrentVoltageCh2();
bool isCurrentStepActive(); // NEU: Prüft ob aktueller Step aktive Noten hat
uint16_t getTotalDuration();
private:
@@ -123,7 +126,7 @@ class SequencerBlock
* @brief Memory limiting
* @return (uint16_t) 1024
* @attention Increasing the value might lead to an overflow
* @note sizeOf(DualVoltageDurationPair) = 6 Byte ==> 6 Byte * 1024 = 6144 Byte
* @note sizeOf(DualVoltageDurationPair) = 8 Byte ==> 8 Byte * 1024 = 8192 Byte
*/
const static uint16_t _MAX_SEQUENCE_STEPS = 1024;
@@ -139,6 +142,7 @@ class SequencerBlock
unsigned long _lastStepTime;
unsigned long _playStartTime;
unsigned long _stepStartTime;
unsigned long _lastAddStepTime; // NEU: Rate-Limiting
// Status flags
bool _isRecording;

View File

@@ -3,6 +3,7 @@
@author: Erik Tóth
@contact: etoth@tsn.at
@date: 2025-10-26
@updated: 2025-12-06
@brief: Header for constant definitions
*/
@@ -13,35 +14,36 @@
// CONSTANTS DEFINITONS
#define N_KEYBOARD_ROW 4 // for PROD. change to 5
#define N_KEYBOARD_COL 3 // for PROD. change to 5
#define N_CV_GATES 2
#define N_SB 2
#define N_CV_GATES 2 // PROD. OK
#define N_SB 2 // PROD. OK
#define BAUDRATE 115200
#define N_MAX_SEQ_STEPS 512
// PIN DEFENTITIONS
// I2C PINS
#define PIN_SDA 15
#define PIN_SCL 16
#define PIN_SDA 15 // PROD. pin OK
#define PIN_SCL 16 // PROD. pin OK
// KEYBOARD PINS
#define PIN_K_R0 7
#define PIN_K_R1 8
#define PIN_K_R2 9
#define PIN_K_R3 10
#define PIN_K_R4 11 // DEV. not in use
#define PIN_K_C0 1
#define PIN_K_C1 2
#define PIN_K_C2 4
#define PIN_K_C3 5 // DEV. not in use
#define PIN_K_C4 6 // DEV. not in use
#define PIN_K_R0 7 // PROD. pin OK
#define PIN_K_R1 8 // PROD. pin OK
#define PIN_K_R2 9 // PROD. pin OK
#define PIN_K_R3 10 // PROD. pin OK
#define PIN_K_R4 11 // DEV. not in use - PROD. pin OK
#define PIN_K_C0 1 // PROD. pin OK
#define PIN_K_C1 2 // PROD. pin OK
#define PIN_K_C2 4 // PROD. pin OK
#define PIN_K_C3 5 // DEV. not in use - PROD. pin OK
#define PIN_K_C4 6 // DEV. not in use - PROD. pin OK
// SEQUENCER BUTTON PINS
#define PIN_SB_1_REC 37 // for PROD. change to 33 / not available on dev board
#define PIN_SB_1_PLAY 38 // for PROD. change to 34 / not available on dev board
#define PIN_SB_2_REC 35
#define PIN_SB_2_PLAY 36
#define PIN_SB_1_REC 38 // for PROD. change to 33 / not available on dev board
#define PIN_SB_1_PLAY 37 // for PROD. change to 34 / not available on dev board
#define PIN_SB_2_REC 35 // 35
#define PIN_SB_2_PLAY 36 // 36
// MISC/INFO PINS
#define PIN_ACTIVE -1 // TODO: if any key is played return HIGH
#define PIN_REC -1 // TODO: if any sb is recording return HIGH
#define PIN_BPM -1 // TODO: get bpm through potentiometer analog value
#define PIN_B_METRONOME -1 // TODO: button activates/deactivates bpm led output
#define PIN_L_METRONOME -1 // TODO: led blinks according to bpm value
#define PIN_VCO1_EN 41 // PROD. pin 37 TODO: if there is an active key mapped to CV-Gate 1 --> HIGH
#define PIN_VCO2_EN 40 // PROD. pin 38 TODO: if there is an active key mapped to CV-Gate 2 --> HIGH
#define PIN_REC 39 // PROD. pin 39 TODO: if any sb is recording LED on (active-low)
#define PIN_BPM 12 // PROD. pin 12 TODO: get bpm through potentiometer analog value -> ADC-Pin
#define PIN_B_METRONOME 13 // PROD. pin 13 TODO: button activates/deactivates bpm led output (pull-up)
#define PIN_L_METRONOME 14 // PROD. pin 14 TODO: led blinks according to bpm value (active-low)
#endif

View File

@@ -3,7 +3,8 @@
@author: Erik Tóth
@contact: etoth@tsn.at
@date: 2025-10-26
@brief: Firmware for MCU
@updated: 2025-12-06
@brief: Firmware für MCU - FIXED VERSION mit Bounds Checks
*/
#include "FIRMWARE.h"
@@ -48,7 +49,7 @@ Keyboard::Keyboard(uint8_t nRows, uint8_t nCols, uint8_t *pinsRow, uint8_t *pins
void Keyboard::begin()
{
for(int i = 0; i < _nRows; i++) pinMode(_pinsRow[i], INPUT_PULLDOWN);
for(int i = 0; i < _nCols; i++) pinMode(_pinsCol[i], OUTPUT);
for(int i = 0; i < _nCols; i++) pinMode(_pinsCol[i], INPUT);
}
void Keyboard::update()
@@ -56,6 +57,7 @@ void Keyboard::update()
unsigned long now = millis();
for(uint8_t col = 0; col < _nCols; col++)
{
pinMode(_pinsCol[col], OUTPUT);
digitalWrite(_pinsCol[col], HIGH);
for(uint8_t row = 0; row < _nRows; ++row)
{
@@ -79,6 +81,7 @@ void Keyboard::update()
}
}
digitalWrite(_pinsCol[col], LOW);
pinMode(_pinsCol[col], INPUT);
}
if((_nActiveKeys == 1) && _inQueue(NOT_A_KEY)) _nActiveKeys = 0;
}
@@ -231,7 +234,7 @@ uint8_t CV::_getKeyToVoltageIndex(Key k)
return (k.row*_col + k.col);
}
// ==================== SequencerBlock ====================
// ==================== SequencerBlock (FIXED) ====================
/*!
* @param maxDurationMS maximum loop duration of recording in milliseconds
@@ -252,6 +255,7 @@ SequencerBlock::SequencerBlock(uint16_t maxDurationMS, uint16_t maxStepCount)
_lastStepTime = 0;
_playStartTime = 0;
_stepStartTime = 0;
_lastAddStepTime = 0; // NEU: Rate-Limiting
}
void SequencerBlock::startRecord()
@@ -262,7 +266,8 @@ void SequencerBlock::startRecord()
_isRecording = true;
_recordStartTime = millis();
_lastStepTime = _recordStartTime;
_lastVoltageCh1 = 0xFFFF; // Ungültiger Wert zum Triggern des ersten Steps
_lastAddStepTime = _recordStartTime; // NEU
_lastVoltageCh1 = 0xFFFF;
_lastVoltageCh2 = 0xFFFF;
}
@@ -276,32 +281,59 @@ void SequencerBlock::stopRecord()
void SequencerBlock::addStep(uint16_t voltage_ch1, uint16_t voltage_ch2)
{
// KRITISCHE SICHERHEITSPRÜFUNGEN ZUERST
if(!_isRecording) return;
// Prüfe ob wir überhaupt noch Platz haben (mit Sicherheitsabstand!)
if(_stepCount >= _MAX_SEQUENCE_STEPS - 1)
{
Serial.println("\n\r[ERROR] Step limit reached! Stopping recording.");
stopRecord();
return;
}
if(timeLimitReached())
{
Serial.println("\n\r[WARNING] Time limit reached! Stopping recording.");
stopRecord();
return;
}
unsigned long now = millis();
// NEU: Rate-Limiting - ignoriere zu häufige Aufrufe
if((unsigned long)(now - _lastAddStepTime) < 5)
{
return;
}
_lastAddStepTime = now;
// Hat sich die Spannung geändert?
bool voltageChanged = (voltage_ch1 != _lastVoltageCh1) || (voltage_ch2 != _lastVoltageCh2);
if(voltageChanged)
{
// WICHTIG: Prüfe nochmal ob wir Platz haben BEVOR wir schreiben!
if(_stepCount >= _MAX_SEQUENCE_STEPS - 1)
{
Serial.println("\n\r[ERROR] Array full! Stopping recording.");
stopRecord();
return;
}
// Vorherigen Step abschließen (wenn vorhanden)
if(_stepCount > 0)
if(_stepCount > 0 && _stepCount <= _MAX_SEQUENCE_STEPS)
{
_finishCurrentStep();
}
// Neuen Step beginnen
if(_canAddStep())
// Neuen Step beginnen - mit Bounds Check!
if(_stepCount < _MAX_SEQUENCE_STEPS)
{
_sequence[_stepCount].voltage_ch1 = voltage_ch1;
_sequence[_stepCount].voltage_ch2 = voltage_ch2;
_sequence[_stepCount].duration = 0;
_sequence[_stepCount].active = (voltage_ch1 > 0 || voltage_ch2 > 0); // NEU: Prüfe ob Note aktiv
_stepCount++;
_lastStepTime = now;
@@ -312,7 +344,8 @@ void SequencerBlock::addStep(uint16_t voltage_ch1, uint16_t voltage_ch2)
else
{
// Gleiche Spannung - Duration des aktuellen Steps aktualisieren
if(_stepCount > 0)
// WICHTIG: Bounds Check!
if(_stepCount > 0 && _stepCount <= _MAX_SEQUENCE_STEPS)
{
_sequence[_stepCount - 1].duration = now - _lastStepTime;
}
@@ -345,9 +378,37 @@ void SequencerBlock::update()
{
if(!_isPlaying || _stepCount == 0) return;
// WICHTIG: Bounds Check BEVOR wir auf Array zugreifen!
if(_currentStep >= _stepCount || _currentStep >= _MAX_SEQUENCE_STEPS)
{
Serial.println("\n\r[ERROR] Invalid step index in update()!");
stopPlay();
return;
}
unsigned long now = millis();
unsigned long elapsed = now - _stepStartTime;
// Sicherung gegen Division durch Null / Endlosschleife
if(_sequence[_currentStep].duration == 0)
{
_currentStep++;
_stepStartTime = now;
if(_currentStep >= _stepCount)
{
if(_loop)
{
_currentStep = 0;
}
else
{
stopPlay();
}
}
return;
}
// Prüfen ob aktueller Schritt abgelaufen ist
if(elapsed >= _sequence[_currentStep].duration)
{
@@ -386,11 +447,13 @@ void SequencerBlock::clear()
_lastVoltageCh1 = 0;
_lastVoltageCh2 = 0;
for(uint8_t i = 0; i < _MAX_SEQUENCE_STEPS; i++)
// Optional: Array löschen (kann je nach Use-Case weggelassen werden)
for(uint16_t i = 0; i < _MAX_SEQUENCE_STEPS; i++)
{
_sequence[i].voltage_ch1 = 0;
_sequence[i].voltage_ch2 = 0;
_sequence[i].duration = 0;
_sequence[i].active = false;
}
}
@@ -411,7 +474,7 @@ bool SequencerBlock::timeLimitReached()
bool SequencerBlock::stepLimitReached()
{
return (_stepCount >= _maxStepCount);
return (_stepCount >= _maxStepCount) || (_stepCount >= _MAX_SEQUENCE_STEPS);
}
uint16_t SequencerBlock::getStepCount()
@@ -422,7 +485,7 @@ uint16_t SequencerBlock::getStepCount()
uint16_t SequencerBlock::getCurrentVoltageCh1()
{
if(!_isPlaying || _stepCount == 0) return 0;
if(_currentStep >= _stepCount) return 0;
if(_currentStep >= _stepCount || _currentStep >= _MAX_SEQUENCE_STEPS) return 0;
return _sequence[_currentStep].voltage_ch1;
}
@@ -430,24 +493,33 @@ uint16_t SequencerBlock::getCurrentVoltageCh1()
uint16_t SequencerBlock::getCurrentVoltageCh2()
{
if(!_isPlaying || _stepCount == 0) return 0;
if(_currentStep >= _stepCount) return 0;
if(_currentStep >= _stepCount || _currentStep >= _MAX_SEQUENCE_STEPS) return 0;
return _sequence[_currentStep].voltage_ch2;
}
uint16_t SequencerBlock::getTotalDuration()
{
uint16_t total = 0;
for(uint8_t i = 0; i < _stepCount; i++)
uint32_t total = 0; // uint32 um Overflow zu vermeiden
for(uint16_t i = 0; i < _stepCount && i < _MAX_SEQUENCE_STEPS; i++)
{
total += _sequence[i].duration;
}
return total;
return (total > 65535) ? 65535 : (uint16_t)total; // Clamp auf uint16
}
bool SequencerBlock::isCurrentStepActive()
{
if(!_isPlaying || _stepCount == 0) return false;
if(_currentStep >= _stepCount || _currentStep >= _MAX_SEQUENCE_STEPS) return false;
return _sequence[_currentStep].active;
}
void SequencerBlock::_finishCurrentStep()
{
if(_stepCount == 0) return;
if(_stepCount > _MAX_SEQUENCE_STEPS) return; // Sicherheitsprüfung
unsigned long now = millis();
uint16_t duration = now - _lastStepTime;

View File

@@ -1,8 +1,7 @@
/*
* Example Code Three - Dual Channel Sequencer
* TODO:
- add predefined sequence of voltage (e.g. for usage as startup sound)
- implement INFO and MISC pins form file FIRMWARE_DEF.h
* Example Code Three - Dual Channel Sequencer (COMPLETE)
* - Alle TODOs implementiert
* - VCO Gates, Recording LED, Metronome
*/
#include "FIRMWARE_DEF.h"
#include "FIRMWARE.h"
@@ -14,16 +13,15 @@ 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] = { /* 83mV = 1/12V */
1*83, 5*83, 9*83, /* ROW 1: B D Fis */
2*83, 6*83, 10*83, /* ROW 2: H Dis G */
3*83, 7*83, 11*83, /* ROW 3: C E Gis */
4*83, 8*83, 12*83 /* ROW 4: Cis F A' */
uint16_t keyToVoltage[N_KEYBOARD_ROW*N_KEYBOARD_COL] = {
1*83, 5*83, 9*83,
2*83, 6*83, 10*83,
3*83, 7*83, 11*83,
4*83, 8*83, 12*83
};
CV cv(&MCP4728, &Wire, N_CV_GATES, cvMap, keyToVoltage, N_KEYBOARD_ROW, N_KEYBOARD_COL);
// Sequencer 30s max, 512 max Steps
SequencerBlock sb1(30000, N_MAX_SEQ_STEPS);
SequencerBlock sb2(30000, N_MAX_SEQ_STEPS);
@@ -38,12 +36,18 @@ ButtonState btn_sb1_rec;
ButtonState btn_sb1_play;
ButtonState btn_sb2_rec;
ButtonState btn_sb2_play;
ButtonState btn_metronome;
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;
bool readButton(byte pin, ButtonState &state)
{
bool reading = digitalRead(pin) == HIGH;
bool reading = digitalRead(pin) == LOW;
bool buttonPressed = false;
if(reading != state.last)
@@ -69,10 +73,11 @@ bool readButton(byte pin, ButtonState &state)
void initButtons()
{
pinMode(PIN_SB_1_REC, INPUT_PULLDOWN);
pinMode(PIN_SB_1_PLAY, INPUT_PULLDOWN);
pinMode(PIN_SB_2_REC, INPUT_PULLDOWN);
pinMode(PIN_SB_2_PLAY, INPUT_PULLDOWN);
pinMode(PIN_SB_1_REC, INPUT_PULLUP);
pinMode(PIN_SB_1_PLAY, INPUT_PULLUP);
pinMode(PIN_SB_2_REC, INPUT_PULLUP);
pinMode(PIN_SB_2_PLAY, INPUT_PULLUP);
pinMode(PIN_B_METRONOME, INPUT_PULLUP);
btn_sb1_rec.current = false;
btn_sb1_rec.last = false;
@@ -89,11 +94,35 @@ void initButtons()
btn_sb2_play.current = false;
btn_sb2_play.last = false;
btn_sb2_play.lastDebounceTime = 0;
btn_metronome.current = false;
btn_metronome.last = false;
btn_metronome.lastDebounceTime = 0;
}
void initOutputs()
{
// VCO Gates
pinMode(PIN_VCO1_EN, OUTPUT);
pinMode(PIN_VCO2_EN, OUTPUT);
digitalWrite(PIN_VCO1_EN, LOW);
digitalWrite(PIN_VCO2_EN, LOW);
// Recording LED (active-low)
pinMode(PIN_REC, OUTPUT);
digitalWrite(PIN_REC, HIGH); // OFF
// Metronome LED (active-low)
pinMode(PIN_L_METRONOME, OUTPUT);
digitalWrite(PIN_L_METRONOME, HIGH); // OFF
// BPM Potentiometer
pinMode(PIN_BPM, INPUT);
}
void handleSequencerButtons()
{
// Sequencer 1 Record Button
// ===== Sequencer 1 Record Button =====
if(readButton(PIN_SB_1_REC, btn_sb1_rec))
{
if(sb1.isRecording())
@@ -106,58 +135,39 @@ void handleSequencerButtons()
{
if(sb1.isPlaying()) sb1.stopPlay();
sb1.startRecord();
last_voltage_ch1 = 0xFFFF;
last_voltage_ch2 = 0xFFFF;
Serial.printf("\n\r[SEQ1] Recording started (2 channels)...");
}
}
// Sequencer 1 Play Button - 3 Modi: Play / Loop / Stop
// ===== Sequencer 1 Play Button =====
if(readButton(PIN_SB_1_PLAY, btn_sb1_play))
{
if(!sb1.isPlaying())
{
// Nicht am Spielen -> Starte Playback (ohne Loop)
if(sb1.isRecording()) sb1.stopRecord();
sb1.setLoop(false);
seq1_loop_active = false;
sb1.startPlay();
Serial.printf("\n\r[SEQ1] Playback started (single)\n\r\tSteps: %i, Duartion: %ims", sb1.getStepCount(), sb1.getTotalDuration());
Serial.printf("\n\r[SEQ1] Playback started (single)\n\r\tSteps: %i, Duration: %ims",
sb1.getStepCount(), sb1.getTotalDuration());
}
else
else if(!seq1_loop_active)
{
// Am Spielen -> Prüfe Loop-Status
if(!sb1.isPlaying()) // Falls schon gestoppt
{
// Starte neu
sb1.setLoop(false);
sb1.startPlay();
Serial.printf("\n\r[SEQ1] Playback started (single)");
}
else
{
// Ist am Spielen - ermittle ob Loop aktiv ist
// Wir testen das indirekt: Wenn ein Sequencer am Ende angekommen ist
// und noch spielt, dann muss Loop aktiv sein
// Alternative: Wir tracken den Loop-Status selbst
static bool seq1_loop_active = false;
if(!seq1_loop_active)
{
// 2. Klick: Loop aktivieren
sb1.setLoop(true);
seq1_loop_active = true;
Serial.printf("\n\r[SEQ1] Loop activated");
}
else
{
// 3. Klick: Stop
sb1.stopPlay();
seq1_loop_active = false;
Serial.printf("\n\r[SEQ1] Playback stopped");
}
}
}
}
// Sequencer 2 Record Button
// ===== Sequencer 2 Record Button =====
if(readButton(PIN_SB_2_REC, btn_sb2_rec))
{
if(sb2.isRecording())
@@ -170,80 +180,196 @@ void handleSequencerButtons()
{
if(sb2.isPlaying()) sb2.stopPlay();
sb2.startRecord();
last_voltage_ch1 = 0xFFFF;
last_voltage_ch2 = 0xFFFF;
Serial.printf("\n\r[SEQ2] Recording started (2 channels)...");
}
}
// Sequencer 2 Play Button - 3 Modi: Play / Loop / Stop
// ===== Sequencer 2 Play Button =====
if(readButton(PIN_SB_2_PLAY, btn_sb2_play))
{
static bool seq2_loop_active = false;
if(!sb2.isPlaying())
{
// Nicht am Spielen -> Starte Playback (ohne Loop)
if(sb2.isRecording()) sb2.stopRecord();
sb2.setLoop(false);
seq2_loop_active = false;
sb2.startPlay();
Serial.printf("\n\r[SEQ2] Playback started (single)");
Serial.printf("\n\r[SEQ2] Playback started (single)\n\r\tSteps: %i, Duration: %ims",
sb2.getStepCount(), sb2.getTotalDuration());
}
else
else if(!seq2_loop_active)
{
if(!seq2_loop_active)
{
// 2. Klick: Loop aktivieren
sb2.setLoop(true);
seq2_loop_active = true;
Serial.printf("\n\r[SEQ2] Loop activated");
}
else
{
// 3. Klick: Stop
sb2.stopPlay();
seq2_loop_active = false;
Serial.printf("\n\r[SEQ2] Playback stopped");
}
}
}
static bool metronome_enabled = false;
static uint16_t current_bpm = 120;
static unsigned long last_beat_time = 0;
static unsigned long last_pulse_end_time = 0;
static bool metronome_led_on = false;
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;
Serial.printf("\n\r[METRONOME] %s (BPM: %d)",
metronome_enabled ? "ON" : "OFF", current_bpm);
if(!metronome_enabled)
{
digitalWrite(PIN_L_METRONOME, HIGH); // Active-low: HIGH = OFF
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
metronome_led_on = true;
last_beat_time = now;
last_pulse_end_time = now + 50; // 50ms Pulse
}
// Pulse beenden?
if(metronome_led_on && (now >= last_pulse_end_time))
{
digitalWrite(PIN_L_METRONOME, HIGH); // Active-low: HIGH = OFF
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\rSerial OK!");
keyboard.begin();
cv.begin(PIN_SDA, PIN_SCL);
unsigned long timeout = millis() + 5000;
while(!cv.begin(PIN_SDA, PIN_SCL))
{
Serial.printf("\n\r[ERROR] CV initialization failed. Retrying...");
delay(500);
if(millis() > timeout)
{
Serial.printf("\n\r[FATAL] CV initialization timeout! Check I2C connection.");
break;
}
}
Serial.printf("\n\r[OK] CV initialized");
initButtons();
initOutputs();
sb1.setLoop(false);
sb2.setLoop(false);
Serial.printf("\n\r=== Dual-Channel Sequencer System Started ===");
Serial.printf("\n\rControls:");
Serial.printf("\n\r PIN_SB_1_REC: SEQ1 Record Start/Stop (CH1+CH2)");
Serial.printf("\n\r PIN_SB_1_PLAY: SEQ1 Play Mode Toggle:");
Serial.printf("\n\r 1st click: Play once");
Serial.printf("\n\r 2nd click: Loop mode");
Serial.printf("\n\r 3rd click: Stop");
Serial.printf("\n\r PIN_SB_2_REC: SEQ2 Record Start/Stop (CH1+CH2)");
Serial.printf("\n\r PIN_SB_2_PLAY: SEQ2 Play Mode (same as SEQ1)");
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");
}
void loop()
{
// ===== 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",
sb1.isRecording(), sb1.isPlaying(), sb1.getStepCount());
Serial.printf("\n\r[DEBUG] SB2: Rec=%d, Play=%d, Steps=%d",
sb2.isRecording(), sb2.isPlaying(), sb2.getStepCount());
lastDebugPrint = millis();
}
// ===== NON-BLOCKING TIMING =====
static unsigned long lastLoopTime = 0;
unsigned long now = millis();
const unsigned long LOOP_INTERVAL = 10;
if((now - lastLoopTime) < LOOP_INTERVAL)
{
return;
}
lastLoopTime = now;
// ===== UPDATE FUNCTIONS =====
keyboard.update();
handleSequencerButtons();
updateMetronome();
updateRecordingLED();
// Sequencer Update (für Wiedergabe)
sb1.update();
sb2.update();
int n = keyboard.getQueueLength();
// Aktuelle Spannungen für beide Kanäle ermitteln
// Aktuelle Spannungen ermitteln
uint16_t voltage_ch1 = 0;
uint16_t voltage_ch2 = 0;
bool cv1_active = false;
bool cv2_active = false;
if(n > 0)
{
@@ -251,6 +377,7 @@ void loop()
if(!isNotKey(k1))
{
voltage_ch1 = keyToVoltage[k1.row * N_KEYBOARD_COL + k1.col];
cv1_active = true;
}
}
@@ -260,38 +387,59 @@ void loop()
if(!isNotKey(k2))
{
voltage_ch2 = keyToVoltage[k2.row * N_KEYBOARD_COL + k2.col];
cv2_active = true;
}
}
// Bei Recording: Beide Kanäle aufnehmen
if(sb1.isRecording())
// Recording
bool voltageChanged = (voltage_ch1 != last_voltage_ch1) || (voltage_ch2 != last_voltage_ch2);
if(sb1.isRecording() && voltageChanged)
{
sb1.addStep(voltage_ch1, voltage_ch2);
}
if(sb2.isRecording())
{
sb2.addStep(voltage_ch1, voltage_ch2);
last_voltage_ch1 = voltage_ch1;
last_voltage_ch2 = voltage_ch2;
}
// CV-Ausgabe: Priorität hat Sequencer-Wiedergabe
if(sb2.isRecording() && voltageChanged)
{
sb2.addStep(voltage_ch1, voltage_ch2);
last_voltage_ch1 = voltage_ch1;
last_voltage_ch2 = voltage_ch2;
}
// CV-Ausgabe & VCO Gates
if(sb1.isPlaying())
{
cv.setVoltage(0, sb1.getCurrentVoltageCh1());
cv.setVoltage(1, sb1.getCurrentVoltageCh2());
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())
{
cv.setVoltage(0, sb2.getCurrentVoltageCh1());
cv.setVoltage(1, sb2.getCurrentVoltageCh2());
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);
}
else
{
// Live-Ausgabe wenn kein Sequencer spielt
// 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);
}
// Time-Limit Warnung
// Time-Limit Check
if(sb1.isRecording() && sb1.timeLimitReached())
{
sb1.stopRecord();
@@ -306,6 +454,4 @@ void loop()
Serial.printf("\n\r[SEQ2] Final: Steps: %i, Duration: %ims",
sb2.getStepCount(), sb2.getTotalDuration());
}
delay(10);
}