Costis
Published © GPL3+

USB Standalone Triple Piano Pedal - Kawai F-30 Hack

Convert a Kawai F-30 or similar to a fully configurable triple continuous USB standalone pedal. No soldering, seamless and reversible!

BeginnerFull instructions provided2 hours3,251
USB Standalone Triple Piano Pedal - Kawai F-30 Hack

Things used in this project

Hardware components

Arduino Micro
Arduino Micro
A clone based on ATmega32U4 could possibly be used but I suggest the original. Other similar form factor devices like Nano do not work as native USB support is needed.
×1
Keyes KY-035 Analog Hall Magnetic Sensor Module
These sensors can be easily found. Do not confuse with KY-003 (not analog) and KY-024 (bulky, does not fit inside). 1 sensor per pedal.
×3
Male/Female Jumper Wire 20cm
Any M/F Jumper wire around 20cm would do, multi color packages are best as you can color code the connections.
×9
5x20mm Round Magnet
You can experiment with different magnets but 20mm is the maximum length to fit beneath the pedals. 1 per pedal.
×3
Mini Solderless Prototype Breadboard 170 Points
×1
Cable Tie, Belt Ty™ In Line
Cable Tie, Belt Ty™ In Line
Any regular Cable tie will do - everyone has many of them lying around.
×8
USB-A to Micro-USB Cable
USB-A to Micro-USB Cable
I would suggest to use a cable with a very slim profile angled Micro USB connector as this will make fitting inside the pedal easier.
×1
Insulating tape
Optional to prevent sensors from touching the base.
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Wire Stripper & Cutter, 18-10 AWG / 0.75-4mm² Capacity Wires
Wire Stripper & Cutter, 18-10 AWG / 0.75-4mm² Capacity Wires
To cut tie wraps

Story

Read more

Schematics

F-30 Hack

Schematic for Kawai F-30 Hack

Code

Kawai F-30 mod code

Arduino
Code to upload to the modified Kawai F-30 pedal.
/* 
Kawai F-30 Foot Pedal modification
*/
 
#include <USB-MIDI.h>

USBMIDI_CREATE_DEFAULT_INSTANCE();

// ***************** User Setup ***************** 
// Variables ending with 1,2,3 correspond to the respective pedals left to right

// Pins where signal cables are connected
const int pedalPin1 = A0;  
const int pedalPin2 = A1;  
const int pedalPin3 = A2;  

// Output controller numbers. Select from below tables
// 0-127: regular control change messages
// 128: monophonic aftertouch
// 129: Pitch Bend Up 
// 130: Pitch Bend Down 
int controllerNumber1 = 67; // Soft (Una Corda) 
int controllerNumber2 = 66; // Sostenuto
int controllerNumber3 = 64; // Hold

// Output controller channels
int controllerChannel1 = 1;
int controllerChannel2 = 1;
int controllerChannel3 = 1;

// Safety thresholds for lowest and highest values to avoid fluctuations when at rest or fully depressed. 
// If multiple messages are sent when at rest increase the lowThreshold. 
// If multiple messages are sent when fully depressed increase the highThreshold. 
const int lowThreshold1 = 3;
const int lowThreshold2 = 3;
const int lowThreshold3 = 3;
const int highThreshold1 = 3;
const int highThreshold2 = 3;
const int highThreshold3 = 3;

// Curve definition. Tables can have any legth equal or larger than 2. Values can be 0-127 . Tables should have the same number of elements and "in" tables should be in ascending order.
// Conversions are done at a readings level so loss of definition is minimized.
int in1[]   = {0, 6,24,78,127};
int out1[]  = {0,32,64,96,127};

int in2[]   = {0, 6,24,78,127};
int out2[]  = {0,32,64,96,127};

int in3[]   = {0, 6,24,78,127};
int out3[]  = {0,32,64,96,127};

// Example curves (modify pedal number accordingly)
// Original unmodified input (quadratic)
//int in3[]   = {0, 127};
//int out3[]  = {0, 127};

//Transformation to Quasi-linear 
//int in3[]   = {0, 6,24,78,127};
//int out3[]  = {0,32,64,96,127};

// Pedal switch (on/off only). Threshold at 100.
//int in3[]   = {0, 99, 100,127};
//int out3[]  = {0,  0, 127,127};

// Reduced range
//int in3[]   = {50, 100};
//int out3[]  = {50, 100};


// Refresh Cycle (milliseconds). Lower values mean more messages are sent during operation.
int refreshCycle = 3;

//  ***************** Implementation  ***************** 
// Do not modify from this point and onward if you do not intent to alter the operation of the pedal.

// Internal Value of Pedals
int pedalValue1 = 0;
int pedalValue2 = 0;
int pedalValue3 = 0;

// Maximum and minimum pedal values reached throughout operation
int pedalMax1 = 0;
int pedalMax2 = 0;
int pedalMax3 = 0;

int pedalMin1 = 1023;
int pedalMin2 = 1023;
int pedalMin3 = 1023;

// Output controller values
int controllerValue1 = 0;
int controllerValue2 = 0;
int controllerValue3 = 0;

// Previous cycle values used to avoid repetition of identical messages
int previousControllerValue1 = 0;
int previousControllerValue2 = 0;
int previousControllerValue3 = 0;

// Flags that turn true if the pedals have been pressed at least once since boot
bool pedalPressed1 = false;
bool pedalPressed2 = false;
bool pedalPressed3 = false;

// Minimum Range allowed (pedalMax - pedalMin). Used to determine if the pedal was ever pressed.
int minRange1 = 50;
int minRange2 = 50;
int minRange3 = 50;

// Range conversion variable init
int outputRange1;
int outputRange2;
int outputRange3;

int pedalUp1;
int pedalUp2;
int pedalUp3;

int pedalDown1;
int pedalDown2;
int pedalDown3;



void setup () {
// Comms setup
  MIDI.begin(1);
  Serial.begin (115200);

// Determine output ranges for the controllers chosen
  outputRange1 = outputRange(controllerNumber1);
  outputRange2 = outputRange(controllerNumber2);
  outputRange3 = outputRange(controllerNumber3);

}
 
void loop () {

// Reading inputs
  pedalValue1 = analogRead(pedalPin1);
  pedalValue2 = analogRead(pedalPin2);
  pedalValue3 = analogRead(pedalPin3);

// Auto calibration. Check for new maximum and minimum. First time after boot that each pedal is depressed it will be calibrated and will go directly to max value during depression.
// After calibration each pedal will behave normally.
  if (pedalValue1 > pedalMax1) {
    pedalMax1 = pedalValue1;
    if (!pedalPressed1 && pedalMax1-pedalMin1 > minRange1){
      pedalPressed1 = true;
      sendPedalOutput(controllerNumber1, outputRange1, controllerChannel1);
    }
  }
  
  if (pedalValue2 > pedalMax2) {
    pedalMax2 = pedalValue2;
    if (!pedalPressed2 && pedalMax2-pedalMin2 > minRange2) {
      pedalPressed2 = true;
      sendPedalOutput(controllerNumber2, outputRange2, controllerChannel2);
    }
  }
  
  if (pedalValue3 > pedalMax3) {
    pedalMax3 = pedalValue3;
    if (!pedalPressed3 && pedalMax3-pedalMin3 > minRange3) {
      pedalPressed3 = true;
      sendPedalOutput(controllerNumber3, outputRange3, controllerChannel3);    
    }
  }

  if (pedalValue1 < pedalMin1) pedalMin1 = pedalValue1;
  if (pedalValue2 < pedalMin2) pedalMin2 = pedalValue2;
  if (pedalValue3 < pedalMin3) pedalMin3 = pedalValue3;


// Store previous values
  previousControllerValue1 = controllerValue1;
  previousControllerValue2 = controllerValue2;
  previousControllerValue3 = controllerValue3;

// Usable range limits for pedal up/down
  pedalUp1 = pedalMin1+lowThreshold1;
  pedalUp2 = pedalMin2+lowThreshold2;
  pedalUp3 = pedalMin3+lowThreshold3;

  pedalDown1 = pedalMax1-highThreshold1;
  pedalDown2 = pedalMax2-highThreshold2;
  pedalDown3 = pedalMax3-highThreshold3;

  
// Convert internal values to output range (0..127 for controllers/aftertouch 0..+/-8191 for Pitchbend Up/Down). No curve version, can substitute the next section if curves are not needed.
//  controllerValue1 = map(constrain(pedalValue1,pedalUp1,pedalDown1),pedalUp1,pedalDown1,0,outputRange1);
//  controllerValue2 = map(constrain(pedalValue2,pedalUp2,pedalDown2),pedalUp2,pedalDown2,0,outputRange2);
//  controllerValue3 = map(constrain(pedalValue3,pedalUp3,pedalDown3),pedalUp3,pedalDown3,0,outputRange3);

// Convert internal values to output range (0..127 for controllers/aftertouch 0..+/-8191 for Pitchbend Up/Down) using the curves defined in "in" and "out" tables.
  controllerValue1 = map(mapToCurve(constrain(pedalValue1,pedalUp1,pedalDown1),pedalUp1,pedalDown1,in1,out1,sizeof(in1)/sizeof(int)),pedalUp1,pedalDown1,0,outputRange1);
  controllerValue2 = map(mapToCurve(constrain(pedalValue2,pedalUp2,pedalDown2),pedalUp2,pedalDown2,in2,out2,sizeof(in2)/sizeof(int)),pedalUp2,pedalDown2,0,outputRange2);
  controllerValue3 = map(mapToCurve(constrain(pedalValue3,pedalUp3,pedalDown3),pedalUp3,pedalDown3,in3,out3,sizeof(in3)/sizeof(int)),pedalUp3,pedalDown3,0,outputRange3);

// Send MIDI messages  
  if (controllerValue1 != previousControllerValue1 && pedalPressed1) sendPedalOutput(controllerNumber1, controllerValue1, controllerChannel1);
  if (controllerValue2 != previousControllerValue2 && pedalPressed2) sendPedalOutput(controllerNumber2, controllerValue2, controllerChannel2);
  if (controllerValue3 != previousControllerValue3 && pedalPressed3) sendPedalOutput(controllerNumber3, controllerValue3, controllerChannel3);

// Sandbox


// Debug

// Pedal (input) values
//  Serial.print (pedalValue1);
//  Serial.print(","); 
//  Serial.print (pedalValue2);
//  Serial.print(","); 
//  Serial.println (pedalValue3);

// Controller (output) values
//  Serial.print (controllerValue1);
//  Serial.print(","); 
//  Serial.print (controllerValue2);
//  Serial.print(","); 
//  Serial.println (controllerValue3);

//Serial.println (outputRange(controllerNumber1));

// Delay for predefined millis
delay(refreshCycle);
}

// Function used to send MIDI messages according to controller number
void sendPedalOutput (int number, int value, int channel) {
  if (number < 128) MIDI.sendControlChange(number, value, channel);
  else if (number == 128) MIDI.sendAfterTouch(value, channel);
  else if (number == 129) MIDI.sendPitchBend(value, channel);
  else if (number == 130) MIDI.sendPitchBend(-value, channel);
}

// Function used to determine the range of a specific controller. This is due to the fact pitch bend has a larger range than regular controllers.
int outputRange (int number) {
  if (number > 128) return 8191;
  else return 127;
}

// Modified multiMap function used to create curves. Original by Rob Tillaart.
int mapToCurve(int val, int pedalUp, int pedalDown, int* _in, int* _out, uint8_t size)
{
  // take care the value is within range
  // val = constrain(val, _in[0], _in[size-1]);
  if (val <= map(_in[0],0,127,pedalUp,pedalDown)) return map(_out[0],0,127,pedalUp,pedalDown);
  if (val >= map(_in[size-1],0,127,pedalUp,pedalDown)) return map(_out[size-1],0,127,pedalUp,pedalDown);

  // search right interval
  uint8_t pos = 1;  // _in[0] already tested
  while(val > map(_in[pos],0,127,pedalUp,pedalDown)) pos++;

  // adjusting range from ..127 to pedal range
  int inPos = map(_in[pos],0,127,pedalUp,pedalDown);
  int outPos = map(_out[pos],0,127,pedalUp,pedalDown);
  int inPrv = map(_in[pos-1],0,127,pedalUp,pedalDown);
  int outPrv = map(_out[pos-1],0,127,pedalUp,pedalDown);

  
  // this will handle all exact "points" in the _in array
  if (val == inPos) return outPos;
  
  // interpolate in the right segment for the rest
  return ((long)val - (long)inPrv) * ((long)outPos - (long)outPrv) / ((long)inPos - (long)inPrv) + (long)outPrv;
}

Credits

Costis

Costis

2 projects • 8 followers

Comments