diff --git a/USBCore.cpp b/USBCore.cpp new file mode 100644 index 0000000..a5bdab1 --- /dev/null +++ b/USBCore.cpp @@ -0,0 +1,864 @@ + + +/* Copyright (c) 2010, Peter Barrett +** Sleep/Wakeup support added by Michael Dreher +** +** Permission to use, copy, modify, and/or distribute this software for +** any purpose with or without fee is hereby granted, provided that the +** above copyright notice and this permission notice appear in all copies. +** +** THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +** WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +** WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR +** BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +** OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +** WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +** ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +** SOFTWARE. +*/ + +//We include our own version of this core USB file with WARBL because it has been modified to remove the CDC serial class to make the device fully class-compliant. +//The line below will remove serial communication to make the device fully class-commpliant and not require drivers on any OS. +//It can be commented out to use Serial.print and to make it unnecessary to double-click the reset button for programming. + + +#define CDCCON_DISABLE + + +#include "USBAPI.h" +#include "PluggableUSB.h" +#include + +#if defined(USBCON) + +/** Pulse generation counters to keep track of the number of milliseconds remaining for each pulse type */ +#define TX_RX_LED_PULSE_MS 100 +volatile u8 TxLEDPulse; /**< Milliseconds remaining for data Tx LED pulse */ +volatile u8 RxLEDPulse; /**< Milliseconds remaining for data Rx LED pulse */ + +//================================================================== +//================================================================== + +extern const u16 STRING_LANGUAGE[] PROGMEM; +extern const u8 STRING_PRODUCT[] PROGMEM; +extern const u8 STRING_MANUFACTURER[] PROGMEM; +extern const DeviceDescriptor USB_DeviceDescriptorIAD PROGMEM; + + +const u16 STRING_LANGUAGE[2] = { + (3<<8) | (2+2), + 0x0409 // English +}; + +#ifndef USB_PRODUCT +// If no product is provided, use USB IO Board +#define USB_PRODUCT "USB IO Board" +#endif + +const u8 STRING_PRODUCT[] PROGMEM = USB_PRODUCT; + + +# define USB_MANUFACTURER "Mowry Stringed Instruments" + + +const u8 STRING_MANUFACTURER[] PROGMEM = USB_MANUFACTURER; + + +#define DEVICE_CLASS 0x02 + + +const DeviceDescriptor USB_DeviceDescriptorIAD = + D_DEVICE(0xEF,0x02,0x01,64,USB_VID,USB_PID,0x100,IMANUFACTURER,IPRODUCT,ISERIAL,1); + + +//================================================================== +//================================================================== + +volatile u8 _usbConfiguration = 0; +volatile u8 _usbCurrentStatus = 0; // meaning of bits see usb_20.pdf, Figure 9-4. Information Returned by a GetStatus() Request to a Device +volatile u8 _usbSuspendState = 0; // copy of UDINT to check SUSPI and WAKEUPI bits + +static inline void WaitIN(void) +{ + while (!(UEINTX & (1< len) { + n = len; + } + + { + LockEP lock(ep); + // Frame may have been released by the SOF interrupt handler + if (!ReadWriteAllowed()) + continue; + + len -= n; + if (ep & TRANSFER_ZERO) + { + while (n--) + Send8(0); + } + else if (ep & TRANSFER_PGM) + { + while (n--) + Send8(pgm_read_byte(data++)); + } + else + { + while (n--) + Send8(*data++); + } + + if (sendZlp) { + ReleaseTX(); + sendZlp = false; + } else if (!ReadWriteAllowed()) { // ...release if buffer is full... + ReleaseTX(); + if (len == 0) sendZlp = true; + } else if ((len == 0) && (ep & TRANSFER_RELEASE)) { // ...or if forced with TRANSFER_RELEASE + // XXX: TRANSFER_RELEASE is never used can be removed? + ReleaseTX(); + } + } + } + //TXLED1; // light the TX LED + //TxLEDPulse = TX_RX_LED_PULSE_MS; //not used for WARBL + return r; +} + +u8 _initEndpoints[USB_ENDPOINTS] = +{ + 0, // Control Endpoint + + //#if !defined(CDCCON_DISABLE) + EP_TYPE_INTERRUPT_IN, // CDC_ENDPOINT_ACM + EP_TYPE_BULK_OUT, // CDC_ENDPOINT_OUT + EP_TYPE_BULK_IN, // CDC_ENDPOINT_IN +//#endif + // Following endpoints are automatically initialized to 0 +}; + +#define EP_SINGLE_64 0x32 // EP0 +#define EP_DOUBLE_64 0x36 // Other endpoints +#define EP_SINGLE_16 0x12 + +static +void InitEP(u8 index, u8 type, u8 size) +{ + UENUM = index; + UECONX = (1< 64){ + recvLength = 64; + } + + // Write data to fit to the end (not the beginning) of the array + WaitOUT(); + Recv((u8*)d + len - length, recvLength); + ClearOUT(); + length -= recvLength; + } + return len; +} + +static u8 SendInterfaces() +{ + u8 interfaces = 0; + +#if !defined(CDCCON_DISABLE) + CDC_GetInterface(&interfaces); +#endif + +#ifdef PLUGGABLE_USB_ENABLED + PluggableUSB().getInterface(&interfaces); +#endif + + return interfaces; +} + +// Construct a dynamic configuration descriptor +// This really needs dynamic endpoint allocation etc +// TODO +static +bool SendConfiguration(int maxlen) +{ + // Count and measure interfaces + InitControl(0); + u8 interfaces = SendInterfaces(); + ConfigDescriptor config = D_CONFIG(_cmark + sizeof(ConfigDescriptor),interfaces); + + // Now send them + InitControl(maxlen); + USB_SendControl(0,&config,sizeof(ConfigDescriptor)); + SendInterfaces(); + return true; +} + +static +bool SendDescriptor(USBSetup& setup) +{ + int ret; + u8 t = setup.wValueH; + if (USB_CONFIGURATION_DESCRIPTOR_TYPE == t) + return SendConfiguration(setup.wLength); + + InitControl(setup.wLength); +#ifdef PLUGGABLE_USB_ENABLED + ret = PluggableUSB().getDescriptor(setup); + if (ret != 0) { + return (ret > 0 ? true : false); + } +#endif + + const u8* desc_addr = 0; + if (USB_DEVICE_DESCRIPTOR_TYPE == t) + { + desc_addr = (const u8*)&USB_DeviceDescriptorIAD; + } + else if (USB_STRING_DESCRIPTOR_TYPE == t) + { + if (setup.wValueL == 0) { + desc_addr = (const u8*)&STRING_LANGUAGE; + } + else if (setup.wValueL == IPRODUCT) { + return USB_SendStringDescriptor(STRING_PRODUCT, strlen(USB_PRODUCT), TRANSFER_PGM); + } + else if (setup.wValueL == IMANUFACTURER) { + return USB_SendStringDescriptor(STRING_MANUFACTURER, strlen(USB_MANUFACTURER), TRANSFER_PGM); + } + else if (setup.wValueL == ISERIAL) { +#ifdef PLUGGABLE_USB_ENABLED + char name[ISERIAL_MAX_LEN]; + PluggableUSB().getShortName(name); + return USB_SendStringDescriptor((uint8_t*)name, strlen(name), 0); +#endif + } + else + return false; + } + + if (desc_addr == 0) + return false; + u8 desc_length = pgm_read_byte(desc_addr); + + USB_SendControl(TRANSFER_PGM,desc_addr,desc_length); + return true; +} + +// Endpoint 0 interrupt +ISR(USB_COM_vect) +{ + SetEP(0); + if (!ReceivedSetupInt()) + return; + + USBSetup setup; + Recv((u8*)&setup,8); + ClearSetupInt(); + + u8 requestType = setup.bmRequestType; + if (requestType & REQUEST_DEVICETOHOST) + WaitIN(); + else + ClearIN(); + + bool ok = true; + if (REQUEST_STANDARD == (requestType & REQUEST_TYPE)) + { + // Standard Requests + u8 r = setup.bRequest; + u16 wValue = setup.wValueL | (setup.wValueH << 8); + if (GET_STATUS == r) + { + if (requestType == (REQUEST_DEVICETOHOST | REQUEST_STANDARD | REQUEST_DEVICE)) + { + Send8(_usbCurrentStatus); + Send8(0); + } + else + { + // TODO: handle the HALT state of an endpoint here + // see "Figure 9-6. Information Returned by a GetStatus() Request to an Endpoint" in usb_20.pdf for more information + Send8(0); + Send8(0); + } + } + else if (CLEAR_FEATURE == r) + { + if((requestType == (REQUEST_HOSTTODEVICE | REQUEST_STANDARD | REQUEST_DEVICE)) + && (wValue == DEVICE_REMOTE_WAKEUP)) + { + _usbCurrentStatus &= ~FEATURE_REMOTE_WAKEUP_ENABLED; + } + } + else if (SET_FEATURE == r) + { + if((requestType == (REQUEST_HOSTTODEVICE | REQUEST_STANDARD | REQUEST_DEVICE)) + && (wValue == DEVICE_REMOTE_WAKEUP)) + { + _usbCurrentStatus |= FEATURE_REMOTE_WAKEUP_ENABLED; + } + } + else if (SET_ADDRESS == r) + { + WaitIN(); + UDADDR = setup.wValueL | (1< 0) { + integrator[j]--; + } + } else if (integrator[j] < MAXIMUM) { //if the button reads high, increase the integrator by 1 + integrator[j]++; + } + + + if (integrator[j] == 0) { //the button is pressed. + pressed[j] = 1; //we make the output the inverse of the input so that a pressed button reads as a "1". + buttonUsed = 1; //flag that there's been button activity, so we know to handle it. + + if (prevOutput[j] == 0 && !longPressUsed[j]) { + justPressed[j] = 1; //the button has just been pressed + } + + else { + justPressed[j] = 0; + } + + if (prevOutput[j] == 1) { //increase a counter so we know when a button has been held for a long press + longPressCounter[j]++; + } + } + + + + else if (integrator[j] >= MAXIMUM) { //the button is not pressed + pressed[j] = 0; + integrator[j] = MAXIMUM; // defensive code if integrator got corrupted + + if (prevOutput[j] == 1 && !longPressUsed[j]) { + released[j] = 1; //the button has just been released + buttonUsed = 1; + } + + longPress[j] = 0; + longPressUsed[j] = 0; //if a button is not pressed, reset the flag that tells us it's been used for a long press. + longPressCounter[j] = 0; + } + + + + if (longPressCounter[j] > 300 && !longPressUsed[j]) { //if the counter gets to a certain level, it's a long press + longPress[j] = 1; + } + + + prevOutput[j] = pressed[j]; //keep track of state for next time around. + + } +} + +// ADC complete ISR for reading sensors. We read the sensors asynchronously by starting a conversion and then coming back by interrupt when it is complete. +ISR (ADC_vect) +{ + + byte prev = 8; + if (lastRead != 0) { + prev = lastRead - 1; + } + + byte next = lastRead + 1; + if (lastRead == 8) { + next = 0; + } + + if (lastRead == 9) { //this time is different because we're about to take a short break and read the pressure sensor. + + digitalWrite2f(pins[8], LOW); //turn off the previous LED + tempToneholeRead[8] = (ADC) - tempToneholeReadA[8]; //get the previous illuminated reading and subtract the ambient light reading + Timer1.resume(); //start the timer to take a short break, conserving some power. + return; + } + + if (firstTime) { + if (lastRead == 0) { + tempSensorValue = (ADC); //we've just returned from starting the pressure sensor reading, so get the conversion and flag that it is ready. + sensorDataReady = 1; + } else { + digitalWrite2f(pins[prev], LOW); //turn off the previous LED + tempToneholeRead[prev] = (ADC) - tempToneholeReadA[prev]; //get the previous illuminated reading and subtract the ambient light reading + } + ADC_read(holeTrans[next]); // start ambient reading for next sensor + firstTime = 0; + if (lastRead != 0) { + digitalWrite2f(pins[lastRead], HIGH); //turn on the current LED + } + return; + } + + //if !firstTime (implied, because we wouldn't have gotten this far otherwise) + tempToneholeReadA[next] = (ADC); //get the ambient reading for the next sensor + ADC_read(holeTrans[lastRead]); //start illuminated reading for current sensor + firstTime = 1; + lastRead ++; +} + +//Timer ISR for adding a small delay after reading new sensors, to conserve a bit of power and give the processor time to catch up if necesary. +void timerDelay(void) +{ + + timerCycle ++; + + if (timerCycle == 2) { + digitalWrite2f(pins[0], HIGH); //turn on the LED for the bell sensor (this won't use any power if the sensor isn't plugged in). It will be on for ~170 uS, which is necesary because it's a slower sensor than the others. + return; + } + + if (timerCycle == 3) { + Timer1.stop(); //stop the timer after running twice to add a delay. + ADC_read(4); //start reading the pressure sensor (pinA4). + firstTime = 1; + lastRead = 0; + timerCycle = 0; + } + //if timerCycle is 1 we just return and wait until next time. +} + +//Determine which holes are covered +void get_fingers() +{ + + for (byte i = 0; i < 9; i++) { + if ((toneholeRead[i]) > (toneholeCovered[i] - 50)) { + bitWrite(holeCovered, i, 1); //use the tonehole readings to decide which holes are covered + } else if ((toneholeRead[i]) <= (toneholeCovered[i] - 54)) { + bitWrite(holeCovered, i, 0); //decide which holes are uncovered -- the "hole uncovered" reading is a little less then the "hole covered" reading, to prevent oscillations. + } + } +} + +//Send the pattern of covered holes to the Configuration Tool +void send_fingers() +{ + + if (communicationMode) { //send information about which holes are covered to the Configuration Tool if we're connected. Because it's MIDI we have to send it in two 7-bit chunks. + sendUSBMIDI(CC, 7, 114, holeCovered >> 7); + sendUSBMIDI(CC, 7, 115, lowByte(holeCovered)); + } +} + +//Return a MIDI note number (0-127) based on the current fingering. The analog readings of the 9 hole sensors are also stored in the tempToneholeRead variable for later use. +int get_note(unsigned int fingerPattern) +{ + int ret = -1; //default for unknown fingering + + switch (modeSelector[mode]) { //determine the note based on the fingering pattern + + // Modification by Artus40 + case kModeBoha: + tempCovered = (0b111111110 & fingerPattern) >> 1; // Ignoring bell sensor + ret = pgm_read_byte(&boha_explicit[tempCovered].melodic_note); + return ret; + // End modifications by Artus40 + + case kModeWhistle: //these first two are the same + + case kModeChromatic: + + tempCovered = (0b011111100 & fingerPattern) >> 2; //use bitmask and bitshift to ignore thumb sensor, R4 sensor, and bell sensor when using tinwhistle fingering. The R4 value will be checked later to see if a note needs to be flattened. + ret = pgm_read_byte(&tinwhistle_explicit[tempCovered].midi_note); + if (modeSelector[mode] == kModeChromatic && bitRead(fingerPattern, 1) == 1) { + ret--; //lower a semitone if R4 hole is covered and we're using the chromatic pattern + } + if (fingerPattern == holeCovered) { + vibratoEnable = pgm_read_byte(&tinwhistle_explicit[tempCovered].vibrato); + } + return ret; + + + + case kModeUilleann: //these two are the same, with the exception of cancelling accidentals. + + case kModeUilleannStandard: //the same as the previous one, but we cancel the accidentals Bb and F# + + + //If back D open, always play the D and allow finger vibrato + if ((fingerPattern & 0b100000000) == 0) { + if (fingerPattern == holeCovered) { + vibratoEnable = 0b000010; + } + return 74; + } + tempCovered = (0b011111110 & fingerPattern) >> 1; //ignore thumb hole and bell sensor + ret = pgm_read_byte(&uilleann_explicit[tempCovered].midi_note); + if (fingerPattern == holeCovered) { + vibratoEnable = pgm_read_byte(&uilleann_explicit[tempCovered].vibrato); + } + if (modeSelector[mode] == kModeUilleannStandard) { //cancel accidentals if we're in standard uilleann mode + if (tempCovered == 0b1001000 || tempCovered == 0b1001010) { + return 71; + } + + if (tempCovered == 0b1101000 || tempCovered == 0b1101010) { + return 69; + } + } + return ret; + + + + case kModeGHB: + + //If back A open, always play the A + if ((fingerPattern & 0b100000000) == 0) { + return 74; + } + tempCovered = (0b011111110 & fingerPattern) >> 1; //ignore thumb hole and bell sensor + ret = pgm_read_byte(&GHB_explicit[tempCovered].midi_note); + return ret; + + + + case kModeRecorder: //this is especially messy, should be cleaned up + + //If back thumb and L1 are open + if ((((fingerPattern & 0b110000000) == 0) && (fingerPattern & 0b011111111) != 0) && (fingerPattern >> 1) != 0b00101100) { + return 76; //play D + } + if (fingerPattern >> 1 == 0b01011010) return 88; //special fingering for high D + if (fingerPattern >> 1 == 0b01001100) return 86; //special fingering for high C + //otherwise check the chart. + tempCovered = (0b011111110 & fingerPattern) >> 1; //ignore thumb hole and bell sensor + ret = pgm_read_byte(&recorder_explicit[tempCovered].midi_note); + //If back thumb is open + if ((fingerPattern & 0b100000000) == 0 && (fingerPattern >> 1) != 0b00101100) { + ret = ret + 12; + } + return ret; + + + + case kModeNorthumbrian: + + //If back A open, always play the A + if ((fingerPattern & 0b100000000) == 0) { + return 74; + } + tempCovered = fingerPattern >> 1; //bitshift once to ignore bell sensor reading + tempCovered = findleftmostunsetbit(tempCovered); //here we find the index of the leftmost uncovered hole, which will be used to determine the note from the general chart. + for (uint8_t i = 0; i < 9; i++) { + if (tempCovered == pgm_read_byte(&northumbrian_general[i].keys)) { + ret = pgm_read_byte(&northumbrian_general[i].midi_note); + return ret; + } + } + break; + + + case kModeGaita: + + tempCovered = fingerPattern >> 1; //bitshift once to ignore bell sensor reading + ret = pgm_read_byte(&gaita_explicit[tempCovered].midi_note); + if (ret == 0) { + ret = -1; + } + return ret; + + + + case kModeGaitaExtended: + + tempCovered = fingerPattern >> 1; //bitshift once to ignore bell sensor reading + ret = pgm_read_byte(&gaita_extended_explicit[tempCovered].midi_note); + if (ret == 0) { + ret = -1; + } + return ret; + + + + case kModeNAF: + + tempCovered = (0b011111110 & fingerPattern) >> 1; //ignore thumb hole and bell sensor + ret = pgm_read_byte(&naf_explicit[tempCovered].midi_note); + return ret; + + + + case kModeEVI: + + tempCovered = (0b011111110 & fingerPattern) >> 1; //ignore thumb hole and bell sensor + ret = pgm_read_byte(&evi_explicit[tempCovered].midi_note); + ret = ret + 4; //transpose up to D so that key selection in the Configuration Tool works properly + return ret; + + + + case kModeKaval: + + //If back thumb is open, always play the B + if ((fingerPattern & 0b100000000) == 0) { + return 71; + } + tempCovered = (0b011111110 & fingerPattern) >> 1; //ignore thumb hole and bell sensor + ret = pgm_read_byte(&kaval_explicit[tempCovered].midi_note); + return ret; + + + + case kModeXiao: + + //Catch a few specific patterns with the thumb hole open: + if ((fingerPattern & 0b100000000) == 0) { + if ((fingerPattern >> 4) == 0b01110) { //if the top 5 holes are as shown + return 70; //play Bb + } + if ((fingerPattern & 0b010000000) == 0) { //if hole L1 is also open + return 71; //play B + } + return 72; //otherwise play a C + } else if ((fingerPattern & 0b010000000) == 0) { + return 69; //if thumb is closed but L1 is open play an A + } + //otherwise check the chart. + tempCovered = (0b001111110 & fingerPattern) >> 1; //ignore thumb hole, L1 hole, and bell sensor + ret = pgm_read_byte(&xiao_explicit[tempCovered].midi_note); + return ret; + + + + case kModeSax: + + //check the chart. + tempCovered = (0b011111100 & fingerPattern) >> 2; //ignore thumb hole, R4 hole, and bell sensor + ret = pgm_read_byte(&sax_explicit[tempCovered].midi_note); + if (((fingerPattern & 0b000000010) != 0) && ret != 47 && ret != 52 && ret != 53 && ret != 54 && ret != 59 && ret != 60 && ret != 61) { //sharpen the note if R4 is covered and the note isn't one of the ones that can't be sharpened (a little wonky but works and keep sthe chart shorter ;) + ret++; + } + if ((fingerPattern & 0b100000000) != 0 && ret > 49) { //if the thumb hole is covered, raise the octave + ret = ret + 12; + } + return ret; + + + + case kModeSaxBasic: + + //check the chart. + tempCovered = (0b011111110 & fingerPattern) >> 1; //ignore thumb hole and bell sensor + ret = pgm_read_byte(&saxbasic_explicit[tempCovered].midi_note); + if ((fingerPattern & 0b100000000) != 0 && ret > 49) { //if the thumb hole is covered, raise the octave + ret = ret + 12; + } + return ret; + + + + case kModeShakuhachi: + + //ignore all unused holes by extracting bits and then logical OR + { + //braces necessary for scope + byte high = (fingerPattern >> 4) & 0b11000; + byte middle = (fingerPattern >> 4) & 0b00011; + middle = middle << 1; + byte low = (fingerPattern >> 2) & 0b0000001; + tempCovered = high | middle; + tempCovered = tempCovered | low; + ret = pgm_read_byte(&shakuhachi_explicit[tempCovered].midi_note); + return ret; + } + + + + case kModeSackpipaMajor: + + case kModeSackpipaMinor: //the same except we'll change C# to C + + //check the chart. + tempCovered = (0b011111100 & fingerPattern) >> 2; //ignore thumb hole, R4 hole, and bell sensor + if ((fingerPattern & 0b111111110) >> 1 == 0b11111111) { //play D if all holes are covered + return 60; //play D + } + if ((fingerPattern & 0b100000000) == 0) { //if the thumb hole is open, play high E + return 74; //play E + } + ret = pgm_read_byte(&sackpipa_explicit[tempCovered].midi_note); + if (modeSelector[mode] == kModeSackpipaMinor) { //flatten the C# if we're in "minor" mode + if (ret == 71) { + return 70; //play C natural instead + } + } + return ret; + + + + case kModeCustom: + tempCovered = (0b011111110 & fingerPattern) >> 1; //ignore thumb hole and bell sensor for now + uint8_t leftmost = findleftmostunsetbit(tempCovered); //here we find the index of the leftmost uncovered hole, which will be used to determine the note from the chart. + + for (uint8_t i = 0; i < 6; i++) { //look only at leftmost uncovered hole for lower several notes + if (leftmost == i) { + customScalePosition = 47 - i; + } + } + + //several ugly special cases + if (tempCovered >> 3 == 0b0111) { + customScalePosition = 39; + } + + else if (tempCovered >> 3 == 0b0110) { + customScalePosition = 41; + } + + else if (tempCovered >> 5 == 0b00) { + customScalePosition = 40; + } + + if (tempCovered == 0b1111111) { + if (!switches[mode][R4_FLATTEN]) { //all holes covered but not R4 flatten + customScalePosition = 48; + } else { + customScalePosition = 47; + } + } + + if (fingerPattern >> 8 == 0 && !switches[mode][THUMB_AND_OVERBLOW] && !breathMode == kPressureThumb && ED[mode][38] != 0) { //thumb hole is open and we're not using it for register + customScalePosition = 38; + } + + ret = ED[mode][customScalePosition]; + + if (bitRead(tempCovered, 0) == 1 && switches[mode][R4_FLATTEN] && ret != 0) { //flatten one semitone if using R4 for that purpose + ret = ret - 1; + } + + return ret; + + + + default: + return ret; + } +} + +// ARTUS40 on 10.08.2022 +// I added this function to handle drone notes with finger patterns. +int get_noteDrone(unsigned int fingerPattern) { + int ret = -1; //default for unknown fingering + + switch (modeSelector[mode]) { //determine the note based on the fingering pattern + // Modification by Artus40 + case kModeBoha: + tempCovered = (0b111111110 & fingerPattern) >> 1; // Ignoring bell sensor + ret = pgm_read_byte(&boha_explicit[tempCovered].drone_note); + return ret; + + default: + return ret; + } +} + +//Add up any transposition based on key and register. +void get_shift() +{ + + shift = ((octaveShift * 12) + noteShift); //adjust for key and octave shift. + + if (newState == 3 && !(modeSelector[mode] == kModeEVI || (modeSelector[mode] == kModeSax && newNote < 62) || (modeSelector[mode] == kModeSaxBasic && newNote < 74) || (modeSelector[mode] == kModeRecorder && newNote < 76)) && !(newNote == 62 && (modeSelector[mode] == kModeUilleann || modeSelector[mode] == kModeUilleannStandard))) { //if overblowing (except EVI, sax in the lower register, and low D with uilleann fingering, which can't overblow) + shift = shift + 12; //add a register jump to the transposition if overblowing. + if (modeSelector[mode] == kModeKaval) { //Kaval only plays a fifth higher in the second register. + shift = shift - 5; + } + } + + if (breathMode == kPressureBell && modeSelector[mode] != kModeUilleann && modeSelector[mode] != kModeUilleannStandard) { //if we're using the bell sensor to control register + if (bitRead(holeCovered, 0) == switches[mode][INVERT]) { + shift = shift + 12; //add a register jump to the transposition if necessary. + if (modeSelector[mode] == kModeKaval) { + shift = shift - 5; + } + } + } + + else if ((breathMode == kPressureThumb && (modeSelector[mode] == kModeWhistle || modeSelector[mode] == kModeChromatic || modeSelector[mode] == kModeNAF || modeSelector[mode] == kModeCustom)) || (breathMode == kPressureBreath && modeSelector[mode] == kModeCustom && switches[mode][THUMB_AND_OVERBLOW])) { //if we're using the left thumb to control the regiser with a fingering patern that doesn't normally use the thumb + + if (bitRead(holeCovered, 8) == switches[mode][INVERT]) { + shift = shift + 12; //add an octave jump to the transposition if necessary. + } + } + + //Some charts require another transposition to bring them to the correct key + if (modeSelector[mode] == kModeGaita || modeSelector[mode] == kModeGaitaExtended) { + shift = shift - 1; + } + + if (modeSelector[mode] == kModeSax) { + shift = shift + 2; + } + + // if ((holeCovered & 0b100000000) == 0 && (modeSelector[mode] == kModeWhistle || modeSelector[mode] == kModeChromatic) && newState == 3){ //with whistle, if we're overblowing and the thumb is uncovered, play the third octave. + // shift = shift + 12; + // } +} + +//State machine that models the way that a tinwhistle etc. begins sounding and jumps octaves in response to breath pressure. +void get_state() +{ + + noInterrupts(); + sensorValue2 = tempSensorValue; //transfer last reading to a non-volatile variable + interrupts(); + + if (sensorValue == sensorValue2) { + return; //don't bother going further if the pressure hasn't changed. + } + + byte scalePosition; + + if (modeSelector[mode] == kModeCustom) { + scalePosition = 110 - customScalePosition; //scalePosition is used to tell where we are on the scale, because higher notes are more difficult to overblow. + } else { + scalePosition = newNote; + } + + pressureChangeRate = sensorValue2 - sensorValue; //calculate the rate of change + + if (ED[mode][DRONES_CONTROL_MODE] == 3) { //use pressure to control drones if that option has been selected. There's a small amount of hysteresis added. + + if (!dronesState && sensorValue2 > 5 + (ED[mode][DRONES_PRESSURE_HIGH_BYTE] << 7 | ED[mode][DRONES_PRESSURE_LOW_BYTE])) { + dronesState = 1; + if (!dronesOn) { + startDrones(); + } + + } + + else if (dronesState && sensorValue2 < (ED[mode][DRONES_PRESSURE_HIGH_BYTE] << 7 | ED[mode][DRONES_PRESSURE_LOW_BYTE])) { + dronesState = 0; + if (dronesOn) { + stopDrones(); + } + } + } + + + upperBound = (sensorThreshold[1] + ((scalePosition - 60) * multiplier)); //calculate the threshold between state 2 and state 3. This will also be used to calculate expression. + + + if (jump && ((millis() - jumpTimer) >= jumpTime)) { + jump = 0; //make it okay to switch registers again if some time has past since we "jumped" or "dropped" because of rapid pressure change. + } + + else if (drop && ((millis() - dropTimer) >= dropTime)) { + drop = 0; + } + + + if (!jump && !drop) { + + if ((breathMode == kPressureBreath || (breathMode == kPressureThumb && modeSelector[mode] == kModeCustom && switches[mode][THUMB_AND_OVERBLOW])) && ((sensorValue2 - sensorValue) > jumpValue) && (sensorValue2 > sensorThreshold[0])) { //if the pressure has increased rapidly (since the last reading) and there's a least enough pressure to turn a note on, jump immediately to the second register + newState = 3; + jump = 1; + jumpTimer = millis(); + sensorValue = sensorValue2; + return; + } + + if (newState == 3 && breathMode > kPressureSingle && ((sensorValue - sensorValue2) > dropValue)) { //if we're in second register and the pressure has dropped rapidly, turn the note off (this lets us drop directly from the second register to note off). + newState = 1; + drop = 1; + dropTimer = millis(); + sensorValue = sensorValue2; + return; + } + } + + //if there haven't been rapid pressure changes and we haven't just jumped registers, choose the state based solely on current pressure. + if (sensorValue2 <= sensorThreshold[0]) { + newState = 1; + } + + //added very small amount of hysteresis for state 2, 4/25/20 + else if (sensorValue2 > sensorThreshold[0] + 1 && (((breathMode != kPressureBreath) && !(breathMode == kPressureThumb && modeSelector[mode] == kModeCustom && switches[mode][THUMB_AND_OVERBLOW])) || (!jump && !drop && (breathMode > kPressureSingle) && (sensorValue2 <= upperBound)))) { //single register mode or within the bounds for state 2 + newState = 2; + } else if (!drop && (sensorValue2 > upperBound)) { //we're in two-register mode and above the upper bound for state 2 + newState = 3; + } + + sensorValue = sensorValue2; //we'll use the current reading as the baseline next time around, so we can monitor the rate of change. + sensorDataReady = 0; //we've used the sensor reading, so don't use it again + + +} + +//calculate pitchbend expression based on pressure +void getExpression() +{ + + //calculate the center pressure value for the current note, regardless of register, unless "override" is turned on and we're not in overblow mode. In that case, use the override bounds instead + + + int lowerBound; + int useUpperBound; + + if (switches[mode][OVERRIDE] && (breathMode != kPressureBreath)) { + lowerBound = (ED[mode][EXPRESSION_MIN] * 9) + 100; + useUpperBound = (ED[mode][EXPRESSION_MAX] * 9) + 100; + } else { + lowerBound = sensorThreshold[0]; + useUpperBound = upperBound; + + } + + unsigned int halfway = ((useUpperBound - lowerBound) >> 1) + lowerBound; + + if (newState == 3) { + halfway = useUpperBound + halfway; + lowerBound = useUpperBound; + } + + if (sensorValue < halfway) { + byte scale = (((halfway - sensorValue) * ED[mode][EXPRESSION_DEPTH] * 20) / (halfway - lowerBound)); //should maybe figure out how to do this without dividing. + expression = - ((scale * scale) >> 3); + } else { + expression = (sensorValue - halfway) * ED[mode][EXPRESSION_DEPTH]; + } + + + if (expression > ED[mode][EXPRESSION_DEPTH] * 200) { + expression = ED[mode][EXPRESSION_DEPTH] * 200; //put a cap on it, because in the upper register or in single-register mode, there's no upper limit + } + + + if (pitchBendMode == kPitchBendNone) { //if we're not using vibrato, send the pitchbend now instead of adding it in later. + pitchBend = 0; + sendPitchbend(); + } +} + +//find how many steps down to the next lower note on the scale. Doing this in a function because the fingering charts can't be read from the main page, due to compilation order. +void findStepsDown() +{ + slideHole = findleftmostunsetbit(holeCovered); //determine the highest uncovered hole, to use for sliding + if (slideHole == 127) { //this means no holes are covered. + // this could mean the highest hole is starting to be uncovered, so use that as the slideHole + slideHole = 7; + //return; + } + unsigned int closedSlideholePattern = holeCovered; + bitSet(closedSlideholePattern, slideHole); //figure out what the fingering pattern would be if we closed the slide hole + stepsDown = constrain(tempNewNote - get_note(closedSlideholePattern), 0, 2); //and then figure out how many steps down it would be if a new note were triggered with that pattern. +} + +//Custom pitchbend algorithms, tin whistle and uilleann by Michael Eskin +void handleCustomPitchBend() +{ + + iPitchBend[2] = 0; //reset pitchbend for the holes that are being used + iPitchBend[3] = 0; + + if (pitchBendMode == kPitchBendSlideVibrato || pitchBendMode == kPitchBendLegatoSlideVibrato) { //calculate slide if necessary. + getSlide(); + } + + + if (modeSelector[mode] != kModeGHB && modeSelector[mode] != kModeNorthumbrian) { //only used for whistle and uilleann + if (vibratoEnable == 1) { //if it's a vibrato fingering pattern + if (slideHole != 2) { + iPitchBend[2] = adjvibdepth; //just assign max vibrato depth to a hole that isn't being used for sliding (it doesn't matter which hole, it's just so it will be added in later). + iPitchBend[3] = 0; + } else { + iPitchBend[3] = adjvibdepth; + iPitchBend[2] = 0; + } + } + + + + + if (vibratoEnable == 0b000010) { //used for whistle and uilleann, indicates that it's a pattern where lowering finger 2 or 3 partway would trigger progressive vibrato. + + if (modeSelector[mode] == kModeWhistle || modeSelector[mode] == kModeChromatic) { + for (byte i = 2; i < 4; i++) { + if ((toneholeRead[i] > senseDistance) && (bitRead(holeCovered, i) != 1 && (i != slideHole))) { //if the hole is contributing, bend down + iPitchBend[i] = ((toneholeRead[i] - senseDistance) * vibratoScale[i]) >> 3; + } else if (i != slideHole) { + iPitchBend[i] = 0; + } + } + if (iPitchBend[2] + iPitchBend[3] > adjvibdepth) { + iPitchBend[2] = adjvibdepth; //cap at max vibrato depth if they combine to add up to more than that (just set one to max and the other to zero) + iPitchBend[3] = 0; + } + } + + + else if (modeSelector[mode] == kModeUilleann || modeSelector[mode] == kModeUilleannStandard) { + + // If the back-D is open, and the vibrato hole completely open, max the pitch bend + if ((holeCovered & 0b100000000) == 0) { + if (bitRead(holeCovered, 3) == 1) { + iPitchBend[3] = 0; + } else { + // Otherwise, bend down proportional to distance + if (toneholeRead[3] > senseDistance) { + iPitchBend[3] = adjvibdepth - (((toneholeRead[3] - senseDistance) * vibratoScale[3]) >> 3); + } else { + iPitchBend[3] = adjvibdepth; + } + } + } else { + + if ((toneholeRead[3] > senseDistance) && (bitRead(holeCovered, 3) != 1) && 3 != slideHole) { + iPitchBend[3] = ((toneholeRead[3] - senseDistance) * vibratoScale[3]) >> 3; + } + + else if ((toneholeRead[3] < senseDistance) || (bitRead(holeCovered, 3) == 1)) { + iPitchBend[3] = 0; // If the finger is removed or the hole is fully covered, there's no pitchbend contributed by that hole. + } + + //if (iPitchBend[3] > adjvibdepth) { + // iPitchBend[3] = adjvibdepth; //cap at 8191 (no pitchbend) if for some reason they add up to more than that + //} + } + } + + } + + } + + + else if (modeSelector[mode] == kModeGHB || modeSelector[mode] == kModeNorthumbrian) { //this one is designed for closed fingering patterns, so raising a finger sharpens the note. + for (byte i = 2; i < 4; i++) { //use holes 2 and 3 for vibrato + if (i != slideHole || (holeCovered & 0b100000000) == 0) { + static unsigned int testNote; // the hypothetical note that would be played if a finger were lowered all the way + if (bitRead(holeCovered, i) != 1) { //if the hole is not fully covered + if (fingersChanged) { //if the fingering pattern has changed + testNote = get_note(bitSet(holeCovered, i)); //check to see what the new note would be + fingersChanged = 0; + } + if (testNote == newNote) { //if the hole is uncovered and covering the hole wouldn't change the current note (or the left thumb hole is uncovered, because that case isn't included in the fingering chart) + if (toneholeRead[i] > senseDistance) { + iPitchBend[i] = 0 - (((toneholeCovered[i] - 50 - toneholeRead[i]) * vibratoScale[i]) >> 3); //bend up, yielding a negative pitchbend value + } else { + iPitchBend[i] = 0 - adjvibdepth; //if the hole is totally uncovered, max the pitchbend + } + } + } else { //if the hole is covered + iPitchBend[i] = 0; //reset the pitchbend to 0 + } + } + } + if ((((iPitchBend[2] + iPitchBend[3]) * -1) > adjvibdepth) && ((slideHole != 2 && slideHole != 3) || (holeCovered & 0b100000000) == 0)) { //cap at vibrato depth if more than one hole is contributing and they add to up to more than the vibrato depth. + iPitchBend[2] = 0 - adjvibdepth; //assign max vibrato depth to a hole that isn't being used for sliding + iPitchBend[3] = 0; + } + } + sendPitchbend(); +} + +//Andrew's version +void handlePitchBend() +{ + + + if (pitchBendMode == kPitchBendSlideVibrato || pitchBendMode == kPitchBendLegatoSlideVibrato) { //calculate slide if necessary. + getSlide(); + } + + + for (byte i = 0; i < 9; i++) { + + if (bitRead(holeLatched, i) == 1 && toneholeRead[i] < senseDistance) { + (bitWrite(holeLatched, i, 0)); //we "unlatch" (enable for vibrato) a hole if it was covered when the note was triggered but now the finger has been completely removed. + } + + if (bitRead(vibratoHoles, i) == 1 && bitRead(holeLatched, i) == 0 && (pitchBendMode == kPitchBendVibrato || i != slideHole)) { //if this is a vibrato hole and we're in a mode that uses vibrato, and the hole is unlatched + if (toneholeRead[i] > senseDistance) { + if (bitRead(holeCovered, i) != 1) { + iPitchBend[i] = (((toneholeRead[i] - senseDistance) * vibratoScale[i]) >> 3); //bend downward + pitchBendOn[i] = 1; + } + } else { + pitchBendOn[i] = 0; + if (bitRead(holeCovered, i) == 1) { + iPitchBend[i] = 0; + } + } + + if (pitchBendOn[i] == 1 && (bitRead(holeCovered, i) == 1)) { + iPitchBend[i] = adjvibdepth; //set vibrato to max downward bend if a hole was being used to bend down and now is covered + } + } + } + + sendPitchbend(); +} + +//calculate slide pitchBend, to be added with vibrato. +void getSlide() +{ + for (byte i = 0; i < 9; i++) { + if (toneholeRead[i] > senseDistance && i == slideHole && stepsDown > 0) { + if (bitRead(holeCovered, i) != 1) { + iPitchBend[i] = ((toneholeRead[i] - senseDistance) * toneholeScale[i]) >> (4 - stepsDown); //bend down toward the next lowest note in the scale, the amount of bend depending on the number of steps down. + } + } else { + iPitchBend[i] = 0; + } + } +} + +void sendPitchbend() +{ + + + pitchBend = 0; //reset the overall pitchbend in preparation for adding up the contributions from all the toneholes. + for (byte i = 0; i < 9; i++) { + pitchBend = pitchBend + iPitchBend[i]; + } + + int noteshift = 0; + if (noteon && pitchBendModeSelector[mode] == kPitchBendLegatoSlideVibrato) { + noteshift = (notePlaying - shift) - newNote; + pitchBend += noteshift * pitchBendPerSemi; + } + + pitchBend = 8192 - pitchBend + expression; + if (pitchBend < 0) { + pitchBend = 0; + } else if (pitchBend > 16383) { + pitchBend = 16383; + } + + if (prevPitchBend != pitchBend) { + + if (noteon) { + + sendUSBMIDI(PITCH_BEND, mainMidiChannel, pitchBend & 0x7F, pitchBend >> 7); + prevPitchBend = pitchBend; + } + } +} + +void calculateAndSendPitchbend() +{ + if (ED[mode][EXPRESSION_ON] && !switches[mode][BAGLESS]) { + getExpression(); //calculate pitchbend based on pressure reading + } + + if (!customEnabled && pitchBendMode != kPitchBendNone) { + handlePitchBend(); + } else if (customEnabled) { + handleCustomPitchBend(); + } +} + + + +//send MIDI NoteOn/NoteOff events when necessary +void sendNote() +{ + const int velDelayMs = switches[mode][SEND_AFTERTOUCH] != 0 ? 3 : 16 ; // keep this minimal to avoid latency if also sending aftertouch, but enough to get a good reading, otherwise use longer + + if ( //several conditions to tell if we need to turn on a new note. + (!noteon + || ( pitchBendModeSelector[mode] != kPitchBendLegatoSlideVibrato && newNote != (notePlaying - shift)) + || ( pitchBendModeSelector[mode] == kPitchBendLegatoSlideVibrato && abs(newNote - (notePlaying - shift)) > midiBendRange - 1 ) ) && //if there wasn't any note playing or the current note is different than the previous one + ((newState > 1 && !switches[mode][BAGLESS]) || (switches[mode][BAGLESS] && play)) && //and the state machine has determined that a note should be playing, or we're in bagless mode and the sound is turned on + !(prevNote == 62 && (newNote + shift) == 86 && !sensorDataReady) && // and if we're currently on a middle D in state 3 (all finger holes covered), we wait until we get a new state reading before switching notes. This it to prevent erroneous octave jumps to a high D. + !(switches[mode][SEND_VELOCITY] && !noteon && ((millis() - velocityDelayTimer) < velDelayMs)) && // and not waiting for the pressure to rise to calculate note on velocity if we're transitioning from not having any note playing. + !(modeSelector[mode] == kModeNorthumbrian && newNote == 60) && //and if we're in Northumbrian mode don't play a note if all holes are covered. That simulates the closed pipe. + !(breathMode != kPressureBell && bellSensor && holeCovered == 0b111111111)) { // don't play a note if the bell sensor and all other holes are covered, and we're not in "bell register" mode. Again, simulating a closed pipe. + + int notewason = noteon; + int notewasplaying = notePlaying; + + // if this is a fresh/tongued note calculate pressure now to get the freshest initial velocity/pressure + if (!notewason) { + if (ED[mode][SEND_PRESSURE]) { + calculatePressure(0); + } + if (switches[mode][SEND_VELOCITY]) { + calculatePressure(1); + } + if (switches[mode][SEND_AFTERTOUCH] & 1) { + calculatePressure(2); + } + if (switches[mode][SEND_AFTERTOUCH] & 2) { + calculatePressure(3); + } + } + + if (notewason && !switches[mode][LEGATO]) { + // send prior noteoff now if legato is selected. + sendUSBMIDI(NOTE_OFF, mainMidiChannel, notePlaying, 64); + notewason = 0; + } + + // need to send pressure prior to note, in case we are using it for velocity + if (ED[mode][SEND_PRESSURE] == 1 || switches[mode][SEND_AFTERTOUCH] != 0 || switches[mode][SEND_VELOCITY] == 1) { + sendPressure(true); + } + + // set it now so that send pitchbend will operate correctly + noteon = 1; //keep track of the fact that there's a note turned on + notePlaying = newNote + shift; + + // send pitch bend immediately prior to note if necessary + if (switches[mode][IMMEDIATE_PB]) { + calculateAndSendPitchbend(); + } + + sendUSBMIDI(NOTE_ON, mainMidiChannel, notePlaying, velocity); //send the new note + + if (notewason) { + // turn off the previous note after turning on the new one (if it wasn't already done above) + // We do it after to signal to synths that the notes are legato (not all synths will respond to this). + sendUSBMIDI(NOTE_OFF, mainMidiChannel, notewasplaying, 64); + } + + pitchBendTimer = millis(); //for some reason it sounds best if we don't send pitchbend right away after starting a new note. + noteOnTimestamp = pitchBendTimer; + + prevNote = newNote; + + if (ED[mode][DRONES_CONTROL_MODE] == 2 && !dronesOn) { //start drones if drones are being controlled with chanter on/off + startDrones(); + } + } + + // Handle the drone pipe in Boha Mode. We keep the same logic, except on a different channel. + if(modeSelector[mode] == kModeBoha && // If we are in Boha mode + (dronesOn && newDroneNote != (droneNotePlaying - shift)) && // and drones are on, and the new note is not the same. + ((newState > 1 && !switches[mode][BAGLESS]) || (switches[mode][BAGLESS] && play)) // plus keep bagless/pressure functionnality + ) { + int droneMidiChannel = ED[mode][DRONES_ON_CHANNEL]; // We get the channel from user configuration. TODO: These should be const, simple hack for now. + int droneMidiVelocity = ED[mode][DRONES_ON_BYTE3]; // And the velocity of the drone. TODO: These should be const, simple hack for now. + int notewason = 1; // Keeping this volatile var to keep legato behaviour, but drones should always have a note on while activated. + int notewasplaying = droneNotePlaying; + + if (!switches[mode][LEGATO]) { + // send prior noteoff now if legato is selected. + sendUSBMIDI(NOTE_OFF, droneMidiChannel, notewasplaying, droneMidiVelocity); + notewason = 0; + } + + droneNotePlaying = newDroneNote + shift; + + sendUSBMIDI(NOTE_ON, droneMidiChannel, droneNotePlaying, droneMidiVelocity); //send the new note + // turn off the previous note after turning on the new one (if it wasn't already done above) + // We do it after to signal to synths that the notes are legato (not all synths will respond to this). + if (notewason) { sendUSBMIDI(NOTE_OFF, droneMidiChannel, notewasplaying, droneMidiVelocity); } + + //prevDroneNote = newDroneNote; + } + + if (noteon) { //several conditions to turn a note off + if ( + ((newState == 1 && !switches[mode][BAGLESS]) || (switches[mode][BAGLESS] && !play)) || //if the state drops to 1 (off) or we're in bagless mode and the sound has been turned off + (modeSelector[mode] == kModeNorthumbrian && newNote == 60) || //or closed Northumbrian pipe + (breathMode != kPressureBell && bellSensor && holeCovered == 0b111111111)) { //or completely closed pipe + sendUSBMIDI(NOTE_OFF, mainMidiChannel, notePlaying, 64); //turn the note off if the breath pressure drops or if we're in uilleann mode, the bell sensor is covered, and all the finger holes are covered. + noteon = 0; //keep track + + sendPressure(true); + + if (ED[mode][DRONES_CONTROL_MODE] == 2 && dronesOn) { //stop drones if drones are being controlled with chanter on/off + stopDrones(); + } + } + } +} + +//Calibrate the sensors and store them in EEPROM +//mode 1 calibrates all sensors, mode 2 calibrates bell sensor only. +void calibrate() +{ + + if (!LEDon) { + digitalWrite2(ledPin, HIGH); + LEDon = 1; + calibrationTimer = millis(); + + if (calibration == 1) { //calibrate all sensors if we're in calibration "mode" 1 + for (byte i = 1; i < 9; i++) { + toneholeCovered[i] = 0; //first set the calibration to 0 for all of the sensors so it can only be increassed by calibrating + toneholeBaseline[i] = 255; //and set baseline high so it can only be reduced + } + } + if (bellSensor) { + toneholeCovered[0] = 0; //also zero the bell sensor if it's plugged in (doesn't matter which calibration mode for this one). + toneholeBaseline[0] = 255; + } + return; //we return once to make sure we've gotten some new sensor readings. + } + + if ((calibration == 1 && ((millis() - calibrationTimer) <= 10000)) || (calibration == 2 && ((millis() - calibrationTimer) <= 5000))) { //then set the calibration to the highest reading during the next ten seconds(or five seconds if we're only calibrating the bell sensor). + if (calibration == 1) { + for (byte i = 1; i < 9; i++) { + if (toneholeCovered[i] < toneholeRead[i]) { //covered calibration + toneholeCovered[i] = toneholeRead[i]; + } + + if (toneholeBaseline[i] > toneholeRead[i]) { //baseline calibration + toneholeBaseline[i] = toneholeRead[i]; + } + } + } + + if (bellSensor && toneholeCovered[0] < toneholeRead[0]) { + toneholeCovered[0] = toneholeRead[0]; //calibrate the bell sensor too if it's plugged in. + } + if (bellSensor && toneholeBaseline[0] > toneholeRead[0]) { + toneholeBaseline[0] = toneholeRead[0]; //calibrate the bell sensor too if it's plugged in. + } + } + + if ((calibration == 1 && ((millis() - calibrationTimer) > 10000)) || (calibration == 2 && ((millis() - calibrationTimer) > 5000))) { + saveCalibration(); + loadPrefs(); //do this so pitchbend scaling will be recalculated. + } +} + +//save sensor calibration (EEPROM bytes up to 34 are used (plus byte 37 to indicate a saved calibration) +void saveCalibration() +{ + + for (byte i = 0; i < 9; i++) { + EEPROM.update((i + 9) * 2, highByte(toneholeCovered[i])); + EEPROM.update(((i + 9) * 2) + 1, lowByte(toneholeCovered[i])); + EEPROM.update((721 + i), lowByte(toneholeBaseline[i])); //the baseline readings can be stored in a single byte because they should be close to zero. + } + calibration = 0; + EEPROM.update(37, 3); //we write a 3 to address 37 to indicate that we have stored a set of calibrations. + digitalWrite2(ledPin, LOW); + LEDon = 0; + +} + +//Load the stored sensor calibrations from EEPROM +void loadCalibration() +{ + for (byte i = 0; i < 9; i++) { + byte high = EEPROM.read((i + 9) * 2); + byte low = EEPROM.read(((i + 9) * 2) + 1); + toneholeCovered[i] = word(high, low); + toneholeBaseline[i] = EEPROM.read(721 + i); + } +} + +//send MIDI messages +void sendUSBMIDI(uint8_t m, uint8_t c, uint8_t d1, uint8_t d2) // send a 3-byte MIDI event over USB +{ + c--; // Channels are zero-based + m &= 0xF0; + c &= 0xF; + d1 &= 0x7F; + d2 &= 0x7F; + midiEventPacket_t msg = {m >> 4, m | c, d1, d2}; + noInterrupts(); + MidiUSB.sendMIDI(msg); + MidiUSB.flush(); + interrupts(); + +} + +void sendUSBMIDI(uint8_t m, uint8_t c, uint8_t d) // send a 2-byte MIDI event over USB +{ + c--; // Channels are zero-based + m &= 0xF0; + c &= 0xF; + d &= 0x7F; + midiEventPacket_t msg = {m >> 4, m | c, d, 0}; + noInterrupts(); + MidiUSB.sendMIDI(msg); + MidiUSB.flush(); + interrupts(); +} + +//check for and handle incoming MIDI messages from the WARBL Configuration Tool. +void receiveMIDI() +{ + + midiEventPacket_t rx; + do { + noInterrupts(); + rx = MidiUSB.read(); //check for MIDI input + interrupts(); + if (rx.header != 0) { + + //Serial.println(rx.byte2); + //Serial.println(rx.byte3); + //Serial.println(""); + + if (rx.byte2 < 119) { //Chrome sends CC 121 and 123 on all channels when it connects, so ignore these. + + if ((rx.byte1 & 0x0f) == 6) { //if we're on channel 7, we may be receiving messages from the configuration tool. + blinkNumber = 1; //blink once, indicating a received message. Some commands below will change this to three (or zero) blinks. + if (rx.byte2 == 102) { //many settings are controlled by a value in CC 102 (always channel 7). + if (rx.byte3 > 0 && rx.byte3 <= 18) { //handle sensor calibration commands from the configuration tool. + if ((rx.byte3 & 1) == 0) { + toneholeCovered[(rx.byte3 >> 1) - 1] -= 5; + if ((toneholeCovered[(rx.byte3 >> 1) - 1] - 54) < 5) { //if the tonehole calibration gets set too low so that it would never register as being uncovered, send a message to the configuration tool. + sendUSBMIDI(CC, 7, 102, (20 + ((rx.byte3 >> 1) - 1))); + } + } else { + toneholeCovered[((rx.byte3 + 1) >> 1) - 1] += 5; + } + } + + if (rx.byte3 == 19) { //save calibration if directed. + saveCalibration(); + blinkNumber = 3; + } + + else if (rx.byte3 == 127) { //begin auto-calibration if directed. + blinkNumber = 0; + calibration = 1; + } + + else if (rx.byte3 == 126) { //when communication is established, send all current settings to tool. + communicationMode = 1; + blinkNumber = 0; + sendSettings(); + } + + + else if (rx.byte3 == 122) { // dump EEPROM + for (int i = 0 ; i < EEPROM.length() ; i++) { + debug_log(EEPROM.read(i)); + delay(3); + blinkNumber = 3; + } + } + + + for (byte i = 0; i < 3; i++) { // update the three selected fingering patterns if prompted by the tool. + if (rx.byte3 == 30 + i) { + fingeringReceiveMode = i; + } + } + + if (rx.byte3 > 32 && rx.byte3 < 60) { + modeSelector[fingeringReceiveMode] = rx.byte3 - 33; + loadPrefs(); + } + + + for (byte i = 0; i < 3; i++) { //update current mode (instrument) if directed. + if (rx.byte3 == 60 + i) { + mode = i; + play = 0; + loadPrefs(); //load the correct user settings based on current instrument. + sendSettings(); //send settings for new mode to tool. + blinkNumber = abs(mode) + 1; + } + } + + for (byte i = 0; i < 4; i++) { //update current pitchbend mode if directed. + if (rx.byte3 == 70 + i) { + pitchBendModeSelector[mode] = i; + loadPrefs(); + blinkNumber = abs(pitchBendMode) + 1; + } + } + + for (byte i = 0; i < 5; i++) { //update current breath mode if directed. + if (rx.byte3 == 80 + i) { + breathModeSelector[mode] = i; + loadPrefs(); //load the correct user settings based on current instrument. + blinkNumber = abs(breathMode) + 1; + } + } + + for (byte i = 0; i < 8; i++) { //update button receive mode (this indicates the row in the button settings for which the next received byte will be). + if (rx.byte3 == 90 + i) { + buttonReceiveMode = i; + } + } + + for (byte i = 0; i < 8; i++) { //update button configuration + if (buttonReceiveMode == i) { + if (rx.byte3 == 119) { //this is a special value for autocalibration because I ran out of values in teh range 0-12 below. + buttonPrefs[mode][i][0] = 19; + blinkNumber = 0; + } + for (byte j = 0; j < 12; j++) { //update column 0 (action). + if (rx.byte3 == 100 + j) { + buttonPrefs[mode][i][0] = j; + blinkNumber = 0; + } + } + for (byte k = 0; k < 5; k++) { //update column 1 (MIDI action). + if (rx.byte3 == 112 + k) { + buttonPrefs[mode][i][1] = k; + } + } + } + } + + for (byte i = 0; i < 3; i++) { //update momentary + if (buttonReceiveMode == i) { + if (rx.byte3 == 117) { + momentary[mode][i] = 0; + noteOnOffToggle[i] = 0; + } else if (rx.byte3 == 118) { + momentary[mode][i] = 1; + noteOnOffToggle[i] = 0; + } + } + } + + if (rx.byte3 == 85) { //set current Instrument as default and save default to settings. + defaultMode = mode; + EEPROM.update(48, defaultMode); + } + + + if (rx.byte3 == 123) { //save settings as the defaults for the current instrument + saveSettings(mode); + blinkNumber = 3; + } + + + else if (rx.byte3 == 124) { //Save settings as the defaults for all instruments + for (byte k = 0; k < 3; k++) { + saveSettings(k); + } + loadFingering(); + loadSettingsForAllModes(); + loadPrefs(); + blinkNumber = 3; + + } + + else if (rx.byte3 == 125) { //restore all factory settings + EEPROM.update(44, 255); //indicates that settings should be resaved at next startup + wdt_enable(WDTO_30MS);//restart the device in order to trigger resaving default settings + while (true) {} + } + } + + + else if (rx.byte2 == 103) { + senseDistanceSelector[mode] = rx.byte3; + loadPrefs(); + } + + else if (rx.byte2 == 117) { + unsigned long v = rx.byte3 * 8191UL / 100; + vibratoDepthSelector[mode] = v; //scale vibrato depth in cents up to pitchbend range of 0-8191 + loadPrefs(); + } + + + for (byte i = 0; i < 3; i++) { //update noteshift + if (rx.byte2 == 111 + i) { + if (rx.byte3 < 50) { + noteShiftSelector[i] = rx.byte3; + } else { + noteShiftSelector[i] = - 127 + rx.byte3; + } + loadPrefs(); + } + } + + + if (rx.byte2 == 104) { //update receive mode, used for advanced pressure range sliders, switches, and expression and drones panel settings (this indicates the variable for which the next received byte on CC 105 will be). + pressureReceiveMode = rx.byte3 - 1; + } + + else if (rx.byte2 == 105) { + if (pressureReceiveMode < 12) { + pressureSelector[mode][pressureReceiveMode] = rx.byte3; //advanced pressure values + loadPrefs(); + } + + else if (pressureReceiveMode < 33) { + ED[mode][pressureReceiveMode - 12] = rx.byte3; //expression and drones settings + loadPrefs(); + } + + else if (pressureReceiveMode == 33) { + LSBlearnedPressure = rx.byte3; + + } + + else if (pressureReceiveMode == 34) { + learnedPressureSelector[mode] = (rx.byte3 << 7) | LSBlearnedPressure; + loadPrefs(); + } + + + else if (pressureReceiveMode < 53) { + switches[mode][pressureReceiveMode - 39] = rx.byte3; //switches in the slide/vibrato and register control panels + loadPrefs(); + } + + else if (pressureReceiveMode == 60) { + midiBendRangeSelector[mode] = rx.byte3; + loadPrefs(); + } + + else if (pressureReceiveMode == 61) { + midiChannelSelector[mode] = rx.byte3; + loadPrefs(); + } + + else if (pressureReceiveMode < 98) { + ED[mode][pressureReceiveMode - 48] = rx.byte3; //more expression and drones settings + loadPrefs(); + + } + + } + + + + if (rx.byte2 == 106 && rx.byte3 > 15) { + + + if (rx.byte3 > 19 && rx.byte3 < 29) { //update enabled vibrato holes for "universal" vibrato + bitSet(vibratoHolesSelector[mode], rx.byte3 - 20); + loadPrefs(); + } + + else if (rx.byte3 > 29 && rx.byte3 < 39) { + bitClear(vibratoHolesSelector[mode], rx.byte3 - 30); + loadPrefs(); + } + + else if (rx.byte3 == 39) { + useLearnedPressureSelector[mode] = 0; + loadPrefs(); + } + + else if (rx.byte3 == 40) { + useLearnedPressureSelector[mode] = 1; + loadPrefs(); + } + + else if (rx.byte3 == 41) { + learnedPressureSelector[mode] = sensorValue; + sendUSBMIDI(CC, 7, 104, 34); //indicate that LSB of learned pressure is about to be sent + sendUSBMIDI(CC, 7, 105, learnedPressureSelector[mode] & 0x7F); //send LSB of learned pressure + sendUSBMIDI(CC, 7, 104, 35); //indicate that MSB of learned pressure is about to be sent + sendUSBMIDI(CC, 7, 105, learnedPressureSelector[mode] >> 7); //send MSB of learned pressure + loadPrefs(); + } + + else if (rx.byte3 == 42) { //autocalibrate bell sensor only + calibration = 2; + blinkNumber = 0; + } + + + else if (rx.byte3 == 43) { + int tempPressure = sensorValue; + ED[mode][DRONES_PRESSURE_LOW_BYTE] = tempPressure & 0x7F; + ED[mode][DRONES_PRESSURE_HIGH_BYTE] = tempPressure >> 7; + sendUSBMIDI(CC, 7, 104, 32); //indicate that LSB of learned drones pressure is about to be sent + sendUSBMIDI(CC, 7, 105, ED[mode][DRONES_PRESSURE_LOW_BYTE]); //send LSB of learned drones pressure + sendUSBMIDI(CC, 7, 104, 33); //indicate that MSB of learned drones pressure is about to be sent + sendUSBMIDI(CC, 7, 105, ED[mode][DRONES_PRESSURE_HIGH_BYTE]); //send MSB of learned drones pressure + } + + + else if (rx.byte3 == 45) { //save current sensor calibration as factory calibration + for (byte i = 0; i < 18; i++) { + EEPROM.update(i, EEPROM.read(i + 18)); + } + for (int i = 731; i < 741; i++) { //save baseline calibration as factory baseline + EEPROM.update(i, EEPROM.read(i - 10)); + } + } + } + + + + for (byte i = 0; i < 8; i++) { //update channel, byte 2, byte 3 for MIDI message for button MIDI command for row i + if (buttonReceiveMode == i) { + if (rx.byte2 == 106 && rx.byte3 < 16) { + buttonPrefs[mode][i][2] = rx.byte3; + } else if (rx.byte2 == 107) { + buttonPrefs[mode][i][3] = rx.byte3; + } else if (rx.byte2 == 108) { + buttonPrefs[mode][i][4] = rx.byte3; + } + } + } + + } + + } + + } //end of ignore CCs 121, 123 + + } while (rx.header != 0); + +} + +//save settings for current instrument as defaults for given instrument (i) +void saveSettings(byte i) +{ + + EEPROM.update(40 + i, modeSelector[mode]); + EEPROM.update(53 + i, noteShiftSelector[mode]); + EEPROM.update(50 + i, senseDistanceSelector[mode]); + + for (byte n = 0; n < kSWITCHESnVariables; n++) { + EEPROM.update((56 + n + (i * kSWITCHESnVariables)), switches[mode][n]); + } + + EEPROM.update(333 + (i * 2), lowByte(vibratoHolesSelector[mode])); + EEPROM.update(334 + (i * 2), highByte(vibratoHolesSelector[mode])); + EEPROM.update(339 + (i * 2), lowByte(vibratoDepthSelector[mode])); + EEPROM.update(340 + (i * 2), highByte(vibratoDepthSelector[mode])); + EEPROM.update(345 + i, useLearnedPressureSelector[mode]); + + for (byte j = 0; j < 5; j++) { //save button configuration for current mode + for (byte k = 0; k < 8; k++) { + EEPROM.update(100 + (i * 50) + (j * 10) + k, buttonPrefs[mode][k][j]); + } + } + + for (byte h = 0; h < 3; h++) { + EEPROM.update(250 + (i * 3) + h, momentary[mode][h]); + } + + for (byte q = 0; q < 12; q++) { + EEPROM.update((260 + q + (i * 20)), pressureSelector[mode][q]); + } + + EEPROM.update(273 + (i * 2), lowByte(learnedPressureSelector[mode])); + EEPROM.update(274 + (i * 2), highByte(learnedPressureSelector[mode])); + + EEPROM.update(313 + i, pitchBendModeSelector[mode]); + EEPROM.update(316 + i, breathModeSelector[mode]); + EEPROM.update(319 + i, midiBendRangeSelector[mode]); + EEPROM.update(322 + i, midiChannelSelector[mode]); + + for (byte n = 0; n < kEXPRESSIONnVariables; n++) { + EEPROM.update((351 + n + (i * kEXPRESSIONnVariables)), ED[mode][n]); + } + + +} + +//load saved fingering patterns +void loadFingering() +{ + + for (byte i = 0; i < 3; i++) { + modeSelector[i] = EEPROM.read(40 + i); + noteShiftSelector[i] = (int8_t)EEPROM.read(53 + i); + + if (communicationMode) { + sendUSBMIDI(CC, 7, 102, 30 + i); //indicate that we'll be sending the fingering pattern for instrument i + sendUSBMIDI(CC, 7, 102, 33 + modeSelector[i]); //send + + if (noteShiftSelector[i] >= 0) { + sendUSBMIDI(CC, 7, (111 + i), noteShiftSelector[i]); + } //send noteShift, with a transformation for sending negative values over MIDI. + else { + sendUSBMIDI(CC, 7, (111 + i), noteShiftSelector[i] + 127); + } + } + } +} + +//load settings for all three instruments from EEPROM +void loadSettingsForAllModes() +{ + + defaultMode = EEPROM.read(48); //load default mode + + for (byte i = 0; i < 3; i++) { + + senseDistanceSelector[i] = EEPROM.read(50 + i); + + for (byte n = 0; n < kSWITCHESnVariables; n++) { + switches[i][n] = EEPROM.read(56 + n + (i * kSWITCHESnVariables)); + } + + vibratoHolesSelector[i] = word(EEPROM.read(334 + (i * 2)), EEPROM.read(333 + (i * 2))); + vibratoDepthSelector[i] = word(EEPROM.read(340 + (i * 2)), EEPROM.read(339 + (i * 2))); + useLearnedPressureSelector[i] = EEPROM.read(345 + i); + + for (byte j = 0; j < 5; j++) { + for (byte k = 0; k < 8; k++) { + buttonPrefs[i][k][j] = EEPROM.read(100 + (i * 50) + (j * 10) + k); + } + } + + for (byte h = 0; h < 3; h++) { + momentary[i][h] = EEPROM.read(250 + (i * 3) + h); + } + + for (byte m = 0; m < 12; m++) { + pressureSelector[i][m] = EEPROM.read(260 + m + (i * 20)); + } + + learnedPressureSelector[i] = word(EEPROM.read(274 + (i * 2)), EEPROM.read(273 + (i * 2))); + + + pitchBendModeSelector[i] = EEPROM.read(313 + i); + breathModeSelector[i] = EEPROM.read(316 + i); + + midiBendRangeSelector[i] = EEPROM.read(319 + i); + midiBendRangeSelector[i] = midiBendRangeSelector[i] > 96 ? 2 : midiBendRangeSelector[i]; // sanity check in case uninitialized + + midiChannelSelector[i] = EEPROM.read(322 + i); + midiChannelSelector[i] = midiChannelSelector[i] > 16 ? 1 : midiChannelSelector[i]; // sanity check in case uninitialized + + + for (byte n = 0; n < kEXPRESSIONnVariables; n++) { + ED[i][n] = EEPROM.read(351 + n + (i * kEXPRESSIONnVariables)); + } + + + + } + + +} + +//This is used the first time the software is run, to copy all the default settings to EEPROM, and is also used to restore factory settings. +void saveFactorySettings() +{ + + for (byte i = 0; i < 3; i++) { //save all the current settings for all three instruments. + mode = i; + saveSettings(i); + } + + for (byte i = 0; i < 18; i++) { //copy sensor calibration from factory settings location (copy 0-17 to 18-35). + EEPROM.update((i + 18), EEPROM.read(i)); + } + + for (int i = 721; i < 731; i++) { //copy sensor baseline calibration from factory settings location. + EEPROM.update((i), EEPROM.read(i + 10)); + } + + EEPROM.update(48, defaultMode); //save default mode + + EEPROM.update(44, 3); //indicates settings have been saved + + blinkNumber = 3; +} + +//send all settings for current instrument to the WARBL Configuration Tool. +void sendSettings() +{ + + + sendUSBMIDI(CC, 7, 110, VERSION); //send software version + + + for (byte i = 0; i < 3; i++) { + sendUSBMIDI(CC, 7, 102, 30 + i); //indicate that we'll be sending the fingering pattern for instrument i + sendUSBMIDI(CC, 7, 102, 33 + modeSelector[i]); //send + + if (noteShiftSelector[i] >= 0) { + sendUSBMIDI(CC, 7, 111 + i, noteShiftSelector[i]); + } //send noteShift, with a transformation for sending negative values over MIDI. + else { + sendUSBMIDI(CC, 7, 111 + i, noteShiftSelector[i] + 127); + } + } + + sendUSBMIDI(CC, 7, 102, 60 + mode); //send current instrument + sendUSBMIDI(CC, 7, 102, 85 + defaultMode); //send default instrument + + sendUSBMIDI(CC, 7, 103, senseDistance); //send sense distance + + sendUSBMIDI(CC, 7, 117, vibratoDepth * 100UL / 8191); //send vibrato depth, scaled down to cents + sendUSBMIDI(CC, 7, 102, 70 + pitchBendMode); //send current pitchBend mode + sendUSBMIDI(CC, 7, 102, 80 + breathMode); //send current breathMode + sendUSBMIDI(CC, 7, 102, 120 + bellSensor); //send bell sensor state + sendUSBMIDI(CC, 7, 106, 39 + useLearnedPressure); //send calibration option + sendUSBMIDI(CC, 7, 104, 34); //indicate that LSB of learned pressure is about to be sent + sendUSBMIDI(CC, 7, 105, learnedPressure & 0x7F); //send LSB of learned pressure + sendUSBMIDI(CC, 7, 104, 35); //indicate that MSB of learned pressure is about to be sent + sendUSBMIDI(CC, 7, 105, learnedPressure >> 7); //send MSB of learned pressure + + sendUSBMIDI(CC, 7, 104, 61); // indicate midi bend range is about to be sent + sendUSBMIDI(CC, 7, 105, midiBendRange); //midi bend range + + sendUSBMIDI(CC, 7, 104, 62); // indicate midi channel is about to be sent + sendUSBMIDI(CC, 7, 105, mainMidiChannel); //midi bend range + + + + for (byte i = 0; i < 9; i++) { + sendUSBMIDI(CC, 7, 106, 20 + i + (10 * (bitRead(vibratoHolesSelector[mode], i)))); //send enabled vibrato holes + } + + for (byte i = 0; i < 8; i++) { + sendUSBMIDI(CC, 7, 102, 90 + i); //indicate that we'll be sending data for button commands row i (click 1, click 2, etc.) + sendUSBMIDI(CC, 7, 102, 100 + buttonPrefs[mode][i][0]); //send action (i.e. none, send MIDI message, etc.) + if (buttonPrefs[mode][i][0] == 1) { //if the action is a MIDI command, send the rest of the MIDI info for that row. + sendUSBMIDI(CC, 7, 102, 112 + buttonPrefs[mode][i][1]); + sendUSBMIDI(CC, 7, 106, buttonPrefs[mode][i][2]); + sendUSBMIDI(CC, 7, 107, buttonPrefs[mode][i][3]); + sendUSBMIDI(CC, 7, 108, buttonPrefs[mode][i][4]); + } + } + + for (byte i = 0; i < kSWITCHESnVariables; i++) { //send settings for switches in the slide/vibrato and register control panels + sendUSBMIDI(CC, 7, 104, i + 40); + sendUSBMIDI(CC, 7, 105, switches[mode][i]); + } + + for (byte i = 0; i < 21; i++) { //send settings for expression and drones control panels + sendUSBMIDI(CC, 7, 104, i + 13); + sendUSBMIDI(CC, 7, 105, ED[mode][i]); + } + + for (byte i = 21; i < 49; i++) { //more settings for expression and drones control panels + sendUSBMIDI(CC, 7, 104, i + 49); + sendUSBMIDI(CC, 7, 105, ED[mode][i]); + } + + for (byte i = 0; i < 3; i++) { + sendUSBMIDI(CC, 7, 102, 90 + i); //indicate that we'll be sending data for momentary + sendUSBMIDI(CC, 7, 102, 117 + momentary[mode][i]); + } + + for (byte i = 0; i < 12; i++) { + sendUSBMIDI(CC, 7, 104, i + 1); //indicate which pressure variable we'll be sending data for + sendUSBMIDI(CC, 7, 105, pressureSelector[mode][i]); //send the data + } +} + +void blink() //blink LED given number of times +{ + + if ((millis() - ledTimer) >= 200) { + ledTimer = millis(); + + if (LEDon) { + digitalWrite2(ledPin, LOW); + blinkNumber--; + LEDon = 0; + return; + } + + else { + digitalWrite2(ledPin, HIGH); + LEDon = 1; + } + } +} + +//interpret button presses. If the button is being used for momentary MIDI messages we ignore other actions with that button (except "secret" actions involving the toneholes). +void handleButtons() +{ + + + //first, some housekeeping + + if (shiftState == 1 && released[1] == 1) { //if button 1 was only being used along with another button, we clear the just-released flag for button 1 so it doesn't trigger another control change. + released[1] = 0; + buttonUsed = 0; //clear the button activity flag, so we won't handle them again until there's been new button activity. + shiftState = 0; + } + + + + //then, a few hard-coded actions that can't be changed by the configuration tool: + //_______________________________________________________________________________ + + if (justPressed[0] && !pressed[2] && !pressed[1]) { + if (ED[mode][DRONES_CONTROL_MODE] == 1) { + if (holeCovered >> 1 == 0b00001000) { //turn drones on/off if button 0 is pressed and fingering pattern is 0 0001000. + justPressed[0] = 0; + specialPressUsed[0] = 1; + if (!dronesOn) { + startDrones(); + } else { + stopDrones(); + } + } + } + + if (switches[mode][SECRET]) { + if (holeCovered >> 1 == 0b00010000) { //change pitchbend mode if button 0 is pressed and fingering pattern is 0 0000010. + justPressed[0] = 0; + specialPressUsed[0] = 1; + changePitchBend(); + } + + else if (holeCovered >> 1 == 0b00000010) { //change instrument if button 0 is pressed and fingering pattern is 0 0000001. + justPressed[0] = 0; + specialPressUsed[0] = 1; + changeInstrument(); + } + } + } + + + //now the button actions that can be changed with the configuration tool. + //_______________________________________________________________________________ + + + for (byte i = 0; i < 3; i++) { + + + if (released[i] && (momentary[mode][i] || (pressed[0] + pressed[1] + pressed[2] == 0))) { //do action for a button release ("click") NOTE: button array is zero-indexed, so "button 1" in all documentation is button 0 here (same for others). + if (!specialPressUsed[i]) { //we ignore it if the button was just used for a hard-coded command involving a combination of fingerholes. + performAction(i); + } + released[i] = 0; + specialPressUsed[i] = 0; + } + + + if (longPress[i] && (pressed[0] + pressed[1] + pressed[2] == 1) && !momentary[mode][i]) { //do action for long press, assuming no other button is pressed. + performAction(5 + i); + longPressUsed[i] = 1; + longPress[i] = 0; + longPressCounter[i] = 0; + } + + + //presses of individual buttons (as opposed to releases) are special cases used only if we're using buttons to send MIDI on/off messages and "momentary" is selected. We'll handle these in a separate function. + if (justPressed[i]) { + justPressed[i] = 0; + handleMomentary(i); //do action for button press. + } + + } + + + if (pressed[1]) { + if (released[0] && !momentary[mode][0]) { //do action for button 1 held and button 0 released + released[0] = 0; + shiftState = 1; + performAction(3); + } + + if (released[2] && !momentary[mode][1]) { //do action for button 1 held and button 2 released + released[2] = 0; + shiftState = 1; + performAction(4); + } + + } + + + buttonUsed = 0; // Now that we've caught any important button acticity, clear the flag so we won't enter this function again until there's been new activity. +} + +//perform desired action in response to buttons +void performAction(byte action) +{ + + + switch (buttonPrefs[mode][action][0]) { + + case 0: + break; //if no action desired for button combination + + case 1: //MIDI command + + if (buttonPrefs[mode][action][1] == 0) { + if (noteOnOffToggle[action] == 0) { + sendUSBMIDI(NOTE_ON, buttonPrefs[mode][action][2], buttonPrefs[mode][action][3], buttonPrefs[mode][action][4]); + noteOnOffToggle[action] = 1; + } else if (noteOnOffToggle[action] == 1) { + sendUSBMIDI(NOTE_OFF, buttonPrefs[mode][action][2], buttonPrefs[mode][action][3], buttonPrefs[mode][action][4]); + noteOnOffToggle[action] = 0; + } + } + + if (buttonPrefs[mode][action][1] == 1) { + sendUSBMIDI(CC, buttonPrefs[mode][action][2], buttonPrefs[mode][action][3], buttonPrefs[mode][action][4]); + } + + if (buttonPrefs[mode][action][1] == 2) { + sendUSBMIDI(PROGRAM_CHANGE, buttonPrefs[mode][action][2], buttonPrefs[mode][action][3]); + } + + if (buttonPrefs[mode][action][1] == 3) { //increase program change + if (program < 127) { + program++; + } else { + program = 0; + } + sendUSBMIDI(PROGRAM_CHANGE, buttonPrefs[mode][action][2], program); + blinkNumber = 1; + } + + if (buttonPrefs[mode][action][1] == 4) { //decrease program change + if (program > 0) { + program--; + } else { + program = 127; + } + sendUSBMIDI(PROGRAM_CHANGE, buttonPrefs[mode][action][2], program); + blinkNumber = 1; + } + + break; + + case 2: //set vibrato/slide mode + changePitchBend(); + break; + + case 3: + changeInstrument(); + break; + + case 4: + play = !play; //turn sound on/off when in bagless mode + break; + + case 5: + if (!momentary[mode][action]) { //shift up unless we're in momentary mode, otherwise shift down + octaveShiftUp(); + blinkNumber = abs(octaveShift); + } else { + octaveShiftDown(); + } + break; + + case 6: + if (!momentary[mode][action]) { //shift down unless we're in momentary mode, otherwise shift up + octaveShiftDown(); + blinkNumber = abs(octaveShift); + } else { + octaveShiftUp(); + } + break; + + case 7: + for (byte i = 1; i < 17; i++) { //send MIDI panic + sendUSBMIDI(CC, i, 123, 0); + dronesOn = 0; //remember that drones are off, because MIDI panic will have most likely turned them off in all apps. + } + break; + + case 8: + breathModeSelector[mode]++; //set breath mode + if (breathModeSelector[mode] == kPressureNModes) { + breathModeSelector[mode] = kPressureSingle; + } + loadPrefs(); + play = 0; + blinkNumber = abs(breathMode) + 1; + if (communicationMode) { + sendUSBMIDI(CC, 7, 102, 80 + breathMode); //send current breathMode + } + break; + + + case 9: //toggle drones + blinkNumber = 1; + if (!dronesOn) { + startDrones(); + } else { + stopDrones(); + } + break; + + + case 10: //semitone shift up + if (!momentary[mode][action]) { + noteShift++; //shift up if we're not in momentary mode + } else { + noteShift--; //shift down if we're in momentary mode, because the button is being released and a previous press has shifted up. + } + break; + + + case 11: //semitone shift down + if (!momentary[mode][action]) { + noteShift--; //shift up if we're not in momentary mode + } else { + noteShift++; //shift down if we're in momentary mode, because the button is being released and a previous press has shifted up. + } + break; + + + case 19: //autocalibrate + calibration = 1; + break; + + + default: + return; + } +} + +void octaveShiftUp() +{ + if (octaveShift < 3) { + octaveShiftSelector[mode]++; //adjust octave shift up, within reason + octaveShift = octaveShiftSelector[mode]; + } +} + +void octaveShiftDown() +{ + if (octaveShift > -4) { + octaveShiftSelector[mode]--; + octaveShift = octaveShiftSelector[mode]; + } +} + +//cycle through pitchbend modes +void changePitchBend() +{ + pitchBendModeSelector[mode] ++; + if (pitchBendModeSelector[mode] == kPitchBendNModes) { + pitchBendModeSelector[mode] = kPitchBendSlideVibrato; + } + loadPrefs(); + blinkNumber = abs(pitchBendMode) + 1; + if (communicationMode) { + sendUSBMIDI(CC, 7, 102, 70 + pitchBendMode); //send current pitchbend mode to configuration tool. + } +} + +//cycle through instruments +void changeInstrument() +{ + mode++; //set instrument + if (mode == 3) { + mode = 0; + } + play = 0; + loadPrefs(); //load the correct user settings based on current instrument. + blinkNumber = abs(mode) + 1; + if (communicationMode) { + sendSettings(); //tell communications tool to switch mode and send all settings for current instrument. + } +} + +void handleMomentary(byte button) +{ + + if (momentary[mode][button]) { + if (buttonPrefs[mode][button][0] == 1 && buttonPrefs[mode][button][1] == 0) { //handle momentary press if we're sending a MIDI message + sendUSBMIDI(NOTE_ON, buttonPrefs[mode][button][2], buttonPrefs[mode][button][3], buttonPrefs[mode][button][4]); + noteOnOffToggle[button] = 1; + } + + //handle presses for shifting the octave or semitone up or down + if (buttonPrefs[mode][button][0] == 5) { + octaveShiftUp(); + } + + if (buttonPrefs[mode][button][0] == 6) { + octaveShiftDown(); + } + + if (buttonPrefs[mode][button][0] == 10) { + noteShift++; + } + + if (buttonPrefs[mode][button][0] == 11) { + noteShift--; + } + + } + +} + +// find leftmost unset bit, used for finding the uppermost uncovered hole when reading from some fingering charts, and for determining the slidehole. +byte findleftmostunsetbit(uint16_t n) +{ + + if ((n & (n + 1)) == 0) { + return 127; // if number contains all 0s then return 127 + } + + int pos = 0; + for (int temp = n, count = 0; temp > 0; temp >>= 1, count++) // Find position of leftmost unset bit. + + if ((temp & 1) == 0) // if temp L.S.B is zero then unset bit pos is + pos = count; + + return pos; + +} + +// Send a debug MIDI message with a value up 16383 (14 bits) +void debug_log(int msg) +{ + sendUSBMIDI(CC, 7, 106, 48); //indicate that LSB is about to be sent + sendUSBMIDI(CC, 7, 119, msg & 0x7F); //send LSB + sendUSBMIDI(CC, 7, 106, 49); //indicate that MSB is about to be sent + sendUSBMIDI(CC, 7, 119, msg >> 7); //send MSB + sendUSBMIDI(CC, 7, 106, 51); //indicates end of two-byte message +} + +//initialize the ADC with custom settings because we aren't using analogRead. +void ADC_init(void) +{ + + ADCSRA &= ~(bit (ADPS0) | bit (ADPS1) | bit (ADPS2)); // clear ADC prescaler bits + ADCSRA = (1 << ADEN) | ((1 << ADPS2)); // enable ADC Division Factor 16 (36 us) + ADMUX = (1 << REFS0); //Voltage reference from Avcc (3.3v) + ADC_read(1); //start an initial conversion (pressure sensor), which will also enable the ADC complete interrupt and trigger subsequent conversions. + +} + +//start an ADC conversion. +void ADC_read(byte pin) +{ + + if (pin >= 18) pin -= 18; // allow for channel or pin numbers + pin = analogPinToChannel(pin); + + ADCSRB = (ADCSRB & ~(1 << MUX5)) | (((pin >> 3) & 0x01) << MUX5); + ADMUX = (1 << REFS0) | (pin & 0x07); + + ADCSRA |= bit (ADSC) | bit (ADIE); //start a conversion and enable the ADC complete interrupt + +} + +void startDrones() +{ + dronesOn = 1; + switch (ED[mode][DRONES_ON_COMMAND]) { + case 0: + sendUSBMIDI(NOTE_ON, ED[mode][DRONES_ON_CHANNEL], ED[mode][DRONES_ON_BYTE2], ED[mode][DRONES_ON_BYTE3]); + break; + case 1: + sendUSBMIDI(NOTE_OFF, ED[mode][DRONES_ON_CHANNEL], ED[mode][DRONES_ON_BYTE2], ED[mode][DRONES_ON_BYTE3]); + break; + case 2: + sendUSBMIDI(CC, ED[mode][DRONES_ON_CHANNEL], ED[mode][DRONES_ON_BYTE2], ED[mode][DRONES_ON_BYTE3]); + break; + } + // TODO: Update so that drones start according to fingering, instead of configured value. + if (modeSelector[mode] == kModeBoha) { + droneNotePlaying = ED[mode][DRONES_ON_BYTE2]; + } +} + +void stopDrones() +{ + dronesOn = 0; + // Override with Boha mode so that the actual note playing on drone is used + if (modeSelector[mode] == kModeBoha) { + sendUSBMIDI(NOTE_OFF, ED[mode][DRONES_OFF_CHANNEL], droneNotePlaying, ED[mode][DRONES_OFF_BYTE3]); + return; + } + + switch (ED[mode][DRONES_OFF_COMMAND]) { + case 0: + sendUSBMIDI(NOTE_ON, ED[mode][DRONES_OFF_CHANNEL], ED[mode][DRONES_OFF_BYTE2], ED[mode][DRONES_OFF_BYTE3]); + break; + case 1: + sendUSBMIDI(NOTE_OFF, ED[mode][DRONES_OFF_CHANNEL], ED[mode][DRONES_OFF_BYTE2], ED[mode][DRONES_OFF_BYTE3]); + break; + case 2: + sendUSBMIDI(CC, ED[mode][DRONES_OFF_CHANNEL], ED[mode][DRONES_OFF_BYTE2], ED[mode][DRONES_OFF_BYTE3]); + break; + } +} + +//load the correct user settings for the current instrument. This is used at startup and any time settings are changed. +void loadPrefs() +{ + + vibratoHoles = vibratoHolesSelector[mode]; + octaveShift = octaveShiftSelector[mode]; + noteShift = noteShiftSelector[mode]; + pitchBendMode = pitchBendModeSelector[mode]; + useLearnedPressure = useLearnedPressureSelector[mode]; + learnedPressure = learnedPressureSelector[mode]; + senseDistance = senseDistanceSelector[mode]; + vibratoDepth = vibratoDepthSelector[mode]; + breathMode = breathModeSelector[mode]; + midiBendRange = midiBendRangeSelector[mode]; + mainMidiChannel = midiChannelSelector[mode]; + + + //set these variables depending on whether "vented" is selected + offset = pressureSelector[mode][(switches[mode][VENTED] * 6) + 0]; + multiplier = pressureSelector[mode][(switches[mode][VENTED] * 6) + 1]; + jumpValue = pressureSelector[mode][(switches[mode][VENTED] * 6) + 2]; + dropValue = pressureSelector[mode][(switches[mode][VENTED] * 6) + 3]; + jumpTime = pressureSelector[mode][(switches[mode][VENTED] * 6) + 4]; + dropTime = pressureSelector[mode][(switches[mode][VENTED] * 6) + 5]; + + + pitchBend = 8192; + expression = 0; + sendUSBMIDI(PITCH_BEND, mainMidiChannel, pitchBend & 0x7F, pitchBend >> 7); + + for (byte i = 0; i < 9; i++) { + iPitchBend[i] = 0; //turn off pitchbend + pitchBendOn[i] = 0; + } + + if (switches[mode][CUSTOM] && pitchBendMode != kPitchBendNone) { + customEnabled = 1; + } else (customEnabled = 0); //decide here whether custom vibrato can currently be used, so we don't have to do it every time we need to check pitchBend. + + if (switches[mode][FORCE_MAX_VELOCITY]) { + velocity = 127; //set velocity + } else { + velocity = 64; + } + + if (!useLearnedPressure) { + sensorThreshold[0] = (sensorCalibration + soundTriggerOffset); //pressure sensor calibration at startup. We set the on/off threshhold just a bit higher than the reading at startup. + } + + else { + sensorThreshold[0] = (learnedPressure + soundTriggerOffset); + } + + sensorThreshold[1] = sensorThreshold[0] + (offset << 2); //threshold for move to second octave + + for (byte i = 0; i < 9; i++) { + toneholeScale[i] = ((8 * (16383 / midiBendRange)) / (toneholeCovered[i] - 50 - senseDistance) / 2); // Precalculate scaling factors for pitchbend. This one is for sliding. We multiply by 8 first to reduce rounding errors. We'll divide again later. + vibratoScale[i] = ((8 * 2 * (vibratoDepth / midiBendRange)) / (toneholeCovered[i] - 50 - senseDistance) / 2); //This one is for vibrato + } + + adjvibdepth = vibratoDepth / midiBendRange; //precalculations for pitchbend range + pitchBendPerSemi = 8192 / midiBendRange; + + inputPressureBounds[0][0] = (ED[mode][INPUT_PRESSURE_MIN] * 9); //precalculate input and output pressure ranges for sending pressure as CC + inputPressureBounds[0][1] = (ED[mode][INPUT_PRESSURE_MAX] * 9); + inputPressureBounds[1][0] = (ED[mode][VELOCITY_INPUT_PRESSURE_MIN] * 9); //precalculate input and output pressure ranges for sending pressure as velocity + inputPressureBounds[1][1] = (ED[mode][VELOCITY_INPUT_PRESSURE_MAX] * 9); + inputPressureBounds[2][0] = (ED[mode][AFTERTOUCH_INPUT_PRESSURE_MIN] * 9); //precalculate input and output pressure ranges for sending pressure as aftertouch + inputPressureBounds[2][1] = (ED[mode][AFTERTOUCH_INPUT_PRESSURE_MAX] * 9); + inputPressureBounds[3][0] = (ED[mode][POLY_INPUT_PRESSURE_MIN] * 9); //precalculate input and output pressure ranges for sending pressure as poly + inputPressureBounds[3][1] = (ED[mode][POLY_INPUT_PRESSURE_MAX] * 9); + + for (byte j = 0; j < 4; j++) { // CC, velocity, aftertouch, poly + pressureInputScale[j] = (1048576 / (inputPressureBounds[j][1] - inputPressureBounds[j][0])); //precalculate scaling factors for pressure input, which will be used to scale it up to a range of 1024. + inputPressureBounds[j][2] = (inputPressureBounds[j][0] * pressureInputScale[j]) >> 10; + } + + outputBounds[0][0] = ED[mode][OUTPUT_PRESSURE_MIN]; //move all these variables to a more logical order so they can be accessed in FOR loops + outputBounds[0][1] = ED[mode][OUTPUT_PRESSURE_MAX]; + outputBounds[1][0] = ED[mode][VELOCITY_OUTPUT_PRESSURE_MIN]; + outputBounds[1][1] = ED[mode][VELOCITY_OUTPUT_PRESSURE_MAX]; + outputBounds[2][0] = ED[mode][AFTERTOUCH_OUTPUT_PRESSURE_MIN]; + outputBounds[2][1] = ED[mode][AFTERTOUCH_OUTPUT_PRESSURE_MAX]; + outputBounds[3][0] = ED[mode][POLY_OUTPUT_PRESSURE_MIN]; + outputBounds[3][1] = ED[mode][POLY_OUTPUT_PRESSURE_MAX]; + + curve[0] = ED[mode][CURVE]; + curve[1] = ED[mode][VELOCITY_CURVE]; + curve[2] = ED[mode][AFTERTOUCH_CURVE]; + curve[3] = ED[mode][POLY_CURVE]; + +} + +//calculate pressure data for CC, velocity, channel pressure, and key pressure if those options are selected +void calculatePressure(byte pressureOption) +{ + + long scaledPressure = sensorValue - 100; // input pressure range is 100-1000. Bring this down to 0-900 + scaledPressure = constrain (scaledPressure, inputPressureBounds[pressureOption][0], inputPressureBounds[pressureOption][1]); + scaledPressure = (((scaledPressure * pressureInputScale[pressureOption]) >> 10) - inputPressureBounds[pressureOption][2]); //scale input pressure up to a range of 0-1024 using the precalculated scale factor + + if (curve[pressureOption] == 1) { //for this curve, cube the input and scale back down. + scaledPressure = ((scaledPressure * scaledPressure * scaledPressure) >> 20); + } + + else if (curve[pressureOption] == 2) { //approximates a log curve with a piecewise linear function. + switch (scaledPressure >> 6) { + case 0: + scaledPressure = scaledPressure << 3; + break; + case 1 ... 2: + scaledPressure = (scaledPressure << 1) + 376; + break; + case 3 ... 5: + scaledPressure = scaledPressure + 566; + break; + default: + scaledPressure = (scaledPressure >> 3) + 901; + break; + } + if (scaledPressure > 1024) { + scaledPressure = 1024; + } + } + + //else curve 0 is linear, so no transformation + + inputPressureBounds[pressureOption][3] = (scaledPressure * (outputBounds[pressureOption][1] - outputBounds[pressureOption][0]) >> 10) + outputBounds[pressureOption][0]; //map to output pressure range + + + if (pressureOption == 1) { //set velocity to mapped pressure if desired + velocity = inputPressureBounds[pressureOption][3]; + } +} + +//send pressure data +void sendPressure(bool force) +{ + + if (ED[mode][SEND_PRESSURE] == 1 && (inputPressureBounds[0][3] != prevCCPressure || force)) { + sendUSBMIDI(CC, ED[mode][PRESSURE_CHANNEL], ED[mode][PRESSURE_CC], inputPressureBounds[0][3]); //send MSB of pressure mapped to the output range + prevCCPressure = inputPressureBounds[0][3]; + } + + if ((switches[mode][SEND_AFTERTOUCH] & 1)) { + // hack + int sendm = (!noteon && sensorValue <= 100) ? 0 : inputPressureBounds[2][3]; + if (sendm != prevChanPressure || force) { + sendUSBMIDI(CHANNEL_PRESSURE, mainMidiChannel, sendm); //send MSB of pressure mapped to the output range + prevChanPressure = sendm; + } + } + + // poly aftertouch uses 2nd lowest bit of ED flag + if ((switches[mode][SEND_AFTERTOUCH] & 2) && noteon) { + // hack + int sendm = (!noteon && sensorValue <= 100) ? 0 : inputPressureBounds[3][3]; + if (sendm != prevPolyPressure || force) { + sendUSBMIDI(KEY_PRESSURE, mainMidiChannel, notePlaying, sendm); //send MSB of pressure mapped to the output range + prevPolyPressure = sendm; + } + } +} diff --git a/warbl_firmware.ino b/warbl_firmware.ino new file mode 100644 index 0000000..3f54491 --- /dev/null +++ b/warbl_firmware.ino @@ -0,0 +1,677 @@ + +/* + Copyright (C) 2018-2021 Andrew Mowry warbl.xyz + + Many thanks to Michael Eskin and Jesse Chappell for their additions and debugging help. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/ +*/ + + + +//#include //can be used to show free RAM +#include //for resetting with watchdog +#include //for timer interrupt for reading sensors at a regular interval +#include //fast digitalWrite library used for toggling IR LEDs +#include +#include +#include // for using PROGMEM for fingering chart storage + +#define GPIO2_PREFER_SPEED 1 //digitalread speed, see: https://github.com/Locoduino/DIO2/blob/master/examples/standard_outputs/standard_outputs.ino + +#define DEBOUNCE_TIME 0.02 //debounce time, in seconds---Integrating debouncing algorithm is taken from debounce.c, written by Kenneth A. Kuhn:http://www.kennethkuhn.com/electronics/debounce.c +#define SAMPLE_FREQUENCY 200 //button sample frequency, in Hz + +#define MAXIMUM (DEBOUNCE_TIME * SAMPLE_FREQUENCY) //the integrator value required to register a button press + +#define VERSION 20 //software version number (without decimal point) + +//MIDI commands +#define NOTE_OFF 0x80 //127 +#define NOTE_ON 0x90 // 144 +#define KEY_PRESSURE 0xA0 // 160 +#define CC 0xB0 // 176 +#define PROGRAM_CHANGE 0xC0 // 192 +#define CHANNEL_PRESSURE 0xD0 // 208 +#define PITCH_BEND 0xE0 // 224 + +// Fingering Patterns +#define kModeWhistle 0 +#define kModeUilleann 1 +#define kModeGHB 2 +#define kModeNorthumbrian 3 +#define kModeChromatic 4 +#define kModeGaita 5 +#define kModeNAF 6 +#define kModeKaval 7 +#define kModeRecorder 8 +#define kModeRegulators 9 //only used for a custom regulators implementation, not the "official" software +#define kModeUilleannStandard 10 //contains no accidentals +#define kModeXiao 11 +#define kModeSax 12 +#define kModeGaitaExtended 13 +#define kModeSaxBasic 14 +#define kModeEVI 15 +#define kModeShakuhachi 16 +#define kModeSackpipaMajor 17 +#define kModeSackpipaMinor 18 +#define kModeCustom 19 +#define kModeNModes 20 +#define kModeBoha 21 + +// Pitch bend modes +#define kPitchBendSlideVibrato 0 +#define kPitchBendVibrato 1 +#define kPitchBendNone 2 +#define kPitchBendLegatoSlideVibrato 3 +#define kPitchBendNModes 4 + +// Register control modes +#define kPressureSingle 0 +#define kPressureBreath 1 +#define kPressureThumb 2 +#define kPressureBell 3 +#define kPressureNModes 4 + +// Secret function drone control MIDI parameters +#define kDroneVelocity 36 +#define kLynchDroneMIDINote 50 +#define kCrowleyDroneMIDINote 51 + +// Drones control mode +#define kNoDroneControl 0 +#define kSecretDroneControl 1 +#define kBaglessDroneControl 2 +#define kPressureDroneControl 3 +#define kDroneNModes 4 + +//Variables in the switches array (settings for the swiches in the slide/vibrato and register control panels) +#define VENTED 0 +#define BAGLESS 1 +#define SECRET 2 +#define INVERT 3 +#define CUSTOM 4 +#define SEND_VELOCITY 5 +#define SEND_AFTERTOUCH 6 //second bit of this one is used for poly +#define FORCE_MAX_VELOCITY 7 +#define IMMEDIATE_PB 8 +#define LEGATO 9 +#define OVERRIDE 10 +#define THUMB_AND_OVERBLOW 11 +#define R4_FLATTEN 12 +#define kSWITCHESnVariables 13 + +//Variables in the ED array (all the settings for the Expression and Drones panels) +#define EXPRESSION_ON 0 +#define EXPRESSION_DEPTH 1 +#define SEND_PRESSURE 2 +#define CURVE 3 // (0 is linear, 1 and 2 are power curves) +#define PRESSURE_CHANNEL 4 +#define PRESSURE_CC 5 +#define INPUT_PRESSURE_MIN 6 +#define INPUT_PRESSURE_MAX 7 +#define OUTPUT_PRESSURE_MIN 8 +#define OUTPUT_PRESSURE_MAX 9 +#define DRONES_ON_COMMAND 10 +#define DRONES_ON_CHANNEL 11 +#define DRONES_ON_BYTE2 12 +#define DRONES_ON_BYTE3 13 +#define DRONES_OFF_COMMAND 14 +#define DRONES_OFF_CHANNEL 15 +#define DRONES_OFF_BYTE2 16 +#define DRONES_OFF_BYTE3 17 +#define DRONES_CONTROL_MODE 18 +#define DRONES_PRESSURE_LOW_BYTE 19 +#define DRONES_PRESSURE_HIGH_BYTE 20 +#define VELOCITY_INPUT_PRESSURE_MIN 21 +#define VELOCITY_INPUT_PRESSURE_MAX 22 +#define VELOCITY_OUTPUT_PRESSURE_MIN 23 +#define VELOCITY_OUTPUT_PRESSURE_MAX 24 +#define AFTERTOUCH_INPUT_PRESSURE_MIN 25 +#define AFTERTOUCH_INPUT_PRESSURE_MAX 26 +#define AFTERTOUCH_OUTPUT_PRESSURE_MIN 27 +#define AFTERTOUCH_OUTPUT_PRESSURE_MAX 28 +#define POLY_INPUT_PRESSURE_MIN 29 +#define POLY_INPUT_PRESSURE_MAX 30 +#define POLY_OUTPUT_PRESSURE_MIN 31 +#define POLY_OUTPUT_PRESSURE_MAX 32 +#define VELOCITY_CURVE 33 +#define AFTERTOUCH_CURVE 34 +#define POLY_CURVE 35 +#define EXPRESSION_MIN 36 +#define EXPRESSION_MAX 37 +#define CUSTOM_FINGERING_1 38 +#define CUSTOM_FINGERING_2 39 +#define CUSTOM_FINGERING_3 40 +#define CUSTOM_FINGERING_4 41 +#define CUSTOM_FINGERING_5 42 +#define CUSTOM_FINGERING_6 43 +#define CUSTOM_FINGERING_7 44 +#define CUSTOM_FINGERING_8 45 +#define CUSTOM_FINGERING_9 46 +#define CUSTOM_FINGERING_10 47 +#define CUSTOM_FINGERING_11 48 +#define kEXPRESSIONnVariables 49 + +//GPIO constants +const uint8_t ledPin = 13; +const uint8_t holeTrans[] = {5, 9, 10, 0, 1, 2, 3, 11, 6}; //the analog pins used for the tone hole phototransistors, in the following order: Bell,R4,R3,R2,R1,L3,L2,L1,Lthumb +const GPIO_pin_t pins[] = {DP11, DP6, DP8, DP5, DP7, DP1, DP0, DP3, DP2}; //the digital pins used for the tone hole leds, in the following order: Bell,R4,R3,R2,R1,L3,L2,L1,Lthumb. Uses a special declaration format for the GPIO library. +const GPIO_pin_t buttons[] = {DP15, DP14, DP16}; //the pins used for the buttons + +//instrument +byte mode = 0; // The current mode (instrument), from 0-2. +byte defaultMode = 0; // The default mode, from 0-2. + +//variables that can change according to instrument. +int8_t octaveShift = 0; //octave transposition +int8_t noteShift = 0; //note transposition, for changing keys. All fingering patterns are initially based on the key of D, and transposed with this variable to the desired key. +byte pitchBendMode = kPitchBendSlideVibrato; //0 means slide and vibrato are on. 1 means only vibrato is on. 2 is all pitchbend off, 3 is legato slide/vibrato. +byte senseDistance = 10; //the sensor value above which the finger is sensed for bending notes. Needs to be higher than the baseline sensor readings, otherwise vibrato will be turned on erroneously. +byte breathMode = kPressureBreath; //the desired presure sensor behavior: single register, overblow, thumb register control, bell register control. +unsigned int vibratoDepth = 1024; //vibrato depth from 0 (no vibrato) to 8191 (one semitone) +bool useLearnedPressure = 0; //whether we use learned pressure for note on threshold, or we use calibration pressure from startup +byte midiBendRange = 2; // +/- semitones that the midi bend range represents +byte mainMidiChannel = 1; // current MIDI channel to send notes on + +//These are containers for the above variables, storing the value used by the three different instruments. First variable in array is for instrument 0, etc. +byte modeSelector[] = {kModeBoha, kModeUilleann, kModeGHB}; //the fingering patterns chosen in the configuration tool, for the three instruments. +int8_t octaveShiftSelector[] = {0, 0, 0}; +int8_t noteShiftSelector[] = {0, 0, 8}; +byte pitchBendModeSelector[] = {1, 1, 1}; +byte senseDistanceSelector[] = {10, 10, 10}; +byte breathModeSelector[] = {1, 1, 0}; +byte useLearnedPressureSelector[] = {0, 0, 0}; +int learnedPressureSelector[] = {0, 0, 0}; +byte LSBlearnedPressure; //used to reconstruct learned pressure from two MIDI bytes. +unsigned int vibratoHolesSelector[] = {0b011111111, 0b011111111, 0b011111111}; +unsigned int vibratoDepthSelector[] = {1024, 1024, 1024}; +byte midiBendRangeSelector[] = {2, 2, 2}; +byte midiChannelSelector[] = {1, 1, 1}; + +bool momentary[3][3] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}} ; //whether momentary click behavior is desired for MIDI on/off message sent with a button. Dimension 0 is mode (instrument), dimension 1 is button 0,1,2. + +byte switches[3][13] = //the settings for the five switches in the vibrato/slide and register control panels + //instrument 0 +{ + { + 1, // vented mouthpiece on or off (there are different pressure settings for the vented mouthpiece) + 0, // bagless mode off or on + 0, // secret button command mode off or on + 0, // whether the functionality for using the right thumb or the bell sensor for increasing the register is inverted. + 0, // off/on for Michael Eskin's custom vibrato approach + 0, // send pressure as NoteOn velocity off or on + 0, // send pressure as aftertouch (channel pressure) off or on, and/or poly aftertouch (2nd bit) + 1, // force maximum velocity (127) + 0, // send pitchbend immediately before Note On (recommnded for MPE) + 1, // send legato (Note On message before Note Off for previous note) + 0, //override pitch expression pressure range + 0, //use both thumb and overblowing for register control with custom fingering chart + 0 //use R4 finger to flatten any note one semitone with custom fingering chart + }, + + //same for instrument 1 + {0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0}, + + //same for instrument 2 + {0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0} +}; + +byte ED[3][49] = //an array that holds all the settings for the Expression and Drones Control panels in the Configuration Tool. + //instrument 0 +{ + { + 0, //EXPRESSION_ON + 3, //EXPRESSION_DEPTH (can have a value of 1-8) + 0, //SEND_PRESSURE + 0, //CURVE (0 is linear) + 1, //PRESSURE_CHANNEL + 7, //PRESSURE_CC + 0, //INPUT_PRESSURE_MIN range is from 0-100, to be scaled later up to maximum input values + 100, //INPUT_PRESSURE_MAX range is from 0-100, to be scaled later + 0, //OUTPUT_PRESSURE_MIN range is from 0-127, to be scaled later + 127, //OUTPUT_PRESSURE_MAX range is from 0-127, to be scaled later + 0, //DRONES_ON_COMMAND + 1, //DRONES_ON_CHANNEL + 51, //DRONES_ON_BYTE2 + 36, //DRONES_ON_BYTE3 + 0, //DRONES_OFF_COMMAND + 1, //DRONES_OFF_CHANNEL + 51, //DRONES_OFF_BYTE2 + 36, //DRONES_OFF_BYTE3 + 0, //DRONES_CONTROL_MODE (0 is no drone control, 1 is use secret button, 2 is use bagless button, 3 is use pressure. + 0, //DRONES_PRESSURE_LOW_BYTE + 0, //DRONES_PRESSURE_HIGH_BYTE + 0, //VELOCITY_INPUT_PRESSURE_MIN + 100, //VELOCITY_INPUT_PRESSURE_MAX + 0, //VELOCITY_OUTPUT_PRESSURE_MIN + 127, //VELOCITY_OUTPUT_PRESSURE_MAX + 0, //AFTERTOUCH_INPUT_PRESSURE_MIN + 100, //AFTERTOUCH_INPUT_PRESSURE_MAX + 0, //AFTERTOUCH_OUTPUT_PRESSURE_MIN + 127, //AFTERTOUCH_OUTPUT_PRESSURE_MAX + 0, //POLY_INPUT_PRESSURE_MIN + 100, //POLY_INPUT_PRESSURE_MAX + 0, //POLY_OUTPUT_PRESSURE_MIN + 127, //POLY_OUTPUT_PRESSURE_MAX + 0, //VELOCITY_CURVE + 0, //AFTERTOUCH_CURVE + 0, //POLY_CURVE + 0, //EXPRESSION_MIN + 100, //EXPRESSION_MAX + 0, //CUSTOM_FINGERING_1 + 74, //CUSTOM_FINGERING_2 + 73, //CUSTOM_FINGERING_3 + 72, //CUSTOM_FINGERING_4 + 71, //CUSTOM_FINGERING_5 + 69, //CUSTOM_FINGERING_6 + 67, //CUSTOM_FINGERING_7 + 66, //CUSTOM_FINGERING_8 + 64, //CUSTOM_FINGERING_9 + 62, //CUSTOM_FINGERING_10 + 61 //CUSTOM_FINGERING_11 + }, + + //same for instrument 1 + {0, 3, 0, 0, 1, 7, 0, 100, 0, 127, 0, 1, 51, 36, 0, 1, 51, 36, 0, 0, 0, 0, 127, 0, 127, 0, 127, 0, 127, 0, 127, 0, 127, 0, 0, 0, 0, 100, 0, 74, 73, 72, 71, 69, 67, 66, 64, 62, 61}, + + //same for instrument 2 + {0, 3, 0, 0, 1, 7, 0, 100, 0, 127, 0, 1, 51, 36, 0, 1, 51, 36, 0, 0, 0, 0, 127, 0, 127, 0, 127, 0, 127, 0, 127, 0, 127, 0, 0, 0, 0, 100, 0, 74, 73, 72, 71, 69, 67, 66, 64, 62, 61} +}; + +byte pressureSelector[3][12] = //a selector array for all the register control variables that can be changed in the Configuration Tool + //instrument 0 +{ + { + 15, 15, 15, 15, 30, 30, //closed mouthpiece: offset, multiplier, jump, drop, jump time, drop time + 3, 7, 100, 7, 9, 9 + }, //vented mouthpiece: offset, multiplier, jump, drop, jump time, drop time + //instrument 1 + { + 15, 15, 15, 15, 30, 30, + 3, 7, 10, 7, 9, 9 + }, + //instrument 2 + { + 15, 15, 15, 15, 30, 30, + 3, 7, 10, 7, 9, 9 + } +}; + +uint8_t buttonPrefs[3][8][5] = //The button configuration settings. Dimension 1 is the three instruments. Dimension 2 is the button combination: click 1, click 2, click3, hold 2 click 1, hold 2 click 3, longpress 1, longpress2, longpress3 + //Dimension 3 is the desired action: Action, MIDI command type (noteon/off, CC, PC), MIDI channel, MIDI byte 2, MIDI byte 3. + //instrument 0 //the actions are: 0 none, 1 send MIDI message, 2 change pitchbend mode, 3 instrument, 4 play/stop (bagless mode), 5 octave shift up, 6 octave shift down, 7 MIDI panic, 8 change register control mode, 9 drones on/off, 10 semitone shift up, 11 semitone shift down +{ { {2, 0, 0, 0, 0}, //for example, this means that clicking button 0 will send a CC message, channel 1, byte 2 = 0, byte 3 = 0. + {8, 0, 0, 0, 0}, //for example, this means that clicking button 1 will change pitchbend mode. + {0, 0, 0, 0, 0}, + {5, 0, 0, 0, 0}, + {6, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0} + }, + + //same for instrument 1 + {{2, 0, 0, 0, 0}, {8, 0, 0, 0, 0}, {9, 0, 0, 0, 0}, {5, 0, 0, 0, 0}, {6, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}}, + + //same for instrument 2 + {{2, 0, 0, 0, 0}, {8, 0, 0, 0, 0}, {9, 0, 0, 0, 0}, {5, 0, 0, 0, 0}, {6, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}} +}; + +//other misc. variables +byte hardwareRevision = 30; +unsigned long ledTimer = 0; //for blinking LED +byte blinkNumber = 1; //the number of remaining blinks when blinking LED to indicate control changes +bool LEDon = 0; //whether the LED is currently on +bool play = 0; //turns sound off and on (with the use of a button action) when in bagless mode +bool bellSensor = 0; //whether the bell sensor is plugged in +bool prevBellSensor = 0; //the previous reading of the bell sensor detection pin +unsigned long initialTime = 0; //for testing +unsigned long finalTime = 0; //for testing +unsigned long cycles = 0; //for testing +byte program = 0; //current MIDI program change value. This always starts at 0 but can be increased/decreased with assigned buttons. +bool dronesState = 0; //keeps track of whether we're above or below the pressure threshold for turning drones on. + +//variables for reading pressure sensor +volatile unsigned int tempSensorValue = 0; //for holding the pressure sensor value inside the ISR +int sensorValue = 0; // first value read from the pressure sensor +int sensorValue2 = 0; // second value read from the pressure sensor, for measuring rate of change in pressure +int prevSensorValue = 0; // previous sensor reading, used to tell if the pressure has changed and should be sent. +int pressureChangeRate = 0; //the difference between current and previous sensor readings +int sensorCalibration = 0; //the sensor reading at startup, used as a base value +byte offset = 15; +byte customScalePosition; //used to indicate the position of the current note on the custom chart scale (needed for state calculation) +int sensorThreshold[] = {260, 0}; //the pressure sensor thresholds for initial note on and shift from register 1 to register 2, before some transformations. +int upperBound = 255; //this represents the pressure transition between the first and second registers. It is calculated on the fly as: (sensorThreshold[1] + ((newNote - 60) * multiplier)) +byte newState; //the note/octave state based on the sensor readings (1=not enough force to sound note, 2=enough force to sound first octave, 3 = enough force to sound second octave) +byte prevState = 1; //the previous state, used to monitor change necessary for adding a small delay when a note is turned on from silence and we're sending not on velocity based on pressure. +boolean sensorDataReady = 0; //tells us that pressure data is available +boolean velocityDelay = 0; //whether we are currently waiting for the pressure to rise after crossing the threshold from having no note playing to have a note playing. This is only used if we're sending velocity based on pressure. +unsigned long velocityDelayTimer = 0; //a timer for the above delay. +bool jump = 0; //whether we jumped directly to second octave from note off because of rapidly increasing pressure. +unsigned long jumpTimer = 0; //records time when we dropped to note off. +int jumpTime = 15; //the amount of time to wait before dropping back down from an octave jump to first octave because of insufficient pressure +bool drop = 0; //whether we dropped directly from second octave to note off +unsigned long dropTimer = 0; //time when we jumped to second octave. +int dropTime = 15 ; //the amount of time to wait (ms) before turning a note back on after dropping directly from second octave to note off +byte jumpValue = 15; +byte dropValue = 15; +byte multiplier = 15; //controls how much more difficult it is to jump to second octave from higher first-octave notes than from lower first-octave notes. Increasing this makes playing with a bag more forgiving but requires more force to reach highest second-octave notes. Can be set according to fingering mode and breath mode (i.e. a higher jump factor may be used for playing with a bag). Array indices 1-3 are for breath mode jump factor, indices 4-6 are for bag mode jump factor. +byte soundTriggerOffset = 3; //the number of sensor values above the calibration setting at which a note on will be triggered (first octave) +int learnedPressure = 0; //the learned pressure reading, used as a base value + +unsigned int inputPressureBounds[4][4] = { //for mapping pressure input range to output range. Dimension 1 is CC, velocity, aftertouch, poly. Dimension 2 is minIn, maxIn, scaledMinIn, mappedPressure + {100, 800, 0, 0}, + {100, 800, 0, 0}, + {100, 800, 0, 0}, + {100, 800, 0, 0}, +}; + +unsigned long pressureInputScale[4] = // precalculated scale factor for mapping the input pressure range, for CC, velocity, aftertouch, and poly. +{0, 0, 0, 0}; + +byte outputBounds[4][2] = { // container for ED output pressure range variables (CC, velocity, aftertouch, poly)-- the ED variables will be copied here so they're in a more logical order. This is a fix for variables that were added later. + {0, 127}, + {0, 127}, + {0, 127}, + {0, 127} +}; + +byte curve[4] = {0, 0, 0, 0}; //similar to above-- more logical odering for the pressure curve variable + + +//variables for reading tonehole sensors +volatile byte lastRead = 0; //the transistor that was last read, so we know which to read the next time around the loop. +unsigned int toneholeCovered[] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; //covered hole tonehole sensor readings for calibration +int toneholeBaseline[] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; //baseline (uncovered) hole tonehole sensor readings +volatile int tempToneholeRead[] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; //temporary storage for tonehole sensor readings with IR LED on, written during the timer ISR +int toneholeRead[] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; //storage for tonehole sensor readings, transferred from the above volatile variable +volatile int tempToneholeReadA[] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; //temporary storage for ambient light tonehole sensor readings, written during the timer ISR +unsigned int holeCovered = 0; //whether each hole is covered-- each bit corresponds to a tonehole. +uint8_t tempCovered = 0; //used when masking holeCovered to ignore certain holes depending on the fingering pattern. +bool fingersChanged = 1; //keeps track of when the fingering pattern has changed. +unsigned int prevHoleCovered = 1; //so we can track changes. +volatile int tempNewNote = 0; +byte prevNote; +byte newNote = -1; //the next note to be played, based on the fingering chart (does not include transposition). +byte notePlaying; //the actual MIDI note being played, which we remember so we can turn it off again. +volatile bool firstTime = 1; // we have the LEDs off ~50% of the time. This bool keeps track of whether each LED is off or on at the end of each timer cycle +volatile byte timerCycle = 0; //the number of times we've entered the timer ISR with the new sensors. +byte newDroneNote; +//byte prevDroneNote; // Has no use for now +byte droneNotePlaying; + +//pitchbend variables +unsigned long pitchBendTimer = 0; //to keep track of the last time we sent a pitchbend message +byte pitchBendOn[] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; //whether pitchbend is currently turned for for a specific hole +int pitchBend = 8192; //total current pitchbend value +int prevPitchBend = 8192; //a record of the previous pitchBend value, so we don't send the same one twice +int iPitchBend[] = {8192, 8192, 8192, 8192, 8192, 8192, 8192, 8192, 8192}; //current pitchbend value for each tonehole +int pitchBendPerSemi = 4096; +int prevChanPressure = 0; +int prevCCPressure = 0; +int prevPolyPressure = 0; +unsigned long pressureTimer = 0; //to keep track of the last time we sent a pressure message +unsigned long noteOnTimestamp = 0; // ms timestamp the note was activated +byte slideHole; //the hole above the current highest uncovered hole. Used for detecting slides between notes. +byte stepsDown = 1; //the number of half steps down from the slideHole to the next lowest note on the scale, used for calculating pitchbend values. +byte vibratoEnable = 0; // if non-zero, send vibrato pitch bend +unsigned int holeLatched = 0b000000000; //holes that are disabled for vibrato because they were covered when the note was triggered. They become unlatched (0) when the finger is removed all the way. +unsigned int vibratoHoles = 0b111111111; //holes to be used for vibrato, left thumb on left, bell sensor far right. +unsigned int toneholeScale[] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; //a scale for normalizing the range of each sensor, for sliding +unsigned int vibratoScale[] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; //same as above but for vibrato +int expression = 0; //pitchbend up or down from current note based on pressure +bool customEnabled = 0; //Whether the custom vibrato above is currently enabled based on fingering pattern and pitchbend mode. +int adjvibdepth; //vibrato depth scaled to MIDI bend range. + +//variables for managing MIDI note output +bool noteon = 0; //whether a note is currently turned on +bool shiftState = 0; //whether the octave is shifted (could be combined with octaveShift) +int8_t shift = 0; //the total amount of shift up or down from the base note 62 (D). This takes into account octave shift and note shift. +byte velocity = 127;//default MIDI note velocity + +//tonehole calibration variables +byte calibration = 0; //whether we're currently calibrating. 1 is for calibrating all sensors, 2 is for calibrating bell sensor only, 3 is for calibrating all sensors plus baseline calibration (normally only done once, in the "factory"). +unsigned long calibrationTimer = 0; + +//variables for reading buttons +unsigned long buttonReadTimer = 0; //for telling when it's time to read the buttons +byte integrator[] = {0, 0, 0}; //stores integration of button readings. When this reaches MAXIMUM, a button press is registered. When it reaches 0, a release is registered. +bool pressed[] = {0, 0, 0}; //whether a button is currently presed (this it the output from the integrator) +bool released[] = {0, 0, 0}; //if a button has just been released +bool justPressed[] = {0, 0, 0}; //if a button has just been pressed +bool prevOutput[] = {0, 0, 0}; //previous state of button, to track state through time +bool longPress[] = {0, 0, 0}; //long button press +unsigned int longPressCounter[] = {0, 0, 0}; //for counting how many readings each button has been held, to indicate a long button press +bool noteOnOffToggle[] = {0, 0, 0}; //if using a button to toggle a noteOn/noteOff command, keep track of state. +bool longPressUsed[] = {0, 0, 0}; //if we used a long button press, we set a flag so we don't use it again unless the button has been released first. +bool buttonUsed = 0; //flags any button activity, so we know to handle it. +bool specialPressUsed[] = {0, 0, 0}; +bool dronesOn = 0; //used to monitor drones on/off. + +//variables for communication with the WARBL Configuration Tool +bool communicationMode = 0; //whether we are currently communicating with the tool. +byte buttonReceiveMode = 100; //which row in the button configuration matrix for which we're currently receiving data. +byte pressureReceiveMode = 100; //which pressure variable we're currently receiving date for. From 1-12: Closed: offset, multiplier, jump, drop, jump time, drop time, Vented: offset, multiplier, jump, drop, jump time, drop time +byte counter = 0; // We use this to know when to send a new pressure reading to the configuration tool. We increment it every time we send a pitchBend message, to use as a simple timer wihout needing to set another actual timer. +byte fingeringReceiveMode = 0; // indicates the mode (instrument) for which a fingering pattern is going to be sent + +void setup() +{ + + DIDR0 = 0xff; // disable digital input circuits for analog pins + DIDR2 = 0xf3; + + pinMode2(ledPin, OUTPUT); // Initialize the LED pin as an output (using the fast DIO2 library). + pinMode2(17, INPUT_PULLUP); //this pin is used to detect when the bell sensor is plugged in (high when plugged in). + + for (byte i = 0; i < 9; i++) { //Initialize the tonehole sensor IR LEDs. + pinMode2f(pins[i], OUTPUT); + } + + pinMode2f(DP15, INPUT_PULLUP); //set buttons as inputs and enable internal pullup + pinMode2f(DP16, INPUT_PULLUP); + pinMode2f(DP14, INPUT_PULLUP); + + //EEPROM.update(44,255); //can be uncommented to force factory settings to be resaved for debugging (after making changes to factory settings). Needs to be recommented again after. + + + if (EEPROM.read(44) != 3 || EEPROM.read(1011) != VERSION) { + EEPROM.update(1011, VERSION); //update the stored software version + saveFactorySettings(); //If we're running the software for the first time, if a factory reset has been requested, or if the software version has changed, copy all settings to EEPROM. + } + + if (EEPROM.read(37) == 3) { + loadCalibration(); //If there has been a calibration saved, reload it at startup. + } + + loadFingering(); + loadSettingsForAllModes(); + mode = defaultMode; //set the startup instrument + + analogRead(A4); // the first analog readings are sometimes nonsense, so we read a few times and throw them away. + analogRead(A4); + sensorCalibration = analogRead(A4); + sensorValue = sensorCalibration; //an initial reading to "seed" subsequent pressure readings + + loadPrefs(); //load the correct user settings based on current instrument. + + //prepare sensors + Timer1.initialize(100); //this timer is only used to add some additional time after reading all sensors, for power savings. + Timer1.attachInterrupt(timerDelay); + Timer1.stop(); //stop the timer because we don't need it until we've read all the sensors once. + ADC_init(); //initialize the ADC and start conversions + + +} + + + + +void loop() +{ + + //cycles ++; //for testing + + receiveMIDI(); + + if ((millis() - buttonReadTimer) >= 5) { //read the state of the control buttons every so often + checkButtons(); + buttonReadTimer = millis(); + } + + if (buttonUsed) { + handleButtons(); //if a button had been used, process the command. We only do this when we need to, so we're not wasting time. + } + + if (blinkNumber > 0) { + blink(); //blink the LED if necessary (indicating control changes, etc.) + } + + if (calibration > 0) { + calibrate(); //calibrate/continue calibrating if the command has been received. + } + + noInterrupts(); + for (byte i = 0; i < 9; i++) { + toneholeRead[i] = tempToneholeRead[i]; //transfer sensor readings to a variable that won't get modified in the ISR + } + interrupts(); + + + for (byte i = 0; i < 9; i++) { + if (calibration == 0) { //if we're not calibrating, compensate for baseline sensor offset (the stored sensor reading with the hole completely uncovered) + toneholeRead[i] = toneholeRead[i] - toneholeBaseline[i]; + } + if (toneholeRead[i] < 0) { //in rare cases the adjusted readings can end up being negative. + toneholeRead[i] = 0; + } + } + + get_fingers(); //find which holes are covered + + + if (prevHoleCovered != holeCovered) { + fingersChanged = 1; + + tempNewNote = get_note(holeCovered); //get the next MIDI note from the fingering pattern if it has changed + send_fingers(); //send new fingering pattern to the Configuration Tool + prevHoleCovered = holeCovered; + if (pitchBendMode == kPitchBendSlideVibrato || pitchBendMode == kPitchBendLegatoSlideVibrato) { + findStepsDown(); + } + + + if (tempNewNote != -1 && newNote != tempNewNote) { //If a new note has been triggered + if (pitchBendMode != kPitchBendNone) { + holeLatched = holeCovered; //remember the pattern that triggered it (it will be used later for vibrato) + for (byte i = 0; i < 9; i++) { + iPitchBend[i] = 0; //and reset pitchbend + pitchBendOn[i] = 0; + } + } + newNote = tempNewNote; //update the next note if the fingering pattern is valid + } + + // CHANGES: I moved the line that was here before inside previous if block, otherwise it does not seem to be actually checked for validity + // Plus, it has no use to update newNote if it is equal to tempNewNote. + + // Handle Drone notes in Boha mode + if (modeSelector[mode] == kModeBoha) { + // Get the drone note + tempNewNote = get_noteDrone(holeCovered); + if (tempNewNote != -1 && newDroneNote != tempNewNote) { + newDroneNote = tempNewNote; + } + } + } + + + if (sensorDataReady) { + get_state();//get the breath state from the pressure sensor if there's been a reading. + } + + unsigned long nowtime = millis(); + + if (switches[mode][SEND_VELOCITY]) { //if we're sending NoteOn velocity based on pressure + if (prevState == 1 && newState != 1) { + velocityDelayTimer = nowtime; //reset the delay timer used for calculating velocity when a note is turned on after silence. + } + prevState = newState; + } + + get_shift(); //shift the next note up or down based on register, key, and characteristics of the current fingering pattern. + + if ((nowtime - pressureTimer) >= ((nowtime - noteOnTimestamp) < 20 ? 2 : 5)) { + pressureTimer = nowtime; + if (abs(prevSensorValue - sensorValue) > 1) { //if pressure has changed more than a little, send it. + if (ED[mode][SEND_PRESSURE]) { + calculatePressure(0); + } + if (switches[mode][SEND_VELOCITY]) { + calculatePressure(1); + } + if (switches[mode][SEND_AFTERTOUCH] & 1) { + calculatePressure(2); + } + if (switches[mode][SEND_AFTERTOUCH] & 2) { + calculatePressure(3); + } + + sendPressure(false); + + if (communicationMode) { + sendUSBMIDI(CC, 7, 116, sensorValue & 0x7F); //send LSB of current pressure to configuration tool + sendUSBMIDI(CC, 7, 118, sensorValue >> 7); //send MSB of current pressure + } + prevSensorValue = sensorValue; + } + + } + + if ((nowtime - pitchBendTimer) >= 9) { //check pitchbend and send pressure data every so often + pitchBendTimer = nowtime; + + calculateAndSendPitchbend(); + + + counter++; + + if (counter == 10) { //we check every 10 ticks to see if the bell sensor has been plugged/unplugged. + + counter = 0; + bellSensor = (digitalRead2(17)); //check if the bell sensor is plugged in + if (prevBellSensor != bellSensor) { + prevBellSensor = bellSensor; + if (communicationMode) { + sendUSBMIDI(CC, 7, 102, 120 + bellSensor); //if it's changed, tell the configuration tool. + } + } + + + //Serial.println(newState); + //Serial.println(ED[mode][VELOCITY_INPUT_PRESSURE_MIN]); + //Serial.println(outputBounds[0][0]); + //Serial.println(inputPressureBounds[0][3]); + //FREERAM_PRINT + + + + } + } + + sendNote(); //send the MIDI note + + +}