Cleaned up Schematics
BIN
dev/da_altium_lib/DA_LIB/History/DA_LIB.~(232).SchLib.Zip
Normal file
BIN
dev/da_altium_lib/DA_LIB/History/DA_LIB.~(233).SchLib.Zip
Normal file
BIN
dev/da_altium_lib/DA_LIB/History/DA_LIB.~(234).SchLib.Zip
Normal file
BIN
dev/da_altium_lib/DA_LIB/History/DA_LIB.~(235).SchLib.Zip
Normal file
BIN
dev/da_altium_lib/DA_LIB/History/DA_LIB.~(236).SchLib.Zip
Normal file
8
dev/general/Firmware/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.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
|
||||||
|
.vscode/ipch
|
||||||
10
dev/general/Firmware/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||||
|
// for the documentation about the extensions.json format
|
||||||
|
"recommendations": [
|
||||||
|
"platformio.platformio-ide"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": [
|
||||||
|
"ms-vscode.cpptools-extension-pack"
|
||||||
|
]
|
||||||
|
}
|
||||||
0
dev/general/Firmware/README.md
Normal file
172
dev/general/Firmware/include/FIRMWARE.h
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
/*
|
||||||
|
@file: FIRMARE.h
|
||||||
|
@author: Erik Tóth
|
||||||
|
@contact: etoth@tsn.at
|
||||||
|
@date: 2025-10-26
|
||||||
|
@updated: 2025-12-06
|
||||||
|
@brief: Header for FIRMWARE.cpp (FIXED VERSION)
|
||||||
|
*/
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <Wire.h>
|
||||||
|
#include <Adafruit_MCP4728.h>
|
||||||
|
|
||||||
|
#ifndef FIRMWARE_H
|
||||||
|
#define FIRMWARE_H
|
||||||
|
|
||||||
|
#define N_MAX_QUEUE 10
|
||||||
|
#define N_MAX_ROWS 8
|
||||||
|
#define N_MAX_COLS 8
|
||||||
|
#define MS_DEBOUNCE 20
|
||||||
|
#define N_MAX_DAC_CH 4
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief Key struct
|
||||||
|
@struct
|
||||||
|
*/
|
||||||
|
struct Key
|
||||||
|
{
|
||||||
|
int row;
|
||||||
|
int col;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief Voltage pair for both channels
|
||||||
|
@note might change arch
|
||||||
|
*/
|
||||||
|
struct DualVoltageDurationPair
|
||||||
|
{
|
||||||
|
uint16_t voltage_ch1;
|
||||||
|
uint16_t voltage_ch2;
|
||||||
|
uint16_t duration;
|
||||||
|
bool active;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief Sentinental value for invalid key
|
||||||
|
*/
|
||||||
|
const Key NOT_A_KEY = {-1, -1};
|
||||||
|
|
||||||
|
bool isNotKey(Key k);
|
||||||
|
bool isEqualKey(Key k1, Key k2);
|
||||||
|
|
||||||
|
class Keyboard
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Keyboard(uint8_t nRows, uint8_t nCols, uint8_t *pinsRow, uint8_t *pinsCol);
|
||||||
|
void begin();
|
||||||
|
void update();
|
||||||
|
int getQueueLength();
|
||||||
|
Key getQueue(uint8_t index);
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint8_t _nRows;
|
||||||
|
uint8_t _nCols;
|
||||||
|
uint8_t *_pinsRow;
|
||||||
|
uint8_t *_pinsCol;
|
||||||
|
|
||||||
|
bool _keyState[N_MAX_COLS][N_MAX_ROWS];
|
||||||
|
bool _keyStateLatest[N_MAX_COLS][N_MAX_ROWS];
|
||||||
|
unsigned long _lastChangeTime[N_MAX_COLS][N_MAX_ROWS];
|
||||||
|
|
||||||
|
Key _activeKeys[N_MAX_QUEUE];
|
||||||
|
uint8_t _nActiveKeys;
|
||||||
|
uint8_t _nSticky;
|
||||||
|
|
||||||
|
void _addActiveKey(uint8_t row, uint8_t col);
|
||||||
|
void _removeActiveKey(uint8_t row, uint8_t col);
|
||||||
|
bool _inQueue(uint8_t row, uint8_t col);
|
||||||
|
bool _inQueue(Key k);
|
||||||
|
bool _isNotKey(Key k);
|
||||||
|
bool _isEqualKey(Key k1, Key k2);
|
||||||
|
};
|
||||||
|
|
||||||
|
class CV
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CV(Adafruit_MCP4728 *dac, TwoWire *wire, uint8_t nCV, MCP4728_channel_t *cvChannelMap, uint16_t *keyToVoltage, uint8_t row, uint8_t col);
|
||||||
|
|
||||||
|
bool begin(uint8_t pinSDA, uint8_t pinSCL);
|
||||||
|
void setVoltage(uint8_t cvIndex, Key k);
|
||||||
|
void setVoltage(uint8_t cvIndex, uint16_t mV);
|
||||||
|
void clearAll();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Adafruit_MCP4728 *_dac;
|
||||||
|
TwoWire *_wire;
|
||||||
|
uint8_t _nCV;
|
||||||
|
uint8_t _row;
|
||||||
|
uint8_t _col;
|
||||||
|
MCP4728_channel_t _cvChannelMap[N_MAX_DAC_CH];
|
||||||
|
uint16_t *_keyToVoltage;
|
||||||
|
uint8_t _getKeyToVoltageIndex(uint8_t row, uint8_t col);
|
||||||
|
uint8_t _getKeyToVoltageIndex(Key k);
|
||||||
|
};
|
||||||
|
|
||||||
|
class SequencerBlock
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SequencerBlock(uint16_t maxDurationMS, uint16_t maxStepCount);
|
||||||
|
|
||||||
|
// Aufnahme-Funktionen
|
||||||
|
void startRecord();
|
||||||
|
void stopRecord();
|
||||||
|
void addStep(uint16_t voltage_ch1, uint16_t voltage_ch2);
|
||||||
|
bool isRecording();
|
||||||
|
|
||||||
|
// Wiedergabe-Funktionen
|
||||||
|
void startPlay();
|
||||||
|
void stopPlay();
|
||||||
|
void update();
|
||||||
|
bool isPlaying();
|
||||||
|
|
||||||
|
// Sequenz-Verwaltung
|
||||||
|
void clear();
|
||||||
|
void setLoop(bool loop);
|
||||||
|
|
||||||
|
// Status-Abfragen
|
||||||
|
bool timeLimitReached();
|
||||||
|
bool stepLimitReached();
|
||||||
|
uint16_t getStepCount();
|
||||||
|
uint16_t getCurrentVoltageCh1();
|
||||||
|
uint16_t getCurrentVoltageCh2();
|
||||||
|
bool isCurrentStepActive(); // NEU: Prüft ob aktueller Step aktive Noten hat
|
||||||
|
uint16_t getTotalDuration();
|
||||||
|
|
||||||
|
private:
|
||||||
|
/*!
|
||||||
|
* @brief Memory limiting
|
||||||
|
* @return (uint16_t) 1024
|
||||||
|
* @attention Increasing the value might lead to an overflow
|
||||||
|
* @note sizeOf(DualVoltageDurationPair) = 8 Byte ==> 8 Byte * 1024 = 8192 Byte
|
||||||
|
*/
|
||||||
|
const static uint16_t _MAX_SEQUENCE_STEPS = 1024;
|
||||||
|
|
||||||
|
// Sequenz memory
|
||||||
|
DualVoltageDurationPair _sequence[_MAX_SEQUENCE_STEPS];
|
||||||
|
uint16_t _stepCount;
|
||||||
|
uint16_t _currentStep;
|
||||||
|
|
||||||
|
// Time management
|
||||||
|
uint16_t _maxDurationMS;
|
||||||
|
uint16_t _maxStepCount;
|
||||||
|
unsigned long _recordStartTime;
|
||||||
|
unsigned long _lastStepTime;
|
||||||
|
unsigned long _playStartTime;
|
||||||
|
unsigned long _stepStartTime;
|
||||||
|
unsigned long _lastAddStepTime; // NEU: Rate-Limiting
|
||||||
|
|
||||||
|
// Status flags
|
||||||
|
bool _isRecording;
|
||||||
|
bool _isPlaying;
|
||||||
|
bool _loop;
|
||||||
|
|
||||||
|
// Last recorded Voltage: at n-th step minus one
|
||||||
|
uint16_t _lastVoltageCh1;
|
||||||
|
uint16_t _lastVoltageCh2;
|
||||||
|
|
||||||
|
// helper functions
|
||||||
|
void _finishCurrentStep();
|
||||||
|
bool _canAddStep();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
49
dev/general/Firmware/include/FIRMWARE_DEF.h
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
@file: FIRMARE_DEF.h
|
||||||
|
@author: Erik Tóth
|
||||||
|
@contact: etoth@tsn.at
|
||||||
|
@date: 2025-10-26
|
||||||
|
@updated: 2026-03-08
|
||||||
|
@brief: Header for constant definitions
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef FIRMWARE_DEF_H
|
||||||
|
#define FIRMWARE_DEF_H
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <Wire.h>
|
||||||
|
// CONSTANTS DEFINITONS
|
||||||
|
#define N_KEYBOARD_ROW 5
|
||||||
|
#define N_KEYBOARD_COL 5
|
||||||
|
#define N_CV_GATES 2
|
||||||
|
#define N_SB 2
|
||||||
|
#define BAUDRATE 115200
|
||||||
|
#define N_MAX_SEQ_STEPS 512
|
||||||
|
// PIN DEFENTITIONS
|
||||||
|
// I2C PINS
|
||||||
|
#define PIN_SDA 15
|
||||||
|
#define PIN_SCL 16
|
||||||
|
// 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
|
||||||
|
#define PIN_K_C0 1
|
||||||
|
#define PIN_K_C1 2
|
||||||
|
#define PIN_K_C2 4
|
||||||
|
#define PIN_K_C3 5
|
||||||
|
#define PIN_K_C4 6
|
||||||
|
// SEQUENCER BUTTON PINS
|
||||||
|
#define PIN_SB_1_REC 33
|
||||||
|
#define PIN_SB_1_PLAY 34
|
||||||
|
#define PIN_SB_2_REC 35
|
||||||
|
#define PIN_SB_2_PLAY 36
|
||||||
|
// MISC/INFO PINS
|
||||||
|
#define PIN_VCO1_EN 38
|
||||||
|
#define PIN_VCO2_EN 39
|
||||||
|
#define PIN_REC 37
|
||||||
|
#define PIN_BPM 12
|
||||||
|
#define PIN_B_METRONOME 14
|
||||||
|
#define PIN_L_METRONOME 13
|
||||||
|
|
||||||
|
#endif
|
||||||
16
dev/general/Firmware/platformio.ini
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
; PlatformIO Project Configuration File
|
||||||
|
;
|
||||||
|
; Build options: build flags, source filter
|
||||||
|
; Upload options: custom upload port, speed and extra flags
|
||||||
|
; Library options: dependencies, extra library storages
|
||||||
|
; Advanced options: extra scripting
|
||||||
|
;
|
||||||
|
; Please visit documentation for the other options and examples
|
||||||
|
; https://docs.platformio.org/page/projectconf.html
|
||||||
|
|
||||||
|
[env:esp32-s3-devkitm-1]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32-s3-devkitm-1
|
||||||
|
framework = arduino
|
||||||
|
build_flags = -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_JTAG_ON_BOOT=1
|
||||||
|
lib_deps = adafruit/Adafruit MCP4728@^1.0.10
|
||||||
523
dev/general/Firmware/src/FIRMWARE.cpp
Normal file
@@ -0,0 +1,523 @@
|
|||||||
|
/*
|
||||||
|
@file: FIRMWARE.cpp
|
||||||
|
@author: Erik Tóth
|
||||||
|
@contact: etoth@tsn.at
|
||||||
|
@date: 2025-10-26
|
||||||
|
@updated: 2026-03-08
|
||||||
|
@brief: Firmware für MCU
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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, MCP4728_GAIN_1X);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SequencerBlock::startRecord()
|
||||||
|
{
|
||||||
|
if(_isPlaying) stopPlay();
|
||||||
|
|
||||||
|
clear();
|
||||||
|
_isRecording = true;
|
||||||
|
_recordStartTime = millis();
|
||||||
|
_lastStepTime = _recordStartTime;
|
||||||
|
_lastAddStepTime = _recordStartTime;
|
||||||
|
_lastVoltageCh1 = 0xFFFF;
|
||||||
|
_lastVoltageCh2 = 0xFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SequencerBlock::stopRecord()
|
||||||
|
{
|
||||||
|
if(!_isRecording) return;
|
||||||
|
|
||||||
|
_finishCurrentStep();
|
||||||
|
_isRecording = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SequencerBlock::addStep(uint16_t voltage_ch1, uint16_t voltage_ch2)
|
||||||
|
{
|
||||||
|
if(!_isRecording) return;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
if((unsigned long)(now - _lastAddStepTime) < 5)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_lastAddStepTime = now;
|
||||||
|
|
||||||
|
bool voltageChanged = (voltage_ch1 != _lastVoltageCh1) || (voltage_ch2 != _lastVoltageCh2);
|
||||||
|
|
||||||
|
if(voltageChanged)
|
||||||
|
{
|
||||||
|
if(_stepCount >= _MAX_SEQUENCE_STEPS - 1)
|
||||||
|
{
|
||||||
|
Serial.println("\n\r[ERROR] Array full! Stopping recording.");
|
||||||
|
stopRecord();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(_stepCount > 0 && _stepCount <= _MAX_SEQUENCE_STEPS)
|
||||||
|
{
|
||||||
|
_finishCurrentStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
_stepCount++;
|
||||||
|
|
||||||
|
_lastStepTime = now;
|
||||||
|
_lastVoltageCh1 = voltage_ch1;
|
||||||
|
_lastVoltageCh2 = voltage_ch2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if(_sequence[_currentStep].duration == 0)
|
||||||
|
{
|
||||||
|
_currentStep++;
|
||||||
|
_stepStartTime = now;
|
||||||
|
|
||||||
|
if(_currentStep >= _stepCount)
|
||||||
|
{
|
||||||
|
if(_loop)
|
||||||
|
{
|
||||||
|
_currentStep = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
stopPlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(elapsed >= _sequence[_currentStep].duration)
|
||||||
|
{
|
||||||
|
_currentStep++;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
505
dev/general/Firmware/src/main.cpp
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
/*
|
||||||
|
* 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};
|
||||||
|
|
||||||
|
Keyboard keyboard(N_KEYBOARD_ROW, N_KEYBOARD_COL, pins_keyboard_row, pins_keyboard_col);
|
||||||
|
|
||||||
|
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] = {
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Button States
|
||||||
|
struct ButtonState {
|
||||||
|
bool current;
|
||||||
|
bool last;
|
||||||
|
unsigned long lastDebounceTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
bool reading = digitalRead(pin) == HIGH;
|
||||||
|
bool buttonPressed = false;
|
||||||
|
|
||||||
|
if(reading != state.last)
|
||||||
|
{
|
||||||
|
state.lastDebounceTime = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
if((millis() - state.lastDebounceTime) > DEBOUNCE_DELAY)
|
||||||
|
{
|
||||||
|
if(reading != state.current)
|
||||||
|
{
|
||||||
|
state.current = reading;
|
||||||
|
if(state.current == true)
|
||||||
|
{
|
||||||
|
buttonPressed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.last = reading;
|
||||||
|
return buttonPressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
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_B_METRONOME, INPUT_PULLDOWN);
|
||||||
|
|
||||||
|
btn_sb1_rec.current = false;
|
||||||
|
btn_sb1_rec.last = false;
|
||||||
|
btn_sb1_rec.lastDebounceTime = 0;
|
||||||
|
|
||||||
|
btn_sb1_play.current = false;
|
||||||
|
btn_sb1_play.last = false;
|
||||||
|
btn_sb1_play.lastDebounceTime = 0;
|
||||||
|
|
||||||
|
btn_sb2_rec.current = false;
|
||||||
|
btn_sb2_rec.last = false;
|
||||||
|
btn_sb2_rec.lastDebounceTime = 0;
|
||||||
|
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
if(readButton(PIN_SB_1_REC, btn_sb1_rec))
|
||||||
|
{
|
||||||
|
if(sb1.isRecording())
|
||||||
|
{
|
||||||
|
sb1.stopRecord();
|
||||||
|
Serial.printf("\n\r[SEQ1->VCO1] Recording stopped. Steps: %i, Duration: %ims",
|
||||||
|
sb1.getStepCount(), sb1.getTotalDuration());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(sb1.isPlaying()) sb1.stopPlay();
|
||||||
|
sb1.startRecord();
|
||||||
|
sb1_last_voltage_ch1 = 0xFFFF;
|
||||||
|
sb1_last_voltage_ch2 = 0xFFFF;
|
||||||
|
Serial.printf("\n\r[SEQ1->VCO1] Recording started...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(readButton(PIN_SB_1_PLAY, btn_sb1_play))
|
||||||
|
{
|
||||||
|
if(!sb1.isPlaying())
|
||||||
|
{
|
||||||
|
if(sb1.isRecording()) sb1.stopRecord();
|
||||||
|
sb1.setLoop(false);
|
||||||
|
seq1_loop_active = false;
|
||||||
|
sb1.startPlay();
|
||||||
|
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->VCO1] Loop activated");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb1.stopPlay();
|
||||||
|
seq1_loop_active = false;
|
||||||
|
Serial.printf("\n\r[SEQ1->VCO1] Playback stopped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(readButton(PIN_SB_2_REC, btn_sb2_rec))
|
||||||
|
{
|
||||||
|
if(sb2.isRecording())
|
||||||
|
{
|
||||||
|
sb2.stopRecord();
|
||||||
|
Serial.printf("\n\r[SEQ2->VCO2] Recording stopped. Steps: %i, Duration: %ims",
|
||||||
|
sb2.getStepCount(), sb2.getTotalDuration());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(sb2.isPlaying()) sb2.stopPlay();
|
||||||
|
sb2.startRecord();
|
||||||
|
sb2_last_voltage_ch1 = 0xFFFF;
|
||||||
|
sb2_last_voltage_ch2 = 0xFFFF;
|
||||||
|
Serial.printf("\n\r[SEQ2->VCO2] Recording started...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(readButton(PIN_SB_2_PLAY, btn_sb2_play))
|
||||||
|
{
|
||||||
|
if(!sb2.isPlaying())
|
||||||
|
{
|
||||||
|
if(sb2.isRecording()) sb2.stopRecord();
|
||||||
|
sb2.setLoop(false);
|
||||||
|
seq2_loop_active = false;
|
||||||
|
sb2.startPlay();
|
||||||
|
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->VCO2] Loop activated");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb2.stopPlay();
|
||||||
|
seq2_loop_active = false;
|
||||||
|
Serial.printf("\n\r[SEQ2->VCO2] 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();
|
||||||
|
|
||||||
|
static unsigned long last_bpm_read = 0;
|
||||||
|
if((now - last_bpm_read) > 100)
|
||||||
|
{
|
||||||
|
int adc_value = analogRead(PIN_BPM);
|
||||||
|
current_bpm = map(adc_value, 0, 4095, 40, 240);
|
||||||
|
last_bpm_read = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
metronome_led_on = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!metronome_enabled) return;
|
||||||
|
|
||||||
|
unsigned long beat_interval = 60000UL / current_bpm;
|
||||||
|
|
||||||
|
if((now - last_beat_time) >= beat_interval)
|
||||||
|
{
|
||||||
|
digitalWrite(PIN_L_METRONOME, LOW);
|
||||||
|
metronome_led_on = true;
|
||||||
|
last_beat_time = now;
|
||||||
|
last_pulse_end_time = now + 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(metronome_led_on && (now >= last_pulse_end_time))
|
||||||
|
{
|
||||||
|
digitalWrite(PIN_L_METRONOME, HIGH);
|
||||||
|
metronome_led_on = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateRecordingLED()
|
||||||
|
{
|
||||||
|
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=== DUAL SEQUENCER: SB1->VCO1 | SB2->VCO2 ===");
|
||||||
|
Serial.printf("\n\rSerial OK!");
|
||||||
|
|
||||||
|
keyboard.begin();
|
||||||
|
|
||||||
|
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=== 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
|
||||||
|
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->VCO1: Rec=%d, Play=%d, Steps=%d",
|
||||||
|
sb1.isRecording(), sb1.isPlaying(), sb1.getStepCount());
|
||||||
|
Serial.printf("\n\r[DEBUG] SB2->VCO2: 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
|
||||||
|
keyboard.update();
|
||||||
|
handleSequencerButtons();
|
||||||
|
updateMetronome();
|
||||||
|
updateRecordingLED();
|
||||||
|
|
||||||
|
sb1.update();
|
||||||
|
sb2.update();
|
||||||
|
|
||||||
|
// KEYBOARD INPUT
|
||||||
|
int n = keyboard.getQueueLength();
|
||||||
|
|
||||||
|
// 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 k = keyboard.getQueue(0);
|
||||||
|
if(!isNotKey(k))
|
||||||
|
{
|
||||||
|
manual_voltage_0 = keyToVoltage[k.row * N_KEYBOARD_COL + k.col];
|
||||||
|
manual_active_0 = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(n > 1)
|
||||||
|
{
|
||||||
|
Key k = keyboard.getQueue(1);
|
||||||
|
if(!isNotKey(k))
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SB2 nimmt ebenfalls den vollen Keyboard-Input auf
|
||||||
|
if(sb2.isRecording())
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 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)
|
||||||
|
{
|
||||||
|
// SB1 Sequenz läuft -> Sequenz-Ausgabe
|
||||||
|
out_vco1 = sb1.getCurrentVoltageCh1();
|
||||||
|
gate_vco1 = sb1.isCurrentStepActive();
|
||||||
|
}
|
||||||
|
else if(sb1_recording)
|
||||||
|
{
|
||||||
|
// SB1 nimmt auf -> Live-Ausgabe damit man hört was man spielt
|
||||||
|
out_vco1 = manual_voltage_0;
|
||||||
|
gate_vco1 = manual_active_0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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->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->VCO2] Time limit reached! Recording stopped.");
|
||||||
|
Serial.printf("\n\r[SEQ2->VCO2] Final: Steps: %i, Duration: %ims",
|
||||||
|
sb2.getStepCount(), sb2.getTotalDuration());
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
dev/general/Keyboard/History/Keyboard.~(6).SchDoc.Zip
Normal file
BIN
dev/general/Keyboard/History/Keyboard.~(7).SchDoc.Zip
Normal file
BIN
dev/general/Keyboard/Schematic.pdf
Normal file
BIN
dev/general/MainSys/GERBER.pdf
Normal file
BIN
dev/general/MainSys/History/CV_GEN.~(17).SchDoc.Zip
Normal file
BIN
dev/general/MainSys/History/EXT.~(45).SchDoc.Zip
Normal file
BIN
dev/general/MainSys/History/GERBER.~(2).Cam.Zip
Normal file
BIN
dev/general/MainSys/History/MAIN.~(63).PcbDoc.Zip
Normal file
BIN
dev/general/MainSys/History/MAIN.~(64).PcbDoc.Zip
Normal file
BIN
dev/general/MainSys/History/MCU.~(60).SchDoc.Zip
Normal file
BIN
dev/general/MainSys/History/MCU.~(61).SchDoc.Zip
Normal file
BIN
dev/general/MainSys/History/MCU.~(62).SchDoc.Zip
Normal file
BIN
dev/general/MainSys/History/MainSys.~(75).PrjPcb.Zip
Normal file
BIN
dev/general/MainSys/History/OS.~(25).SchDoc.Zip
Normal file
BIN
dev/general/MainSys/History/PM.~(30).SchDoc.Zip
Normal file
BIN
dev/general/MainSys/History/SC.~(15).SchDoc.Zip
Normal file
BIN
dev/general/MainSys/History/TOP.~(69).SchDoc.Zip
Normal file
BIN
dev/general/MainSys/History/VCF_A.~(26).SchDoc.Zip
Normal file
BIN
dev/general/MainSys/History/VCO.~(56).SchDoc.Zip
Normal file
BIN
dev/general/MainSys/History/VCO.~(57).SchDoc.Zip
Normal file
262
dev/general/MainSys/Job1.OutJob
Normal file
@@ -338,6 +338,23 @@ DItemRevisionGUID=
|
|||||||
GenerateClassCluster=0
|
GenerateClassCluster=0
|
||||||
DocumentUniqueId=
|
DocumentUniqueId=
|
||||||
|
|
||||||
|
[Document18]
|
||||||
|
DocumentPath=Job1.OutJob
|
||||||
|
AnnotationEnabled=1
|
||||||
|
AnnotateStartValue=1
|
||||||
|
AnnotationIndexControlEnabled=0
|
||||||
|
AnnotateSuffix=
|
||||||
|
AnnotateScope=All
|
||||||
|
AnnotateOrder=-1
|
||||||
|
DoLibraryUpdate=1
|
||||||
|
DoDatabaseUpdate=1
|
||||||
|
ClassGenCCAutoEnabled=1
|
||||||
|
ClassGenCCAutoRoomEnabled=0
|
||||||
|
ClassGenNCAutoScope=None
|
||||||
|
DItemRevisionGUID=
|
||||||
|
GenerateClassCluster=0
|
||||||
|
DocumentUniqueId=
|
||||||
|
|
||||||
[GeneratedDocument1]
|
[GeneratedDocument1]
|
||||||
DocumentPath=Project Outputs for MainSys\Design Rule Check - MAIN.html
|
DocumentPath=Project Outputs for MainSys\Design Rule Check - MAIN.html
|
||||||
DItemRevisionGUID=
|
DItemRevisionGUID=
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Change Component Parameter Comment in Y1 Old= New==Value
|
||||||
|
Replace Symbol from Y1 Y X322540MPB4SI in C:\HTL\5AHEL\DA\github\audio-synth\dev\general\MainSys\MCU.SchDoc with Y X322540MPB4SI from DA_LIB.IntLib
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Change Component Parameter Comment in R28 Old= New==VALUE
|
||||||
|
Replace Symbol from R28 R_POT TC33X-2-103E in C:\HTL\5AHEL\DA\github\audio-synth\dev\general\MainSys\VCO.SchDoc with R_POT TC33X-2-103E from DA_LIB.IntLib
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Change Component Parameter Comment in R28 Old= New==VALUE
|
||||||
|
Change Component Parameter Comment in R32 Old= New==VALUE
|
||||||
|
Change Component Parameter Comment in R36 Old= New==VALUE
|
||||||
|
Replace Symbol from R28 R_POT TC33X-2-103E in C:\HTL\5AHEL\DA\github\audio-synth\dev\general\MainSys\VCO.SchDoc with R_POT TC33X-2-103E from DA_LIB.IntLib
|
||||||
|
Replace Symbol from R32 R_POT TC33X-2-103E in C:\HTL\5AHEL\DA\github\audio-synth\dev\general\MainSys\VCO.SchDoc with R_POT TC33X-2-103E from DA_LIB.IntLib
|
||||||
|
Replace Symbol from R36 R_POT TC33X-2-103E in C:\HTL\5AHEL\DA\github\audio-synth\dev\general\MainSys\VCO.SchDoc with R_POT TC33X-2-103E from DA_LIB.IntLib
|
||||||
BIN
docs/general/BOM_doc.xlsx
Normal file
BIN
docs/general/GERBER.pdf/GERBER-001.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/general/GERBER.pdf/GERBER-002.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
docs/general/Schematics (1).pdf/Schematics-001.png
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
BIN
docs/general/Schematics (1).pdf/Schematics-002.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
docs/general/Schematics (1).pdf/Schematics-003.png
Normal file
|
After Width: | Height: | Size: 5.1 MiB |
BIN
docs/general/Schematics (1).pdf/Schematics-004.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
docs/general/Schematics (1).pdf/Schematics-005.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
docs/general/Schematics (1).pdf/Schematics-006.png
Normal file
|
After Width: | Height: | Size: 5.4 MiB |
BIN
docs/general/Schematics (1).pdf/Schematics-007.png
Normal file
|
After Width: | Height: | Size: 4.7 MiB |
BIN
docs/general/Schematics (1).pdf/Schematics-008.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
docs/general/Schematics (1).pdf/Schematics-009.png
Normal file
|
After Width: | Height: | Size: 4.5 MiB |
BIN
docs/general/Schematics.pdf/Schematics-001.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
docs/general/Schematics.pdf/Schematics-002.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |