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

Thanks again for the excellent explanation. I am still having trouble understanding the JP1 socket in the schematic. I understand the SJ7-9 are solder jumpers to select one for each SIG for A0-A2. Allowing you to use the same board for each MUX. Where does SJ1-6 go to? Also, What are the values of C1 and R1? I am sure R1 and R2 are the same value. I remove the underlines in PIN_D0-3 to get your code to compile. Was I wrong to do this? I apologize in advance for my lack of understanding and your patience with me who should know more about electronics and C programming to be allowed on this forum.
 
Actually it makes me happy that I can help and that anyone is interested :) And all what counts is that you are asking correct questions :)

I understand the SJ7-9 are solder jumpers to select one for each SIG for A0-A2. Allowing you to use the same board for each MUX.
That is correct.

Where does SJ1-6 go to?
As I mentioned earlier in this thread originally I was thinking about implementing power-saving feature that would allow me to temporarily turn off the power delivered to Hall sensors, SJ4 and SJ1 are connected to transistors that power up Hall sensors. The other end of SJ4 and SJ1 would go to digital lines of microcontroller (Teensy) that would allow me to control power delivered by Hall sensors. But currently I abandoned idea because Hall sensors when powered down take significant time to power up and become fully stable (like 50μs), so I am afraid I would not be able to turn them on/off sequentially at desired scan rate (1kHz) without problems. That is my assumption, I did not check it in real-life yet. Still, if I ever come back to the idea of power saving I will have hardware ready. So for the time being SJ4 and SJ go to ground (meaning transistors Q1 and Q2 deliver power to Hall sensors constantly.

Also, What are the values of C1 and R1? I am sure R1 and R2 are the same value.
The value of C1 is 100nF. Typical decoupling cap. R1 and R2 resistance is 2kΩ but these values are not critical - should be just enough to saturate transistors when connected to ground.

I remove the underlines in PIN_D0-3 to get your code to compile. Was I wrong to do this?
No, actually you are right :) . They should indeed be PIND0, PIND1, PIND2 and PIND3.

Final comment: please note that the original mux schematic was for my keyboard only setup (3 muxes, hence SJ solder jumpers allow three connections only). If you want to adapt the schematic, you would of course need to expand that to cover 12 muxes.

As for myself, in addition to mux boards presented in this thread, I have have a separate board for 3 muxes that handle all 48 pots, microcontroller, connector for flat cable (to connect keyboard muxes), connector for RGB strip and ribbon controller (Softpot), see my previous message: https://forum.pjrc.com/index.php?th...aftertouch-for-teensy-synth.75078/post-363366
 
Last edited:
I really should read the entire tread a lot more carefully. I keep missing critical information. Grounding the transistors will free 9 pins for a total of 12 analog inputs for a 16 wire cable:) .

Would you mind giving me more code on how you correct each key difference and determining when the after touch begins taking readings. Having code you already created would be a real time saver. If you coded this in the STM32F103 that requires difference programming, I am sure I could code the Teensy for this myself.
 
Sure I can help you with the code but my current code is very complex so for purpose of demonstrating the idea, you need something easier.
In principle, no matter how precisely you setup your Hall sensor there are going to be differences between them.
If you looked at the picture I posted earlier, these are actual readings from 48 sensors:
1779225392277.png


As you can see the "off" values vary a lot (from 2500 to 2800). On values are more consistent (1300-1400). You are going to see different values in your setup due to different distances between sensors and magnets, different magnet strength, different characteristic of AD converter.

So how do you deal with that? What I am doing is storing per-key minimum and maximum value ever read. Then I arbitrarily pick levels of 1/3 and 2/3 of the key travel distance as key off and key on points. This creates hysteresis that prevents from "bouncing" - i.e. avoids generating several false note on/off messages due to noise from AD readings.

The code that implements this idea is here. I wrote it for illustration purposes here as a starting point of your own endeavor. It builds upon previously posted example

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 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 ;
            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;
              int8_t velocity = 100; //calculate velocify here (I leave it for the other time)
              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*/ );
            }
        }
    }
}

This code assumes that you are doing "calibration" first - just press all keys one by one at least once. This would set min/max ranges for each key appropriately.
 
Last edited:
Thank you for providing me answers to all my questions. It time now for me work on this project. Building the pedal will take a lot of my time.
 
Some more progress. I have now added more Teensys :) the synth is now 8-way multi-timbral, had independent arps for each channel, plus drum machine channel. I also added foot controller (based on basic Arduino Mini) that sends MIDI to main synth. This way I can still play something when I run out of hands :)

DEMO is here:

 
Thank you very much for this amazing project and your well explained example code.
That triggerd me to design my own pcb that fits to a fatar keybed to "re"midify it. I am still waiting for some small magnets for further attempts.

But the first attempts with your code are promising. Here is a sketch of what I am trying to do and a picture of the first mounted pcb.
 

Attachments

  • mounting-hall-kl.jpg
    mounting-hall-kl.jpg
    37.5 KB · Views: 17
  • hall-mountedl-kl.jpg
    hall-mountedl-kl.jpg
    94.8 KB · Views: 17
  • mk-br-vs-hall-kl.jpg
    mk-br-vs-hall-kl.jpg
    150.3 KB · Views: 14
Since you are using Fatar keybed, did you purchase it separately or are you upgrading existing midi controller. I am asking because from what I know Fatar does not sell keyboards to customers and the only place I know where you can buy Fatar keyboards alone (for DIY projects) is Doepfer (Germany). Also what model of Fatar keybed are you using? It looks like 61TP/9S ?
 
Back
Top