Added CV and Gate outputs via 2x i2c MCP4725 modules

This commit is contained in:
2025-10-15 13:30:25 +03:00
parent e0da8f123d
commit d4772ad52d
15 changed files with 2299 additions and 4 deletions

278
AcidStepSequencer.ino Normal file
View File

@ -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;
}
}
}

59
AcidTHarmonizer.ino Normal file
View File

@ -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];
}

48
AciduinoCV.ino Normal file
View File

@ -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 <Arduino.h>
#include <Wire.h>
#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();
}

62
CV.ino Normal file
View File

@ -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);
}
}

33
CV_Test/CV_Test.ino Normal file
View File

@ -0,0 +1,33 @@
#include <Wire.h>
#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);
}

298
HardwareInterface.ino Normal file
View File

@ -0,0 +1,298 @@
#include <EEPROM.h>
// 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;
}
}

55
MidiExternalSync.ino Normal file
View File

@ -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;
}
}
}

187
PageGenerative.ino Normal file
View File

@ -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);
}

177
PageLive.ino Normal file
View File

@ -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);
}
}
}

183
PageSequencer.ino Normal file
View File

@ -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);
}
}

View File

@ -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
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.

187
UserInterface.ino Normal file
View File

@ -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;
}
}
}

99
config.h Normal file
View File

@ -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

454
uClock.cpp Normal file
View File

@ -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();
}
}

173
uClock.h Normal file
View File

@ -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 <Arduino.h>
#include <inttypes.h>
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__ */