mirror of
https://github.com/erik-toth/audio-synth.git
synced 2025-12-06 11:20:02 +00:00
Compare commits
2 Commits
60950e6a0c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c8a90ce7d | |||
| 06fa584b6d |
Binary file not shown.
BIN
dev/digital/ESP32-S3/History/ESP32-S3.~(23).SchDoc.Zip
Normal file
BIN
dev/digital/ESP32-S3/History/ESP32-S3.~(23).SchDoc.Zip
Normal file
Binary file not shown.
BIN
dev/digital/ESP32-S3/History/ESP32-S3.~(24).SchDoc.Zip
Normal file
BIN
dev/digital/ESP32-S3/History/ESP32-S3.~(24).SchDoc.Zip
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
5
dev/digital/Firmware_TEST/.gitignore
vendored
5
dev/digital/Firmware_TEST/.gitignore
vendored
@@ -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
|
||||
|
||||
Binary file not shown.
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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 if(!seq1_loop_active)
|
||||
{
|
||||
sb1.setLoop(true);
|
||||
seq1_loop_active = true;
|
||||
Serial.printf("\n\r[SEQ1] Loop activated");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
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 if(!seq2_loop_active)
|
||||
{
|
||||
sb2.setLoop(true);
|
||||
seq2_loop_active = true;
|
||||
Serial.printf("\n\r[SEQ2] Loop activated");
|
||||
}
|
||||
else
|
||||
{
|
||||
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");
|
||||
}
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user