Post by sleeptotem on Feb 4, 2021 21:51:28 GMT
Hello,
Here is my second DIY project using the Teensy 4.0.
Features:
- 7 cv ins
- 8 buttons (Red button is a mode button the other 7 are for notes/mutes)
- 1 Clickable encoder
- 128x32 oled screen which displays relevant information
- Two modes, NoteS and MUTES
- 8 selectable scales (Mayor, Doric, Frigian, Lydian, Mixolydian, Aeolian, Locrian, Augmented)
- Can change key to any of the 12 chromatic notes
- Can change octave from -2 to 6
- Can adjust note probability from 0 -100
- Each midi channel has its own mutes states, scale, octave, key and note probability.
- Each cv in triggers a note of
- Holding the red button and pressing one of the 7 main buttons changes the current midi channel
- Holding the red button and moving the encoder changes the selected scale
- Clicking the encoder will mute/unmute all notes in the current channel
- In NoteS mode each of the 7 main buttons triggers one of the notes
- In NoteS mode moving the encoder will increase/decrease the current key
- In MUTES mode each of the 7 main buttons toggles the notes mute state
- In MUTES mode moving the encoder will increase/decrease the octave
- Holding one of the 7 main buttons a moving the encoder will increase/decrease note probability (can change multiple at once)
- Can send notes up to 7 midi channels (on inital load all channels are muted except for channel 1)
In the future I would like store settings on the EPPROM so you can save your changes.
Heres a video showing how it integrates with the deluge
And heres the code, any questions/feedback let me know
Here is my second DIY project using the Teensy 4.0.
Features:
- 7 cv ins
- 8 buttons (Red button is a mode button the other 7 are for notes/mutes)
- 1 Clickable encoder
- 128x32 oled screen which displays relevant information
- Two modes, NoteS and MUTES
- 8 selectable scales (Mayor, Doric, Frigian, Lydian, Mixolydian, Aeolian, Locrian, Augmented)
- Can change key to any of the 12 chromatic notes
- Can change octave from -2 to 6
- Can adjust note probability from 0 -100
- Each midi channel has its own mutes states, scale, octave, key and note probability.
- Each cv in triggers a note of
- Holding the red button and pressing one of the 7 main buttons changes the current midi channel
- Holding the red button and moving the encoder changes the selected scale
- Clicking the encoder will mute/unmute all notes in the current channel
- In NoteS mode each of the 7 main buttons triggers one of the notes
- In NoteS mode moving the encoder will increase/decrease the current key
- In MUTES mode each of the 7 main buttons toggles the notes mute state
- In MUTES mode moving the encoder will increase/decrease the octave
- Holding one of the 7 main buttons a moving the encoder will increase/decrease note probability (can change multiple at once)
- Can send notes up to 7 midi channels (on inital load all channels are muted except for channel 1)
In the future I would like store settings on the EPPROM so you can save your changes.
Heres a video showing how it integrates with the deluge
And heres the code, any questions/feedback let me know
#include <MIDI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT);
MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, MIDI);
const int SERIAL_DATA_WIDTH = 8;
const int PULSE_WIDTH_USEC = 5;
const int ploadPin = 10; // Connects to Parallel load pin the 165
const int dataPin = 12; // Connects to the Q7 pin the 165
const int clockPin = 11; // Connects to the Clock pin the 165
const int noteVelocity = 100;
const int encoderSW = 7;
const int encoderDT = 8;
const int encoderCLK = 9;
const int cvIns[8] = { A0, A1, A2, A3, A6, A7, A8, A9 };
const long holdThreshold = 7000;
const int notes[8][7] = {
{ 0, 2, 4, 5, 7, 9, 11 }, /// Jonica
{ 0, 2, 3, 5, 7, 9, 10 }, /// Dorica
{ 0, 1, 3, 5, 7, 8, 10 }, /// Frigia
{ 0, 2, 4, 6, 7, 9, 11 }, /// Lidia
{ 0, 2, 4, 5, 7, 9, 10 }, /// Mixo
{ 0, 2, 3, 5, 7, 9, 10 }, /// Eolica
{ 0, 1, 3, 5, 6, 8, 10 }, /// Locria
{ 0, 2, 4, 6, 8, 10, 11 }, /// Aumentada
};
int midiChannel = 1;
int currentMode = 1;
long modeButtonTimer = 0;
unsigned long buttonTimers[8] = { 0, 0, 0, 0, 0, 0, 0, 0 };
unsigned int muteStates[7][7] = {
{ 0, 0, 0, 0, 0, 0, 0 },
{ 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1 }
};
unsigned int noteProbabilities[7][7] = {
{ 100, 100, 100, 100, 100, 100, 100 },
{ 100, 100, 100, 100, 100, 100, 100 },
{ 100, 100, 100, 100, 100, 100, 100 },
{ 100, 100, 100, 100, 100, 100, 100 },
{ 100, 100, 100, 100, 100, 100, 100 },
{ 100, 100, 100, 100, 100, 100, 100 },
{ 100, 100, 100, 100, 100, 100, 100 }
};
int cvStates[8] = { 0, 0, 0, 0, 0, 0, 0, 0 };
int cvPrevStates[8] = { 0, 0, 0, 0, 0, 0, 0, 0 };
int noteKeyCounters[7] = { 0, 0, 0, 0, 0, 0, 0 };
int octaveCounters[7] = { 10, 10, 10, 10, 10, 10, 10 };
int scaleCounters[7] = { 0, 0, 0, 0, 0, 0, 0 };
int octaves[7] = { 5, 5, 5, 5, 5, 5, 5 };
int noteKeys[7] = { 0, 0, 0, 0, 0, 0, 0 };
int scales[7] = { 0, 0, 0, 0, 0, 0, 0 };
int cvControlModes[7] = { 0, 0, 0, 0, 0, 0, 0 };
int midiChannelIndex = midiChannel - 1;
int currentScale = scales[midiChannelIndex];
int currentOctave = octaves[midiChannelIndex];
int currentNoteKey = noteKeys[midiChannelIndex];
boolean isScreenRefreshing = false;
boolean shouldRefreshScreen = true;
int encoderState;
int encoderLastState;
int encoderSWState;
int encoderSWLastState;
unsigned int pinValues;
unsigned int oldPinValues;
void updateSelectedModes() {
midiChannelIndex = midiChannel - 1;
currentScale = scales[midiChannelIndex];
currentOctave = octaves[midiChannelIndex];
currentNoteKey = noteKeys[midiChannelIndex];
}
void readCvIns() {
updateSelectedModes();
for(int i = 0; i <= 7; i++){
cvStates[i] = digitalRead(cvIns[i]);
if(cvStates[i] != cvPrevStates[i]) {
if(cvStates[i] == 1){
for(int j = 0; j < 7; j++) {
if(!muteStates[j][i]) {
int chanceToPlay = noteProbabilities[j][i] == 100 ? 1 : random(0, 100) <= noteProbabilities[j][i];
if(chanceToPlay == 1 && noteProbabilities[midiChannelIndex][i] != 0){
int cvNote = notes[scales[j]][i] + (octaves[j] * 12) + noteKeys[j];
MIDI.sendNoteOn(cvNote, noteVelocity, j+1);
MIDI.sendNoteOff(cvNote, 0, j + 1);
}
}
}
}
cvPrevStates[i] = cvStates[i];
}
}
}
void readEncoder() {
encoderState = digitalRead(encoderCLK);
encoderSWState = digitalRead(encoderSW);
if (encoderState != encoderLastState){
boolean isButtonHeld = false;
if (digitalRead(encoderDT) != encoderState) {
for(int i = 0; i < 8; i++){
if((pinValues >> i) & 1){
isButtonHeld = true;
if(i == 0 && scaleCounters[midiChannelIndex] > 0) scaleCounters[midiChannelIndex]--;
if(i != 0 && noteProbabilities[midiChannelIndex][i-1] > 0) noteProbabilities[midiChannelIndex][i-1]--;
}
}
if(!isButtonHeld) {
if(currentMode){
if(octaveCounters[midiChannelIndex] > 0) octaveCounters[midiChannelIndex]--;
}else{
if(noteKeyCounters[midiChannelIndex] > 0) noteKeyCounters[midiChannelIndex]--;
}
}
} else {
for(int i = 0; i < 8; i++){
if((pinValues >> i) & 1){
isButtonHeld = true;
if(i == 0 && scaleCounters[midiChannelIndex] < 14) scaleCounters[midiChannelIndex]++;
if(i != 0 && noteProbabilities[midiChannelIndex][i-1] < 100) noteProbabilities[midiChannelIndex][i-1]++;
}
}
if(!isButtonHeld){
if(currentMode){
if(octaveCounters[midiChannelIndex] < 16) octaveCounters[midiChannelIndex]++;
}else{
if(noteKeyCounters[midiChannelIndex] < 22) noteKeyCounters[midiChannelIndex]++;
}
}
}
octaves[midiChannelIndex] = round(octaveCounters[midiChannelIndex]/2);
noteKeys[midiChannelIndex] = round(noteKeyCounters[midiChannelIndex]/2);
scales[midiChannelIndex] = round(scaleCounters[midiChannelIndex]/2);
encoderLastState = encoderState;
shouldRefreshScreen = true;
}
if(encoderSWState != encoderSWLastState) {
if(!encoderSWState){
for(int i = 0; i < 8; i++){
muteStates[midiChannelIndex][i - 1] = !muteStates[midiChannelIndex][i - 1];
}
}
encoderSWLastState = encoderSWState;
shouldRefreshScreen = true;
}
}
void readShiftRegisters() {
long bitVal;
unsigned int bytesVal = 0;
digitalWrite(ploadPin, LOW);
delayMicroseconds(PULSE_WIDTH_USEC);
digitalWrite(ploadPin, HIGH);
for (int i = 0; i < SERIAL_DATA_WIDTH; i++) {
bitVal = digitalRead(dataPin);
bytesVal |= (bitVal << ((SERIAL_DATA_WIDTH-1) - i));
if((oldPinValues >> i) & 1) buttonTimers[i]++;
digitalWrite(clockPin, HIGH);
delayMicroseconds(PULSE_WIDTH_USEC);
digitalWrite(clockPin, LOW);
}
pinValues = bytesVal;
delayMicroseconds(PULSE_WIDTH_USEC);
}
void triggerButtonActions() {
updateSelectedModes();
for(int i = 0; i < SERIAL_DATA_WIDTH; i++) {
int currentPin = (pinValues >> i) & 1;
int previousPin = (oldPinValues >> i) & 1;
if(currentPin != previousPin){
if (currentPin){
if(i != 0) {
if((pinValues >> 0) & 1){
if(i <= 7) midiChannel = i;
midiChannelIndex = midiChannel -1;
shouldRefreshScreen = true;
} else {
if(currentMode == 0) {
int note = notes[currentScale][i-1] + (currentOctave * 12) + currentNoteKey;
MIDI.sendNoteOn(note, noteVelocity, midiChannel);
MIDI.sendNoteOff(note, 0, midiChannel);
}
}
}
} else {
if(i == 0){
if(buttonTimers[i] < holdThreshold) currentMode = !currentMode;
} else {
if(currentMode == 1 && buttonTimers[0] == 0 && buttonTimers[i] < holdThreshold) {
muteStates[midiChannelIndex][i - 1] = !muteStates[midiChannelIndex][i - 1];
}
}
buttonTimers[i] = 0;
shouldRefreshScreen = true;
}
}
}
}
void refreshScreen() {
isScreenRefreshing = true;
display.clearDisplay();
for(int i = 0; i < 8; i++) {
if (i == 0) {
display.setCursor(0, 2);
switch(currentNoteKey) {
case 1:
display.print("C#");
break;
case 2:
display.print("D");
break;
case 3:
display.print("D#");
break;
case 4:
display.print("E");
break;
case 5:
display.print("F");
break;
case 6:
display.print("F#");
break;
case 7:
display.print("G");
break;
case 8:
display.print("G#");
break;
case 9:
display.print("A");
break;
case 10:
display.print("A#");
break;
case 11:
display.print("B");
break;
default:
display.print("C");
break;
}
int octaveHorizontal = currentOctave < 2 ? 18 : 24;
display.setCursor(octaveHorizontal, 2);
display.print(currentOctave - 2);
display.setCursor(0, 13);
switch(currentScale) {
case 1:
display.print("Dor");
break;
case 2:
display.print("Fri");
break;
case 3:
display.print("LID");
break;
case 4:
display.print("MIX");
break;
case 5:
display.print("Eol");
break;
case 6:
display.print("dim");
break;
case 7:
display.print("AUG");
break;
default:
display.print("MAY");
break;
}
display.setCursor(24, 13);
display.print(midiChannel);
display.setCursor(0, 23);
currentMode == 0 ? display.print("NoteS") : display.print("MUTES");
} else {
int vAlign = (i * 13) + 23;
display.setCursor(12, vAlign);
if(buttonTimers[i] > holdThreshold) {
display.print(round(noteProbabilities[midiChannelIndex][i - 1]));
} else {
muteStates[midiChannelIndex][i - 1] == 1 ? display.print("X") : display.print("O");
}
}
}
display.display();
isScreenRefreshing = false;
shouldRefreshScreen = false;
}
void setup() {
Serial.begin(115200);
MIDI.begin();
display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);
delay(100);
display.setTextSize(1);
display.setTextColor(WHITE);
display.setRotation(3);
pinMode (encoderCLK,INPUT);
pinMode (encoderDT,INPUT);
pinMode(dataPin, INPUT);
pinMode(ploadPin, OUTPUT);
pinMode(clockPin, OUTPUT);
digitalWrite(clockPin, LOW);
digitalWrite(ploadPin, HIGH);
for(int i = 0; i < 7; i++) {
pinMode(cvIns[i], INPUT);
attachInterrupt(digitalPinToInterrupt(cvIns[i]), readCvIns, CHANGE);
}
readShiftRegisters();
oldPinValues = pinValues;
}
void loop() {
readShiftRegisters();
readEncoder();
triggerButtonActions();
oldPinValues = pinValues;
if(shouldRefreshScreen && !isScreenRefreshing) refreshScreen();
}