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__ */