From d4772ad52de3193857f76e52d3ebf525f99c2b25 Mon Sep 17 00:00:00 2001 From: Oleksiy H Date: Wed, 15 Oct 2025 13:30:25 +0300 Subject: [PATCH] Added CV and Gate outputs via 2x i2c MCP4725 modules --- AcidStepSequencer.ino | 278 ++++++++++++++++++++++++++ AcidTHarmonizer.ino | 59 ++++++ AciduinoCV.ino | 48 +++++ CV.ino | 62 ++++++ CV_Test/CV_Test.ino | 33 +++ HardwareInterface.ino | 298 +++++++++++++++++++++++++++ MidiExternalSync.ino | 55 +++++ PageGenerative.ino | 187 +++++++++++++++++ PageLive.ino | 177 ++++++++++++++++ PageSequencer.ino | 183 +++++++++++++++++ README.md | 10 +- UserInterface.ino | 187 +++++++++++++++++ config.h | 99 +++++++++ uClock.cpp | 454 ++++++++++++++++++++++++++++++++++++++++++ uClock.h | 173 ++++++++++++++++ 15 files changed, 2299 insertions(+), 4 deletions(-) create mode 100644 AcidStepSequencer.ino create mode 100644 AcidTHarmonizer.ino create mode 100644 AciduinoCV.ino create mode 100644 CV.ino create mode 100644 CV_Test/CV_Test.ino create mode 100644 HardwareInterface.ino create mode 100644 MidiExternalSync.ino create mode 100644 PageGenerative.ino create mode 100644 PageLive.ino create mode 100644 PageSequencer.ino create mode 100644 UserInterface.ino create mode 100644 config.h create mode 100644 uClock.cpp create mode 100644 uClock.h diff --git a/AcidStepSequencer.ino b/AcidStepSequencer.ino new file mode 100644 index 0000000..7495e97 --- /dev/null +++ b/AcidStepSequencer.ino @@ -0,0 +1,278 @@ +// Acid StepSequencer, a Roland TB303 step sequencer engine clone +// author: midilab contact@midilab.co +// under MIT license +#include "uClock.h" + +#define NOTE_STACK_SIZE 3 + +// MIDI clock, start, stop, note on and note off byte definitions - based on MIDI 1.0 Standards. +#define MIDI_CLOCK 0xF8 +#define MIDI_START 0xFA +#define MIDI_STOP 0xFC +#define NOTE_ON 0x90 +#define NOTE_OFF 0x80 +#define MIDI_CC 0xB0 + +// sequencer data +typedef struct +{ + uint8_t note; + int16_t length; +} STACK_NOTE_DATA; + +typedef struct +{ + uint8_t note:7; + uint8_t accent:1; + uint8_t glide:1; + uint8_t rest:1; + uint8_t tie:1; + uint8_t reserved:5; +} SEQUENCER_STEP_DATA; +// 2 bytes per step + +typedef struct +{ + SEQUENCER_STEP_DATA step[STEP_MAX_SIZE]; + int8_t step_init_point; + uint8_t step_length; +} SEQUENCER_TRACK_DATA; +// 32 bytes per 16 step + 2 bytes config = 34 bytes [STEP_MAX_SIZE=16] + +typedef struct +{ + SEQUENCER_TRACK_DATA data; + uint8_t step_location; + uint8_t channel; + bool mute; + STACK_NOTE_DATA stack[NOTE_STACK_SIZE]; +} SEQUENCER_TRACK; + +// main sequencer data is constantly change inside uClock 16PPQN and 96PPQN ISR callbacks, so volatile him! +SEQUENCER_TRACK volatile _sequencer[TRACK_NUMBER]; + +uint8_t _selected_track = 0; +uint8_t _selected_pattern = 0; + +// make sure all above sequencer data are modified atomicly only +// eg. ATOMIC(_sequencer[track]data.step[0].accent = 1); ATOMIC(_sequencer[track].data.step_length = 7); + +// shared data to be used for user interface interaction and feedback +bool _playing = false; +uint8_t _harmonize = 0; +uint16_t _step_edit = 0; +uint8_t _last_octave = 3; +uint8_t _last_note = 0; +int8_t _transpose = 0; // zero is centered C +uint8_t _selected_mode = 0; + +void sendMidiMessage(uint8_t command, uint8_t byte1, uint8_t byte2, uint8_t channel) +{ + // send midi message + command = command | (uint8_t)channel; + Serial.write(command); + Serial.write(byte1); + Serial.write(byte2); +} + +// The callback function wich will be called by uClock each Pulse of 16PPQN clock resolution. Each call represents exactly one step. +void ClockOut16PPQN(uint32_t * tick) +{ + uint8_t step, next_step; + uint16_t length; + int8_t note; + + for ( uint8_t track = 0; track < TRACK_NUMBER; track++ ) { + + if ( _sequencer[track].mute == true ) { + continue; + } + + length = NOTE_LENGTH; + + // get actual step location. + _sequencer[track].step_location = uint32_t(*tick + _sequencer[track].data.step_init_point) % _sequencer[track].data.step_length; + + // send note on only if this step are not in rest mode + if ( _sequencer[track].data.step[_sequencer[track].step_location].rest == 0 ) { + + // check for slide or tie event ahead of _sequencer[track].step_location + step = _sequencer[track].step_location; + next_step = step; + for ( uint8_t i = 1; i < _sequencer[track].data.step_length; i++ ) { + next_step = ++next_step % _sequencer[track].data.step_length; + if (_sequencer[track].data.step[step].glide == 1 && _sequencer[track].data.step[next_step].rest == 0) { + length = NOTE_LENGTH + 5; + break; + } else if (_sequencer[track].data.step[next_step].tie == 1 && _sequencer[track].data.step[next_step].rest == 1) { + length = NOTE_LENGTH + (i * 6); + } else if ( _sequencer[track].data.step[next_step].rest == 0 || _sequencer[track].data.step[next_step].tie == 0) { + break; + } + } + + // find a free note stack to fit in + for ( uint8_t i = 0; i < NOTE_STACK_SIZE; i++ ) { + if ( _sequencer[track].stack[i].length == -1 ) { + if ( _harmonize == 1 ) { + note = harmonizer(_sequencer[track].data.step[_sequencer[track].step_location].note); + } else { + note = _sequencer[track].data.step[_sequencer[track].step_location].note; + } + note += _transpose; + // in case transpose push note away from the lower or higher midi note range barrier do not play it + if ( note < 0 || note > 127 ) { + break; + } + _sequencer[track].stack[i].note = note; + _sequencer[track].stack[i].length = length; + // send note on + sendMidiMessage(NOTE_ON, note, _sequencer[track].data.step[_sequencer[track].step_location].accent ? ACCENT_VELOCITY : NOTE_VELOCITY, _sequencer[track].channel); + sendCVNote(note, track); //add accent here + sendGateOn(track); + break; + } + } + + } + + } + +} + +void clearStackNote(int8_t track = -1) +{ + if ( track <= -1 ) { + // clear all tracks stack note + for ( uint8_t i = 0; i < TRACK_NUMBER; i++ ) { + // clear and send any note off + for ( uint8_t j = 0; j < NOTE_STACK_SIZE; j++ ) { + ATOMIC( + sendMidiMessage(NOTE_OFF, _sequencer[i].stack[j].note, 0, _sequencer[i].channel); + _sequencer[i].stack[j].length = -1; + sendGateOff(i); + ) + } + } + } else { + // clear and send any note off + for ( uint8_t i = 0; i < NOTE_STACK_SIZE; i++ ) { + ATOMIC( + sendMidiMessage(NOTE_OFF, _sequencer[track].stack[i].note, 0, _sequencer[track].channel); + _sequencer[track].stack[i].length = -1; + sendGateOff(track); + ) + } + } + +} + +// The callback function wich will be called by uClock each Pulse of 96PPQN clock resolution. +void ClockOut96PPQN(uint32_t * tick) +{ + uint8_t track; + + // Send MIDI_CLOCK to external hardware + Serial.write(MIDI_CLOCK); + + for ( track = 0; track < TRACK_NUMBER; track++ ) { + + // handle note on stack + for ( uint8_t i = 0; i < NOTE_STACK_SIZE; i++ ) { + if ( _sequencer[track].stack[i].length != -1 ) { + --_sequencer[track].stack[i].length; + if ( _sequencer[track].stack[i].length == 0 ) { + sendMidiMessage(NOTE_OFF, _sequencer[track].stack[i].note, 0, _sequencer[track].channel); + _sequencer[track].stack[i].length = -1; + sendGateOff(track); + } + } + } + + } + + // user feedback about sequence time events + tempoInterface(tick); +} + +// The callback function wich will be called when clock starts by using Clock.start() method. +void onClockStart() +{ + Serial.write(MIDI_START); + _playing = 1; +} + +// The callback function wich will be called when clock stops by using Clock.stop() method. +void onClockStop() +{ + Serial.write(MIDI_STOP); + + // clear all tracks stack note + clearStackNote(); + + _playing = 0; +} + +void setTrackChannel(uint8_t track, uint8_t channel) +{ + --track; + --channel; + ATOMIC(_sequencer[track].channel = channel); +} + +void initAcidStepSequencer(uint8_t mode) +{ + uint8_t track; + + // Initialize serial communication + if ( mode == 0 ) { + // the default MIDI serial speed communication at 31250 bits per second + Serial.begin(31250); + } else if ( mode == 1 ) { + // for usage with a PC with a serial to MIDI bridge + Serial.begin(115200); + } + + // Inits the clock + uClock.init(); + + // Set the callback function for the clock output to send MIDI Sync message. + uClock.setClock96PPQNOutput(ClockOut96PPQN); + + // Set the callback function for the step sequencer on 16ppqn + uClock.setClock16PPQNOutput(ClockOut16PPQN); + + // Set the callback function for MIDI Start and Stop messages. + uClock.setOnClockStartOutput(onClockStart); + uClock.setOnClockStopOutput(onClockStop); + + // Set the clock BPM to 126 BPM + uClock.setTempo(126); + + // initing sequencer memory data + for ( track = 0; track < TRACK_NUMBER; track++ ) { + + _sequencer[track].channel = track; + _sequencer[track].data.step_init_point = 0; + _sequencer[track].data.step_length = STEP_MAX_SIZE; + _sequencer[track].step_location = 0; + _sequencer[track].mute = false; + + // initing note data + for ( uint16_t i = 0; i < STEP_MAX_SIZE; i++ ) { + _sequencer[track].data.step[i].note = 48; + _sequencer[track].data.step[i].accent = 0; + _sequencer[track].data.step[i].glide = 0; + _sequencer[track].data.step[i].tie = 0; + _sequencer[track].data.step[i].rest = 0; + } + + // initing note stack data + for ( uint8_t i = 0; i < NOTE_STACK_SIZE; i++ ) { + _sequencer[track].stack[i].note = 0; + _sequencer[track].stack[i].length = -1; + } + + } + +} diff --git a/AcidTHarmonizer.ino b/AcidTHarmonizer.ino new file mode 100644 index 0000000..f8a90c5 --- /dev/null +++ b/AcidTHarmonizer.ino @@ -0,0 +1,59 @@ +// +// MODE temperament +// +// MAJOR MODES +uint8_t _ionian[8] = { 0, 2, 4, 5, 7, 9, 11, 12 }; +uint8_t _dorian[8] = { 0, 2, 3, 5, 7, 9, 10, 12 }; +uint8_t _phrygian[8] = { 0, 1, 3, 5, 7, 8, 10, 12 }; +uint8_t _lydian[8] = { 0, 2, 4, 6, 7, 9, 11, 12 }; +uint8_t _mixolidian[8] = { 0, 2, 4, 5, 7, 9, 10, 12 }; +uint8_t _aeolian[8] = { 0, 2, 3, 5, 7, 8, 10, 12 }; +uint8_t _locrian[8] = { 0, 1, 3, 5, 6, 8, 10, 12 }; + +// MINOR MODES +// ascending melodic minor +uint8_t _melodic[8] = { 0, 2, 3, 5, 7, 9, 11, 12 }; +// phrygian ♮6 (dorian ♭2) +uint8_t _phrygian6[8] = { 0, 1, 3, 5, 7, 9, 10, 12 }; +// lydian augmented (lydian ♯5) +uint8_t _lydian5[8] = { 0, 2, 4, 6, 8, 9, 11, 12 }; +// lydian dominant (also, "lydian ♭7", acoustic scale, or mixolydian ♯4) +uint8_t _lydian7[8] = { 0, 2, 4, 6, 7, 9, 10, 12 }; +// mixolydian ♭6 (or melodic major or simply "fifth mode") +uint8_t _mixolydian6[8] = { 0, 2, 4, 5, 7, 8, 10, 12 }; +// locrian ♮2 (also known as "half-diminished" scale) +uint8_t _locrian2[8] = { 0, 2, 3, 5, 6, 8, 10, 12 }; +// super Locrian (also "altered dominant scale", or "altered scale") +uint8_t _super_locrian[8] = { 0, 1, 3, 4, 6, 8, 10, 12 }; + +uint8_t * _mode[] = { + // MAJOR MODES + _ionian, + _dorian, + _phrygian, + _lydian, + _mixolidian, + _aeolian, + _locrian, + // MINOR MODES + _melodic, + _phrygian6, + _lydian5, + _lydian7, + _mixolydian6, + _locrian2, + _super_locrian +}; + +#define MODES_NUMBER (sizeof(_mode) / sizeof(uint16_t)) // its array pointer we are holding here + +uint8_t harmonizer(uint8_t note) +{ + uint8_t octave, interval; + + octave = floor(note/12); + interval = floor((note%12)/1.5); + + return (octave*12) + _mode[_selected_mode][interval]; +} + diff --git a/AciduinoCV.ino b/AciduinoCV.ino new file mode 100644 index 0000000..7b81c46 --- /dev/null +++ b/AciduinoCV.ino @@ -0,0 +1,48 @@ +// Acid StepSequencer, a Roland TB303 step sequencer engine clone +// author: midilab contact@midilab.co +// under MIT license +// CV functionality added by Oleksiy Hrachov + +#include +#include +#include "MCP4725.h" + +#include "config.h" + +MCP4725 DAC1(DAC_1_ADDR); +MCP4725 DAC2(DAC_2_ADDR); + +bool updateCV1 = true; +bool updateCV2 = true; +bool sendGate1 = false; +bool sendGate2 = false; +uint8_t CV1Note; +uint8_t CV2Note; + +void setup() +{ + // AcidStepSequencer Interface + initAcidStepSequencer(MIDI_MODE); + setTrackChannel(1, TRACK1_CHANNEL); + setTrackChannel(2, TRACK2_CHANNEL); + + // pins, buttons, leds and pots config + configureInterface(); + + // last pattern user had load before power off + loadLastPattern(); + + Wire.begin(); + Wire.setClock(400000); + DAC1.begin(); + DAC2.begin(); + DAC1.setMaxVoltage(5.1); + DAC2.setMaxVoltage(5.1); +} + +// User interaction goes here +void loop() +{ + processInterface(); + processCV(); +} diff --git a/CV.ino b/CV.ino new file mode 100644 index 0000000..c13f802 --- /dev/null +++ b/CV.ino @@ -0,0 +1,62 @@ +float noteToVoltage(uint8_t note) { + float voltage = (note - 24) * 0.083; + return voltage; +} + +void sendCVNote(uint8_t note, uint8_t track) { + if (track == 0 && note != CV1Note) { + CV1Note = note; + updateCV1 = true; + } + if (track == 1 && note != CV2Note) { + CV2Note = note; + updateCV2 = true; + } +} + +void sendGateOn(uint8_t track) { + if (track == 0) { + sendGate1 = true; + } + if (track == 1) { + sendGate2 = true; + } +} + +void sendGateOff(uint8_t track) { + if (track == 0) { + sendGate1 = false; + } + if (track == 1) { + sendGate2 = false; + } +} + +void sendVoltage(uint8_t voltage, uint8_t dac) { + if (dac == 0) { + DAC1.setVoltage(voltage); + } else { + DAC2.setVoltage(voltage); + } +} + +void processCV() { + if (updateCV1) { + sendVoltage(noteToVoltage(CV1Note), 0); + updateCV1 = false; + } + if (updateCV2) { + //sendVoltage(noteToVoltage(CV2Note), 1); + //updateCV2 = false; + } + if (sendGate1) { + sendVoltage(5, 1); + } else { + sendVoltage(0, 1); + } + if (sendGate2) { + //sendVoltage(5, 1); + } else { + //sendVoltage(0, 1); + } +} \ No newline at end of file diff --git a/CV_Test/CV_Test.ino b/CV_Test/CV_Test.ino new file mode 100644 index 0000000..a9c87a4 --- /dev/null +++ b/CV_Test/CV_Test.ino @@ -0,0 +1,33 @@ +#include +#include "MCP4725.h" + +MCP4725 DAC1(0x60); +MCP4725 DAC2(0x61); + +float output1 = 0; +float output2 = 5.1; + +void setup() { + Wire.begin(); + DAC1.begin(); + DAC2.begin(); + DAC1.setMaxVoltage(5.1); + DAC2.setMaxVoltage(5.1); +} + +void loop() { + if (output1 < 5.1) { + output1 += 0.1; + } else { + output1 = 0; + } + DAC1.setVoltage(output1); + + if (output2 > 0) { + output2 -= 0.1; + } else { + output2 = 5.1; + } + DAC2.setVoltage(output2); + delay(1); +} diff --git a/HardwareInterface.ino b/HardwareInterface.ino new file mode 100644 index 0000000..8196d46 --- /dev/null +++ b/HardwareInterface.ino @@ -0,0 +1,298 @@ +#include + +// pot data +typedef struct +{ + uint8_t pin; + uint16_t state; + bool lock; +} POT_DATA; + +// button data +typedef struct +{ + uint8_t pin; + bool state; + uint8_t hold_seconds; + bool hold_trigger; +} BUTTON_DATA; + +POT_DATA _pot[POT_NUMBER]; +BUTTON_DATA _button[BUTTON_NUMBER]; + +// pattern memory layout 72 bytes(2 tracks with 16 steps) +// byte1: pattern_exist +// byte2: _transpose +// byte3: _harmonize +// byte4: _selected_mode +// byte5...: _sequencer[0].data, _sequencer[1].data +// EEPROM data access: 1024 bytes total +// total 14 patterns +// use for load and save pattern +#define PATTERN_SIZE (sizeof(SEQUENCER_TRACK_DATA)*TRACK_NUMBER)+4 +#define PATTERN_NUMBER 14 + +SEQUENCER_TRACK_DATA _track_data; + +void loadLastPattern() +{ + uint8_t last_pattern; + + EEPROM.get(1023, last_pattern); + + if ( last_pattern < PATTERN_NUMBER ) { + loadPattern(last_pattern); + } +} + +void resetPattern(uint8_t number) +{ + uint16_t eeprom_address = PATTERN_SIZE; + + // get address base for pattern + eeprom_address *= number; + + // load defaults + ATOMIC( + _transpose = 0; + _harmonize = 0; + _selected_mode = 0; + ); + // reset sequencer data + for ( uint8_t track = 0; track < TRACK_NUMBER; track++ ) { + ATOMIC(_sequencer[track].mute = true); + clearStackNote(track); + _track_data.step_init_point = 0; + _track_data.step_length = STEP_MAX_SIZE; + // initing note data + for ( uint8_t i = 0; i < STEP_MAX_SIZE; i++ ) { + _track_data.step[i].note = 48; + _track_data.step[i].accent = 0; + _track_data.step[i].glide = 0; + _track_data.step[i].rest = 0; + } + + ATOMIC(memcpy((void*)&_sequencer[track].data, &_track_data, sizeof(SEQUENCER_TRACK_DATA))); + ATOMIC(_sequencer[track].mute = false); + } + // mark pattern slot as not in use + EEPROM.write(eeprom_address, 0); +} + +void loadPattern(uint8_t number) +{ + uint16_t eeprom_address = PATTERN_SIZE; + uint8_t pattern_exist, harmonize, selected_mode = 0; + int8_t transpose = 0; + + if ( number >= PATTERN_NUMBER ) { + return; + } + + // get address base for pattern + eeprom_address *= number; + + // do we have pattern data to read it here? + EEPROM.get(eeprom_address, pattern_exist); + if ( pattern_exist != 1 ) { + resetPattern(number); + // save last pattern loaded + EEPROM.write(1023, number); + return; + } + + // global pattern config + EEPROM.get(++eeprom_address, transpose); + EEPROM.get(++eeprom_address, harmonize); + EEPROM.get(++eeprom_address, selected_mode); + // constrains to avoid trash data from memory + if ( transpose > 12 ) { + transpose = 12; + } else if ( transpose < -12 ) { + transpose = -12; + } + if (selected_mode >= MODES_NUMBER) { + selected_mode = MODES_NUMBER-1; + } + ATOMIC( + _transpose = transpose; + _harmonize = harmonize; + _selected_mode = selected_mode; + ); + // track data + for (uint8_t track=0; track < TRACK_NUMBER; track++) { + ATOMIC(_sequencer[track].mute = true); + clearStackNote(track); + EEPROM.get(++eeprom_address, _track_data); // 34 bytes long + // constrains to avoid trash data from memory + if ( _track_data.step_length > STEP_MAX_SIZE ) { + _track_data.step_length = STEP_MAX_SIZE; + } + ATOMIC(memcpy((void*)&_sequencer[track].data, &_track_data, sizeof(SEQUENCER_TRACK_DATA))); + ATOMIC(_sequencer[track].mute = false); + eeprom_address += (sizeof(SEQUENCER_TRACK_DATA)-1); + } + + // save last pattern loaded + EEPROM.write(1023, number); + + _selected_pattern = number; + +} + +void savePattern(uint8_t number) +{ + uint16_t eeprom_address = PATTERN_SIZE; + + if ( number >= PATTERN_NUMBER ) { + return; + } + + // get address base for pattern + eeprom_address *= number; + + // mark pattern slot as in use pattern_exist + EEPROM.write(eeprom_address, 1); + // global pattern config + EEPROM.write(++eeprom_address, _transpose); + EEPROM.write(++eeprom_address, _harmonize); + EEPROM.write(++eeprom_address, _selected_mode); + // track data + for (uint8_t track=0; track < TRACK_NUMBER; track++) { + memcpy(&_track_data, (void*)&_sequencer[track].data, sizeof(SEQUENCER_TRACK_DATA)); + EEPROM.put(++eeprom_address, _track_data); // 34 bytes long + eeprom_address += (sizeof(SEQUENCER_TRACK_DATA)-1); + } + +} + +void connectPot(uint8_t pot_id, uint8_t pot_pin) +{ + _pot[pot_id].pin = pot_pin; + // get first state data + _pot[pot_id].state = analogRead(_pot[pot_id].pin); + _pot[pot_id].lock = true; +} + +void connectButton(uint8_t button_id, uint8_t button_pin) +{ + _button[button_id].pin = button_pin; + // use internal pullup for buttons + pinMode(_button[button_id].pin, INPUT_PULLUP); + // get first state data + _button[button_id].state = digitalRead(_button[button_id].pin); + _button[button_id].hold_seconds = 0; + _button[button_id].hold_trigger = false; +} + +void lockPotsState(bool lock) +{ + for ( uint8_t i = 0; i < POT_NUMBER; i++ ) { + _pot[i].lock = lock; + } +} + +bool pressed(uint8_t button_id) +{ + bool value; + + value = digitalRead(_button[button_id].pin); + + // using internal pullup pressed button goes LOW + if ( value != _button[button_id].state && value == LOW ) { + _button[button_id].state = value; + return true; + } else { + _button[button_id].state = value; + return false; + } +} + +bool doublePressed(uint8_t button1_id, uint8_t button2_id) +{ + bool value1, value2; + + value1 = digitalRead(_button[button1_id].pin); + value2 = digitalRead(_button[button2_id].pin); + + // using internal pullup pressed button goes LOW + if ( value1 == LOW && value2 == LOW ) { + _button[button1_id].state = LOW; + _button[button2_id].state = LOW; + return true; + } else { + return false; + } +} + +bool holded(uint8_t button_id, uint8_t seconds) +{ + bool value; + + value = digitalRead(_button[button_id].pin); + + // using internal pullup pressed button goes LOW + if ( _button[button_id].hold_trigger == false && value == LOW ) { + if ( _button[button_id].hold_seconds == 0 ) { + _button[button_id].hold_seconds = (uint8_t)(millis()/1000); + } else if ( abs((uint8_t)(millis()/1000) - _button[button_id].hold_seconds) >= seconds ) { + _button[button_id].hold_trigger = true; // avoid released triger after action. + return true; + } + return false; + } else if ( value == HIGH ) { + _button[button_id].hold_trigger = false; + _button[button_id].hold_seconds = 0; + return false; + } +} + +bool released(uint8_t button_id) +{ + bool value; + + value = digitalRead(_button[button_id].pin); + + // using internal pullup released button goes HIGH + if ( value != _button[button_id].state && value == HIGH && _button[button_id].hold_trigger == false ) { + _button[button_id].state = value; + return true; + } else { + _button[button_id].state = value; + return false; + } +} + +int16_t getPotChanges(uint8_t pot_id, uint16_t min_value, uint16_t max_value) +{ + uint16_t value, value_ranged, last_value_ranged; + uint8_t pot_sensitivity = POT_SENSITIVITY; + + // get absolute value + value = analogRead(_pot[pot_id].pin); + + // range that value and our last_value + value_ranged = (value / (ADC_RESOLUTION / ((max_value - min_value) + 1))) + min_value; + last_value_ranged = (_pot[pot_id].state / (ADC_RESOLUTION / ((max_value - min_value) + 1))) + min_value; + + // a lock system to not mess with some data(pots are terrible for some kinda of user interface data controls, but lets keep it low cost!) + if ( _pot[pot_id].lock == true ) { + // user needs to move 1/8 of total adc range to get pot unlocked + if ( abs(value - _pot[pot_id].state) < (ADC_RESOLUTION/8) ) { + return -1; + } + } + + if ( abs(value_ranged - last_value_ranged) >= pot_sensitivity ) { + _pot[pot_id].state = value; + if ( _pot[pot_id].lock == true ) { + _pot[pot_id].lock = false; + } + if ( value_ranged > max_value ) { + value_ranged = max_value; + } + return value_ranged; + } else { + return -1; + } +} diff --git a/MidiExternalSync.ino b/MidiExternalSync.ino new file mode 100644 index 0000000..7a0aeb2 --- /dev/null +++ b/MidiExternalSync.ino @@ -0,0 +1,55 @@ +// MIDI clock, start, stop, note on and note off byte definitions - based on MIDI 1.0 Standards. +#define MIDI_CLOCK 0xF8 +#define MIDI_START 0xFA +#define MIDI_STOP 0xFC + +byte in_data = 0X00; + +// avr general timer2 - 8bits +// 250usecs timmer to read midi input clock/control messages +void setExternalSync(bool on) +{ + if (on == true) { + noInterrupts(); + TCCR2A = 0; // set entire TCCR2A register to 0 + TCCR2B = 0; // same for TCCR2B + TCNT2 = 0; // initialize counter value to 0 + // set compare match register for 4000 Hz increments + OCR2A = 124; // = 16000000 / (32 * 4000) - 1 (must be <256) + // turn on CTC mode + TCCR2B |= (1 << WGM21); + // Set CS22, CS21 and CS20 bits for 32 prescaler + TCCR2B |= (0 << CS22) | (1 << CS21) | (1 << CS20); + // enable timer compare interrupt + TIMSK2 |= (1 << OCIE2A); + interrupts(); + uClock.setMode(uClock.EXTERNAL_CLOCK); + } else { + uClock.setMode(uClock.INTERNAL_CLOCK); + noInterrupts(); + // disable timer compare interrupt + TIMSK2 |= (0 << OCIE2A); + interrupts(); + } +} + +ISR(TIMER2_COMPA_vect, ISR_NOBLOCK) +{ + if(Serial.available() > 0) { + in_data = Serial.read(); + switch (in_data) { + case MIDI_CLOCK: + uClock.clockMe(); + return; + case MIDI_START: + uClock.start(); + return; + case MIDI_STOP: + uClock.stop(); + return; + default: + return; + } + } +} + diff --git a/PageGenerative.ino b/PageGenerative.ino new file mode 100644 index 0000000..d9d2dbd --- /dev/null +++ b/PageGenerative.ino @@ -0,0 +1,187 @@ +/* +[generative] +knobs: ramdon low range note, ramdon high range note, number of notes to use(1 to 12 or 1 to 7 if harmonized), ramdomizer signatures + +buttons: harmonizer scale-, harmonizer scale+, ramdomize it, shift left sequence, shift rigth sequence, play/stop +*/ + +uint8_t _lower_note = 36; +uint8_t _range_note = 34; +uint8_t _accent_probability = ACCENT_PROBABILITY_GENERATION; +uint8_t _glide_probability = GLIDE_PROBABILITY_GENERATION; +uint8_t _rest_probability = REST_PROBABILITY_GENERATION; +uint8_t _tie_probability = TIE_PROBABILITY_GENERATION; +uint8_t _number_of_tones = 3; + +uint8_t _allowed_tones[12] = {0}; + +void shiftSequence(int8_t offset) +{ + uint8_t shift = _sequencer[_selected_track].data.step_init_point+offset; + // clear stack note(also send any note on to off) before shift sequence init point + //clearStackNote(_selected_track); + ATOMIC(_sequencer[_selected_track].data.step_init_point = shift); +} + +void processGenerativeButtons() +{ + + if ( released(GENERIC_BUTTON_1) ) { + // previous harmonic mode + if ( _selected_mode > 0 ) { + ATOMIC(--_selected_mode); + } else if ( _selected_mode == 0 ) { + ATOMIC(_harmonize = 0); + } + } + + if ( released(GENERIC_BUTTON_2) ) { + // next harmonic mode + if ( _selected_mode < MODES_NUMBER-1 ) { + if ( _harmonize == 0 ) { + ATOMIC(_harmonize = 1); + } else { + ATOMIC(++_selected_mode); + } + } + } + + if ( pressed(GENERIC_BUTTON_3) ) { + // ramdomize it! + acidRandomize(); + } + + if ( pressed(GENERIC_BUTTON_4) ) { + shiftSequence(-1); + } + + if ( pressed(GENERIC_BUTTON_5) ) { + shiftSequence(1); + } + +} + +void processGenerativeLeds() +{ + + if ( _harmonize == 1 ) { + if ( _selected_mode == MODES_NUMBER-1 ) { + digitalWrite(GENERIC_LED_1, LOW); + digitalWrite(GENERIC_LED_2, HIGH); + } else { + digitalWrite(GENERIC_LED_1, LOW); + digitalWrite(GENERIC_LED_2, LOW); + } + } else { + digitalWrite(GENERIC_LED_1, HIGH); + digitalWrite(GENERIC_LED_2, LOW); + } + + digitalWrite(GENERIC_LED_3, LOW); + digitalWrite(GENERIC_LED_4, LOW); + digitalWrite(GENERIC_LED_5, LOW); + +} + +void processGenerativePots() +{ + uint16_t value; + + // GENERIC_POT_1: lower range note to be generated + value = getPotChanges(GENERIC_POT_1, 0, 127); + if ( value != -1 ) { + _lower_note = value; + } + + // GENERIC_POT_2: high range note to be generated + value = getPotChanges(GENERIC_POT_2, 0, 127); + if ( value != -1 ) { + _range_note = value; + } + + // GENERIC_POT_3: number of notes to use on sequence(1 to 12 or 1 to 7 if harmonized) + if ( _harmonize == 1 ) { + value = getPotChanges(GENERIC_POT_3, 1, 7); + } else if ( _harmonize == 0 ) { + value = getPotChanges(GENERIC_POT_3, 1, 12); + } + if ( value != -1 ) { + _number_of_tones = value; + uint8_t note = 0; + for ( uint8_t i=0; i < 12; i++ ) { + if ( i%(12/_number_of_tones) == 0 && i != 0 ) { + note += (12/_number_of_tones); + } + _allowed_tones[i] = note; + } + } + + // GENERIC_POT_4: ramdomizer signatures + value = getPotChanges(GENERIC_POT_4, 0, 80); + if ( value != -1 ) { + //_accent_probability = value???; + //_glide_probability value???; + _rest_probability = 80-value; + } + +} + +uint8_t getNoteByMaxNumOfTones(uint8_t note) +{ + uint8_t octave, relative_note; + + octave = note/12; + relative_note = note%12; + return _allowed_tones[relative_note] + _lower_note + (octave*12); +} + +void acidRandomize() +{ + uint8_t note, high_note, accent, glide, tie, rest, last_step; + + // random it all + ATOMIC(_sequencer[_selected_track].mute = true); + clearStackNote(_selected_track); + + for ( uint16_t i = 0; i < STEP_MAX_SIZE; i++ ) { + + // step on/off + _sequencer[_selected_track].data.step[i].rest = random(0, 100) < _rest_probability ? 0 : 1; + + // random tie and reset accent and glide in case of a rest + if (_sequencer[_selected_track].data.step[i].rest) { + + _sequencer[_selected_track].data.step[i].note = 36; + _sequencer[_selected_track].data.step[i].accent = 0; + _sequencer[_selected_track].data.step[i].glide = 0; + _sequencer[_selected_track].data.step[i].tie = 0; + + if (i == 0) { + last_step = STEP_MAX_SIZE-1; + } else { + last_step = i-1; + } + + // only tie probrablity when last step has a note or another tie event + if (_sequencer[_selected_track].data.step[last_step].rest == 0 || _sequencer[_selected_track].data.step[last_step].tie == 1) + _sequencer[_selected_track].data.step[i].tie = random(0, 100) < _tie_probability ? 1 : 0; + + continue; + + } + + high_note = _lower_note+_range_note; + if ( high_note > 127 ) { + high_note = 127; + } + note = getNoteByMaxNumOfTones(random(_lower_note, high_note)); + + accent = random(0, 100) < _accent_probability ? 1 : 0; + glide = random(0, 100) < _glide_probability ? 1 : 0; + + _sequencer[_selected_track].data.step[i].note = note; + _sequencer[_selected_track].data.step[i].accent = accent; + _sequencer[_selected_track].data.step[i].glide = glide; + } + ATOMIC(_sequencer[_selected_track].mute = false); +} diff --git a/PageLive.ino b/PageLive.ino new file mode 100644 index 0000000..c1cdf58 --- /dev/null +++ b/PageLive.ino @@ -0,0 +1,177 @@ +/* +[midi controller] +knobs: cutoff freq./decay, resonance/accent, env mod/tunning, tempo + +buttons: previous pattern, next pattern, ctrl A/ctrl B, tempo -, tempo +, play/stop +*/ +// TODO: implement pickup by value for controllers + +uint8_t _selected_ctrl = 0; +// used for led visual feedback on buttons hold action +bool _pattern_saved = false; +bool _pattern_cleared = false; +uint32_t _page_live_blink_timer = 0; +uint32_t _feedback_blink_timer = 0; + +void processControllerButtons() +{ + + // previous pattern + if ( released(GENERIC_BUTTON_1) ) { + if ( _selected_pattern != 0 ) { + lockPotsState(true); + // auto save? + //savePattern(_selected_pattern); + loadPattern(--_selected_pattern); + } + } + + // next pattern + if ( released(GENERIC_BUTTON_2) ) { + if ( _selected_pattern < PATTERN_NUMBER-1 ) { + lockPotsState(true); + // auto save? + //savePattern(_selected_pattern); + loadPattern(++_selected_pattern); + } + } + + // save pattern + if ( holded(GENERIC_BUTTON_1, 2) ) { + savePattern(_selected_pattern); + _pattern_saved = true; + _feedback_blink_timer = millis(); + } + + // reset and delete pattern + if ( holded(GENERIC_BUTTON_2, 2) ) { + resetPattern(_selected_pattern); + _pattern_cleared = true; + _feedback_blink_timer = millis(); + } + + // toogle between ctrl A and ctrl B setup for potentiometers + if ( pressed(GENERIC_BUTTON_3) ) { + lockPotsState(true); + _selected_ctrl = !_selected_ctrl; + } + + // decrement 1 bpm from tempo + if ( pressed(GENERIC_BUTTON_4) ) { + uClock.setTempo(uClock.getTempo()-1); + } + + // increment 1 bpm from tempo + if ( pressed(GENERIC_BUTTON_5) ) { + uClock.setTempo(uClock.getTempo()+1); + } + +} + +void processControllerLeds() +{ + static bool blink_state = true; + + // blink interface here for button 3 to 5 + if ( millis() - _page_live_blink_timer >= 150 ) { + blink_state = !blink_state; + _page_live_blink_timer = millis(); + } + + if ( _pattern_saved == true ) { + digitalWrite(GENERIC_LED_1 , blink_state); + if ( millis() - _feedback_blink_timer >= 600 ) { + _pattern_saved = false; + } + } else if ( _selected_pattern == 0 ) { // first pattern? + digitalWrite(GENERIC_LED_1 , HIGH); + } else { + digitalWrite(GENERIC_LED_1 , LOW); + } + + if ( _pattern_cleared == true ) { + digitalWrite(GENERIC_LED_2, blink_state); + if ( millis() - _feedback_blink_timer >= 600 ) { + _pattern_cleared = false; + } + } else if ( _selected_pattern == PATTERN_NUMBER-1 ) { // last pattern? + digitalWrite(GENERIC_LED_2 , HIGH); + } else { + digitalWrite(GENERIC_LED_2 , LOW); + } + + if ( _selected_ctrl == 0 ) { + digitalWrite(GENERIC_LED_3, LOW); + } else if ( _selected_ctrl == 1 ) { + digitalWrite(GENERIC_LED_3, HIGH); + } + + digitalWrite(GENERIC_LED_4, LOW); + digitalWrite(GENERIC_LED_5, LOW); +} + +void processControllerPots() +{ + uint16_t value; + uint8_t ctrl; + +#ifdef USE_MIDI_CTRL + + // GENERIC_POT_1: cutoff freq./decay + value = getPotChanges(GENERIC_POT_1, 0, 127); + if ( value != -1 ) { + // send cc + if ( _selected_ctrl == 0 ) { + ctrl = MIDI_CTRL_CUTOFF; + } else if ( _selected_ctrl == 1 ) { + ctrl = MIDI_CTRL_DECAY; + } + ATOMIC(sendMidiMessage(MIDI_CC, ctrl, value, _sequencer[_selected_track].channel)) + } + + // GENERIC_POT_2: resonance/accent + value = getPotChanges(GENERIC_POT_2, 0, 127); + if ( value != -1 ) { + // send cc + if ( _selected_ctrl == 0 ) { + ctrl = MIDI_CTRL_RESONANCE; + } else if ( _selected_ctrl == 1 ) { + ctrl = MIDI_CTRL_ACCENT; + } + ATOMIC(sendMidiMessage(MIDI_CC, ctrl, value, _sequencer[_selected_track].channel)) + } + + // GENERIC_POT_3: env mod/wave + value = getPotChanges(GENERIC_POT_3, 0, 127); + if ( value != -1 ) { + // send cc + if ( _selected_ctrl == 0 ) { + ctrl = MIDI_CTRL_ENVMOD; + } else if ( _selected_ctrl == 1 ) { + ctrl = MIDI_CTRL_WAVE; + } + ATOMIC(sendMidiMessage(MIDI_CC, ctrl, value, _sequencer[_selected_track].channel)) + } + +#endif + + // GENERIC_POT_4: sequencer step length/global harmonic mode transpose + if ( _selected_ctrl == 0 ) { + value = getPotChanges(GENERIC_POT_4, 1, STEP_MAX_SIZE); + if ( value != -1 ) { + //clearStackNote(_selected_track); + ATOMIC(_sequencer[_selected_track].data.step_length = value); + if ( _step_edit >= _sequencer[_selected_track].data.step_length ) { + _step_edit = _sequencer[_selected_track].data.step_length-1; + } + } + } else if ( _selected_ctrl == 1 ) { + value = getPotChanges(GENERIC_POT_4, 0, 24); + if ( value != -1 ) { + //clearStackNote(); + // -12 (0) +12 + ATOMIC(_transpose = value-12); + } + } + +} diff --git a/PageSequencer.ino b/PageSequencer.ino new file mode 100644 index 0000000..4aec2b0 --- /dev/null +++ b/PageSequencer.ino @@ -0,0 +1,183 @@ +/* +[step edit] +knobs: octave, note, global tunning, sequence length + +buttons: prev step, next step, rest, glide/tie, accent, play/stop +*/ +void sendPreviewNote(uint8_t step) +{ + unsigned long milliTime, preMilliTime; + uint8_t note; + + // enable or disable harmonizer + if ( _harmonize == 1 ) { + note = harmonizer(_sequencer[_selected_track].data.step[step].note); + } else { + note = _sequencer[_selected_track].data.step[step].note; + } + ATOMIC(sendMidiMessage(NOTE_ON, note, _sequencer[_selected_track].data.step[step].accent ? ACCENT_VELOCITY : NOTE_VELOCITY, _sequencer[_selected_track].channel)) + + // avoid delay() call because of uClock timmer1 usage + //delay(200); + preMilliTime = millis(); + while ( true ) { + milliTime = millis(); + if (abs(milliTime - preMilliTime) >= 200) { + break; + } + } + + ATOMIC(sendMidiMessage(NOTE_OFF, note, 0, _sequencer[_selected_track].channel)) +} + +void processSequencerPots() +{ + static int8_t octave, note, step_note; + static int16_t value; + uint8_t relative_step = uint8_t(_step_edit + _sequencer[_selected_track].data.step_init_point) % _sequencer[_selected_track].data.step_length; + + // GENERIC_POT_1: Note Octave Selector + octave = getPotChanges(GENERIC_POT_1, 0, 10); + if ( octave != -1 ) { + _last_octave = octave; + } + + // GENERIC_POT_2: Note Selector (generic C to B, no octave) + note = getPotChanges(GENERIC_POT_2, 0, 11); + if ( note != -1 ) { + _last_note = note; + } + + // changes on octave or note pot? + if ( octave != -1 || note != -1 ) { + //ATOMIC(_sequencer[_selected_track].data.step[relative_step].note = (_last_octave * 8) + _last_note); + note = (_last_octave * 8) + _last_note; + ATOMIC(_sequencer[_selected_track].data.step[relative_step].note = note); + if ( _playing == false && _sequencer[_selected_track].data.step[relative_step].rest == 0 ) { + sendPreviewNote(relative_step); + } + } + + // GENERIC_POT_3: global tunning (afects booth tracks) or track tunning + value = getPotChanges(GENERIC_POT_3, 0, 24); + if ( value != -1 ) { + //clearStackNote(); + // -12 (0) +12 + ATOMIC(_transpose = value-12); + } + + // GENERIC_POT_4: sequencer step length + value = getPotChanges(GENERIC_POT_4, 1, STEP_MAX_SIZE); + if ( value != -1 ) { + //clearStackNote(_selected_track); + ATOMIC(_sequencer[_selected_track].data.step_length = value); + if ( relative_step >= _sequencer[_selected_track].data.step_length ) { + _step_edit = _sequencer[_selected_track].data.step_length-1; + } + } + +} + +void processSequencerButtons() +{ + uint8_t relative_step = uint8_t(_step_edit + _sequencer[_selected_track].data.step_init_point) % _sequencer[_selected_track].data.step_length; + + // previous step edit + if ( released(GENERIC_BUTTON_1) ) { + if ( _step_edit != 0 ) { + // add a lock here for octave and note to not mess with edit mode when moving steps around + lockPotsState(true); + --_step_edit; + } + if ( _playing == false && _sequencer[_selected_track].data.step[relative_step].rest == 0 ) { + sendPreviewNote(relative_step-1); + } + } + + // next step edit + if ( released(GENERIC_BUTTON_2) ) { + if ( _step_edit < _sequencer[_selected_track].data.step_length-1 ) { + // add a lock here for octave and note to not mess with edit mode when moving steps around + lockPotsState(true); + ++_step_edit; + } + if ( _playing == false && _sequencer[_selected_track].data.step[relative_step].rest == 0 ) { + sendPreviewNote(relative_step+1); + } + } + + // step rest + if ( pressed(GENERIC_BUTTON_3) ) { + ATOMIC(_sequencer[_selected_track].data.step[relative_step].rest = !_sequencer[_selected_track].data.step[relative_step].rest); + if ( _playing == false && _sequencer[_selected_track].data.step[relative_step].rest == 0 ) { + sendPreviewNote(relative_step); + } + } + + // step glide/tie + if ( pressed(GENERIC_BUTTON_4) ) { + // if last step is on or it has a tie, we manage tie data + if ((_sequencer[_selected_track].data.step[relative_step-1].rest == 0 || _sequencer[_selected_track].data.step[relative_step-1].tie) && _sequencer[_selected_track].data.step[relative_step].rest && relative_step != 0) { + ATOMIC(_sequencer[_selected_track].data.step[relative_step].tie = !_sequencer[_selected_track].data.step[relative_step].tie); + // otherwise glide step + } else { + ATOMIC(_sequencer[_selected_track].data.step[relative_step].glide = !_sequencer[_selected_track].data.step[relative_step].glide); + } + } + + // step accent + if ( pressed(GENERIC_BUTTON_5) ) { + ATOMIC(_sequencer[_selected_track].data.step[relative_step].accent = !_sequencer[_selected_track].data.step[relative_step].accent); + if ( _playing == false && _sequencer[_selected_track].data.step[relative_step].rest == 0 ) { + sendPreviewNote(relative_step); + } + } +} + +void processSequencerLeds() +{ + uint8_t relative_step = uint8_t(_step_edit + _sequencer[_selected_track].data.step_init_point) % _sequencer[_selected_track].data.step_length; + + // Editing First Step? + if ( _step_edit == 0 ) { + digitalWrite(GENERIC_LED_1 , HIGH); + } else { + digitalWrite(GENERIC_LED_1 , LOW); + } + + // Editing Last Step? + if ( _step_edit == _sequencer[_selected_track].data.step_length-1 ) { + digitalWrite(GENERIC_LED_2 , HIGH); + } else { + digitalWrite(GENERIC_LED_2 , LOW); + } + + // Rest + if ( _sequencer[_selected_track].data.step[relative_step].rest == 1 ) { + digitalWrite(GENERIC_LED_3 , HIGH); + } else { + digitalWrite(GENERIC_LED_3 , LOW); + } + + // Glide/Tie + uint8_t tie_glide_staus = 0; + // if last step is on or it has a tie, check for tie event + if ((_sequencer[_selected_track].data.step[relative_step-1].rest == 0 || _sequencer[_selected_track].data.step[relative_step-1].tie) && _sequencer[_selected_track].data.step[relative_step].rest && relative_step != 0) { + tie_glide_staus = _sequencer[_selected_track].data.step[relative_step].tie; + } else { + tie_glide_staus = _sequencer[_selected_track].data.step[relative_step].glide; + } + + if ( tie_glide_staus ) { + digitalWrite(GENERIC_LED_4 , HIGH); + } else { + digitalWrite(GENERIC_LED_4 , LOW); + } + + // Accent + if ( _sequencer[_selected_track].data.step[relative_step].accent == 1 ) { + digitalWrite(GENERIC_LED_5 , HIGH); + } else { + digitalWrite(GENERIC_LED_5 , LOW); + } +} diff --git a/README.md b/README.md index 24cd1b2..6fc6395 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ -# AciduinoCV - -AciduinoCV +# AciduinoCV -Fork of Aciduino V1 sequencer by midilab (https://github.com/midilab/aciduino/tree/master/v1/Aciduino) that adds i2c CV and Gate outputs \ No newline at end of file +Arduino-based 2-channel MIDI and CV sequencer. + +This project is based on [Aciduino V1 by midilab](https://github.com/midilab/aciduino/tree/master/v1/Aciduino) (MIT licence). +Original code © midilab contact@midilab.co, licensed under MIT (see below). +Modifications © Oleksiy Hrachov, licensed under GPLv3. diff --git a/UserInterface.ino b/UserInterface.ino new file mode 100644 index 0000000..62277ad --- /dev/null +++ b/UserInterface.ino @@ -0,0 +1,187 @@ +/* +[page select]: press button1 and button2 together +knobs: none, none, none, none + +buttons: track 1, track 2, [live mode], [generative], [step edit], play/stop +*/ + +uint32_t _page_blink_timer = 0; +uint8_t _bpm_blink_timer = 1; +uint8_t _selected_page = 0; + +void configureInterface() +{ + // Buttons config + connectButton(GENERIC_BUTTON_1, GENERIC_BUTTON_1_PIN); + connectButton(GENERIC_BUTTON_2, GENERIC_BUTTON_2_PIN); + connectButton(GENERIC_BUTTON_3, GENERIC_BUTTON_3_PIN); + connectButton(GENERIC_BUTTON_4, GENERIC_BUTTON_4_PIN); + connectButton(GENERIC_BUTTON_5, GENERIC_BUTTON_5_PIN); + connectButton(GENERIC_BUTTON_6, GENERIC_BUTTON_6_PIN); + + // Pots config + connectPot(GENERIC_POT_1, GENERIC_POT_1_PIN); + connectPot(GENERIC_POT_2, GENERIC_POT_2_PIN); + connectPot(GENERIC_POT_3, GENERIC_POT_3_PIN); + connectPot(GENERIC_POT_4, GENERIC_POT_4_PIN); + + // Leds config + pinMode(GENERIC_LED_1, OUTPUT); + pinMode(GENERIC_LED_2, OUTPUT); + pinMode(GENERIC_LED_3, OUTPUT); + pinMode(GENERIC_LED_4, OUTPUT); + pinMode(GENERIC_LED_5, OUTPUT); + pinMode(GENERIC_LED_6, OUTPUT); + digitalWrite(GENERIC_LED_1, LOW); + digitalWrite(GENERIC_LED_2, LOW); + digitalWrite(GENERIC_LED_3, LOW); + digitalWrite(GENERIC_LED_4, LOW); + digitalWrite(GENERIC_LED_5, LOW); + digitalWrite(GENERIC_LED_6, LOW); + + // first read to fill our registers + getPotChanges(GENERIC_POT_1, 0, ADC_RESOLUTION); + getPotChanges(GENERIC_POT_2, 0, ADC_RESOLUTION); + getPotChanges(GENERIC_POT_3, 0, ADC_RESOLUTION); + getPotChanges(GENERIC_POT_4, 0, ADC_RESOLUTION); +} + +void processInterface() +{ + static int16_t tempo; + + // set external sync on/off + if ( holded(GENERIC_BUTTON_6, 2) ) { + if ( uClock.getMode() == uClock.INTERNAL_CLOCK ) { + setExternalSync(true); + } else { + setExternalSync(false); + } + } + + // global controllers play and tempo + // play/stop + if ( pressed(GENERIC_BUTTON_6) ) { + if ( _playing == false ) { + // Starts the clock, tick-tac-tick-tac... + uClock.start(); + } else { + // stop the clock + uClock.stop(); + } + } + + // internal/external led control + if ( uClock.getMode() == uClock.INTERNAL_CLOCK ) { + if ( _playing == false ) { + digitalWrite(GENERIC_LED_6 , LOW); + } + } else { + // external clock keeps the timer led always on + digitalWrite(GENERIC_LED_6 , HIGH); + } + + // page select request + if ( doublePressed(GENERIC_BUTTON_1, GENERIC_BUTTON_2) ) { + lockPotsState(true); + _selected_page = 0; + } + + switch ( _selected_page ) { + + // Select Track/Page + case 0: + processPageButtons(); + processPageLeds(); + break; + + // Midi controller + case 1: + processControllerButtons(); + processControllerLeds(); + processControllerPots(); + break; + + // Generative + case 2: + processGenerativeButtons(); + processGenerativeLeds(); + processGenerativePots(); + break; + + // Sequencer + case 3: + processSequencerButtons(); + processSequencerLeds(); + processSequencerPots(); + break; + } + +} + +void processPageButtons() +{ + + if ( pressed(GENERIC_BUTTON_1) ) { + _selected_track = 0; + } + + if ( pressed(GENERIC_BUTTON_2) ) { + _selected_track = 1; + } + + if ( pressed(GENERIC_BUTTON_3) ) { + lockPotsState(true); + _selected_page = 1; + } + + if ( pressed(GENERIC_BUTTON_4) ) { + lockPotsState(true); + _selected_page = 2; + } + + if ( pressed(GENERIC_BUTTON_5) ) { + lockPotsState(true); + _selected_page = 3; + } + +} + +void processPageLeds() +{ + static bool blink_state = true; + + // blink interface here for button 3 to 5 + if ( millis() - _page_blink_timer >= 300 ) { + blink_state = !blink_state; + _page_blink_timer = millis(); + } + + digitalWrite(GENERIC_LED_3, blink_state); + digitalWrite(GENERIC_LED_4, blink_state); + digitalWrite(GENERIC_LED_5, blink_state); + + if ( _selected_track == 0 ) { + digitalWrite(GENERIC_LED_1, HIGH); + digitalWrite(GENERIC_LED_2, LOW); + } else if ( _selected_track == 1 ) { + digitalWrite(GENERIC_LED_1, LOW); + digitalWrite(GENERIC_LED_2, HIGH); + } +} + +void tempoInterface(uint32_t * tick) +{ + if (uClock.getMode() == uClock.INTERNAL_CLOCK) { + // BPM led indicator + if ( !(*tick % (96)) || (*tick == 0) ) { // first compass step will flash longer + _bpm_blink_timer = 8; + digitalWrite(GENERIC_LED_6 , HIGH); + } else if ( !(*tick % (24)) ) { // each quarter led on + digitalWrite(GENERIC_LED_6 , HIGH); + } else if ( !(*tick % _bpm_blink_timer) ) { // get led off + digitalWrite(GENERIC_LED_6 , LOW); + _bpm_blink_timer = 1; + } + } +} diff --git a/config.h b/config.h new file mode 100644 index 0000000..ee2466d --- /dev/null +++ b/config.h @@ -0,0 +1,99 @@ +#ifndef __CONFIG_H__ +#define __CONFIG_H__ + +// +// MIDI Config +// +// MIDI_STANDARD(0) for 31250 standard compilant MIDI devices(use with MIDI 5 pin connector) +// MIDI_SERIAL(1) for 115200 compilant with serial devices(use with PC and a serial-to-midi converter) +#define MIDI_MODE 0 +// MIDI Channel +#define TRACK1_CHANNEL 1 +#define TRACK2_CHANNEL 2 + +// +// MIDI Controller config +// +#define USE_MIDI_CTRL +#define MIDI_CTRL_TUNNING 79 +#define MIDI_CTRL_CUTOFF 80 +#define MIDI_CTRL_RESONANCE 81 +#define MIDI_CTRL_ENVMOD 82 +#define MIDI_CTRL_DECAY 83 +#define MIDI_CTRL_ACCENT 84 +#define MIDI_CTRL_WAVE 85 + +// +// User interface config +// +#define SEQUENCER_MIN_BPM 50 +#define SEQUENCER_MAX_BPM 177 + +// +// Generative config +// +#define ACCENT_PROBABILITY_GENERATION 50 +#define GLIDE_PROBABILITY_GENERATION 30 +#define TIE_PROBABILITY_GENERATION 80 +#define REST_PROBABILITY_GENERATION 10 + +// +// Sequencer config +// +#define TRACK_NUMBER 2 // you can go up to 8 but no interface ready to control it +#define STEP_MAX_SIZE 16 +#define NOTE_LENGTH 3 // min: 1 max: 5 DO NOT EDIT BEYOND!!! +#define NOTE_VELOCITY 90 +#define ACCENT_VELOCITY 127 + +// +// Hardware config +// +#define POT_NUMBER 4 +#define BUTTON_NUMBER 6 + +// Hardware config +#define ADC_RESOLUTION 1024 +#define POT_SENSITIVITY 2 +// Pin configuration(double check your schematic before configure those pins) +// Pots +#define GENERIC_POT_1_PIN A3 +#define GENERIC_POT_2_PIN A2 +#define GENERIC_POT_3_PIN A1 +#define GENERIC_POT_4_PIN A0 +// Buttons +#define GENERIC_BUTTON_1_PIN 2 +#define GENERIC_BUTTON_2_PIN 3 +#define GENERIC_BUTTON_3_PIN 4 +#define GENERIC_BUTTON_4_PIN 5 +#define GENERIC_BUTTON_5_PIN 6 +#define GENERIC_BUTTON_6_PIN 7 +// Leds +#define GENERIC_LED_1 8 +#define GENERIC_LED_2 9 +#define GENERIC_LED_3 10 +#define GENERIC_LED_4 11 +#define GENERIC_LED_5 12 +#define GENERIC_LED_6 13 + +typedef enum { + GENERIC_POT_1, + GENERIC_POT_2, + GENERIC_POT_3, + GENERIC_POT_4 +} POT_HARDWARE_INTERFACE; + +typedef enum { + GENERIC_BUTTON_1, + GENERIC_BUTTON_2, + GENERIC_BUTTON_3, + GENERIC_BUTTON_4, + GENERIC_BUTTON_5, + GENERIC_BUTTON_6 +} BUTTON_HARDWARE_INTERFACE; + +// CV DACs +#define DAC_1_ADDR 0x60 +#define DAC_2_ADDR 0x61 + +#endif diff --git a/uClock.cpp b/uClock.cpp new file mode 100644 index 0000000..195c442 --- /dev/null +++ b/uClock.cpp @@ -0,0 +1,454 @@ +/*! + * @file uClock.cpp + * Project BPM clock generator for Arduino + * @brief A Library to implement BPM clock tick calls using hardware timer interruption. Tested on ATmega168/328, ATmega16u4/32u4 and ATmega2560 and Teensy LC. + * @version 1.0.0 + * @author Romulo Silva + * @date 01/04/2022 + * @license MIT - (c) 2022 - Romulo Silva - contact@midilab.co + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#include "uClock.h" + +// +// Timer setup for work clock +// +#if defined(TEENSYDUINO) && !defined(__AVR_ATmega32U4__) +IntervalTimer _uclockTimer; +void uclockISR(); +void uclockInitTimer() +{ + ATOMIC( + + // begin at 120bpm (20833us) + _uclockTimer.begin(uclockISR, 20833); + + // Set the interrupt priority level, controlling which other interrupts + // this timer is allowed to interrupt. Lower numbers are higher priority, + // with 0 the highest and 255 the lowest. Most other interrupts default to 128. + // As a general guideline, interrupt routines that run longer should be given + // lower priority (higher numerical values). + _uclockTimer.priority(0); + ) +} +#else +void uclockInitTimer() +{ + ATOMIC( + // Timer1 init + // begin at 120bpm (48.0007680122882 Hz) + TCCR1A = 0; // set entire TCCR1A register to 0 + TCCR1B = 0; // same for TCCR1B + TCNT1 = 0; // initialize counter value to 0 + // set compare match register for 48.0007680122882 Hz increments + OCR1A = 41665; // = 16000000 / (8 * 48.0007680122882) - 1 (must be <65536) + // turn on CTC mode + TCCR1B |= (1 << WGM12); + // Set CS12, CS11 and CS10 bits for 8 prescaler + TCCR1B |= (0 << CS12) | (1 << CS11) | (0 << CS10); + // enable timer compare interrupt + TIMSK1 |= (1 << OCIE1A); + ) +} +#endif + +namespace umodular { namespace clock { + +static inline uint32_t phase_mult(uint32_t val) +{ + return (val * PHASE_FACTOR) >> 8; +} + +static inline uint32_t clock_diff(uint32_t old_clock, uint32_t new_clock) +{ + if (new_clock >= old_clock) { + return new_clock - old_clock; + } else { + return new_clock + (4294967295 - old_clock); + } +} + +uClockClass::uClockClass() +{ + tempo = 120; + start_timer = 0; + last_interval = 0; + sync_interval = 0; + state = PAUSED; + mode = INTERNAL_CLOCK; + resetCounters(); + + onClock96PPQNCallback = NULL; + onClock32PPQNCallback = NULL; + onClock16PPQNCallback = NULL; + onClockStartCallback = NULL; + onClockStopCallback = NULL; + + // first interval calculus + setTempo(tempo); +} + +void uClockClass::init() +{ + uclockInitTimer(); +} + +void uClockClass::start() +{ + resetCounters(); + start_timer = millis(); + + if (onClockStartCallback) { + onClockStartCallback(); + } + + if (mode == INTERNAL_CLOCK) { + state = STARTED; + } else { + state = STARTING; + } +} + +void uClockClass::stop() +{ + state = PAUSED; + start_timer = 0; + resetCounters(); + if (onClockStopCallback) { + onClockStopCallback(); + } +} + +void uClockClass::pause() +{ + if (mode == INTERNAL_CLOCK) { + if (state == PAUSED) { + start(); + } else { + stop(); + } + } +} + +void uClockClass::setTimerTempo(float bpm) +{ + // 96 ppqn resolution + tick_us_interval = (60000000 / 24 / bpm); + tick_hertz_interval = 1/((float)tick_us_interval/1000000); + +#if defined(TEENSYDUINO) && !defined(__AVR_ATmega32U4__) + ATOMIC( + _uclockTimer.update(tick_us_interval); + ) +#else + uint32_t ocr; + uint8_t tccr = 0; + + // 16bits avr timer setup + if ((ocr = AVR_CLOCK_FREQ / ( tick_hertz_interval * 1 )) < 65535) { + // Set CS12, CS11 and CS10 bits for 1 prescaler + tccr |= (0 << CS12) | (0 << CS11) | (1 << CS10); + } else if ((ocr = AVR_CLOCK_FREQ / ( tick_hertz_interval * 8 )) < 65535) { + // Set CS12, CS11 and CS10 bits for 8 prescaler + tccr |= (0 << CS12) | (1 << CS11) | (0 << CS10); + } else if ((ocr = AVR_CLOCK_FREQ / ( tick_hertz_interval * 64 )) < 65535) { + // Set CS12, CS11 and CS10 bits for 64 prescaler + tccr |= (0 << CS12) | (1 << CS11) | (1 << CS10); + } else if ((ocr = AVR_CLOCK_FREQ / ( tick_hertz_interval * 256 )) < 65535) { + // Set CS12, CS11 and CS10 bits for 256 prescaler + tccr |= (1 << CS12) | (0 << CS11) | (0 << CS10); + } else if ((ocr = AVR_CLOCK_FREQ / ( tick_hertz_interval * 1024 )) < 65535) { + // Set CS12, CS11 and CS10 bits for 1024 prescaler + tccr |= (1 << CS12) | (0 << CS11) | (1 << CS10); + } else { + // tempo not achiavable + return; + } + + ATOMIC( + TCCR1B = 0; + OCR1A = ocr-1; + TCCR1B |= (1 << WGM12); + TCCR1B |= tccr; + ) +#endif +} + +void uClockClass::setTempo(float bpm) +{ + if (mode == EXTERNAL_CLOCK) { + return; + } + + if (bpm < MIN_BPM || bpm > MAX_BPM) { + return; + } + + setTimerTempo(bpm); + + tempo = bpm; +} + +float inline uClockClass::freqToBpm(uint32_t freq) +{ + float usecs = 1/((float)freq/1000000.0); + return (float)((float)(usecs/24.0) * 60.0); +} + +float uClockClass::getTempo() +{ + if (mode == EXTERNAL_CLOCK) { + uint32_t acc = 0; + for (uint8_t i=0; i < EXT_INTERVAL_BUFFER_SIZE; i++) { + acc += ext_interval_buffer[i]; + } + if (acc != 0) { + return freqToBpm(acc / EXT_INTERVAL_BUFFER_SIZE); + } + } + return tempo; +} + +void uClockClass::setMode(uint8_t tempo_mode) +{ + mode = tempo_mode; +} + +uint8_t uClockClass::getMode() +{ + return mode; +} + +void uClockClass::clockMe() +{ + if (mode == EXTERNAL_CLOCK) { + ATOMIC( + handleExternalClock() + ) + } +} + +void uClockClass::resetCounters() +{ + external_clock = 0; + internal_tick = 0; + external_tick = 0; + div32th_counter = 0; + div16th_counter = 0; + mod6_counter = 0; + indiv32th_counter = 0; + indiv16th_counter = 0; + inmod6_counter = 0; + ext_interval_idx = 0; +} + +// TODO: Tap stuff +void uClockClass::tap() +{ + // tap me +} + +// TODO: Shuffle stuff +void uClockClass::shuffle() +{ + // shuffle me +} + +void uClockClass::handleExternalClock() +{ + + switch (state) { + case PAUSED: + break; + + case STARTING: + state = STARTED; + external_clock = micros(); + break; + + case STARTED: + + uint32_t u_timer = micros(); + last_interval = clock_diff(external_clock, u_timer); + external_clock = u_timer; + + if (inmod6_counter == 0) { + indiv16th_counter++; + indiv32th_counter++; + } + + if (inmod6_counter == 3) { + indiv32th_counter++; + } + + // slave tick me! + external_tick++; + inmod6_counter++; + + if (inmod6_counter == 6) { + inmod6_counter = 0; + } + + // accumulate interval incomming ticks data for getTempo() smooth reads on slave mode + if(++ext_interval_idx >= EXT_INTERVAL_BUFFER_SIZE) { + ext_interval_idx = 0; + } + ext_interval_buffer[ext_interval_idx] = last_interval; + + if (external_tick == 1) { + interval = last_interval; + } else { + interval = (((uint32_t)interval * (uint32_t)PLL_X) + (uint32_t)(256 - PLL_X) * (uint32_t)last_interval) >> 8; + } + break; + } +} + +void uClockClass::handleTimerInt() +{ + if (mode == EXTERNAL_CLOCK) { + // sync tick position with external tick clock + if ((internal_tick < external_tick) || (internal_tick > (external_tick + 1))) { + internal_tick = external_tick; + div32th_counter = indiv32th_counter; + div16th_counter = indiv16th_counter; + mod6_counter = inmod6_counter; + } + + uint32_t counter = interval; + uint32_t u_timer = micros(); + sync_interval = clock_diff(external_clock, u_timer); + + if (internal_tick <= external_tick) { + counter -= phase_mult(sync_interval); + } else { + if (counter > sync_interval) { + counter += phase_mult(counter - sync_interval); + } + } + + // update internal clock timer frequency + float bpm = freqToBpm(counter); + if (bpm != tempo) { + if (bpm >= MIN_BPM && bpm <= MAX_BPM) { + tempo = bpm; + setTimerTempo(tempo); + } + } + } + + if (onClock96PPQNCallback) { + onClock96PPQNCallback(&internal_tick); + } + + if (mod6_counter == 0) { + if (onClock32PPQNCallback) { + onClock32PPQNCallback(&div32th_counter); + } + if (onClock16PPQNCallback) { + onClock16PPQNCallback(&div16th_counter); + } + div16th_counter++; + div32th_counter++; + } + + if (mod6_counter == 3) { + if (onClock32PPQNCallback) { + onClock32PPQNCallback(&div32th_counter); + } + div32th_counter++; + } + + // tick me! + internal_tick++; + mod6_counter++; + + if (mod6_counter == 6) { + mod6_counter = 0; + } + +} + +// elapsed time support +uint8_t uClockClass::getNumberOfSeconds(uint32_t time) +{ + if ( time == 0 ) { + return time; + } + return ((_timer - time) / 1000) % SECS_PER_MIN; +} + +uint8_t uClockClass::getNumberOfMinutes(uint32_t time) +{ + if ( time == 0 ) { + return time; + } + return (((_timer - time) / 1000) / SECS_PER_MIN) % SECS_PER_MIN; +} + +uint8_t uClockClass::getNumberOfHours(uint32_t time) +{ + if ( time == 0 ) { + return time; + } + return (((_timer - time) / 1000) % SECS_PER_DAY) / SECS_PER_HOUR; +} + +uint8_t uClockClass::getNumberOfDays(uint32_t time) +{ + if ( time == 0 ) { + return time; + } + return ((_timer - time) / 1000) / SECS_PER_DAY; +} + +uint32_t uClockClass::getNowTimer() +{ + return _timer; +} + +uint32_t uClockClass::getPlayTime() +{ + return start_timer; +} + +} } // end namespace umodular::clock + +umodular::clock::uClockClass uClock; + +volatile uint32_t _timer = 0; + +// +// TIMER INTERRUPT HANDLER +// +// +#if defined(TEENSYDUINO) && !defined(__AVR_ATmega32U4__) +void uclockISR() +#else +ISR(TIMER1_COMPA_vect) +#endif +{ + // global timer counter + _timer = millis(); + + if (uClock.state == uClock.STARTED) { + uClock.handleTimerInt(); + } +} diff --git a/uClock.h b/uClock.h new file mode 100644 index 0000000..bba07fe --- /dev/null +++ b/uClock.h @@ -0,0 +1,173 @@ +/*! + * @file uClock.h + * Project BPM clock generator for Arduino + * @brief A Library to implement BPM clock tick calls using hardware timer interruption. Tested on ATmega168/328, ATmega16u4/32u4 and ATmega2560 and Teensy LC. + * @version 1.0.0 + * @author Romulo Silva + * @date 01/04/2022 + * @license MIT - (c) 2022 - Romulo Silva - contact@midilab.co + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef __U_CLOCK_H__ +#define __U_CLOCK_H__ + +#include +#include + +namespace umodular { namespace clock { + +#define AVR_CLOCK_FREQ 16000000 + +#define PHASE_FACTOR 16 + +#define PLL_X 220 + +// for smooth slave tempo calculate display you should raise this value +// in between 64 to 128. +// note: this doesn't impact on sync time, only display time getTempo() +// if you dont want to use it, set it to 1 for memory save +#define EXT_INTERVAL_BUFFER_SIZE 24 + +#define SECS_PER_MIN (60UL) +#define SECS_PER_HOUR (3600UL) +#define SECS_PER_DAY (SECS_PER_HOUR * 24L) + +#define MIN_BPM 1 +#define MAX_BPM 300 + +#define ATOMIC(X) noInterrupts(); X; interrupts(); + +class uClockClass { + + private: + + void setTimerTempo(float bpm); + float inline freqToBpm(uint32_t freq); + + void (*onClock96PPQNCallback)(uint32_t * tick); + void (*onClock32PPQNCallback)(uint32_t * tick); + void (*onClock16PPQNCallback)(uint32_t * tick); + void (*onClockStartCallback)(); + void (*onClockStopCallback)(); + + // internal clock control + volatile uint32_t internal_tick; + volatile uint32_t div32th_counter; + volatile uint32_t div16th_counter; + volatile uint8_t mod6_counter; + + // external clock control + volatile uint32_t external_clock; + volatile uint32_t external_tick; + volatile uint32_t indiv32th_counter; + volatile uint32_t indiv16th_counter; + volatile uint8_t inmod6_counter; + volatile uint32_t interval; + volatile uint32_t last_interval; + uint32_t sync_interval; + + uint32_t tick_us_interval; + float tick_hertz_interval; + + float tempo; + uint32_t start_timer; + uint8_t mode; + + volatile uint32_t ext_interval_buffer[EXT_INTERVAL_BUFFER_SIZE]; + uint16_t ext_interval_idx; + + public: + + enum { + INTERNAL_CLOCK = 0, + EXTERNAL_CLOCK + }; + + enum { + PAUSED = 0, + STARTING, + STARTED + }; + + uint8_t state; + + uClockClass(); + + void setClock96PPQNOutput(void (*callback)(uint32_t * tick)) { + onClock96PPQNCallback = callback; + } + + void setClock32PPQNOutput(void (*callback)(uint32_t * tick)) { + onClock32PPQNCallback = callback; + } + + void setClock16PPQNOutput(void (*callback)(uint32_t * tick)) { + onClock16PPQNCallback = callback; + } + + void setOnClockStartOutput(void (*callback)()) { + onClockStartCallback = callback; + } + + void setOnClockStopOutput(void (*callback)()) { + onClockStopCallback = callback; + } + + void init(); + void handleTimerInt(); + void handleExternalClock(); + void resetCounters(); + + // external class control + void start(); + void stop(); + void pause(); + void setTempo(float bpm); + float getTempo(); + + // external timming control + void setMode(uint8_t tempo_mode); + uint8_t getMode(); + void clockMe(); + + // todo! + void shuffle(); + void tap(); + + // elapsed time support + uint8_t getNumberOfSeconds(uint32_t time); + uint8_t getNumberOfMinutes(uint32_t time); + uint8_t getNumberOfHours(uint32_t time); + uint8_t getNumberOfDays(uint32_t time); + uint32_t getNowTimer(); + uint32_t getPlayTime(); +}; + +} } // end namespace umodular::clock + +extern umodular::clock::uClockClass uClock; + +extern "C" { + extern volatile uint16_t _clock; + extern volatile uint32_t _timer; +} + +#endif /* __U_CLOCK_H__ */