Midi Joystick Callibration

Status
Not open for further replies.

Ripper

Well-known member
Hi guys.
I'm building a midi controller and for that i built a midi joystick.

At the moment the serial monitor reports this for pitchbend when i have the joystick in neutral position after giving it a slight touch and go:

Code:
Joystick Pitchbend pitchraw=512
Joystick Pitchbend Pitch=16
Joystick Pitchbend pitchraw=511
Joystick Pitchbend Pitch=0
Joystick Pitchbend pitchraw=510
Joystick Pitchbend Pitch=-17
It doesn't really stay in 0.

How could i smooth it so that it stays in zero when released?

play with this?

Code:
   if (abs(pitch - pitchLag) > [COLOR=#ff0000]0[/COLOR] && pitchUpdate > MIDIdelay ) {

This is my full code for the joystick:

Code:
// include the ResponsiveAnalogRead library for analog smoothing
#include <ResponsiveAnalogRead.h>
#include <Bounce.h>  // Bounce library makes button change detection easy
// ******CONSTANT VALUES******** - customize code behaviour here!


// SET THESE SIX VALUES!
const int pitchPin = 0; // PIN numbers ** MUST CHANGE!
const int modPin = 1;
const int modPin2 = 1;
const int pitchMaxRaw = 1015; // Max reading with full bend up... as raw 10-bit value
const int modMaxRaw = 1022; // Max reading full mod down


const int channel = 1; // MIDI channel
const int MIDIdelay = 5; // will update MIDI only if this many milliseconds have passed
//******VARIABLES***********
// data variables and a lagged copy to compare before updating MIDI value
int pitch;
int mod;
int mod2;
int pitchRaw;
int modRaw;
int modRaw2;
int pitchLag;
int modLag;
int modLag2;
elapsedMillis pitchUpdate;
elapsedMillis modUpdate;
elapsedMillis modUpdate2;


// ititialize the ReponsiveAnalogRead objects
ResponsiveAnalogRead readPitch = {pitchPin, true};
ResponsiveAnalogRead readMod = {modPin, true};
ResponsiveAnalogRead readMod2 = {modPin2, true};


void setup() {
  pinMode(1, INPUT_PULLUP);
  pinMode(2, INPUT_PULLUP);
 
}


void loop() 


{
 delay(100);


  getAnalogData();
  while (usbMIDI.read()) {
  }
}






//************ANALOG SECTION**************
void getAnalogData() {
  readPitch.update();
  if (readPitch.hasChanged()) {
    pitchRaw = readPitch.getValue();
    Serial.print("Joystick Pitchbend pitchraw=");
    Serial.println(pitchRaw);
    // remap to output data and send if changed and MIDIdelay has lapsed since last update for that parameter
    pitch = map(pitchRaw, 2, 1020, -8192, 8192);
    pitch = max(pitch, -8192); // need this now that the bottom isn't zero
    pitch = min(pitch, 8192); // cap to avoid overflow
[COLOR=#ff0000]    if (abs(pitch - pitchLag) > 0 && pitchUpdate > MIDIdelay ) {[/COLOR]
      pitchLag = pitch;
      usbMIDI.sendPitchBend(pitch, channel);
      Serial.print("Joystick Pitchbend Pitch=");
      Serial.println(pitch);
      pitchUpdate = 0;
    }
  }




  readMod.update();
  if (readMod.hasChanged()) {
    modRaw = readMod.getValue();
    // remap to output data and send if changed and MIDIdelay has lapsed since last update for that parameter
    mod = map(modRaw, 516, 1022, 0, 127); // N.B. - map does not limit values
    mod = max(mod, 0); // minimum could overflow is modRaw is greater than calabrated -This way Y Center-Down will only send from 0 to 127 values
    mod = min(mod,127);
    if (mod != modLag && modUpdate > MIDIdelay ) { // resolution reduction should be enough wi
      modLag = mod;
      usbMIDI.sendControlChange(1, mod, channel); // CC = 1 is mod wheel in standard MIDI
      modUpdate = 0;
    }
  }
  readMod2.update();
  if (readMod2.hasChanged()) {
    modRaw2 = readMod2.getValue();
    // remap to output data and send if changed and MIDIdelay has lapsed since last update for that parameter
    mod2 = map(modRaw2, 3, 503, 0, 127); // N.B. - map does not limit values
    mod2 = max(mod2, 0); // minimum could overflow if modRaw2 is greater than calibrated
    mod2 = min(mod2, 127); // This way Y Center-Up will only send from 0 till 127 values
    mod = min(mod2,127);
    if (mod2 != modLag2 && modUpdate2 > MIDIdelay ) { // resolution reduction should be enough wi
      modLag2 = mod2;
      usbMIDI.sendControlChange(2, mod2, channel); // CC = 2 is Breath Control in standard MIDI -In this case it is 
      modUpdate2 = 0;
    }
  {
  }
  }
  }

The rest of the project (wrong thread name):
https://forum.pjrc.com/threads/44582-Send-1-Sysex-message-depending-on-momentary-switch-State
 
ADC reading is inherently noisy, example if you fire one of the examples that streams ADC values to the serial port a degree of jitter is expected. Given the jitter your post mapping function pitch output looks correct. To Improve matters you need to do some combination of:

stablise/filter your power supply to the stick
use Aref https://www.arduino.cc/reference/en/language/functions/analog-io/analogreference/
Use AGnd for your joytstick
Shield the joystick and leads from any nearby sources of RF interference
Sleep the CPU during analog reads (normally only used for things where precision is over-riding design goal)
Multiple sample and average (will take longer between reads)
Smooth results in a ring buffer (will slow response to changes)
Change your code to 'dead spot' around the center ~512 space (will prevent fine adjustment)
Add hysteresis (a moveable dead spot, helps with hand jitter but hinders slight adjustment)
Look at what you want the pitch value to do - should this actual use a log curve rather than the linear map (flat near the middile, big steps at max travel)?

You will probably need a mixture of the above to get where you want to go
 
Hi GremlinWrangler, thanks for your input.
I will read some more on the subjects you suggested and try to make it a bit better. It's not that it is not giving a decent result but we always want things to be better than they already are. ;)

Ah, i already had it connected to Aref and AGnd.

Anyway if, to be able to have a real 0, i have to sacrifice fine adjustment probably it will stay as it is now.

Once again, thank you!
 
Last edited:
Centre for the Center

Ok... so I made you start a new thread so that someone else might offer a better idea how to get you what you want (GW's comments are right up that alley but I was hoping there would be others too).

But I also wanted whatever progress you make to be accessible to others rather than getting buried at the tail end of that rambling insanity.

As I alluded in your main thread pitch control is a topic unto itself for a few reasons.

  1. It matters - some humans can detect pitch differences of a few cents and nearly everybody can hear two tones beating when the difference is around one Hertz (whereas most MIDI parameters at seven bit resolution are fine)
  2. Theoretically you should be able to trade of responsiveness for higher resolution if the noise is primarily at higher frequencies than the human movements you're attempting to track
  3. Accuracy matters most at the center of the range and in particular at the resting point of the control. If the control doesn't centre at the middle of the voltage divider range then getting it to recognize the centre is a major issue.
  4. The noise issue is complicated on its own. Solutions generally involve roll-your-own dead-band controls (which prevent updates on small variations in the source signal) or using the ResponsiveAnalogRead() library (which does something similar but also monitors for update frequency before entering it's sleep mode --as well has having some other features of interest for this case!)
  5. Non-linear mappings might provide a more intuitive feel for pitch modulation but implementing this is not trivial.

As GW points out shielding and some careful attention to the analog signal will help but in my experience it only lessens the problem that you still need to solve. But given you're looking to get as much of the 14-bits that's possible to transmit it's definitely essential you minimize issue before attacking it in the digital world.


Regarding self-calibration, that part is relatively simple as the code can fine-tune itself at the edges by starting with extreme values that are some distance from the edges to start but then replacing them with the actual extremes as read at the pin.
Example: Self-calibrating-scaled-output-MIDI-Guitar-Pick-project

Calibrating the centre point might be a bit tricky. You could assume the control is at rest at startup (if within x percent of midpoint) and use that reading as the centre and then have two linear scales to each edge (if the centre read is sufficiently off the centre voltage this would be the best correction you could make simply in that it would feel linear enough but still allow full access to the edges and still centering at rest).

If you have a center tap (a pin from the potentiometer that corresponds to the spot to which it physically centers) then it appears you can force it to a virtual ground calibrated to 1/2 ARef and know that it will read correctly when at rest. (Or you could read the difference from centre to that pin at start-up and use that in calibration.) I don't believe this is a common feature on joysticks but is on some pots used as pitch bend; particularly on fully analog gear.

Can you clarify the physical characteristics of your control? (A data-sheet if such a thing exists for it.)

You mentioned a screw adjust in the previous thread. Can you clarify what that does? (Photo?)

At 10 bits what is your current 'normal center' reading?
What are the edges?


To solve the main problem you've identified (making sure it's at center when at rest) I would consider a correction routine that recalibrates the center whenever the signal has stayed within a narrow range within some threshold of the actual center. It would establish a dead-band center from the raw value at last sent MIDI message.

The 'misfire' effect from this would be innocuous (the user would have to be very-steadily holding a slight pitch bend for a very long time before it would 'correct' in error to a not-distant pitch) and when fired correctly (from the resting middle having drifted because the springs aren't returning it like before??) it would also not sound 'wrong'.

Alternately it could be run only at startup if the center is sufficiently stable or a calibration button could be added to trigger the routine.

As a final issue it may be beneficial to adjust the rate MIDI messages are sent based on circumstances. You might want more messages sent as the pitch returns to zero-shift or when the rate of change in the control is very large but these two circumstances look very different in the data.

But you might notice a lot of these ideas start with knowing if you are already close to the center... so once you get some of this working it will be a lot easier to add related features.
 
Thank you oddson for your considerations.
I will read it more carefully later on and let you know the stuff you need.

P.S.: I'm not sure if i will be able to do it the following days once that my daughter is getting married this weekend and there's stuff to prepare. :rolleyes:
 
This is the joystick similar to mine (Mine only says R204B1-R4 in the sticker):

https://www.ebay.com/itm/R204B1-M4-...979433?hash=item3b000ddea9:g:oYQAAOSw38BaWCeY

The screw lets me fine tune X and Y (I tried my best to center it)

The sketch as it is now (Only slightly changed the joystick values for the sake of trying to get the raw value without tunning, comparing to the OP)

Code:
// include the ResponsiveAnalogRead library for analog smoothing
#include <ResponsiveAnalogRead.h>
#include <Bounce.h>  // Bounce library makes button change detection easy
// ******CONSTANT VALUES******** - customize code behaviour here!


// SET THESE SIX VALUES!
const int pitchPin = 0; // PIN numbers ** MUST CHANGE!
const int modPin = 1;
const int modPin2 = 1;
const int pitchMaxRaw = 1022; // Max reading with full bend up... as raw 10-bit value
const int modMaxRaw = 1022; // Max reading full mod down


const int channel = 1; // MIDI channel
const int MIDIdelay = 5; // will update MIDI only if this many milliseconds have passed
//******VARIABLES***********
// data variables and a lagged copy to compare before updating MIDI value
int pitch;
int mod;
int mod2;
int pitchRaw;
int modRaw;
int modRaw2;
int pitchLag;
int modLag;
int modLag2;
elapsedMillis pitchUpdate;
elapsedMillis modUpdate;
elapsedMillis modUpdate2;


// ititialize the ReponsiveAnalogRead objects
ResponsiveAnalogRead readPitch = {pitchPin, true};
ResponsiveAnalogRead readMod = {modPin, true};
ResponsiveAnalogRead readMod2 = {modPin2, true};


void setup() {
  pinMode(1, INPUT_PULLUP);
  pinMode(2, INPUT_PULLUP);
 
}


void loop() 


{
 delay(100);


  getAnalogData();
  while (usbMIDI.read()) {
  }
}






//************ANALOG SECTION**************
void getAnalogData() {
  readPitch.update();
  if (readPitch.hasChanged()) {
    pitchRaw = readPitch.getValue();
    Serial.print("Joystick Pitchbend pitchraw=");
    Serial.println(pitchRaw);
    // remap to output data and send if changed and MIDIdelay has lapsed since last update for that parameter
    pitch = map(pitchRaw, 1, 1015, -8191, 8192);
    pitch = max(pitch, -8191); // need this now that the bottom isn't zero
    pitch = min(pitch, 8192); // cap to avoid overflow
    if (abs(pitch - pitchLag) > 0 && pitchUpdate > MIDIdelay ) {
      pitchLag = pitch;
      usbMIDI.sendPitchBend(pitch, channel);
      Serial.print("Joystick Pitchbend Pitch=");
      Serial.println(pitch);
      pitchUpdate = 0;
    }
  }




  readMod.update();
  if (readMod.hasChanged()) {
    modRaw = readMod.getValue();
    // remap to output data and send if changed and MIDIdelay has lapsed since last update for that parameter
    mod = map(modRaw, 516, 1022, 0, 127); // N.B. - map does not limit values
    mod = max(mod, 0); // minimum could overflow is modRaw is greater than calabrated -This way Y Center-Down will only send from 0 to 127 values
    mod = min(mod,127);
    if (mod != modLag && modUpdate > MIDIdelay ) { // resolution reduction should be enough wi
      modLag = mod;
      usbMIDI.sendControlChange(1, mod, channel); // CC = 1 is mod wheel in standard MIDI
      modUpdate = 0;
    }
  }
  readMod2.update();
  if (readMod2.hasChanged()) {
    modRaw2 = readMod2.getValue();
    // remap to output data and send if changed and MIDIdelay has lapsed since last update for that parameter
    mod2 = map(modRaw2, 3, 503, 0, 127); // N.B. - map does not limit values
    mod2 = max(mod2, 0); // minimum could overflow if modRaw2 is greater than calibrated
    mod2 = min(mod2, 127); // This way Y Center-Up will only send from 0 till 127 values
    mod = min(mod2,127);
    if (mod2 != modLag2 && modUpdate2 > MIDIdelay ) { // resolution reduction should be enough wi
      modLag2 = mod2;
      usbMIDI.sendControlChange(2, mod2, channel); // CC = 2 is Breath Control in standard MIDI -In this case it is 
      modUpdate2 = 0;
    }
  {
  }
  }
  }

The serial monitor gives me this at start up after sending the sketch:

Centered:
Code:
Joystick Pitchbend pitchraw=512
Joystick Pitchbend Pitch=65


Full Down
Code:
Joystick Pitchbend pitchraw=0
Joystick Pitchbend Pitch=-8191
Full Up
Code:
Joystick Pitchbend pitchraw=1023
Joystick Pitchbend Pitch=8192



With this map it behaves pretty close to what is preferable, but it's not there yet.
It will be at 512/511 most of times when i release the stick, but not always.
 
Status
Not open for further replies.
Back
Top