ARKeytar - Arduino Based MIDI Controller Keytar
ARKeytar is an expressive Arduino based MIDI controller shaped as a maple wood keytar, with two softpots, a MIDI keyboard and CC controls.
Components and supplies
Arduino Nano R3
Potentiometer, Slide
Rotary potentiometer (generic)
Shift Register- Parallel to Serial
SoftPot Membrane Potentiometer - 500mm
Tools and machines
Soldering iron (generic)
Apps and platforms
Arduino IDE
Project description
Code
ARKeytar v5.21
arduino
ARKeytar code version v5.21. Date: 2022-05-01
1/* 2ARKeytar v5.21 32022-05-01, Andrea Gregorini 4*/ 5 6 7#include <MIDI.h> 8MIDI_CREATE_DEFAULT_INSTANCE(); 9 10#include <ShiftIn.h> 11ShiftIn<2> shift; 12 13 14 15 16 17/* panicButton */ 18const int panicButton = 2; 19int panicButtonRead = 0; 20int pressedPanicButton = 0; 21 22 23/* ledPins */ 24const int ledVerde = 3; // Green 25const int ledRosso = 4; // Red 26int ledOnCount = 0; 27 28/* shift register */ 29int num; 30int incoming; 31int midiChannel; 32int ccPot[2] = {0, 1}; 33int ccPotOld[2] = {0,0}; 34const int listaControlChange[11] = {7, 74, 71, 11, 1, 5, 73, 91, 93, 72, 10}; 35 36/* octave up and octave down */ 37const int up = 6; 38const int down = 5; 39 40int transp[2] = {0, 0}; 41int transpSlide[2] = {0, 0}; 42int pressedUp = 0; int pressedDown = 0; 43int pressedSlideUp = 0; int pressedSlideDown = 0; 44int transpUp; int transpDown; 45int transpSlideUp; int transpSlideDown; 46 47 48 49 50/* rotPot and slidePot */ 51int cicliMidiCC[2] = {6, 6}; 52int jj[2] = {0, 0}; 53 54/* CC messages variables */ 55int ccValue[2]; 56int ccRead[2]; 57int ccValue_Old[2] = {0, 0}; 58 59 60/* Note range and tuning */ 61const int threshold = 998; // read values discarded above threshold 62 63int lowestNote; // first soft pot 64int keyRange; // number of keys on a single softpot 65 66int pitchRange; 67int tuning[2]; // semitone shift between soft pots 68int minNote[2]; 69int maxNote[2]; 70int octaveShift; 71int semitoneShift; 72 73 74/* pitch bending */ 75int analogPitchRange; 76float intervallo; 77float pitchRangeFloat; 78int pitchNota[2]; 79int pitchSent[2] = {-1,-1}; 80int pitchNotaOld[2]; 81int pitchPriority = 0; 82 83/* Pitch stabilization */ 84 85int pitchTolerance; 86float DEFAULT_pitchSnap; 87float pitchSnap; 88int snapOn[2] = {0,0}; 89float calcFloat[2]; 90float calcInt[2]; 91float decPart[2]; 92 93 94/* A3 = softpot1, A4 = softpot2, A1 = slidepot, A0 = rotpot */ 95const int analogPins[4] = {A3, A4, A1, A0}; 96float lettura[2]; 97int letturaInt[2]; 98float lettura_Sent[2]; 99 100float midiNoteFloat[2]; 101int midiNote[2]; 102int midiNote_Sent[2]; 103int midiNote_Old[2]; 104int sensorPressed[2] = {0, 0}; 105 106int count[2] = {0, 0}; 107 108int timePressed[2]; 109 110 111/* Pitch values to be applied when notes are snapped */ 112const int range12[25] = 113{0, 683, 1365, 2048, 2731, 3413, 4096, 4778, 1145461, 6144, 6826, 7509, 8192, 8874, 9557, 11510239, 10922, 11605, 12287, 12970, 13653, 11614335, 15018, 15700, 16383}; 117 118const int range24[49] = 119{0, 341, 683, 1024, 1365, 1707, 2048, 2389, 1202731, 3072, 3413, 3754, 4096, 4437, 4778, 1215120, 5461, 5802, 6144, 6485, 6826, 7168, 1227509, 7850, 8192, 8533, 8874, 9215, 9557, 1239898, 10239, 10581, 10922, 11263, 11605, 12411946, 12287, 12629, 12970, 13311, 13653, 12513994, 14335, 14676, 15018, 15359, 15700, 12616042, 16383}; 127 128 129 130 131 132 133 134 135 136 137 138 139 140void setup() { 141 142 MIDI.begin(MIDI_CHANNEL_OMNI); 143 Serial.begin(31250); 144 145 146 /* Buttons */ 147 pinMode(panicButton, INPUT); 148 pinMode(up, INPUT); 149 pinMode(down, INPUT); 150 151 152 pinMode(ledVerde, OUTPUT); 153 /* HIGH = led off. LOW = led on. This is due to the led type used */ 154 digitalWrite(ledVerde, HIGH); 155 pinMode(ledRosso, OUTPUT); 156 digitalWrite(ledRosso, HIGH); 157 158 159 160 /* Analog ports initialization */ 161 for (int ii = 0; ii <= 1; ii++) { 162 pinMode(analogPins[ii], INPUT_PULLUP); 163 } 164 for (int ii = 2; ii <= 3; ii++) { 165 pinMode(analogPins[ii], INPUT); 166 } 167 168 /* Read shift registers */ 169 shift.begin(8, 9, 11, 12); 170 if(shift.update()) // read in all values. returns true if any button has changed 171 incoming = readShiftReg(); 172 saveShiftRegRead(); // Assign incoming values to variables 173 174 175 /* Softpot settings */ 176 lowestNote = 60; 177 keyRange = 32; 178 179 tuning[0] = 0; 180 tuning[1] = 5; 181 182 octaveShift = 0; 183 octaveShift = octaveShift*12; 184 semitoneShift = 0; 185 186 /* Apply transopose if read by shift registers or defined */ 187 for (int ii = 0; ii <= 1; ii++) { 188 minNote[ii] = lowestNote + tuning[ii] + semitoneShift + octaveShift; 189 maxNote[ii] = minNote[ii] + keyRange; 190 } 191 192 193 if (bitRead(incoming,14)== 1) { 194 pitchRange = 24; //(+-semitones) 195 } else { 196 pitchRange = 12; //(+-semitones) 197 } 198 199 pitchTolerance = (pitchRange-12)/12 * (50-100) + 100; 200 DEFAULT_pitchSnap = 0.2; 201 pitchSnap = DEFAULT_pitchSnap; 202 intervallo = 16383 / (2*pitchRange); 203 pitchRangeFloat = float(pitchRange); 204 205 206 analogPitchRange = 1023/(keyRange)*pitchRange; 207 208 209 /* Nothing pressed at the beginning */ 210 sensorPressed[0] = 0; 211 sensorPressed[1] = 0; 212 midiNote_Old[0] = 0; 213 midiNote_Old[1] = 0; 214 215 216} 217 218 219 220 221void loop() { 222 223 panic(); 224 readPots(); 225 transpose(); 226 noPressure(); 227 newPressure(); 228 firstRelease(); 229 prolongedRelease(); 230 assignPitchPriority(); 231 prolongedPressure(); 232 potsReadSendCC(); 233 234} 235 236 237 238 239 240int readShiftReg() { 241 for(int i = 0; i < shift.getDataWidth(); i++){ 242 bitWrite(num, i, shift.state(i)); 243 } 244 245 int incomingNumber = 0; 246 for (int bitNumber = 8; bitNumber <= 15; bitNumber++) { 247 bitWrite(incoming, incomingNumber, bitRead(num,bitNumber)); 248 incomingNumber++; 249 } 250 for (int bitNumber = 0; bitNumber <= 7; bitNumber++) { 251 bitWrite(incoming, incomingNumber, bitRead(num,bitNumber)); 252 incomingNumber++; 253 } 254 return incoming; 255} 256 257void saveShiftRegRead() { 258 259 bitWrite(midiChannel, 0, bitRead(incoming,0)); 260 bitWrite(midiChannel, 1, bitRead(incoming,1)); 261 bitWrite(midiChannel, 2, bitRead(incoming,2)); 262 bitWrite(midiChannel, 3, bitRead(incoming,3)); 263 264 bitWrite(ccPot[0], 0, bitRead(incoming,4)); 265 bitWrite(ccPot[0], 1, bitRead(incoming,5)); 266 bitWrite(ccPot[0], 2, bitRead(incoming,6)); 267 bitWrite(ccPot[0], 3, bitRead(incoming,7)); 268 269 bitWrite(ccPot[1], 0, bitRead(incoming,8)); 270 bitWrite(ccPot[1], 1, bitRead(incoming,9)); 271 bitWrite(ccPot[1], 2, bitRead(incoming,10)); 272 bitWrite(ccPot[1], 3, bitRead(incoming,11)); 273 274 /* 275 bitRead(incoming,12) Not assigned, unused 276 bitRead(incoming,13) Not assigned, unused 277 bitRead(incoming,14) used to select pitch range 278 bitRead(incoming,15) used to select if transpose acts on both softpots or just second one. 279 */ 280 281} 282 283void noteOn(int pitch, int velocity) { 284 Serial.write(144 + midiChannel); // Note on command 285 Serial.write(pitch); // Note pitch 286 Serial.write(velocity); // Note velocity 0-127 287} 288 289void pitchBend(byte lsb, byte msb) { 290 Serial.write(224 + midiChannel); // Pitch bend command 291 Serial.write(lsb); // Pitch lsb 292 Serial.write(msb); // Pitch msb 293} 294 295void pitchBendMessage(int pitchNota) { 296 int shiftedValue = pitchNota << 1; 297 byte lsb = lowByte(shiftedValue) >> 1; // Pitch lsb 298 byte msb = highByte(shiftedValue); // Pitch msb 299 pitchBend(lsb, msb); 300} 301 302void midiCC(int CCnumber, int ccValue) { 303 Serial.write(176 + midiChannel); // Control change command 304 Serial.write(CCnumber); // CC number 305 Serial.write(ccValue); // Volume 0-127 306} 307 308void transposeButtonsAction() { 309 if (transpUp == HIGH && pressedUp == 0) { 310 pressedUp = 1; 311 312 if (bitRead(incoming, 15) == 0) { 313 transp[0] = transp[0] + 12; 314 transp[1] = transp[1] + 12; 315 } else if (bitRead(incoming, 15) == 1) { 316 transp[1] = transp[1] + 12; 317 } 318 } 319 320 if (transpDown == HIGH && pressedDown == 0) { 321 pressedDown = 1; 322 if (bitRead(incoming, 15) == 0) { 323 transp[0] = transp[0] - 12; 324 transp[1] = transp[1] - 12; 325 } else if (bitRead(incoming, 15) == 1) { 326 transp[1] = transp[1] - 12; 327 } 328 } 329 330 if (transpUp == LOW && pressedUp == 1) { 331 pressedUp = 0; 332 } 333 334 if (transpDown == LOW && pressedDown == 1) { 335 pressedDown = 0; 336 } 337} 338 339void transposeSlideAction() { 340 341 if (transpSlideUp == HIGH && pressedSlideUp == 0) { 342 pressedSlideUp = 1; 343 344 if (bitRead(incoming, 15) == 0) { 345 transpSlide[0] = transpSlide[0] + 12; 346 transpSlide[1] = transpSlide[1] + 12; 347 } else if (bitRead(incoming, 15) == 1) { 348 transpSlide[1] = transpSlide[1] + 12; 349 } 350 } 351 352 if (transpSlideDown == HIGH && pressedSlideDown == 0) { 353 pressedSlideDown = 1; 354 if (bitRead(incoming, 15) == 0) { 355 transpSlide[0] = transpSlide[0] - 12; 356 transpSlide[1] = transpSlide[1] - 12; 357 } else if (bitRead(incoming, 15) == 1) { 358 transpSlide[1] = transpSlide[1] - 12; 359 } 360 } 361 362 if (transpSlideUp == LOW && pressedSlideUp == 1) { 363 if (bitRead(incoming, 15) == 0) { 364 transpSlide[0] = transpSlide[0] - 12; 365 transpSlide[1] = transpSlide[1] - 12; 366 } else if (bitRead(incoming, 15) == 1) { 367 transpSlide[1] = transpSlide[1] - 12; 368 } 369 pressedSlideUp = 0; 370 } 371 372 if (transpSlideDown == LOW && pressedSlideDown == 1) { 373 if (bitRead(incoming, 15) == 0) { 374 transpSlide[0] = transpSlide[0] + 12; 375 transpSlide[1] = transpSlide[1] + 12; 376 } else if (bitRead(incoming, 15) == 1) { 377 transpSlide[1] = transpSlide[1] + 12; 378 } 379 pressedSlideDown = 0; 380 } 381} 382 383void ledCtrl(int ledNum) { 384 if (ledNum == 0) { 385 digitalWrite(ledRosso, LOW); 386 ledOnCount = 0; 387 } 388 if (ledNum == 1) { 389 digitalWrite(ledVerde, LOW); 390 ledOnCount = 0; 391 } 392 if (ledNum > 1 && ledOnCount < 3) { 393 ledOnCount = ledOnCount + 1; 394 } 395 if (ledNum > 1 && ledOnCount >= 3) { 396 digitalWrite(ledRosso, HIGH); 397 digitalWrite(ledVerde, HIGH); 398 ledOnCount = 0; 399 } 400} 401 402 403 404void panic() { 405 406 panicButtonRead = digitalRead(panicButton); 407 if (panicButtonRead == HIGH && pressedPanicButton == 0) { 408 409 for (int panicChannel = 1; panicChannel <= 16; panicChannel++) { 410 MIDI.sendControlChange(123,0,panicChannel); 411 } 412 pressedPanicButton = 1; 413 } else { 414 pressedPanicButton = 0; 415 } 416} 417 418void readPots() { 419 for (int ii = 0; ii <= 1; ii++) { 420 lettura[ii] = analogRead(analogPins[ii]); 421 lettura[ii] = analogRead(analogPins[ii]); 422 423 unsigned long timeStart = micros(); 424 while(micros() - timeStart < 5000){ 425 MIDI.read(); 426 } 427 428 letturaInt[ii] = int(lettura[ii]); 429 430 midiNoteFloat[ii] = (lettura[ii]-0)*(maxNote[ii]-minNote[ii])/(1023-0) + minNote[ii]; 431 midiNote[ii] = int(midiNoteFloat[ii]) + transp[ii] + transpSlide[ii]; 432 } 433 434 MIDI.read(); 435} 436 437void transpose() { 438 transpUp = digitalRead(up); 439 transpDown = digitalRead(down); 440 441 shift.begin(8, 9, 11, 12); 442 if(shift.update()) 443 incoming = readShiftReg(); 444 445 transposeButtonsAction(); 446 MIDI.read(); 447} 448 449void noPressure() { 450 for (int ii = 0; ii <= 1; ii++) { 451 452 if (letturaInt[ii] >= threshold ) { 453 ledCtrl(23); 454 sensorPressed[ii] = 0; 455 timePressed[ii] = 0; 456 } 457 MIDI.read(); 458 } 459} 460 461void newPressure() { 462 for (int ii = 0; ii <= 1; ii++) { 463 if(letturaInt[ii]<=threshold && sensorPressed[ii]==0 && midiNote[ii]!=midiNote_Old[ii]){ 464 465 ledCtrl(1); 466 467 pitchNota[ii] = 8192; 468 pitchBendMessage(pitchNota[ii]); 469 470 noteOn(midiNote[ii], 0x7F); 471 472 sensorPressed[ii] = 1; 473 count[ii] = 1; 474 475 timePressed[ii] = 1; 476 477 midiNote_Old[ii] = midiNote[ii]; 478 midiNote_Sent[ii] = midiNote[ii]; 479 lettura_Sent[ii] = lettura[ii]; 480 481 int kk; 482 if (ii == 0) {kk = 1;} else if (ii == 1) {kk = 0;} 483 484 if (timePressed[kk] >= 1 && timePressed[ii] < timePressed[kk]) { 485 pitchPriority = ii + 1; 486 }else if (timePressed[kk] == 0) { 487 pitchPriority = ii + 1; 488 } 489 490 MIDI.read(); 491 } 492 } 493} 494 495void firstRelease() { 496 for (int ii = 0; ii <= 1; ii++) { 497 if ((letturaInt[ii] >= threshold) && count[ii] == 1) { 498 499 ledCtrl(0); 500 noteOn(midiNote_Old[ii], 0x00); 501 502 sensorPressed[ii] = 0; 503 midiNote_Old[ii] = midiNote[ii]; 504 count[ii] = count[ii] + 1; 505 506 MIDI.read(); 507 } 508 } 509} 510 511void prolongedRelease() { 512 for (int ii = 0; ii <= 1; ii++) { 513 if (letturaInt[ii] >= threshold && count[ii] >=2) { 514 ledCtrl(23); 515 516 sensorPressed[ii] = 0; 517 count[ii] = 0; 518 timePressed[ii] = 0; 519 520 MIDI.read(); 521 } 522 } 523} 524 525void assignPitchPriority() { 526 527 if (timePressed[1] == 0 && sensorPressed[0] == 1 && timePressed[0] >= 0) { 528 pitchPriority = 1; 529 } else if (timePressed[0] == 0 && sensorPressed[1] == 1 && timePressed[1] >= 0) { 530 pitchPriority = 2; 531 } 532} 533 534void prolongedPressure() { 535 for (int ii = 0; ii <= 1; ii++) { 536 if (letturaInt[ii] <= threshold && sensorPressed[ii] == 1 && pitchPriority == ii+1) { 537 538 ledCtrl(23); 539 540 if (lettura[ii] <= lettura_Sent[ii] - analogPitchRange) { 541 lettura[ii] = lettura_Sent[ii] - analogPitchRange + 1; 542 } 543 if (lettura[ii] >= lettura_Sent[ii] + analogPitchRange) { 544 lettura[ii] = lettura_Sent[ii] + analogPitchRange + 1; 545 } 546 547 pitchNota[ii] = map(lettura[ii], lettura_Sent[ii] - analogPitchRange, 548 lettura_Sent[ii] + analogPitchRange, 0,16383); 549 550 /* Pitch stabilization */ 551 pitchStabilization(ii); 552 553 /* Send stabilized pitch */ 554 if (pitchSent[ii] != pitchNota[ii]) { 555 pitchBendMessage(pitchNota[ii]); 556 pitchSent[ii] = pitchNota[ii]; 557 } 558 559 timePressed[ii] = timePressed[ii] + 1; 560 561 MIDI.read(); 562 } 563 } 564} 565 566void pitchStabilization(int iii) { 567 568 if ((pitchNota[iii] >= 8192 - pitchTolerance) && 569 (pitchNota[iii] <= 8192 + pitchTolerance)) { 570 pitchNota[iii] = 8192; 571 } 572 573 calcFloat[iii] = pitchNota[iii]/intervallo; 574 calcInt[iii] = (int)calcFloat[iii]; 575 decPart[iii] = calcFloat[iii] - calcInt[iii]; 576 577 /* Snap pitch towards up (1), and down (2) */ 578 /* (1) */ 579 if (decPart[iii] <= pitchSnap){ 580 snapOn[iii] = 0; 581 if (bitRead(incoming,14) == 0) { 582 pitchNota[iii] = range12[(int)calcInt[iii]]; 583 } else { 584 pitchNota[iii] = range24[(int)calcInt[iii]]; 585 } 586 pitchNotaOld[iii] = pitchNota[iii]; 587 588 } else { 589 snapOn[iii] = snapOn[iii] + 1; 590 if (snapOn[iii] <= 2 && abs(pitchNota[iii]-pitchNotaOld[iii])> 15) { 591 pitchNota[iii] = pitchNotaOld[iii]; 592 } 593 } 594 595 /* (2) */ 596 if (decPart[iii] >= (1 - pitchSnap)){ 597 snapOn[iii] = 0; 598 if (bitRead(incoming,14) == 0) { 599 pitchNota[iii] = range12[(int)calcInt[iii]+1]; 600 } else { 601 pitchNota[iii] = range24[(int)calcInt[iii]+1]; 602 } 603 pitchNotaOld[iii] = pitchNota[iii]; 604 605 } else { 606 snapOn[iii] = snapOn[iii] + 1; 607 if (snapOn[iii] <= 2 && abs(pitchNota[iii]-pitchNotaOld[iii])> 15) { 608 pitchNota[iii] = pitchNotaOld[iii]; 609 } 610 } 611 612} 613 614void potsReadSendCC() { 615 for (int ii = 0; ii <= 1; ii++) { 616 617 if (ccPot[ii] == 13) { 618 cicliMidiCC[ii] = 7; 619 } else { 620 cicliMidiCC[ii] = 7; 621 } 622 623 if ((jj[ii] % (cicliMidiCC[ii] - ii)) == 0) { 624 if (ii == 0) { 625 unsigned long timeStart = micros(); 626 while(micros() - timeStart < 4000){ 627 MIDI.read(); 628 } 629 } 630 631 ccRead[ii] = analogRead(analogPins[ii+2]); 632 633 jj[ii] = jj[ii] + 1; 634 if (jj[ii] == cicliMidiCC[ii] - ii) { jj[ii] = 0; } 635 MIDI.read(); 636 637 ccValue[ii] = map(ccRead[ii], 0, 1023, 0, 127); 638 639 if (ccValue[ii] != ccValue_Old[ii] && ccPot[ii] != 12 && ccPot[ii] != 13){ 640 midiCC(listaControlChange[ccPot[ii]], ccValue[ii]); 641 } else if (ccPot[ii] == 12) { 642 643 if (ccRead[ii] <= 100) { 644 transpSlideDown = HIGH; 645 646 } else if (ccRead[ii] >= 800) { 647 transpSlideUp = HIGH; 648 649 } else { 650 transpSlideDown = LOW; 651 transpSlideUp = LOW; 652 } 653 654 transposeSlideAction(); 655 } else if (ccPot[ii] == 13) { 656 /* Control pitch snap with rotPot */ 657 pitchSnap = (ccRead[ii])*0.4/1023 + 0.2; 658 ccPotOld[ii] = 13; 659 } 660 ccValue_Old[ii] = ccValue[ii]; 661 } else { 662 jj[ii] = jj[ii] + 1; 663 664 if (jj[ii] == 2*cicliMidiCC[ii] - ii) { jj[ii] = 0; } 665 } 666 667 if (ccPotOld[ii] == 13 && ccPot[ii] != 13) { 668 pitchSnap = DEFAULT_pitchSnap; 669 ccPotOld[ii] = ccPot[ii]; 670 } 671 672 } 673 674} 675
Pitch Bend Tables
python
This code computes, for a given pitch range, the pitch bend values corresponding to exact notes for pitch snapping. The code saves these values in a txt file. The content of the output file is ready to be copy-pasted in the Arduino IDE.
1#Python 3.6 2import numpy as np 3 4# Pitch range value, interpreted, as in synthesizers, as +- number of semitones 5pitchRange = 12 6 7# Generate the array containing all the pitch bend values corresponding to 8# exact notes for the given pitch range. 9range12 = np.round(np.arange(-8192, 8192, 16383 / 2 / pitchRange) + 8192) 10 11# Name of the variable containing exact notes pitch bend value in the 12# Arduino code 13varName = 'range' + str(int(pitchRange)) +\\ 14 '[' + str(int(2 * pitchRange + 1)) + ']' 15 16# Name of the txt file 17fileName = 'pitchValues' + str(int(pitchRange)) + '.txt' 18 19# Write the txt file with Arduino syntax, ready to be copy-pasted in the 20# IDE 21with open(fileName, 'w') as f: 22 f.write('const int ' + varName + ' = \ 23') 24 f.write('{') 25 for pitchValue in range12[0:-1]: 26 f.write(str(int(pitchValue))) 27 f.write(', ') 28 f.write(str(int(range12[-1]))) 29 f.write('};') 30
Downloadable files
MIDI in and MIDI out circuits
The switch on the RX port is needed so that the connection to the keyboard is interrupted when new code needs to be uploaded on the Nano, since the serial port is used in such a situation.
MIDI in and MIDI out circuits

Shift registers
The switches are connected to the 8 central pins of each shift register as indicated in the example at the bottom.
Shift registers

Soft pots
Soft pots connections to the analog pins.
Soft pots

Comments
Only logged in users can leave comments