Velocity-sensitive, contactless MIDI keyboard with polyphonic aftertouch for Teensy synth

It took a little time - the hardware (49 hall sensors and 3 multiplexers) are up and working with a step down converter.
Now it is coding time...

Did you implement velocity by calculating time or pressure?
 

Attachments

  • polyATboards_done.jpg
    polyATboards_done.jpg
    79.4 KB · Views: 65
  • teensy41board.jpg
    teensy41board.jpg
    56.9 KB · Views: 61
  • Polybus-board-pinskl.jpg
    Polybus-board-pinskl.jpg
    54 KB · Views: 44
Velocity is just distance divided by time. I read the values from all sensors at fixed intervals (approx 1000 times per second so it is 1ms). I keep the previous reading values and calculate distance as (current reading - previous reading). Since time between readings is constant, distance traveled during 1ms is strictly proportional to velocity. For simplicity I assume linear relationship between distance and hall sensor reading (although it is not physically accurate, it is also not physically accurate that velocity translates linearly to loudness or anything else, so I don't really care as long as it works in practice and sounds good to my ears).


Here is a sample code that you might use as starting point:

C++:
#define PIN_D0 0
#define PIN_D1 1
#define PIN_D2 2
#define PIN_D3 3

#define HOW_MANY_MUXES 6
#define MUX_CHANNELS 16

int anMinReadData[HOW_MANY_MUXES * MUX_CHANNELS ] = { 0 };
int anMaxReadData[HOW_MANY_MUXES * MUX_CHANNELS ] = { 0 };
int anPrevReadData[ HOW_MANY_MUXES * MUX_CHANNELS ] = { 0 };
int anReadData[ HOW_MANY_MUXES * MUX_CHANNELS ];
int8_t anKeyStates[ HOW_MANY_MUXES * MUX_CHANNELS ] = { 0 };

void setup()
{
    // D0..D3 to the mux address pins
    pinMode( PIN_D0, OUTPUT ); // configure pings
    pinMode( PIN_D1, OUTPUT );
    pinMode( PIN_D2, OUTPUT );
    pinMode( PIN_D3, OUTPUT );
 
 
    // we want 12 bit resolution
    analogReadResolution(12);
   // and NO averaging because it is VERY slow with averaging
   // this setting allows to go down to 6usec per analogRead()
    analogReadAveraging(1);
 
    // initially
    // write some heuristic value to min/ max
    // these will be adjusted later. in final product you might
    // read those from non-volatile memory from previous runs
    for( int i = 0; i < MUX_CHANNELS; i++ )
    {
        for( int j = 0; j < HOW_MANY_MUXES; j++ )
        {
            int index = i + j * MUX_CHANNELS ;
            anMinReadData[ index ] = 2048 - 100; // middle of AD scale minus 100
            anMaxReadData[ index ] = 2048 + 100; // middle of AD scale plus 100
        }
    }
}
void setMux( int addr )
{
    digitalWriteFast( PIN_D0, ( addr & 1 ) ? HIGH : LOW );
    digitalWriteFast( PIN_D1, ( addr & 2 ) ? HIGH : LOW );
    digitalWriteFast( PIN_D2, ( addr & 4 ) ? HIGH : LOW );
    digitalWriteFast( PIN_D3, ( addr & 8 ) ? HIGH : LOW );
}


// the code below assumes that you have muxes connected to
// consecutive analog inputs PIN_A0, A1, A2 and so on.
void loop()
{
    // this reads ALL data from ALL muxes
    for( int i = 0; i < MUX_CHANNELS; i++ )
    {
        setMux( i );
        delayMicroseconds( 1 ); // settling time required for mux output to stabilize
 
        for( int j = 0; j < HOW_MANY_MUXES; j++ )
        {
            int index = i + j * MUX_CHANNELS ;
            anPrevReadData[ index ] = anReadData[ index ]; // store previous value
            anReadData[ index ] = analogRead( PIN_A0 + j );
 
            // store minimums
            if( anMinReadData[ index ] < anReadData[ index ]  )
            {
                anMinReadData[ index ] =  anReadData[ index ];
            }

            // store maximums
            if( anMaxReadData[ index ] > anReadData[ index ]  )
            {
                anMaxReadData[ index ] =  anReadData[ index ];
            }
            int nMinMaxDistance = anMaxReadData[ index ] - anMinReadData[ index ];
            // ON level is 2/3 of the maximum down key travel distance (i.e. 1/3 from min)
            int nKeyOnLevel = anMinReadData[ index ] + nMinMaxDistance / 3;
            // OFF level is 1/3 of the maximum down key travel distance (i.e. 1/3 from max)
            int nKeyOffLevel = anMaxReadData[ index ] - nMinMaxDistance / 3;

            if( anKeyStates[ index ] == 0 /* key off */ &&  anReadData[ index ] < nKeyOnLevel )
            {
              anKeyStates[ index ] = 1;
              //calculate velocify here (assumes that "deeper" press (less distance) results in smaller value read from the sensor)
              // bigger distance directly maps to bigger velocity
              // saturation at 127 is needed not to exceed midi max velocity
              int8_t velocity = min( 127, anPrevReadData[ index ] - anReadData[ index ] );
              usbMIDI.sendNoteOn( index, velocity, 1 /*channel*/  );
            }
            else
            if( anKeyStates[ index ] == 1 /* key on */ &&  anReadData[ index ] > nKeyOffLevel )
            {
              anKeyStates[ index ] = 0;
              usbMIDI.sendNoteOff( index, 0, 1 /*channel*/ );
            }
        }
    }
}


Congrats on your progress.
 
Last edited:
Thank you tomas. That helps a lot!

After spending some time in regulating the distances of the magnets and bending the sensors (the holes beneath the sensors helped :) ). My first idea was: Generating arrays with estimated and not measured analog values could do for the minimum and maximum values of the fatar keybed. (BTW: Next time I must be more precise with positioning the magnets! - I still have another 5 octave TP8S with broken AT and some spare PCBs...) And the estimated maximum values must respect the different distances between the magnets of white and black keys and the sensors.

And I need time of flight sensors as ribbon controller alternatives additional to the pitch bend and modulation wheels. I did some promising experiments with VL53L0X and a capacitive touch screen with a nice user interface ... :cool: enough work for the next year.
 
Last edited:
One more time thank you.

After adapting your code to my south-north turned magnets and therefore "growing pressure values" for key presses - it worked fine. But I'm not happy with the differences of the black and white key action of the Fatar keybed...

I will use more arrays one with static values that represents the color of white or black keys
C++:
int ebonyIvoryl[] = {0,1,0,1,0,0,1,0,1,0,1,0,0,1, ...};
and another one (maybe later 2) that stores a timeStamp for passing specific pressure values to calculate sendNoteOn (and sendNoteOff) velocity. And later to calculate the poly AT.
 
Last edited:
Yes, it definitely needs fine-tuning on case-by-case basis as reading largely depend on release distance from magnets and their travel distance.

PolyAT is tricky. It depends on rubber/felt material and how "deep" after touch zone you want. In my case, I have three different thresholds: one threshold for key off, one threshold level for key on, and one more (the "deepest") for after touch start. This way after touch messages are NOT sent on very light presses, they are only sent when you press slightly harder. Another obstacle is that the distance (measured in ADC readings) inside after touch area is relatively small (less than 64 distinct values from ADC) so I increase resolution by averaging. If you average 2 readings into 1 you get 1 extra but, averaging 4 readings gives you 2 extra bits. For purpose of after touch I average 16 readings (so I get 4 extra bits, so I have 12+4 bits of resolution). This gives me opportunity to send full 0...127 range of poly after touch (key pressure) messages. The side effect (but positive one) is that they are also sent slower (at maximum 62.5 times per second = 1kHz/16) not to overload the synth with midi messages (of course I keep last value so I don't send the same value if pressure did not change).
 
Back
Top