//************************************ //************************************ //** Zen Harp ** //** 6/22/2024 //** //** Low latency, Scale selection ** //************************************ //************************************ //************************************ //** Global Variables ** //************************************ //************************************ // loop stuff int havefirsthit = false; bool state_change_trigger = false; //top pad as loop start button float loopstate = 0; bool looperon = false; unsigned long loop_time_record[100]; //these variabless store the recorded loop byte loop_note_record[100]; byte loop_volume_record[100]; unsigned long bouncemillis = 0; unsigned long bounce[17]; int bouncetime = 20; bool noteonneedsprocessing[15]; bool noteoffneedsprocessing[15]; int inflight_note[15]; int record_samples = 0; unsigned long record_duration = 0; unsigned long beatduration = 0; unsigned long anotherbeatduration = 0; int record_index = 0; int active_record = 0; //0= nothing recorded //1= recording in progress //2= player on unsigned long record_start = 0; //Debugging int debugmode; bool acc_plot = false;// bool plotonce = false; unsigned long next = 0; //Teensy pin assignments const byte driverpin = 2; //common pad drive pin const byte MIDIpin = 0; const byte modebutton = 1;//mode button const byte zpin = 21; //accelerometer axes inputs const byte xpin = 23; const byte ypin = 22; const byte Rled = 13; //tricolor LED pins const byte Gled = 14; const byte Bled = 15; const byte LEDpin = 13; //teensy led //chord selection variables boolean chord1 = 0; //determined by pads 12-14 boolean chord2 = 0; boolean chord3 = 0; byte chordselect = 0; //chord selection byte lastchordselect = 0; byte playing_mode = 0; //chord pallet selection ///RGB LED variables byte Rledinput = 255; byte Gledinput = 255; byte Bledinput = 255; unsigned long RGBledtimer = 0; byte padnote[15] = { 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74 }; //arbitrary initial pad note values //first chord pallet 1 3m 4 5 byte chordA0[15] = { 45, 48, 52, 57, 60, 64, 69, 72, 76, 81, 84, 88, 93, 96, 100 };//Am byte chordA3[15] = { 41, 45, 48, 53, 57, 60, 65, 69, 72, 77, 81, 84, 89, 93, 96 };//F byte chordA1[15] = { 43, 47, 50, 55, 59, 62, 67, 71, 74, 79, 83, 86, 91, 107, 110 };//G byte chordA2[15] = { 48, 52, 55, 60, 64, 67, 72, 76, 79, 84, 88, 91, 96, 100, 103 };//C byte chordA4[15] = { 40, 43, 47, 52, 55, 59, 64, 67, 71, 76, 79, 83, 88, 91, 107 };//Em byte chordA6[15] = { 38, 41, 45, 50, 53, 57, 62, 65, 69, 74, 86, 89, 93, 98, 101 };//Dm byte chordA5[15] = { 47, 51, 53, 59, 63, 65, 71, 75, 77, 83, 87, 95, 99, 101, 107 };//Bdim byte chordA7[15] = { 52, 56, 59, 62, 64, 68, 71, 74, 76, 80, 83, 86, 88, 92, 95 };//E7 //misc varibles byte channel = 0x91; //arbitrary MIDI channel--change as desired ** byte channel2 = 0x92; //** byte pad[15] = {3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 20, 19, 18, 16, 17};//mapping from pad to teensy pins boolean padactive[15]; //state of pad as touched (HIGH) or not (LOW) byte padstate[15]; //state of pad touch as it is processed // 0 = ready for new pad touch // 1 = have touch, waiting for volume // 2 = have volume, waiting to be played (note on) // 3 = played, waiting to be turned off (note off) // 4=disable pad long int padlasttime[15]; //last time pad was triggered byte padlastchannel[15]; //last channel held to turn right note off after key changes byte padlastnote[15]; //last note held to turn right note off after key change byte padvolume[15]; //current note volume byte pnum = 0; //index for pads through each loop //capactive sensing variables long int chargetime[15]; //sensed charge time for each pad bool limitreached = false; unsigned long starttime = 0; unsigned long deltatime = 0; int hysteresishigh = 40; //turn on threshold for touch ** int hysteresislow = 20; //turn off threshold for touch ** //MIDI variables int notevolume = 0; int pitch = 0; long int min_note_duration = 100000;//micros ** //accelerometer variables int circularaccbufferx[21]; // holds samples of A/D taken before and after pad hit (about +/- 5 ms int circularaccbuffery[21]; int circularaccbufferz[21]; int circbuffpointer = 0; int triggerpoint = 0; //time of pad hit long acc_calibrationx = 0; //A/D calibration values long acc_calibrationy = 0; long acc_calibrationz = 0; boolean hithappened = LOW; unsigned long hittime = 0; float motionenergy = 0; //calibration float cap_calibration[15]; //calibration value for each pad float tempcap_calibration[15]; int calstate = 0; unsigned long cal_timer = 0; float tempacc_calibrationz = 0; unsigned long totaltime; //************************************ //************************************ //** Set-Up ** //************************************ //************************************ void setup() { if (padstate == 1) Serial1.begin(31250); // open serial comm if ((debugmode == 5) || (debugmode == 2) || (debugmode == 3) || (debugmode == 3)) Serial1.begin(9600); pinMode(LEDpin, OUTPUT); //teensy built in LED pinMode(Rled, OUTPUT); pinMode(Bled, OUTPUT); pinMode(Gled, OUTPUT); pinMode(driverpin, OUTPUT); pinMode(MIDIpin, OUTPUT); pinMode(modebutton, INPUT); pinMode(pad[0], INPUT); pinMode(pad[1], INPUT); pinMode(pad[2], INPUT); pinMode(pad[3], INPUT); pinMode(pad[4], INPUT); pinMode(pad[5], INPUT); pinMode(pad[6], INPUT); pinMode(pad[7], INPUT); pinMode(pad[8], INPUT); pinMode(pad[9], INPUT); pinMode(pad[10], INPUT); pinMode(pad[11], INPUT); pinMode(pad[12], INPUT); pinMode(pad[13], INPUT); pinMode(pad[14], INPUT); digitalWrite(modebutton, HIGH);//seet pullup resistor analogWrite(Rled, 256); //blank LEDs analogWrite(Gled, 256); analogWrite(Bled, 256); }//end setup //************************************ //************************************ //** Main Loop ** //************************************ //************************************ // the main loop routine runs over and over again forever: void loop() { debugmode = 0; //0= nothing //2= show pin capacitance sensing values //3= show accelerometer values as text //4= plot accelerometer values (use Arduino plotting function) //5= print overflows on timing if (millis() > next + 1000) { if (debugmode == 4) acc_plot = true; if (debugmode == 2) chargedata_dump(); if (debugmode == 3)acceleration_dump(); } //set mode by presses of mode button if (digitalRead(modebutton) == LOW) { if (playing_mode == 0) //red { analogWrite(Rled, 0); analogWrite(Gled, 200); delay (1000); analogWrite(Rled, 256); analogWrite(Gled, 256); analogWrite(Bled, 256); playing_mode = 1; looperon=true; padstate[14]=0; // Serial.println("playing mode 1"); } else if (playing_mode == 1) { analogWrite(Gled, 0); analogWrite(Bled, 220); delay (1000); analogWrite(Rled, 256); analogWrite(Gled, 256); analogWrite(Bled, 256); playing_mode = 0; looperon=false; //Serial.println("playing mode 0"); } // else if (playing_mode == 2) // { // analogWrite(Bled, 0); // delay (1000); // analogWrite(Rled, 256); // analogWrite(Gled, 256); // analogWrite(Bled, 256); // playing_mode = 0; // } } //******* pad calibration only when inactive for 2 seconds //reset if activity for (int p = 0; p < 14; p++) { if (padactive[p] == true) { cal_timer = millis(); //reset calibration deadman timer calstate = 0; } } //one second of quiet, do tentative accel calibration if ((calstate == 0) && (cal_timer + 1000) < millis()) { calstate = 1; tempacc_calibrationz = analogRead(zpin); for (int x = 0; x < 15; x++) cap_calibration[x] = tempcap_calibration[x]; } //two seconds of quiet commit cap calibration and commit accel calibration if ((calstate == 1) && ((cal_timer + 2000) < millis())) // { calstate = 2; for (int x = 0; x < 15; x++) cap_calibration[x] = tempcap_calibration[x]; acc_calibrationz = tempacc_calibrationz; //collected during scan //delay(1000); } //************************************ // Touch Sensing //*********************************** //loop through each of 15 pads according to pnum if (pnum < 14) pnum++; else pnum = 0; limitreached = false;//overflow detection //index for collecting 20 acceleration values if (circbuffpointer < 20) circbuffpointer++; //!!!set to 20 tops else circbuffpointer = 0; //accel. axis circular buffer pointer //*********************interleaved x-axis accelerometer read ************************************** circularaccbufferx[circbuffpointer] = analogRead(xpin); //delayMicroseconds(30); //a/d conversion time about 26us? //*********************interleave x-axis end****************************************************** //CHARGEUP //set driver pin high and measure rise time of selected pad digitalWrite(driverpin, HIGH); // common driver pin high starttime = micros(); while ((digitalRead(pad[pnum]) == LOW) && (limitreached == false)) //digital read is pretty slow it seems { if (micros() - starttime > 1000) { limitreached = true; if (debugmode == 5) //debug output to console { Serial.print ("limitreached up on pin:"); Serial.print(pnum); Serial.println(""); } } } //end charge while loop deltatime = micros() - starttime; //time it took to charge up (longer if being touched) //top off input pin to full voltage pinMode(pad[pnum], OUTPUT); digitalWrite(pad[pnum], HIGH); //set pullup resistor to on delayMicroseconds(100);//not needed if have delay from A/D pinMode(pad[pnum], INPUT); //turn off pull up resistor //*********************interleaved y-axis accelerometer read ************************************** circularaccbuffery[circbuffpointer] = analogRead(ypin); //delayMicroseconds(30); //*********************interleave y-axis end ****************************************************** //CHARGE DOWN digitalWrite(driverpin, LOW); starttime = micros(); while ((digitalRead(pad[pnum]) == HIGH) && (limitreached == false)) { if (micros() - starttime > 1000) { limitreached = true; if (debugmode == 0) //debug mode console output { Serial.print ("limitreached down on pin:"); Serial.print(pnum); Serial.println(""); } } } //end discharging while loops deltatime = deltatime + micros() - starttime; //add rise and fall times together chargetime[pnum] = deltatime; //*********************interleaved z-axis accelerometer read ***************************** circularaccbufferz[circbuffpointer] = analogRead(zpin); //delayMicroseconds(30); //*********************interleave #3 ****************************************************** //full drain input pin to zero voltage pinMode(pad[pnum], OUTPUT); digitalWrite(pad[pnum], LOW); //set pullup resistor to on delayMicroseconds(100);//not needed if have delay from A/D pinMode(pad[pnum], INPUT); //turn off pull up resistor if (calstate == 1) { tempcap_calibration[pnum] = chargetime[pnum]; //grab calibration values if no touches } //************************************ //** End of Touch Sensing ** //************************************ //************************************ //** Touch Logic ** //************************************ //touch determination **************************** if (chargetime[pnum] - cap_calibration[pnum] > hysteresishigh) { padactive[pnum] = HIGH; hithappened = HIGH; //exclude chord keys hittime = millis(); // Serial.print("active:"); // Serial.println(pnum); } else if (chargetime[pnum] - cap_calibration[pnum] < hysteresishigh) { padactive[pnum] = LOW; } //special non playing pads--chord selectors if (padactive[12] == true) chord1 = HIGH; else chord1 = LOW; if (padactive[13] == true) chord2 = HIGH; else chord2 = LOW; if ((looperon == false) && (padactive[14] == true)) chord3 = HIGH; else chord3 = LOW; //decode chord selector values to chords if ((chord1 == LOW) && (chord2 == LOW) && (chord3 == LOW)) chordselect = 0; else if ((chord1 == HIGH) && (chord2 == LOW) && (chord3 == LOW)) chordselect = 1; else if ((chord1 == LOW) && (chord2 == HIGH) && (chord3 == LOW)) chordselect = 2; else if ((chord1 == HIGH) && (chord2 == HIGH) && (chord3 == LOW)) chordselect = 3; else if ((chord1 == LOW) && (chord2 == LOW) && (chord3 == HIGH)) chordselect = 4; else if ((chord1 == HIGH) && (chord2 == LOW) && (chord3 == HIGH)) chordselect = 5; else if ((chord1 == LOW) && (chord2 == HIGH) && (chord3 == HIGH)) chordselect = 6; else if ((chord1 == HIGH) && (chord2 == HIGH) && (chord3 == HIGH)) chordselect = 7; //load chord pallets ************************************************* if (chordselect != lastchordselect) //only do this if new chord { lastchordselect = chordselect; if (playing_mode == 0) { if (chordselect == 0)for (int i = 0; i < 15; i++) padnote[i] = chordA0[i]; if (chordselect == 1)for (int i = 0; i < 15; i++) padnote[i] = chordA1[i]; if (chordselect == 2)for (int i = 0; i < 15; i++) padnote[i] = chordA2[i]; if (chordselect == 3)for (int i = 0; i < 15; i++) padnote[i] = chordA3[i]; if (chordselect == 4)for (int i = 0; i < 15; i++) padnote[i] = chordA4[i]; if (chordselect == 5)for (int i = 0; i < 15; i++) padnote[i] = chordA5[i]; if (chordselect == 6)for (int i = 0; i < 15; i++) padnote[i] = chordA6[i]; if (chordselect == 7)for (int i = 0; i < 15; i++) padnote[i] = chordA7[i]; } if (playing_mode == 1) { if (chordselect == 0)for (int i = 0; i < 15; i++) padnote[i] = chordA0[i]; if (chordselect == 1)for (int i = 0; i < 15; i++) padnote[i] = chordA1[i]; if (chordselect == 2)for (int i = 0; i < 15; i++) padnote[i] = chordA2[i]; if (chordselect == 3)for (int i = 0; i < 15; i++) padnote[i] = chordA3[i]; if (chordselect == 4)for (int i = 0; i < 15; i++) padnote[i] = chordA4[i]; if (chordselect == 5)for (int i = 0; i < 15; i++) padnote[i] = chordA5[i]; if (chordselect == 6)for (int i = 0; i < 15; i++) padnote[i] = chordA6[i]; if (chordselect == 7)for (int i = 0; i < 15; i++) padnote[i] = chordA7[i]; } for (int z = 0; z < 12; z++) { usbMIDI.sendNoteOn(inflight_note[z], 0, channel2); } } //deactivate these pins for all other functions padstate[12] = 4; padstate[13] = 4; if (looperon == false) padstate[14] = 4; //regular notes if ((padstate[pnum] == 0) && (padactive[pnum] == HIGH)) //ready for new note { // Serial.print("ready for new note:"); // Serial.println(pnum); padlasttime[pnum] = micros(); //keep this fresh as long as pad is held--enforces minimum note on time triggerpoint = 0; padstate[pnum] = 1; //1 marks pending note before volume is determined } //************************************ //** Volume (acceleration) Sensing ** //************************************ //stall to collect acceleration data after pad hit if (circbuffpointer == 10) //wait 10 loops (1/2 of buffer)after note hit, then find max { //***plot z accel and energy*********************************** if ((acc_plot == true) && (plotonce == true)) { float integrate = 0; plotonce = false; for (int x = 0; x < 20; x++) { Serial.print("acc:"); Serial.print((circularaccbufferz[x] - acc_calibrationz)); Serial.print(","); Serial.print("int:"); integrate = integrate + abs(circularaccbufferz[x] - acc_calibrationz); Serial.println((integrate)); } for (int x = 0; x < 6; x++)//print separator line { Serial.print("acc:"); Serial.print(0); Serial.print(","); Serial.print("int:"); Serial.println(0); } } //end of plot************************************** //load up pending all notes with volume numbers for (int x = 0; x < 15; x++) { if (padstate[x] == 1)// ready for volume //todo: make better velocity curve { motionenergy = 0; for (int x = 0; x < 20; x++) //find area under accelerometer curve after hit { motionenergy = motionenergy + abs(circularaccbufferz[x] - acc_calibrationz); } // Serial.print("motionenergy:"); // Serial.println(motionenergy); padvolume[x] = map(motionenergy, 0, 5000, 0, 127); //linear velocity curve if (padvolume[x] > 127) padvolume[x] = 127; if (padvolume[x] < 3) padvolume[x] = 3; //piecewise velocity curve // if (motionenergy < 500) padvolume[x] = 10; // else if (motionenergy < 2000) padvolume[x] = 80; // else padvolume[x] = 127; padstate[x] = 2; // Serial.print("volume:"); // Serial.println(padvolume[x]); } } } //************************************ //** End of Volume Sensing ** //************************************ //play notes ***************************************************************** for (int x = 0; x < 15; x++) { if (padstate[x] == 2) { padstate[x] = 3; //3 is ready for note off plotonce = true; pitch = padnote[x]; padlastchannel[x] = channel; padlastnote[x] = padnote[x]; if ((looperon == true) && (x == 14)) { state_change_trigger = true; //Serial.println("state change"); } else { //play channel 1 unless looper on and recording if ((looperon == false) || (active_record == 3)) { usbMIDI.sendNoteOn(pitch, padvolume[x], channel); //Serial.println(x); } else { usbMIDI.sendNoteOn(pitch, padvolume[x], channel2); //Serial.println(x); } if (active_record == 1)noteonneedsprocessing[x] = 1; } noteOn(channel, pitch, padvolume[x]); } //turn off notes ************************************************** for (int x = 0; x < 15; x++) { if ((padactive[x] == LOW) && (padstate[x] == 3) && (micros() - padlasttime[x] > min_note_duration)) //need reset { padstate[x] = 0; pitch = padlastnote[x]; channel = padlastchannel[x]; if (x != 14) { if ((looperon == false) || (active_record == 3)) usbMIDI.sendNoteOff(pitch, 0, channel); else usbMIDI.sendNoteOff(pitch, 0, channel2); if (active_record == 1) noteoffneedsprocessing[x] = 1; } noteOn(channel, pitch, 0); } } } //end of play notes with no looping //************************************************************************* //************************************************************************* //****** looper state manchine **************************************** //************************************************************************* //(1)arm if ((loopstate == 0) && (state_change_trigger == true)) { state_change_trigger = false; //Serial.println("moving to armed"); analogWrite(Rled, 200); //purple is armed analogWrite(Gled, 240); analogWrite(Bled, 200); loopstate = 1; active_record = 1; //recording in progress } //(2) record notes if (loopstate == 1) { for (int x = 0; x < 15; x++) { //note on if (noteonneedsprocessing[x] == true) { // Serial.print ("playing note on: "); // Serial.println (x); noteonneedsprocessing[x] = false; havefirsthit++; if (havefirsthit == 1)//start recording on first hit { record_start = millis(); record_index = 0; analogWrite(Rled, 200); //blank LEDs analogWrite(Gled, 230); analogWrite(Bled, 256); } loop_time_record[record_index] = millis() - record_start; // loop_note_record[record_index] = x; loop_volume_record[record_index] = padvolume[x]; record_index++; if (record_index > 98) { Serial.println("overflow"); loopstate = 0; } } // note off if (noteoffneedsprocessing[x] == true) { // Serial.print ("playing note off:"); // Serial.println (x); noteoffneedsprocessing[x] = false; loop_time_record[record_index] = millis() - record_start; // loop_note_record[record_index] = x; loop_volume_record[record_index] = 0; record_index++; if (record_index > 98) { Serial.println("overflow"); loopstate = 0; } } } } //(3) stop recording if ((loopstate == 1) && (state_change_trigger == true)) { state_change_trigger = false; // Serial.println("moving to done"); loopstate = 2; active_record = 3; //recording done analogWrite(Rled, 256); //blank LEDs analogWrite(Gled, 246); analogWrite(Bled, 200); loop_time_record[record_index] = millis() - record_start; //one more record of end time loop_note_record[record_index] = 0; loop_volume_record[record_index] = 0; record_samples = record_index; record_duration = loop_time_record[record_samples] - loop_time_record[0]; beatduration = loop_time_record[record_index - 2]; anotherbeatduration = loop_time_record[record_index ]; record_start = millis(); //loop_time_record[0]; record_index=0; } //(3) flush for new recording if ((loopstate == 2) && (state_change_trigger == true)) { state_change_trigger = false; // Serial.println("flushing buffer"); havefirsthit = 0; loopstate = 0; record_index = 0; active_record = 0; //nothing recorded for (int y = 0; y < 100; y++) { loop_time_record[y] = 0; loop_note_record[y] = 0; loop_volume_record[y] = 0; } for (int y = 0; y < 120; y++) { usbMIDI.sendNoteOn(y, 0, channel); usbMIDI.sendNoteOn(y, 0, channel2); // Serial.println(y); } analogWrite(Rled, 256); //blank LEDs analogWrite(Gled, 256); analogWrite(Bled, 256); } //***************************************************************************** //loop state machine end //***************************************************************************** //***************************************************************************** //loop player //***************************************************************************** if (active_record == 3) //recording done { //record_ player********************************* if (loop_time_record[record_index] < (millis() - record_start)) { usbMIDI.sendNoteOn(padnote[loop_note_record[record_index]], loop_volume_record[record_index], channel2); inflight_note[loop_note_record[record_index]] = padnote[loop_note_record[record_index]]; // a record of open notes stored by pad number record_index++; if (record_index > record_samples) { record_start = millis(); record_index = 0; } } } //***************************************************************************** //loop player end //***************************************************************************** }//end main loop //************************************ //************************************ //** Functions ** //************************************ //************************************ //********Functions (Subroutines) // plays a MIDI note. Doesn't range check void noteOn(int cmd, int pitch, int velocity) { if (debugmode == 1) { Serial1.write(cmd); Serial1.write(pitch); Serial1.write(velocity); } } void acceleration_dump(void) //debuging routine //useful for testing accelerometer { next = millis(); int indexer = 0; for (int p = 0; p < 20; p++) { if (p == 10) //!! { Serial.println(""); Serial.println("hit point"); } Serial.println(""); Serial.print(p); Serial.print(" z:"); Serial.print(circularaccbufferz[circbuffpointer + indexer] - int(acc_calibrationz)); Serial.print(" raw: "); Serial.print(circularaccbufferz[circbuffpointer + indexer]); Serial.print(" cal:"); Serial.print(int(acc_calibrationz)); indexer++; if (indexer + circbuffpointer > 19) indexer = -circbuffpointer; ///!!!! } } void chargedata_dump(void) //debugging routine //useful for testing capacitive sensing and pad wiring { next = millis(); for (int x = 0; x < 15; x++) //prints out all charge times { Serial.print(" pin:"); Serial.print(x); Serial.print("="); Serial.print(chargetime[x] - cap_calibration[x]); Serial.print("/"); Serial.print(padactive[x]); if (x == 9) Serial.println(""); } Serial.println(""); Serial.println(""); /*Serial.print ("padvol#1: "); Serial.print (padstate[1]); Serial.print ("-xvalley: "); Serial.print(-xaxisvalley); Serial.println(""); */ } //check for free RAM--thanks to jeelabs.org int freeRam () { extern int __heap_start, *__brkval; int v; return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); } void acceltest(void)//output accelerometer data { Serial.print(" \tZ: "); Serial.print(analogRead(xpin)); Serial.println(); delay(200); } void acceleration_dump2(void) //debuging routine //useful for testing accelerometer { Serial.print(" \tZ: "); Serial.println(analogRead(xpin)); int indexer = 0; Serial.print("acc_calibrationz:"); Serial.println(acc_calibrationz); for (int p = 0; p < 20; p++) { if (p == 10) //!! { Serial.println(""); Serial.println("hit point"); } Serial.print(" z:"); Serial.print(circularaccbufferz[circbuffpointer + indexer] - acc_calibrationz); indexer++; if (indexer + circbuffpointer > 19) indexer = -circbuffpointer; ///!!!! } Serial.println (""); Serial.print (" z cal raw:"); Serial.println (acc_calibrationz, 5);