mirror of
https://github.com/erik-toth/audio-synth.git
synced 2025-12-06 12:00:02 +00:00
537 lines
13 KiB
C++
537 lines
13 KiB
C++
/*
|
|
@file: FIRMWARE.cpp
|
|
@author: Erik Tóth
|
|
@contact: etoth@tsn.at
|
|
@date: 2025-10-26
|
|
@updated: 2025-12-06
|
|
@brief: Firmware für MCU - FIXED VERSION mit Bounds Checks
|
|
*/
|
|
|
|
#include "FIRMWARE.h"
|
|
|
|
// ==================== Helper-Functions ====================
|
|
|
|
bool isNotKey(Key k)
|
|
{
|
|
if((k.row == NOT_A_KEY.row) && (k.col == NOT_A_KEY.col)) return true;
|
|
else return false;
|
|
}
|
|
|
|
bool isEqualKey(Key k1, Key k2)
|
|
{
|
|
if((k1.row == k2.row) && (k1.col == k2.col)) return true;
|
|
else return false;
|
|
}
|
|
|
|
// ==================== Keyboard ====================
|
|
|
|
Keyboard::Keyboard(uint8_t nRows, uint8_t nCols, uint8_t *pinsRow, uint8_t *pinsCol)
|
|
{
|
|
_nRows = nRows;
|
|
_nCols = nCols;
|
|
_pinsRow = pinsRow;
|
|
_pinsCol = pinsCol;
|
|
|
|
_nActiveKeys = 0;
|
|
_nSticky = 2;
|
|
|
|
for(uint8_t i = 0; i < _nRows; i++)
|
|
{
|
|
for(uint8_t j = 0; j < _nCols; j++)
|
|
{
|
|
_keyState[i][j] = false;
|
|
_keyStateLatest[i][j] = false;
|
|
_lastChangeTime[i][j] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
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], INPUT);
|
|
}
|
|
|
|
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)
|
|
{
|
|
bool reading = (digitalRead(_pinsRow[row]) == HIGH);
|
|
|
|
if(reading != _keyStateLatest[row][col])
|
|
{
|
|
_keyStateLatest[row][col] = reading;
|
|
_lastChangeTime[row][col] = now;
|
|
}
|
|
|
|
if((now - _lastChangeTime[row][col]) > MS_DEBOUNCE)
|
|
{
|
|
if(reading != _keyState[row][col])
|
|
{
|
|
_keyState[row][col] = reading;
|
|
|
|
if(reading) _addActiveKey(row, col);
|
|
else _removeActiveKey(row, col);
|
|
}
|
|
}
|
|
}
|
|
digitalWrite(_pinsCol[col], LOW);
|
|
pinMode(_pinsCol[col], INPUT);
|
|
}
|
|
if((_nActiveKeys == 1) && _inQueue(NOT_A_KEY)) _nActiveKeys = 0;
|
|
}
|
|
|
|
int Keyboard::getQueueLength()
|
|
{
|
|
return _nActiveKeys;
|
|
}
|
|
|
|
Key Keyboard::getQueue(uint8_t index)
|
|
{
|
|
if(index < _nActiveKeys) return _activeKeys[index];
|
|
else return NOT_A_KEY;
|
|
}
|
|
|
|
bool Keyboard::_inQueue(uint8_t row, uint8_t col)
|
|
{
|
|
for(uint8_t i = 0; i < _nActiveKeys; i++)
|
|
{
|
|
if((_activeKeys[i].row == row) && (_activeKeys[i].col == col)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Keyboard::_inQueue(Key k)
|
|
{
|
|
for(uint8_t i = 0; i < _nActiveKeys; i++)
|
|
{
|
|
if(_isEqualKey(_activeKeys[i], k)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Keyboard::_isNotKey(Key k)
|
|
{
|
|
return isNotKey(k);
|
|
}
|
|
|
|
bool Keyboard::_isEqualKey(Key k1, Key k2)
|
|
{
|
|
return isEqualKey(k1, k2);
|
|
}
|
|
|
|
void Keyboard::_addActiveKey(uint8_t row, uint8_t col)
|
|
{
|
|
if(_inQueue(NOT_A_KEY))
|
|
{
|
|
for(int i = 0; i < _nSticky; i++)
|
|
{
|
|
if(_isNotKey(_activeKeys[i]))
|
|
{
|
|
_activeKeys[i] = {row, col};
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
else if((_nActiveKeys < N_MAX_QUEUE) && !(_inQueue(row, col)))
|
|
{
|
|
_activeKeys[_nActiveKeys++] = {row, col};
|
|
}
|
|
else return;
|
|
}
|
|
|
|
void Keyboard::_removeActiveKey(uint8_t row, uint8_t col)
|
|
{
|
|
bool notKeyReplaced = true;
|
|
|
|
for(uint8_t i = 0; i < _nActiveKeys; i++)
|
|
{
|
|
if((_activeKeys[i].row == row) && (_activeKeys[i].col == col))
|
|
{
|
|
if(i < _nSticky)
|
|
{
|
|
_activeKeys[i] = NOT_A_KEY;
|
|
notKeyReplaced = false;
|
|
}
|
|
|
|
if((_isNotKey(_activeKeys[i])) && (_nActiveKeys-1 >= _nSticky))
|
|
{
|
|
_activeKeys[i] = _activeKeys[_nSticky];
|
|
notKeyReplaced = true;
|
|
}
|
|
|
|
for(uint8_t j = i; j < _nActiveKeys-1; j++)
|
|
{
|
|
if(j >= _nSticky) _activeKeys[j] = _activeKeys[j + 1];
|
|
}
|
|
|
|
if(notKeyReplaced || (i > _nSticky)) _nActiveKeys--;
|
|
else if(_isNotKey(_activeKeys[_nSticky-1])) _nActiveKeys--;
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ==================== CV ====================
|
|
|
|
CV::CV(Adafruit_MCP4728 *dac, TwoWire *wire, uint8_t nCV, MCP4728_channel_t *cvChannelMap, uint16_t *keyToVoltage, uint8_t row, uint8_t col)
|
|
{
|
|
_dac = dac;
|
|
_wire = wire;
|
|
_nCV = nCV;
|
|
_row = row;
|
|
_col = col;
|
|
_keyToVoltage = keyToVoltage;
|
|
|
|
for(uint8_t i = 0; i < N_MAX_DAC_CH; i++)
|
|
{
|
|
_cvChannelMap[i] = i < _nCV ? cvChannelMap[i] : (MCP4728_channel_t)(0);
|
|
}
|
|
}
|
|
|
|
bool CV::begin(uint8_t pinSDA, uint8_t pinSCL)
|
|
{
|
|
if((_wire->begin(pinSDA, pinSCL) && _dac->begin(96U, _wire)))
|
|
{
|
|
clearAll();
|
|
return true;
|
|
}
|
|
else return false;
|
|
}
|
|
|
|
void CV::setVoltage(uint8_t cvIndex, uint16_t mV)
|
|
{
|
|
if(cvIndex >= _nCV) return;
|
|
MCP4728_channel_t ch = _cvChannelMap[cvIndex];
|
|
_dac->setChannelValue(ch, map(mV, 0, 2048, 0, 4095), MCP4728_VREF_INTERNAL);
|
|
}
|
|
|
|
void CV::setVoltage(uint8_t cvIndex, Key k)
|
|
{
|
|
if(cvIndex >= _nCV) return;
|
|
if(isNotKey(k)) setVoltage(cvIndex, 0);
|
|
else setVoltage(cvIndex, _keyToVoltage[_getKeyToVoltageIndex(k)]);
|
|
}
|
|
|
|
void CV::clearAll()
|
|
{
|
|
for(uint8_t i = 0; i < _nCV; i++) setVoltage(i, 0);
|
|
}
|
|
|
|
uint8_t CV::_getKeyToVoltageIndex(uint8_t row, uint8_t col)
|
|
{
|
|
return (row*_col + col);
|
|
}
|
|
|
|
uint8_t CV::_getKeyToVoltageIndex(Key k)
|
|
{
|
|
return (k.row*_col + k.col);
|
|
}
|
|
|
|
// ==================== SequencerBlock (FIXED) ====================
|
|
|
|
/*!
|
|
* @param maxDurationMS maximum loop duration of recording in milliseconds
|
|
* @param maxStepCount maximum number of steps that can be recorded
|
|
*/
|
|
SequencerBlock::SequencerBlock(uint16_t maxDurationMS, uint16_t maxStepCount)
|
|
{
|
|
_maxDurationMS = maxDurationMS;
|
|
_maxStepCount = maxStepCount;
|
|
_stepCount = 0;
|
|
_currentStep = 0;
|
|
_isRecording = false;
|
|
_isPlaying = false;
|
|
_loop = false;
|
|
_lastVoltageCh1 = 0;
|
|
_lastVoltageCh2 = 0;
|
|
_recordStartTime = 0;
|
|
_lastStepTime = 0;
|
|
_playStartTime = 0;
|
|
_stepStartTime = 0;
|
|
_lastAddStepTime = 0; // NEU: Rate-Limiting
|
|
}
|
|
|
|
void SequencerBlock::startRecord()
|
|
{
|
|
if(_isPlaying) stopPlay();
|
|
|
|
clear();
|
|
_isRecording = true;
|
|
_recordStartTime = millis();
|
|
_lastStepTime = _recordStartTime;
|
|
_lastAddStepTime = _recordStartTime; // NEU
|
|
_lastVoltageCh1 = 0xFFFF;
|
|
_lastVoltageCh2 = 0xFFFF;
|
|
}
|
|
|
|
void SequencerBlock::stopRecord()
|
|
{
|
|
if(!_isRecording) return;
|
|
|
|
_finishCurrentStep();
|
|
_isRecording = false;
|
|
}
|
|
|
|
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 && _stepCount <= _MAX_SEQUENCE_STEPS)
|
|
{
|
|
_finishCurrentStep();
|
|
}
|
|
|
|
// 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;
|
|
_lastVoltageCh1 = voltage_ch1;
|
|
_lastVoltageCh2 = voltage_ch2;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Gleiche Spannung - Duration des aktuellen Steps aktualisieren
|
|
// WICHTIG: Bounds Check!
|
|
if(_stepCount > 0 && _stepCount <= _MAX_SEQUENCE_STEPS)
|
|
{
|
|
_sequence[_stepCount - 1].duration = now - _lastStepTime;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool SequencerBlock::isRecording()
|
|
{
|
|
return _isRecording;
|
|
}
|
|
|
|
void SequencerBlock::startPlay()
|
|
{
|
|
if(_stepCount == 0) return;
|
|
if(_isRecording) stopRecord();
|
|
|
|
_isPlaying = true;
|
|
_currentStep = 0;
|
|
_playStartTime = millis();
|
|
_stepStartTime = _playStartTime;
|
|
}
|
|
|
|
void SequencerBlock::stopPlay()
|
|
{
|
|
_isPlaying = false;
|
|
_currentStep = 0;
|
|
}
|
|
|
|
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)
|
|
{
|
|
_currentStep++;
|
|
|
|
// Sequenz-Ende erreicht?
|
|
if(_currentStep >= _stepCount)
|
|
{
|
|
if(_loop)
|
|
{
|
|
_currentStep = 0;
|
|
_stepStartTime = now;
|
|
}
|
|
else
|
|
{
|
|
stopPlay();
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_stepStartTime = now;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool SequencerBlock::isPlaying()
|
|
{
|
|
return _isPlaying;
|
|
}
|
|
|
|
void SequencerBlock::clear()
|
|
{
|
|
_stepCount = 0;
|
|
_currentStep = 0;
|
|
_lastVoltageCh1 = 0;
|
|
_lastVoltageCh2 = 0;
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
void SequencerBlock::setLoop(bool loop)
|
|
{
|
|
_loop = loop;
|
|
}
|
|
|
|
bool SequencerBlock::timeLimitReached()
|
|
{
|
|
if(!_isRecording) return false;
|
|
|
|
unsigned long now = millis();
|
|
unsigned long elapsed = now - _recordStartTime;
|
|
|
|
return (elapsed >= _maxDurationMS);
|
|
}
|
|
|
|
bool SequencerBlock::stepLimitReached()
|
|
{
|
|
return (_stepCount >= _maxStepCount) || (_stepCount >= _MAX_SEQUENCE_STEPS);
|
|
}
|
|
|
|
uint16_t SequencerBlock::getStepCount()
|
|
{
|
|
return _stepCount;
|
|
}
|
|
|
|
uint16_t SequencerBlock::getCurrentVoltageCh1()
|
|
{
|
|
if(!_isPlaying || _stepCount == 0) return 0;
|
|
if(_currentStep >= _stepCount || _currentStep >= _MAX_SEQUENCE_STEPS) return 0;
|
|
|
|
return _sequence[_currentStep].voltage_ch1;
|
|
}
|
|
|
|
uint16_t SequencerBlock::getCurrentVoltageCh2()
|
|
{
|
|
if(!_isPlaying || _stepCount == 0) return 0;
|
|
if(_currentStep >= _stepCount || _currentStep >= _MAX_SEQUENCE_STEPS) return 0;
|
|
|
|
return _sequence[_currentStep].voltage_ch2;
|
|
}
|
|
|
|
uint16_t SequencerBlock::getTotalDuration()
|
|
{
|
|
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 > 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;
|
|
|
|
_sequence[_stepCount - 1].duration = duration;
|
|
}
|
|
|
|
bool SequencerBlock::_canAddStep()
|
|
{
|
|
if(_stepCount >= _maxStepCount) return false;
|
|
if(_stepCount >= _MAX_SEQUENCE_STEPS) return false;
|
|
if(timeLimitReached()) return false;
|
|
|
|
return true;
|
|
} |