mirror of
https://github.com/jhbruhn/eurorack.git
synced 2025-03-15 02:55:49 +00:00
Remove unused files from midi2cv, implement some polyphony related
things
This commit is contained in:
parent
73bf23d79e
commit
210f5e1c77
10 changed files with 192 additions and 232935 deletions
|
@ -1,657 +0,0 @@
|
||||||
#include <MIDI.h>
|
|
||||||
#include <SPI.h>
|
|
||||||
#include <DAC57X4.h>
|
|
||||||
#include <EEPROM.h>
|
|
||||||
#include <usbh_midi.h>
|
|
||||||
#include <usbhub.h>
|
|
||||||
#include <midi_RingBuffer.h>
|
|
||||||
|
|
||||||
#define MIDI_CHANNEL 1
|
|
||||||
|
|
||||||
#define OCTAVE_RANGE 10
|
|
||||||
#define MAX_CV 10.0
|
|
||||||
#define NOTES_PER_OCTAVE 12
|
|
||||||
#define CV_PER_OCTAVE 1.0
|
|
||||||
#define CV_PER_NOTE CV_PER_OCTAVE / NOTES_PER_OCTAVE
|
|
||||||
|
|
||||||
#define BASE_NOTE 21
|
|
||||||
#define MAX_NOTE 108
|
|
||||||
#define NOTE_RANGE OCTAVE_RANGE * NOTES_PER_OCTAVE
|
|
||||||
|
|
||||||
// Define Pitch Bend range to be a major second
|
|
||||||
#define BEND_RANGE ((CV_PER_NOTE) * 1)
|
|
||||||
|
|
||||||
#define PIN_USB_RESET 7
|
|
||||||
|
|
||||||
#define PIN_GATE_0 A5
|
|
||||||
#define PIN_GATE_1 A1
|
|
||||||
#define PIN_GATE_2 A2
|
|
||||||
#define PIN_GATE_3 A3
|
|
||||||
|
|
||||||
#define ROTARY_SWITCH_PIN0 2
|
|
||||||
#define ROTARY_SWITCH_PIN1 3
|
|
||||||
#define ROTARY_SWITCH_PIN2 4
|
|
||||||
|
|
||||||
#define PIN_SPEED A4
|
|
||||||
|
|
||||||
#define PIN_FLIP_SWITCH_0 A6
|
|
||||||
#define PIN_FLIP_SWITCH_1 A7
|
|
||||||
|
|
||||||
#define FLIP_SWITCH_UP 0
|
|
||||||
#define FLIP_SWITCH_MIDDLE 1
|
|
||||||
#define FLIP_SWITCH_DOWN 2
|
|
||||||
|
|
||||||
|
|
||||||
#define MODE_MONO 0
|
|
||||||
#define MODE_ARP 1
|
|
||||||
#define MODE_SEQ 2
|
|
||||||
|
|
||||||
|
|
||||||
#define MIDI_CLOCK_THRESHOLD_MILLIS 500
|
|
||||||
|
|
||||||
#define MIDI_CLOCK_DIVIDE_QUARTER 24
|
|
||||||
#define MIDI_CLOCK_DIVIDE_HALF MIDI_CLOCK_DIVIDE_QUARTER * 2
|
|
||||||
#define MIDI_CLOCK_DIVIDE_WHOLE MIDI_CLOCK_DIVIDE_HALF * 2
|
|
||||||
#define MIDI_CLOCK_DIVIDE_EIGTH MIDI_CLOCK_DIVIDE_QUARTER / 2
|
|
||||||
#define MIDI_CLOCK_DIVIDE_EIGHT_T MIDI_CLOCK_DIVIDE_QUARTER / 3
|
|
||||||
#define MIDI_CLOCK_DIVIDE_SIXTEENTH MIDI_CLOCK_DIVIDE_EIGTH / 2
|
|
||||||
#define MIDI_CLOCK_DIVIDE_SIXTEENTH_T MIDI_CLOCK_DIVIDE_EIGTH / 3
|
|
||||||
#define MIDI_CLOCK_DIVIDE_THIRTY_SECOND MIDI_CLOCK_DIVIDE_SIXTEENTH / 2
|
|
||||||
#define MIDI_CLOCK_DIVIDE_THIRTY_SECOND_T MIDI_CLOCK_DIVIDE_SIXTEENTH / 3
|
|
||||||
|
|
||||||
#define MIDI_CLOCK_DIVISION_COUNT 9
|
|
||||||
const int clockDivisions[] = { MIDI_CLOCK_DIVIDE_WHOLE, MIDI_CLOCK_DIVIDE_HALF,
|
|
||||||
MIDI_CLOCK_DIVIDE_QUARTER,
|
|
||||||
MIDI_CLOCK_DIVIDE_EIGTH, MIDI_CLOCK_DIVIDE_EIGHT_T,
|
|
||||||
MIDI_CLOCK_DIVIDE_SIXTEENTH, MIDI_CLOCK_DIVIDE_SIXTEENTH_T,
|
|
||||||
MIDI_CLOCK_DIVIDE_THIRTY_SECOND, MIDI_CLOCK_DIVIDE_THIRTY_SECOND_T
|
|
||||||
};
|
|
||||||
|
|
||||||
#define MANUAL_CLOCK_MIN 20
|
|
||||||
#define MANUAL_CLOCK_MAX 2000
|
|
||||||
|
|
||||||
#define SEQUENCE_COUNT 6
|
|
||||||
#define SEQUENCE_LENGTH_MAX 128
|
|
||||||
|
|
||||||
#define ARP_DIRECTION_UP 0
|
|
||||||
#define ARP_DIRECTION_DOWN 1
|
|
||||||
#define ARP_DIRECTION_BOTH 2
|
|
||||||
#define ARP_DIRECTION_ORDER 3
|
|
||||||
#define ARP_DIRECTION_RANDOM 4
|
|
||||||
#define ARP_DIRECTION_NONE 5
|
|
||||||
|
|
||||||
#define ARP_MODE_LATCH FLIP_SWITCH_UP
|
|
||||||
#define ARP_MODE_FORGET FLIP_SWITCH_MIDDLE
|
|
||||||
#define ARP_MODE_ADD FLIP_SWITCH_DOWN
|
|
||||||
|
|
||||||
|
|
||||||
DAC57X4 dac(4, 2, SS);
|
|
||||||
|
|
||||||
|
|
||||||
class USBMidiParser
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
USBMidiParser(USBH_MIDI *usbThing);
|
|
||||||
int available();
|
|
||||||
void begin(unsigned baud);
|
|
||||||
byte read();
|
|
||||||
void write(unsigned char data);
|
|
||||||
private:
|
|
||||||
midi::RingBuffer<byte, 64> rxBuffer;
|
|
||||||
USBH_MIDI *usb;
|
|
||||||
};
|
|
||||||
|
|
||||||
USBMidiParser::USBMidiParser(USBH_MIDI *usbThing) {
|
|
||||||
usb = usbThing;
|
|
||||||
}
|
|
||||||
|
|
||||||
int USBMidiParser::available() {
|
|
||||||
uint8_t buf[20];
|
|
||||||
byte length = 0;
|
|
||||||
if((length = usb->RecvData(buf)) > 0) {
|
|
||||||
for(byte i = 0; i < length; i++)
|
|
||||||
rxBuffer.write(buf[i]);
|
|
||||||
}
|
|
||||||
return rxBuffer.getLength();
|
|
||||||
}
|
|
||||||
|
|
||||||
void USBMidiParser::begin(unsigned baud) {
|
|
||||||
rxBuffer.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
byte USBMidiParser::read() {
|
|
||||||
return rxBuffer.read();
|
|
||||||
}
|
|
||||||
|
|
||||||
void USBMidiParser::write(unsigned char asd) {}
|
|
||||||
|
|
||||||
USB Usb;
|
|
||||||
USBH_MIDI usbMidi(&Usb);
|
|
||||||
USBMidiParser parser(&usbMidi);
|
|
||||||
boolean usbMidiEnabled = false;
|
|
||||||
|
|
||||||
MIDI_CREATE_DEFAULT_INSTANCE();
|
|
||||||
MIDI_CREATE_INSTANCE(USBMidiParser, parser, MIDIUSB);
|
|
||||||
|
|
||||||
byte currentMode = MODE_MONO;
|
|
||||||
|
|
||||||
byte flipSwitch0 = FLIP_SWITCH_UP;
|
|
||||||
byte flipSwitch1 = FLIP_SWITCH_UP;
|
|
||||||
byte rotarySwitch = 0;
|
|
||||||
int speedValue = 0;
|
|
||||||
|
|
||||||
|
|
||||||
float cv[] = {0, 0, 0, 0}; // 0 to MAX_CV
|
|
||||||
bool gate[] = {LOW, LOW, LOW, LOW}; // LOW or HIGH
|
|
||||||
bool midiStartSignal = LOW;
|
|
||||||
bool midiClockSignal = LOW;
|
|
||||||
byte activeNotes[NOTE_RANGE];
|
|
||||||
byte noteCount = 1;
|
|
||||||
byte activeNoteCount = 0;
|
|
||||||
int currentPitchBend = 0;
|
|
||||||
byte currentModulation = 0;
|
|
||||||
byte currentVelocity = 0;
|
|
||||||
bool trigger = false;
|
|
||||||
bool released = false;
|
|
||||||
|
|
||||||
byte lastChannel = 0;
|
|
||||||
|
|
||||||
int clockCounter = 0;
|
|
||||||
unsigned long lastClockMillis = 0;
|
|
||||||
byte currentClockDivide = 0;
|
|
||||||
byte nextClockDivide = 0;
|
|
||||||
|
|
||||||
unsigned long lastManualClockMillis = 0;
|
|
||||||
|
|
||||||
byte currentSequencePosition = 0;
|
|
||||||
|
|
||||||
byte currentArpPosition = 0;
|
|
||||||
byte activeArpNotes[NOTE_RANGE];
|
|
||||||
byte activeArpNoteCount = 0;
|
|
||||||
bool arpLatchReadyForNew = true;
|
|
||||||
|
|
||||||
void setup() {
|
|
||||||
pinMode(PIN_GATE_0, OUTPUT);
|
|
||||||
pinMode(PIN_GATE_1, OUTPUT);
|
|
||||||
pinMode(PIN_GATE_2, OUTPUT);
|
|
||||||
pinMode(PIN_GATE_3, OUTPUT);
|
|
||||||
|
|
||||||
MIDI.begin(MIDI_CHANNEL_OMNI);
|
|
||||||
MIDI.setHandleClock(onMidiClock);
|
|
||||||
MIDI.setHandleNoteOn(onMidiNoteOn);
|
|
||||||
MIDI.setHandleNoteOff(onMidiNoteOff);
|
|
||||||
MIDI.setHandleStart(onMidiStart);
|
|
||||||
MIDI.setHandleControlChange(onMidiControlChange);
|
|
||||||
MIDI.setHandlePitchBend(onMidiPitchBend);
|
|
||||||
|
|
||||||
if(Usb.Init() == -1) {
|
|
||||||
usbMidiEnabled = false;
|
|
||||||
} else {
|
|
||||||
usbMidiEnabled = true;
|
|
||||||
|
|
||||||
MIDIUSB.begin(MIDI_CHANNEL_OMNI);
|
|
||||||
MIDIUSB.setHandleClock(onMidiClock);
|
|
||||||
MIDIUSB.setHandleNoteOn(onMidiNoteOn);
|
|
||||||
MIDIUSB.setHandleNoteOff(onMidiNoteOff);
|
|
||||||
MIDIUSB.setHandleStart(onMidiStart);
|
|
||||||
MIDIUSB.setHandleControlChange(onMidiControlChange);
|
|
||||||
MIDIUSB.setHandlePitchBend(onMidiPitchBend);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onMidiClock() {
|
|
||||||
if(clockCounter == 0) {
|
|
||||||
midiClockSignal = HIGH;
|
|
||||||
}
|
|
||||||
clockCounter++;
|
|
||||||
if(clockCounter == clockDivisions[currentClockDivide]) {
|
|
||||||
clockCounter = 0;
|
|
||||||
currentClockDivide = nextClockDivide;
|
|
||||||
}
|
|
||||||
lastClockMillis = millis();
|
|
||||||
}
|
|
||||||
|
|
||||||
void onMidiNoteOn(byte channel, byte note, byte velocity) {
|
|
||||||
if(channel > MIDI_CHANNEL + 1) return;
|
|
||||||
if(note < BASE_NOTE || note > MAX_NOTE) return;
|
|
||||||
|
|
||||||
lastChannel = channel;
|
|
||||||
|
|
||||||
if(velocity == 0) { onMidiNoteOff(channel, note, velocity); return; }
|
|
||||||
|
|
||||||
activeNotes[note - BASE_NOTE] = noteCount++;
|
|
||||||
activeNoteCount++;
|
|
||||||
currentVelocity = velocity;
|
|
||||||
trigger = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void onMidiNoteOff(byte channel, byte note, byte velocity) {
|
|
||||||
if(channel > MIDI_CHANNEL + 1) return;
|
|
||||||
activeNotes[note - BASE_NOTE] = 0;
|
|
||||||
activeNoteCount--;
|
|
||||||
if(activeNoteCount == 0) {
|
|
||||||
currentVelocity = 0;
|
|
||||||
noteCount = 1;
|
|
||||||
}
|
|
||||||
released = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void onMidiControlChange(byte channel, byte number, byte value) {
|
|
||||||
if(channel > MIDI_CHANNEL + 1) return;
|
|
||||||
currentModulation = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
void onMidiPitchBend(byte channel, int bend) {
|
|
||||||
if(channel > MIDI_CHANNEL + 1) return;
|
|
||||||
currentPitchBend = bend;
|
|
||||||
}
|
|
||||||
|
|
||||||
void onMidiStart() {
|
|
||||||
midiStartSignal = HIGH;
|
|
||||||
clockCounter = 0;
|
|
||||||
currentClockDivide = nextClockDivide;
|
|
||||||
}
|
|
||||||
|
|
||||||
float midiToCV(byte note) {
|
|
||||||
if(note < BASE_NOTE) return 0;
|
|
||||||
if(note - BASE_NOTE > NOTE_RANGE) return MAX_CV;
|
|
||||||
return (note - BASE_NOTE) * CV_PER_NOTE;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte getMostRecentNote() {
|
|
||||||
byte note;
|
|
||||||
unsigned long maxTime = 0;
|
|
||||||
for(byte i = 0; i < NOTE_RANGE; i++) {
|
|
||||||
if(activeNotes[i] != 0 && activeNotes[i] >= maxTime) {
|
|
||||||
note = i;
|
|
||||||
maxTime = activeNotes[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return note;
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop() {
|
|
||||||
readSwitches();
|
|
||||||
|
|
||||||
if(flipSwitch0 == FLIP_SWITCH_UP) currentMode = MODE_MONO;
|
|
||||||
else if(flipSwitch0 == FLIP_SWITCH_MIDDLE) currentMode = MODE_SEQ;
|
|
||||||
else if(flipSwitch0 == FLIP_SWITCH_DOWN) currentMode = MODE_ARP;
|
|
||||||
|
|
||||||
readSpeed();
|
|
||||||
MIDI.read();
|
|
||||||
if(usbMidiEnabled) {
|
|
||||||
Usb.Task();
|
|
||||||
if(usbMidi)
|
|
||||||
MIDIUSB.read();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(millis() - lastClockMillis > MIDI_CLOCK_THRESHOLD_MILLIS) {
|
|
||||||
// Apparently we are not getting any midi clocks, so we will just generate our own signal.
|
|
||||||
long waitTime = map(speedValue, 0, 1023, (60000) / MANUAL_CLOCK_MIN, (60000) / MANUAL_CLOCK_MAX);
|
|
||||||
if(millis() - lastManualClockMillis > waitTime) {
|
|
||||||
// Time for another clock signal!
|
|
||||||
lastManualClockMillis = millis();
|
|
||||||
midiClockSignal = HIGH;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
nextClockDivide = map(speedValue, 0, 1023, 0, MIDI_CLOCK_DIVISION_COUNT);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool triggerOut = false;
|
|
||||||
|
|
||||||
|
|
||||||
switch(currentMode) {
|
|
||||||
case MODE_MONO:
|
|
||||||
if(activeNoteCount > 0) {
|
|
||||||
// Find most recent hit key
|
|
||||||
byte note = getMostRecentNote();
|
|
||||||
|
|
||||||
float value = midiToCV(note + BASE_NOTE);
|
|
||||||
cv[0] = value + mapfloat(currentPitchBend, 0, MIDI_PITCHBEND_MAX, -BEND_RANGE, BEND_RANGE);
|
|
||||||
cv[1] = mapfloat(currentVelocity, 0, 127, 0, MAX_CV);
|
|
||||||
cv[2] = mapfloat(currentModulation, 0, 127, 0, MAX_CV);
|
|
||||||
triggerOut = trigger;
|
|
||||||
if(lastChannel == MIDI_CHANNEL + 1 && trigger) { // IF on second channel, we retrigger the gate.
|
|
||||||
gate[0] = LOW;
|
|
||||||
} else {
|
|
||||||
gate[0] = HIGH;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
gate[0] = LOW;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case MODE_SEQ:
|
|
||||||
if(flipSwitch1 == FLIP_SWITCH_UP) {
|
|
||||||
// Rec mode.
|
|
||||||
if(activeNoteCount > 0) {
|
|
||||||
// Find most recent hit key
|
|
||||||
byte note = getMostRecentNote();
|
|
||||||
float value = midiToCV(note + BASE_NOTE);
|
|
||||||
cv[0] = value + mapfloat(currentPitchBend, 0, MIDI_PITCHBEND_MAX, -BEND_RANGE, BEND_RANGE);
|
|
||||||
gate[0] = HIGH;
|
|
||||||
if(trigger) {
|
|
||||||
triggerOut = true;
|
|
||||||
|
|
||||||
EEPROM.update(rotarySwitch, EEPROM[rotarySwitch] + 1);
|
|
||||||
byte notePosition = EEPROM[rotarySwitch] - 1;
|
|
||||||
EEPROM.update(SEQUENCE_COUNT + rotarySwitch * SEQUENCE_LENGTH_MAX + notePosition, note);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
gate[0] = LOW;
|
|
||||||
triggerOut = false;
|
|
||||||
}
|
|
||||||
} else if(flipSwitch1 == FLIP_SWITCH_MIDDLE) {
|
|
||||||
// Play Mode
|
|
||||||
if(activeNoteCount > 0 && EEPROM[rotarySwitch] > 0) {
|
|
||||||
byte note = getMostRecentNote();
|
|
||||||
byte currentSequenceValue = EEPROM[SEQUENCE_COUNT + rotarySwitch * SEQUENCE_LENGTH_MAX + currentSequencePosition];
|
|
||||||
note = currentSequenceValue + note - EEPROM[SEQUENCE_COUNT + rotarySwitch * SEQUENCE_LENGTH_MAX + 0];
|
|
||||||
|
|
||||||
if(trigger && millis() - lastClockMillis > MIDI_CLOCK_THRESHOLD_MILLIS) {
|
|
||||||
// no midi input but a key was pressed, so we start the sequence now instead of at next clock signal
|
|
||||||
midiClockSignal = HIGH;
|
|
||||||
lastManualClockMillis = millis();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(midiClockSignal) { // trigger a new note!
|
|
||||||
triggerOut = true;
|
|
||||||
float value = midiToCV(note + BASE_NOTE);
|
|
||||||
cv[0] = value + mapfloat(currentPitchBend, 0, MIDI_PITCHBEND_MAX, -BEND_RANGE, BEND_RANGE);
|
|
||||||
currentSequencePosition = (currentSequencePosition + 1) % EEPROM[rotarySwitch];
|
|
||||||
}
|
|
||||||
gate[0] = HIGH;
|
|
||||||
} else {
|
|
||||||
gate[0] = LOW;
|
|
||||||
currentSequencePosition = 0;
|
|
||||||
}
|
|
||||||
} else if(flipSwitch1 == FLIP_SWITCH_DOWN) {
|
|
||||||
// clear on note
|
|
||||||
if(activeNoteCount > 0 && trigger) {
|
|
||||||
EEPROM.update(rotarySwitch, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case MODE_ARP:
|
|
||||||
if(flipSwitch1 == ARP_MODE_LATCH) {
|
|
||||||
// ARP Mode LATCH
|
|
||||||
if(trigger) {
|
|
||||||
if(activeNoteCount == 1) {
|
|
||||||
// first note of the new arpeggio pressed
|
|
||||||
currentArpPosition = 0;
|
|
||||||
activeArpNoteCount = 1;
|
|
||||||
for(int i = 0; i < NOTE_RANGE; i++) {
|
|
||||||
if(activeNotes[i] != 0) currentArpPosition = i; // This is the first note of our arpeggio, so set it to this
|
|
||||||
activeArpNotes[i] = activeNotes[i];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// new note for current arpeggio
|
|
||||||
for(int i = 0; i < NOTE_RANGE; i++) {
|
|
||||||
if(activeNotes[i] != 0 && activeArpNotes[i] == 0) activeArpNoteCount++;
|
|
||||||
if(activeNotes[i] != 0) // We don't want to disable any notes, only activate new ones
|
|
||||||
activeArpNotes[i] = activeNotes[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if(flipSwitch1 == ARP_MODE_FORGET) {
|
|
||||||
// activeArpNotes is equivalent to activeNotes. ezpz
|
|
||||||
for(int i = 0; i < NOTE_RANGE; i++) {
|
|
||||||
activeArpNotes[i] = activeNotes[i];
|
|
||||||
}
|
|
||||||
activeArpNoteCount = activeNoteCount;
|
|
||||||
if(released && activeNoteCount == 0) {
|
|
||||||
currentArpPosition = 0;
|
|
||||||
}
|
|
||||||
} else if(flipSwitch1 == ARP_MODE_ADD) {
|
|
||||||
// we just add any active Note until some mysterious signal tells us to reset (modulation > 63!)
|
|
||||||
if(currentModulation < 64) {
|
|
||||||
byte active = 0;
|
|
||||||
for(int i = 0; i < NOTE_RANGE; i++) {
|
|
||||||
if(activeNotes[i] > 0) {
|
|
||||||
activeArpNotes[i] = activeNotes[i];
|
|
||||||
|
|
||||||
}
|
|
||||||
if(activeArpNotes[i] != 0)
|
|
||||||
active++;
|
|
||||||
}
|
|
||||||
activeArpNoteCount = active;
|
|
||||||
} else {
|
|
||||||
activeArpNoteCount = 0;
|
|
||||||
for(int i = 0; i < NOTE_RANGE; i++)
|
|
||||||
activeArpNotes[i] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if(activeArpNoteCount > 0) {
|
|
||||||
|
|
||||||
if( activeArpNotes[currentArpPosition] == 0) {
|
|
||||||
// find the first note in the arp.
|
|
||||||
switch(rotarySwitch) {
|
|
||||||
case ARP_DIRECTION_UP:
|
|
||||||
case ARP_DIRECTION_BOTH:
|
|
||||||
for(int i = 0; i < NOTE_RANGE; i++)
|
|
||||||
if(activeArpNotes[i] != 0) {
|
|
||||||
currentArpPosition = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ARP_DIRECTION_DOWN:
|
|
||||||
for(int i = NOTE_RANGE - 1; i >= 0; i--)
|
|
||||||
if(activeArpNotes[i] != 0) {
|
|
||||||
currentArpPosition = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ARP_DIRECTION_RANDOM:
|
|
||||||
byte newIndex = 0;
|
|
||||||
|
|
||||||
do {
|
|
||||||
newIndex = random(0, 255);
|
|
||||||
} while(activeArpNotes[newIndex] == 0 || currentArpPosition == newIndex);
|
|
||||||
|
|
||||||
currentArpPosition = newIndex;
|
|
||||||
break;
|
|
||||||
case ARP_DIRECTION_ORDER:
|
|
||||||
byte minValue = 0;
|
|
||||||
byte minValueIndex = 0;
|
|
||||||
for(int i = 0; i < NOTE_RANGE; i++) {
|
|
||||||
if(i == currentArpPosition || activeArpNotes[i] == 0) continue;
|
|
||||||
if(activeArpNotes[i] < minValue) {
|
|
||||||
minValue = activeArpNotes[i];
|
|
||||||
minValueIndex = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
currentArpPosition = minValueIndex;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
byte note = currentArpPosition; // This is the current Arp Note to play
|
|
||||||
|
|
||||||
if(midiClockSignal) { // trigger a new note!
|
|
||||||
triggerOut = true;
|
|
||||||
float value = midiToCV(note + BASE_NOTE);
|
|
||||||
cv[0] = value + mapfloat(currentPitchBend, 0, MIDI_PITCHBEND_MAX, -BEND_RANGE, BEND_RANGE);
|
|
||||||
// now we have to find the next arp position. this depends on the current mode.
|
|
||||||
bool foundNote = false;
|
|
||||||
|
|
||||||
switch(rotarySwitch) {
|
|
||||||
case ARP_DIRECTION_UP:
|
|
||||||
if(activeArpNoteCount == 1) break;
|
|
||||||
|
|
||||||
// We just go up and if we reach the last note in the arp, we go back to the first one
|
|
||||||
for(int i = currentArpPosition + 1; i < NOTE_RANGE; i++) {
|
|
||||||
if(activeArpNotes[i] != 0) {
|
|
||||||
// found the next note!
|
|
||||||
currentArpPosition = i;
|
|
||||||
foundNote = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!foundNote) {
|
|
||||||
// We have to find the first note at the beginning of the arpeggio now...
|
|
||||||
for(int i = 0; i < NOTE_RANGE; i++) {
|
|
||||||
if(activeArpNotes[i] != 0) {
|
|
||||||
// found the next note!
|
|
||||||
currentArpPosition = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
case ARP_DIRECTION_DOWN:
|
|
||||||
if(activeArpNoteCount == 1) break;
|
|
||||||
// We just go down and if we reach the first note in the arp, we go forward to the last one
|
|
||||||
for(int i = currentArpPosition - 1; i >= 0; i--) {
|
|
||||||
if(activeArpNotes[i] != 0) {
|
|
||||||
// found the next note!
|
|
||||||
currentArpPosition = i;
|
|
||||||
foundNote = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!foundNote) {
|
|
||||||
// We have to find the last note at the beginning of the arpeggio now...
|
|
||||||
for(int i = NOTE_RANGE - 1; i >= 0; i--) {
|
|
||||||
if(activeArpNotes[i] != 0) {
|
|
||||||
// found the next note!
|
|
||||||
currentArpPosition = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ARP_DIRECTION_BOTH:
|
|
||||||
if(activeArpNoteCount == 1) break;
|
|
||||||
// We just go up and if we reach the last note in the arp, we go back one note
|
|
||||||
for(int i = currentArpPosition + 1; i < NOTE_RANGE; i++) {
|
|
||||||
if(activeArpNotes[i] != 0) {
|
|
||||||
// found the next note!
|
|
||||||
currentArpPosition = i;
|
|
||||||
foundNote = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// we are going downwards, so let's search!
|
|
||||||
if(!foundNote) {
|
|
||||||
for(int i = currentArpPosition - 1; i >= 0; i--) {
|
|
||||||
if(activeArpNotes[i] != 0) {
|
|
||||||
// found the next note!
|
|
||||||
currentArpPosition = i;
|
|
||||||
foundNote = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
case ARP_DIRECTION_ORDER:
|
|
||||||
if(activeArpNoteCount == 1) break;
|
|
||||||
// We find the note that has the least distance to the current notes value.
|
|
||||||
// If we can't find one, we assign the note with the lowest value
|
|
||||||
byte minValue = 255;
|
|
||||||
byte minValueIndex = 0;
|
|
||||||
for(int i = 0; i < NOTE_RANGE; i++) {
|
|
||||||
if(i == currentArpPosition || activeArpNotes[i] == 0) continue;
|
|
||||||
byte distance = abs(activeArpNotes[i] - activeArpNotes[currentArpPosition]);
|
|
||||||
if(distance < minValue) {
|
|
||||||
minValue = distance;
|
|
||||||
minValueIndex = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(minValue == 255) { // We did not find a higher note, so let's go back to the first one.
|
|
||||||
minValue = 0;
|
|
||||||
for(int i = 0; i < NOTE_RANGE; i++) {
|
|
||||||
if(i == currentArpPosition || activeArpNotes[i] == 0) continue;
|
|
||||||
if(activeArpNotes[i] < minValue) {
|
|
||||||
minValue = activeArpNotes[i];
|
|
||||||
minValueIndex = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentArpPosition = minValueIndex;
|
|
||||||
break;
|
|
||||||
case ARP_DIRECTION_RANDOM:
|
|
||||||
byte newIndex = 0;
|
|
||||||
|
|
||||||
do {
|
|
||||||
newIndex = random(0, 255);
|
|
||||||
} while(activeArpNotes[newIndex] == 0 || currentArpPosition == newIndex);
|
|
||||||
|
|
||||||
currentArpPosition = newIndex;
|
|
||||||
|
|
||||||
break;
|
|
||||||
case ARP_DIRECTION_NONE:
|
|
||||||
// Noone knows what happens here.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
gate[0] = HIGH;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
} else {
|
|
||||||
gate[0] = LOW;
|
|
||||||
currentArpPosition = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
gate[1] = triggerOut ? HIGH : LOW;
|
|
||||||
gate[2] = midiStartSignal;
|
|
||||||
gate[3] = midiClockSignal;
|
|
||||||
|
|
||||||
trigger = false;
|
|
||||||
released = false;
|
|
||||||
midiStartSignal = LOW;
|
|
||||||
midiClockSignal = LOW;
|
|
||||||
|
|
||||||
writeDACs();
|
|
||||||
writeGates();
|
|
||||||
}
|
|
||||||
|
|
||||||
float mapfloat(float x, float in_min, float in_max, float out_min, float out_max)
|
|
||||||
{
|
|
||||||
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
|
|
||||||
}
|
|
||||||
|
|
||||||
void readSwitches() {
|
|
||||||
int analogValue = analogRead(PIN_FLIP_SWITCH_0);
|
|
||||||
if(analogValue < 100) flipSwitch0 = FLIP_SWITCH_UP;
|
|
||||||
else if(analogValue < 900) flipSwitch0 = FLIP_SWITCH_DOWN;
|
|
||||||
else flipSwitch0 = FLIP_SWITCH_MIDDLE;
|
|
||||||
analogValue = analogRead(PIN_FLIP_SWITCH_1);
|
|
||||||
if(analogValue < 100) flipSwitch1 = FLIP_SWITCH_UP;
|
|
||||||
else if(analogValue < 900) flipSwitch1 = FLIP_SWITCH_DOWN;
|
|
||||||
else flipSwitch1 = FLIP_SWITCH_MIDDLE;
|
|
||||||
|
|
||||||
rotarySwitch = (!digitalRead(ROTARY_SWITCH_PIN0) << 2) & (!digitalRead(ROTARY_SWITCH_PIN1) << 1) & !digitalRead(ROTARY_SWITCH_PIN2);
|
|
||||||
}
|
|
||||||
|
|
||||||
void readSpeed() {
|
|
||||||
speedValue = analogRead(PIN_SPEED);
|
|
||||||
}
|
|
||||||
|
|
||||||
void writeDACs() {
|
|
||||||
dac.SetDAC(cv[0], 1);
|
|
||||||
dac.SetDAC(cv[1], 2);
|
|
||||||
dac.SetDAC(cv[2], 3);
|
|
||||||
dac.SetDAC(cv[3], 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
void writeGates() {
|
|
||||||
digitalWrite(PIN_GATE_0, gate[0] == HIGH ? LOW : HIGH);
|
|
||||||
digitalWrite(PIN_GATE_1, gate[1] == HIGH ? LOW : HIGH);
|
|
||||||
digitalWrite(PIN_GATE_2, gate[2] == HIGH ? LOW : HIGH);
|
|
||||||
digitalWrite(PIN_GATE_3, gate[3] == HIGH ? LOW : HIGH);
|
|
||||||
}
|
|
|
@ -30,6 +30,7 @@ FAMILY = f3xx
|
||||||
MEMORY_MODE = flash
|
MEMORY_MODE = flash
|
||||||
U8G2 = enabled
|
U8G2 = enabled
|
||||||
# USB = enabled
|
# USB = enabled
|
||||||
|
OPTIMIZE = TRUE
|
||||||
|
|
||||||
# Preferred upload command
|
# Preferred upload command
|
||||||
UPLOAD_COMMAND = upload_jtag
|
UPLOAD_COMMAND = upload_jtag
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
#include "part.h"
|
||||||
|
|
||||||
|
bool Part::is_message_handled(uint8_t channel, uint8_t note) // TODO: add origin
|
||||||
|
{
|
||||||
|
if (this->part_data().midi_filter_channel_enabled && channel != this->part_data().midi_filter_channel)
|
||||||
|
return false;
|
||||||
|
if (this->part_data().midi_filter_lowest_note > note || this->part_data().midi_filter_highest_note < note)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Part::NoteOn(uint8_t channel, uint8_t note, uint8_t velocity)
|
||||||
|
{
|
||||||
|
if (!is_message_handled(channel, note))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (velocity == 0)
|
||||||
|
return NoteOff(channel, note, velocity);
|
||||||
|
|
||||||
|
uint8_t voiceIndex = 0;
|
||||||
|
if (this->part_data().part_voice_count > 1) { // only use voiceallocator for polyphonic config
|
||||||
|
voiceIndex = voiceAllocator.NoteOn(note);
|
||||||
|
}
|
||||||
|
|
||||||
|
Voice* voice = &voices[voiceIndex];
|
||||||
|
|
||||||
|
voice->NoteOn(note, velocity);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Part::NoteOff(uint8_t channel, uint8_t note, uint8_t velocity)
|
||||||
|
{
|
||||||
|
if (!is_message_handled(channel, note))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
uint8_t voiceIndex = 0;
|
||||||
|
if (this->part_data().part_voice_count > 1) { // only use voiceallocator for polyphonic config
|
||||||
|
voiceIndex = voiceAllocator.NoteOff(note);
|
||||||
|
}
|
||||||
|
|
||||||
|
Voice* voice = &voices[voiceIndex];
|
||||||
|
voice->NoteOff(note, velocity);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Part::PitchBend(uint8_t channel, uint16_t pitch_bend)
|
||||||
|
{
|
||||||
|
if (!is_message_handled(channel, this->part_data().midi_filter_lowest_note))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < this->part_data().part_voice_count; i++)
|
||||||
|
voices[i].PitchBend(pitch_bend);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Part::Aftertouch(uint8_t channel, uint8_t note, uint8_t velocity) {
|
||||||
|
if(!is_message_handled(channel, note)) return false;
|
||||||
|
|
||||||
|
uint8_t voiceIndex = 0;
|
||||||
|
if(this->part_data().part_voice_count > 1)
|
||||||
|
voiceIndex = this->voiceAllocator.Find(note);
|
||||||
|
voices[voiceIndex].Aftertouch(velocity);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Part::Aftertouch(uint8_t channel, uint8_t velocity) {
|
||||||
|
if(!is_message_handled(channel, this->part_data().midi_filter_lowest_note)) return false;
|
||||||
|
|
||||||
|
for(size_t i = 0; i < this->part_data().part_voice_count; i++)
|
||||||
|
voices[i].Aftertouch(velocity);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
|
@ -1,7 +1,12 @@
|
||||||
#ifndef MIDI2CV_PART_H
|
#pragma once
|
||||||
#define MIDI2CV_PART_H
|
|
||||||
|
|
||||||
#include <inttypes.h>
|
#include "stmlib/algorithms/voice_allocator.h"
|
||||||
|
#include "stmlib/stmlib.h"
|
||||||
|
#include "voice.h"
|
||||||
|
|
||||||
|
using namespace stmlib;
|
||||||
|
|
||||||
|
const int kMaximumPolyphony = 4;
|
||||||
|
|
||||||
#define TOTAL_COLUMN_COUNT 4
|
#define TOTAL_COLUMN_COUNT 4
|
||||||
|
|
||||||
|
@ -80,13 +85,26 @@ class Part {
|
||||||
data.output_type_row_3[i] = GATE_OFF;*/
|
data.output_type_row_3[i] = GATE_OFF;*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void ProcessMidiInput(/* TODO: Inputs */);
|
|
||||||
|
|
||||||
uint8_t RequiredColumns();
|
bool NoteOn(uint8_t channel, uint8_t note, uint8_t velocity);
|
||||||
|
bool NoteOff(uint8_t channel, uint8_t note, uint8_t velocity);
|
||||||
|
bool Aftertouch(uint8_t channel, uint8_t note, uint8_t velocity);
|
||||||
|
bool Aftertouch(uint8_t channel, uint8_t velocity);
|
||||||
|
bool ControlChange(uint8_t channel, uint8_t controller, uint8_t value);
|
||||||
|
bool ProgramChange(uint8_t channel, uint8_t program);
|
||||||
|
bool PitchBend(uint8_t channel, uint16_t pitch_bend);
|
||||||
|
|
||||||
|
uint8_t required_columns();
|
||||||
|
|
||||||
|
inline PartData* mutable_part_data() { return &data; }
|
||||||
|
inline PartData part_data() { return data; }
|
||||||
|
void Refresh(); // called whenever the settings change (for example voice count etc)
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool is_message_handled(uint8_t channel, uint8_t note); // TODO: add origin
|
||||||
PartData data;
|
PartData data;
|
||||||
|
VoiceAllocator<kMaximumPolyphony> voiceAllocator;
|
||||||
|
Voice voices[kMaximumPolyphony];
|
||||||
};
|
};
|
||||||
|
|
||||||
extern Part parts[];
|
extern Part parts[];
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
232256
midi2cv/pure_midi.dxf
232256
midi2cv/pure_midi.dxf
File diff suppressed because it is too large
Load diff
|
@ -64,13 +64,13 @@ void UI::Draw()
|
||||||
void UI::LoadState()
|
void UI::LoadState()
|
||||||
{
|
{
|
||||||
for (size_t i = 0; i < PART_COUNT; i++)
|
for (size_t i = 0; i < PART_COUNT; i++)
|
||||||
this->parts[i]->data = settings->part(i);
|
*this->parts[i]->mutable_part_data() = settings->part(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UI::SaveState()
|
void UI::SaveState()
|
||||||
{
|
{
|
||||||
for (size_t i = 0; i < PART_COUNT; i++) {
|
for (size_t i = 0; i < PART_COUNT; i++) {
|
||||||
*(settings->mutable_part(i)) = this->parts[i]->data;
|
*(settings->mutable_part(i)) = this->parts[i]->part_data();
|
||||||
}
|
}
|
||||||
|
|
||||||
settings->SaveState();
|
settings->SaveState();
|
||||||
|
|
|
@ -12,22 +12,22 @@ static const char* kMidiThruStrings[] = { "off", "on", "polychain" };
|
||||||
|
|
||||||
PartMenu::PartMenu(Part* _part)
|
PartMenu::PartMenu(Part* _part)
|
||||||
: part(_part)
|
: part(_part)
|
||||||
, item_voice_count("voice count", (uint8_t*)&_part->data.part_voice_count, 1, 4, 1)
|
, item_voice_count("voice count", (uint8_t*)&_part->mutable_part_data()->part_voice_count, 1, 4, 1)
|
||||||
, item_voice_detail("voice detail", (uint8_t*)&_part->data.part_voice_detail, kVoiceDetailStrings, 4)
|
, item_voice_detail("voice detail", (uint8_t*)&_part->mutable_part_data()->part_voice_detail, kVoiceDetailStrings, 4)
|
||||||
, item_midi_filter_enabled("MIDI filter", &_part->data.midi_filter_channel_enabled, "on", "off")
|
, item_midi_filter_enabled("MIDI filter", &_part->mutable_part_data()->midi_filter_channel_enabled, "on", "off")
|
||||||
, item_midi_channel("MIDI channel", (uint8_t*)&_part->data.midi_filter_channel, kMidiChannelStrings, 17, [_part] {
|
, item_midi_channel("MIDI channel", (uint8_t*)&_part->mutable_part_data()->midi_filter_channel, kMidiChannelStrings, 17, [_part] {
|
||||||
return _part->data.midi_filter_channel_enabled;
|
return _part->part_data().midi_filter_channel_enabled;
|
||||||
})
|
})
|
||||||
, item_midi_input("MIDI input", (uint8_t*)&_part->data.midi_filter_input, kMidiInputStrings, 3, [_part] {
|
, item_midi_input("MIDI input", (uint8_t*)&_part->mutable_part_data()->midi_filter_input, kMidiInputStrings, 3, [_part] {
|
||||||
return _part->data.midi_filter_channel_enabled;
|
return _part->part_data().midi_filter_channel_enabled;
|
||||||
})
|
})
|
||||||
, item_midi_lowest_note("MIDI lowest", (uint8_t*)&_part->data.midi_filter_lowest_note, [_part] {
|
, item_midi_lowest_note("MIDI lowest", (uint8_t*)&_part->mutable_part_data()->midi_filter_lowest_note, [_part] {
|
||||||
return _part->data.midi_filter_channel_enabled;
|
return _part->part_data().midi_filter_channel_enabled;
|
||||||
})
|
})
|
||||||
, item_midi_highest_note("MIDI highest", (uint8_t*)&_part->data.midi_filter_highest_note, [_part] {
|
, item_midi_highest_note("MIDI highest", (uint8_t*)&_part->mutable_part_data()->midi_filter_highest_note, [_part] {
|
||||||
return _part->data.midi_filter_channel_enabled;
|
return _part->part_data().midi_filter_channel_enabled;
|
||||||
})
|
})
|
||||||
, item_midi_thru_mode("MIDI thru", (uint8_t*)&_part->data.midi_thru_mode, kMidiThruStrings, 3)
|
, item_midi_thru_mode("MIDI thru", (uint8_t*)&_part->mutable_part_data()->midi_thru_mode, kMidiThruStrings, 3)
|
||||||
{
|
{
|
||||||
this->menu.add_item(&this->item_voice_count);
|
this->menu.add_item(&this->item_voice_count);
|
||||||
this->menu.add_item(&this->item_voice_detail);
|
this->menu.add_item(&this->item_voice_detail);
|
||||||
|
|
43
midi2cv/voice.cc
Normal file
43
midi2cv/voice.cc
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
#include "voice.h"
|
||||||
|
#include "stmlib/algorithms/note_stack.h"
|
||||||
|
|
||||||
|
using namespace stmlib;
|
||||||
|
|
||||||
|
void Voice::NoteOn(uint8_t note, uint8_t velocity)
|
||||||
|
{
|
||||||
|
this->noteStack.NoteOn(note, velocity);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Voice::NoteOff(uint8_t note, uint8_t velocity)
|
||||||
|
{
|
||||||
|
this->noteStack.NoteOff(note);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Voice::PitchBend(uint16_t pitch_bend)
|
||||||
|
{
|
||||||
|
this->pitchBend = pitch_bend;
|
||||||
|
}
|
||||||
|
|
||||||
|
float Voice::PitchOutputSemitones()
|
||||||
|
{
|
||||||
|
if (this->noteStack.size() == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
NoteEntry note = this->noteStack.note_by_priority(NOTE_STACK_PRIORITY_LAST);
|
||||||
|
|
||||||
|
// insanely temporary stuff ahead
|
||||||
|
|
||||||
|
// ULTRA NAIVE pitch to cv assuming max value is 10V
|
||||||
|
float semitones = note.note;
|
||||||
|
|
||||||
|
float pitch_bend = (this->pitchBend - 32768.0) / 65536.0 * 2.0; // -1 to 1
|
||||||
|
|
||||||
|
semitones += pitch_bend * 1.5f; // pitch bend range: 1.5 semitones
|
||||||
|
|
||||||
|
return semitones;
|
||||||
|
};
|
||||||
|
|
||||||
|
void Voice::Reset()
|
||||||
|
{
|
||||||
|
this->noteStack.Clear();
|
||||||
|
}
|
31
midi2cv/voice.h
Normal file
31
midi2cv/voice.h
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "stmlib/algorithms/note_stack.h"
|
||||||
|
|
||||||
|
const int kMaximumNoteStackSize = 16;
|
||||||
|
|
||||||
|
using namespace stmlib;
|
||||||
|
|
||||||
|
class Voice {
|
||||||
|
public:
|
||||||
|
Voice()
|
||||||
|
{
|
||||||
|
noteStack.Init();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NoteOn(uint8_t note, uint8_t velocity);
|
||||||
|
void NoteOff(uint8_t note, uint8_t velocity);
|
||||||
|
void Aftertouch(uint8_t velocity);
|
||||||
|
void ControlChange(uint8_t controller, uint8_t value);
|
||||||
|
void ProgramChange(uint8_t program);
|
||||||
|
void PitchBend(uint16_t pitch_bend);
|
||||||
|
|
||||||
|
void Reset();
|
||||||
|
|
||||||
|
float PitchOutputSemitones(); //
|
||||||
|
|
||||||
|
|
||||||
|
private:
|
||||||
|
NoteStack<kMaximumNoteStackSize> noteStack;
|
||||||
|
uint16_t pitchBend;
|
||||||
|
};
|
2
stmlib
2
stmlib
|
@ -1 +1 @@
|
||||||
Subproject commit 688459c60596d95d4973b3d1e85b92c45000d79a
|
Subproject commit 66684b7f071f6cdd141a24f71a39a3fff41bafa0
|
Loading…
Reference in a new issue