/** * AI_SI Midi Piano * Sparkfun Arduino Pro Micro 16MHz 5V * version 2 * usb and DIN midi out * very large latency and slow on MIDI OUT when used with usb midi * still freezes after some time - when used with usb midi * solution: do not use both at the same time: switch between DIN and usb * * touchpad Synaptics using T1004 capacitive sensing ASIC designed by Synaptics two modes: mouse-compatible relative mode and absolute mode with pressure sensing http://sparktronics.blogspot.com/2008/05/synaptics-t1004-based-touchpad-to-ps2.html Majority of touchpads out there operate with PS/2 standard. A PS2 data bus has 4 pins: - Data - Clock - Ground - Vcc (+5V) // to generate midinote using some random and statistics // save last notes in a sequence and decide based on that // some tonality options // melodic line is the key moment here // stop note = note off - left button // chord option - right button // polyphonic would be when one note playing - being met with another note - so that both play // - for this sending to a second midi channel would be needed - and alternating channels // additional button: SI (standard intelligence) or AI (advanced intelligence) // some leds: green (ON), yellow (mode), red (note out activity) // etc **/ const bool test = false; //false true; // true to send usb midi - this can be changed witzh a midipin switch bool usbmidi = false; //false true; // true to send additional classic midi on hw Serial1 bool midibaudrate = true; //true uint8_t midiCH = 1; // midi channel for notes uint8_t midiCC = 0; // midi channel for CC // maybe have a pot to set this // finger area check - affects scale slide unsigned int sensibility = 500; // 50 minimum, 200 optimum //pins #define PS2_CLK 6 // touchpad ps/2 clock pin = 7 #define PS2_DATA 7 // touchpad ps/2 data pin = 6 #define LEDPIN_ORANGE 10 // orange to show AI selected #define LEDPIN_RED 14 // red - for midi out traffic #define LEDPIN_GREEN 16 // green for device powered and status ok #define MODEPIN 15 // switch live between SI and AI #define MIDIPIN 18 // A0 // switch live between DIN and USB midi out // how many notes to remember // fifo or some global counter/pointer - overwriting the current value in arrays const unsigned int numnotes = 127; // globals // global to hold touchpad z,x,y sensor data unsigned int data[3]; // global to hold touchpad previous data unsigned int prevdata[3]; // global to hold touchpad rendered midi data byte midinote[3]; // holds note, velocity and timing byte prevmidinote[3]; // holds note, velocity and timing // timing should measure how long the finger is held on touchpad! // to basic note we can attach additional notes - to make a chord byte midichord[2]; // holds a number of added note values byte prevmidichord[2]; // holds a number of added note values // remember touch as firsttouch true when starting tone // and put to false of finger pressure drops below threshold bool firsttouch = false; bool lasttouch = false; bool noteoff = true; // always use it on next nete triggered - force it sooner with left button bool leftbutt = false; bool rightbutt = false; bool chord = false; // make a chord or not bool slide = false; // sense that we have a slide movement unsigned long notetime = millis(); byte preveventtime = notetime/1000; // remember as relative // variable to hold and dim the midi led pwm unsigned int dimled = 0; // we always multiply it by 2 byte tonality=0; // index of below // tonal modes - scales - affects melodics, harmonics // C=1, Csharp=Dflat=2, D=3, Dsharp=Eflat=4, E=5, F=6, Fsharp=Gflat=7, G=8, Gsharp=Aflat=9, A=10, Asharp=Bflat=11, B(H)=12 // C-Major: C, D, E, F, G, A, B(H) // C-minor: C, D, Eflat, F, G, Aflat, B(H)flat // Gipsy/Hungarian/Ciprian scale: C, D, Eflat, Fsharp, G, Aflat, B(H) // Arabic/Byzantine scale: C, Dflat, E, F, G, Aflat, B // Frygian scale : C, Dflat, E, F, G, Aflat, Bflat // Blues scale: C, D, Eflat, F, Gflat, A, Bflat // Dorian scale: C, D, Eflat, F, G, A, Bflat // check against value > 0 //const prog_uint8_t melodics[6][12] PROGMEM = { //const prog_uint8_t melodics[6][12] PROGMEM = { const byte melodics[6][12] = { { 1, 0, 3, 0, 5, 6, 0, 8, 0, 10, 0, 12}, // C-major { 1, 0, 3, 4, 0, 6, 0, 8, 9, 0, 11, 0}, // C-minor { 1, 0, 3, 4, 0, 0, 7, 8, 9, 0, 0, 12}, // Ciprian/ Gipsy minor { 1, 2, 0, 0, 5, 6, 0, 8, 9, 0, 0, 12}, // Arabic/Byzantine { 1, 0, 3, 4, 0, 6, 7, 0, 0, 10, 11, 0}, // Blues { 1, 0, 3, 4, 0, 6, 0, 8, 0, 10, 11, 0} // Dorian }; // when registers full - start from zero or pop the first and add the last // or change keys for all elements? Too much work? // or remember current index and use this to find what is in the past: // smaller indexes = near past <- current index -> higher indexes far past // register to remember sequence of events: //maybe duo note + velocity //maybe trio note + velocity + timestamp // or separate lists: uint8_t register_notes[numnotes]; uint8_t register_volumes[numnotes]; uint8_t register_times[numnotes]; // 1 here means 0.1 second, 127 means 12 seconds // counter of events - fills indexes up to numnotes - then resets unsigned int events=0; // compare it to available melodic lines list and select the next notes from // the most likely (or least likely) decision for next // have some statistics regarding up down - combine this decision with the melodic note selection // based on timestamps history detect the need for small or large tonal jumps // remember last statistics on power down byte prefstatistics[3]={80,10,10}; // preferred percentage of time per each movement type byte statistics[3]={80,10,10}; // measured percentage of time per each movement type byte preferred = 0; // preferred next decision // this keeps symnaptics and midi under control // seems that midiusb crashes #define INTERVAL_LENGTH_US 75000UL // microsecs = 50ms, 75ms or 100ms unsigned long previousMicros; // self-adjustment of midinote boundaries unsigned int maxx,minx,maxy,miny,maxz,minz; // this is for synaptics #include PS2 touchpad(PS2_CLK, PS2_DATA); /** * sequence to set the absolute touchpad sensor mode: * if a Set Sample Rate 20 ($F3, $14) command is preceded by four Set Resolution * commands encoding an 8-bit argument, the 8-bit argument is stored as the new value for * the TouchPad mode byte * to set the mode byte to 0xC1 (absolute mode, high packet rate, Wmode enabled) use sequence: * 0xf0 0xe8 0x03 0xe8 0x00 0xe8 0x00 0xe8 0x01 0xF3 0x14 * we use (absolute mode, high packet rate, Wmode enabled) sequence: * 0xf0 0xe8 0x03 0xE8 0x00 0xE8 0x01 0xE8 0x00 0xF3 0x14 * it is important to ensure that the device is disabled (0xF5) before sending this command sequence * to receive Absolute mode packets, follow this sequence with an Enable (0xF4) command. * * Data packets are sent in response to Read Data (0xEB) commands. If Stream mode is * selected and data reporting is enabled, data packets are also sent unsolicited whenever * finger motion and/or button state changes occur. Synaptics recommends using Stream * mode instead of Read Data commands to obtain data packets **/ void touchpad_init(){ touchpad.write(0xff); // reset touchpad.read(); // ack byte touchpad.read(); // blank */ touchpad.read(); // blank */ //sequence start touchpad.write(0xf0); // remote mode touchpad.read(); // ack delayMicroseconds(100); touchpad.write(0xe8); // set resolution touchpad.read(); // ack byte touchpad.write(0x03); // x1 ( x1 * 64 + x2 * 16 + x3 * 4 + x4 == modebyte ) touchpad.read(); // ack byte touchpad.write(0xe8); // set resolution touchpad.read(); // ack byte touchpad.write(0x00); // x2 touchpad.read(); // ack byte touchpad.write(0xe8); // set resolution touchpad.read(); // ack byte touchpad.write(0x01); // x3 touchpad.read(); // ack byte touchpad.write(0xe8); // set resolution touchpad.read(); // ack byte touchpad.write(0x00); // x4 touchpad.read(); // ack byte touchpad.write(0xf3); // set samplerate 20 (this stores the mode) touchpad.read(); // ack byte touchpad.write(0x14); touchpad.read(); // ack byte //sequence stop delayMicroseconds(100); } // midi stuff #include void noteOn(uint8_t channel, uint8_t pitch, uint8_t velocity) { midiEventPacket_t noteOn = {0x09, 0x90 | channel, pitch, velocity}; if(usbmidi) MidiUSB.sendMIDI(noteOn); } void noteOff(uint8_t channel, uint8_t pitch) { midiEventPacket_t noteOff = {0x08, 0x80 | channel, pitch, 0}; if(usbmidi) MidiUSB.sendMIDI(noteOff); } void controlChange(uint8_t channel, uint8_t control, uint8_t value) { midiEventPacket_t event = {0x0B, 0xB0 | channel, control, value}; if(usbmidi) MidiUSB.sendMIDI(event); } // change the globals void checkMidiOut(){ // this would need to be a slide switch if(digitalRead(MIDIPIN)==0) { // discard USB - use DIN bool usbmidi = false; //false true; bool midibaudrate = true; //true } else { // normal discard DIN - use USB bool usbmidi = true; bool midibaudrate = false; } } void setup(){ randomSeed(analogRead(A3)); pinMode(LEDPIN_GREEN, OUTPUT); // power up - and status ok digitalWrite(LEDPIN_GREEN, LOW); pinMode(MODEPIN, INPUT_PULLUP); // switch live between SI and AI pinMode(MIDIPIN, INPUT_PULLUP); // switch live between DIN and USB MIDI OUT pinMode(LEDPIN_ORANGE, OUTPUT); // show AI selected digitalWrite(LEDPIN_ORANGE, LOW); pinMode(LEDPIN_RED, OUTPUT); // midi traffic digitalWrite(LEDPIN_RED, LOW); // here check the MIDIPIN level - do it via function for loop() also // changes usbmidi and midibaudrate globals checkMidiOut(); if(midibaudrate){ // if we use the Tx pin with MIDI OUT we use MIDI baud rate Serial1.begin(31250); // Set MIDI baud rate } //MIDIUSB library needs activation to later function normally!!! // should not block Serial1 - if midiusb not connected byte index = 0; for(byte i=0;i<5;i++){ index = i+50; noteOn(midiCH, index, 100); //noteOff(midiCH, index); //controlChange(midiCC, i, 100); // Flush the output. if(usbmidi) MidiUSB.flush(); delay(500); } midinote[0]=index; midinote[1]=100; if(test){ // internal usb serial port is Serial Serial.begin(115200); // wait for serial port to connect. Needed for native USB serial port only while (!Serial) { ; } Serial.println("Usb serial connected"); } // ps2 lib version touchpad_init(); if (test) { Serial.println("Touchpad initialization OK "); } maxx = 0; minx = 32000; maxy = 0; miny = 32000; maxz = 0; minz = 32000; //setup the defined register_data size for(uint8_t i=0; i= INTERVAL_LENGTH_US){ previousMicros += INTERVAL_LENGTH_US; // ps2 library is ok when reading of data done in the loop! touchpad.write(0xeb); // demand data // Absolute packet format - each motion report consists of six bytes touchpad.read(); // byte 1 is ACK byte - ignore // above maybe holds bits (1, 0, Finger, Reserved, 0, Gesture, Right, Left) // to read a button just check the bit value // read next byte - this one also holds the button down byte mstat1 = touchpad.read(); byte mxy = touchpad.read(); byte cz = touchpad.read(); byte mstat2 = touchpad.read(); byte mx = touchpad.read(); byte my = touchpad.read(); delayMicroseconds(100); // collect the bits for x and y unsigned int cx = (((mstat2 & 0x10) << 8) | ((mxy & 0x0F) << 8) | mx ); unsigned int cy = (((mstat2 & 0x20) << 7) | ((mxy & 0xF0) << 4) | my ); // below are just bits 0 or 1 - if trackpad buttons pushed bool left = mstat1 & 0x01; bool right = (mstat1 & 0x02) >> 1; // find some option for multitouch detection - at least for two fingers // check if xy position changed in time od same touch // for single tone if(cz<10){ //it drops below threshold - finger lifted firsttouch = false; lasttouch = true; slide = false; // reset } // if left button clicked - trigger noteoff for current note // force it off in any case - even if note not triggered if(left>0) leftbutt = true; else leftbutt = false; // if right button clicked // can be some more complex function: either a chord or tonal change if(right>0) rightbutt = true; else rightbutt = false; // if both - change tonality explicitly if(rightbutt && leftbutt){ //tonality = random(sizeof(melodics)); // maybe some other way chord = true; } else if(rightbutt){ // do a chord - only if button held down! chord = true; } else { chord = false; // reset to no chord } if(leftbutt && noteoff){ // here we get multiple noteoffs written noteoff=false; // remember that we used it already leftbutt = false; if(midinote[1]>0){ noteOff(midiCH, midinote[0]); // if midichord not empty if(midichord[0]>0) noteOff(midiCH, midichord[0]); if(midichord[1]>0) noteOff(midiCH, midichord[1]); // Flush the output collected above if(usbmidi) MidiUSB.flush(); } // also on hw serial if(midibaudrate && midinote[1]>0){ noteOffSend(0x80, (char)midinote[0]); if(midichord[0]>0) noteOffSend(0x80, (char)midichord[0]); if(midichord[1]>0) noteOffSend(0x80, (char)midichord[1]); } //reset midichord[0]=0; midichord[1]=0; chord = false; // reset to no chord } // a bit less sensitivity to pressure here // check if xy distance big enough to mean additional finger added // or finger sliding over touchpad //if(cz>20 && cz<100 && millis()-notetime>250 && millis()-notetime<1000){ // here we get double same notes - not good if(cz>20 && !chord && millis()-notetime>50){ // prevdata[0] x: low value is around 1400, high value around 5000 // prevdata[1] y: low value is around 1200, high value around 4000 // prevdata[2] z // this works ok when going up the scale but not downwords //prevdata should be referential // maybe have som option to set the sensibility if(abs(prevdata[2]-cz)>sensibility && (abs(prevdata[0]-cx)>sensibility || abs(prevdata[1]-cy)>sensibility) ){ firsttouch=false; // means that we trigger a new touch lasttouch = true; slide = true; // remember not to repeat each tone } } // a touch was registered // either new or a finger is still there // i should register the first touch: // if cz did not fall below the limit // with this number change the sensitivity to touches // 128,0,0 and 128,4069,4069 is invalid! if (cz > 10 && cz < 128 && !firsttouch) { // new timestamp notetime = millis(); // midi activity led - pwm: pinned to velocity digitalWrite(LEDPIN_RED, HIGH); if(test){ Serial.print(cz); //z = pressure Serial.print(" "); Serial.print(cx); //x Serial.print(" "); Serial.print(cy); //y Serial.print(" "); Serial.print(left); //left Serial.print(" "); Serial.print(right); //right Serial.println(" "); } // remember what has been registered as active to check for changed finger position prevdata[0] = cx; //x = horizontal position prevdata[1] = cy; //y = vertical position prevdata[2] = cz; //z = pressure // autocalibrate if (cx < minx) { minx = cx; } if (cx > maxx) { maxx = cx; } if (cy < miny) { miny = cy; } if (cy > maxy) { maxy = cy; } cz += 45; if (cz > 127) cz = 127; // determine range data[0] = map(cx,minx,maxx,24,96); //x data[1] = map(cy,miny,maxy,24,96); //y //data[2] = map(cz,minz,maxz,0,127); //z=pressure /** data[0] = map(cx,0,127,24,96); //x data[1] = map(cy,0,127,24,96); //y **/ data[2] = cz; //z=pressure // send midinote off // for multiple channels polyphony this will be different if(midinote[1]>0){ noteOff(midiCH, midinote[0]); // if midichord not empty if(midichord[0]>0){ noteOff(midiCH, midichord[0]); } if(midichord[1]>0){ noteOff(midiCH, midichord[1]); } } // remember before changed prevmidinote[0] = midinote[0]; // note prevmidinote[1] = midinote[1]; // velocity prevmidinote[2] = midinote[2]; // if needed // this is mostly for Serial1 prevmidichord[0]=midichord[0]; prevmidichord[1]=midichord[1]; midichord[0]=0; // reset midichord[1]=0; // reset // do something on the basis of data above and create a midinote generateMidiNote(); // check if slide - do not have double note if(slide && midinote[0]==prevmidinote[0]){ slide = false; return; // do not continue - go a new round } // send midinote - this collects note off above and note on below! //MIDIUSB library note on if(chord){ // in case of chord make velocity a bit higher! if(midinote[1]<75) midinote[1]=75; // make a sequence of notes from midinote - midichord variable // new midichord midichord[0]=midinote[0]+3; midichord[1]=midinote[0]+5; // use just once per touch -basic note noteOn(midiCH, midinote[0], midinote[1]); // chord notes noteOn(midiCH, midichord[0], midinote[1]); noteOn(midiCH, midichord[1], midinote[1]); } else { //controlChange(midiCC, midinote[0], midinote[1]); noteOn(midiCH, midinote[0], midinote[1]); } // Flush the combined midiusb output if(usbmidi) MidiUSB.flush(); // duplicate on hw MIDIOUT if(midibaudrate){ // send raw data //0xB0, note[i][bank], 0x60 - controllers // send midinote off noteOffSend(0x80, (char)prevmidinote[0]); // check the prev chord if(prevmidichord[0]>0) noteOffSend(0x80, (char)prevmidichord[0]); if(prevmidichord[1]>0) noteOffSend(0x80, (char)prevmidichord[1]); // reset old prevmidichord[0]=0; prevmidichord[1]=0; if(chord){ // in case of chord make velocity a bit higher! if(midinote[1]<75) midinote[1]=75; //send note on noteOnSend(0x90, (char)midinote[0], (char)midinote[1]); // add chord notes - same velocity as basic note if(midichord[0]>0) noteOnSend(0x90, (char)midichord[0], (char)midinote[1]); if(midichord[1]>0) noteOnSend(0x90, (char)midichord[1], (char)midinote[1]); } else { //send note on noteOnSend(0x90, (char)midinote[0], (char)midinote[1]); } } if(chord){ chord = false; // reset } // register note event // counter of events - fills indexes up to numnotes - then resets byte prevtime = preveventtime; if(events==0){ // in case this is first event prevtime = 0; // set this as reference } else if(events>numnotes){ // in case the buffer is full - turn over events=0; // reset index/counter prevtime = register_times[numnotes]; } // this should be relative time = from the previous event register_times[events]=(byte)(millis()/1000 - prevtime); // 1 here means 0.1 second, 127 means 12 seconds // remember current time as global for the next time around preveventtime = notetime/1000; // remember as relative register_notes[events]=midinote[0]; register_volumes[events]=midinote[1]; // now update index by 1 events++; // tell us that we had first touch already firsttouch = true; noteoff = true; // reset and be prepared to catch noteoff } chord = false; // reset to no chord slide = false; // reset // seems usbmidi goes to sleep after some time // send noteoff every ten secs if(millis()-notetime>10000){ notetime = millis(); noteOff(midiCH, midinote[0]); // Flush the output if(usbmidi) MidiUSB.flush(); } } if(millis()-notetime>100){ digitalWrite(LEDPIN_RED, LOW); } } void generateMidiNote(){ // we have data and prevdata globals // we should generate here midinote trio: note, velocity, time // if we hold finger down - do not trigger another note byte newnote = (data[0]+data[1])/2; // note: now here x y combined // adapt to selected tonality // newnote = checkScale(newnote); midinote[0] = (byte)newnote; // note midinote[1] = (byte)data[2]; // velocity = now here pressure (z) midinote[2] = 64; // measure of timestamp = times 100 // we would want to use multiple alternating channels for polyphonics } // adapt selected note to melodic used - not yet ok byte checkScale(byte note){ // tonality var defines the scale used - index of melodics[6][12] // change it as global and use here // scales defined in melodics[6][12] // first we have to change the note to relative position in basic octave // eg: whatever C to C0 // A-1=9, A0=21, A1=33, A2=45, A3=57, A4=69, A5=81, A6=93, A7=105, A8=117 // C-1=0, C0=12, C1=24, C2=36, C3=48, C4=60, C5=72, C6=84, C7=96, C8=108 byte key=0; int refnote = note; if(note>=108) { refnote=note-108; key=9; } else if(note>=96) { refnote=note-96; key=8; } else if(note>=84) { refnote=note-84; key=7; } else if(note>=72) { refnote=note-72; key=6; } else if(note>=60) { refnote=note-60; key=5; } else if(note>=48) { refnote=note-48; key=4; } else if(note>=36) { refnote=note-36; key=3; } else if(note>=24) { refnote=note-24; key=2; } else if(note>=12) { refnote=note-12; key=1; } // else it is ok // default byte scale[12] = {1,2,3,4,5,6,7,8,9,10,11,12}; // adapt to get the scale with chosen tonality index for(byte a=0; a<12; a++){ scale[a] = melodics[tonality][a]; } // now check for our note in scale - zero means to check next or previous note // eg: fifth note corresponds to fifth scale array element // if zero - check the next and previous // refnote is 1 to 12 - position of note is 0 for 1 and 11 for 12 // but could be that we go above or below current octave byte notepos=refnote-1; if(scale[notepos]!=0){ refnote=scale[notepos]; } else { // check around our refnote for a valid candidate within 12 tone octave // we should never be in the most top or most low octave if(scale[notepos+1]!=0) refnote=scale[notepos+1]; // one up else if(scale[notepos-1]!=0) refnote=scale[notepos-1]; // one down else if(scale[notepos+2]!=0) refnote=scale[notepos+2]; // two up else if(scale[notepos-2]!=0) refnote=scale[notepos-2]; // two down /** if(refnote+1<12 && scale[notepos+1]!=0) refnote=scale[notepos+1]; // one up else if(refnote-1>0 && scale[notepos-1]!=0) refnote=scale[notepos-1]; // one down else if(refnote+2<=12 && scale[notepos+2]!=0) refnote=scale[notepos+2]; // two up else if(refnote-2>0 && scale[notepos-2]!=0) refnote=scale[notepos-2]; // two down // three halftones away //else if(refnote+3<=12 && scale[notepos+3]!=0) refnote=scale[notepos+3]; //else if(refnote-3>0 && scale[notepos-3]!=0) refnote=scale[notepos-3]; **/ } // put back to absolute position // here we get tones that are over 127 limit! note = refnote + 12*key; return note; }