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: 49
  • hall-mountedl-kl.jpg
    hall-mountedl-kl.jpg
    94.8 KB · Views: 47
  • mk-br-vs-hall-kl.jpg
    mk-br-vs-hall-kl.jpg
    150.3 KB · Views: 49
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 ?
 
The first test with small and cheap magnets gave ambivalent results: Your basic code is working. But the analog values do not seem to be high enough.

The round magnets (4x1mm) seem to be to poor/small and to far away from the hall sensors. The highest analog value I get is 2500 the inital values are from 1950 to 2150, my hallsensors are running on 3.3 V. Reversing magnets will not suffice.

Next step is ordering bigger magnets (5x2mm) - that will result in smaller distances and more magnetic strength. Another problem is the position of the magnets on the black keys, these are a little bit higher than those of the white keys. Eventually this could be solved by code.
 
Last edited:
I am using 6x2mm neodymium magnets that are pretty strong. My hall sensors use 4.5V supply (5V - 0.5V drop on transistor switches). That makes output range significantly higher.
 
First test with the stronger 5x2mm magnets shows initial values from 2000 to 2150. Those increase up to 3000 (white keys) and 2800 (black keys). Increasing the pressure more shows 100-150 on top. That will be good for further attempts. And with 16 hall sensors connected they need to be stabilized - but the boards are prepared. ;) BTW: UV-Glue will be a better choice than Alleskleber.
 

Attachments

  • fatar-tp8s-magnets5x2.jpg
    fatar-tp8s-magnets5x2.jpg
    62.9 KB · Views: 30
Last edited:
:) Most of the soldering is done, only connections to the daughter-mux-boards and capacitors are missing. Top right you can see hall sensor number 49 - this one will get an extra analog line. First tests are promising.
 

Attachments

  • hall-sensors-49keys.jpg
    hall-sensors-49keys.jpg
    73.5 KB · Views: 38
Last edited:
Reviewing the code you most recently sent, I am wondering which library you installed that responds to usbMIDI.sendNoteOff( index, 0, 1 /*channel*/ );
There are so many to choose from. I would really like to use the one you selected.
 
The keyboards are nice. I hope I do them justice with building the organ pedal board. I got keyboards, presets, bench adjustment mechanism and keyboard parts from OSI; Organ Supply Industries. I am attaching a picture now since I don't remember doing that at this forum. 🤨 Or did you mean circuit board, which I don't remember adding to this forum.

Back to using Teensy for key and pot sensing, will it be OK to attach one 500 ohm pot at one multiplex board where a Hall Effect sensor would go? I wouldn't think it would overload the BC560 PNP's. I need only three pots for crescendo and two swells.
 

Attachments

  • keyboard (1).jpg
    keyboard (1).jpg
    304.6 KB · Views: 16
  • keyboard color.jpg
    keyboard color.jpg
    260 KB · Views: 17
  • Keyboard side.jpg
    Keyboard side.jpg
    51.8 KB · Views: 15
Read your May 17, 2026 many times. Some but not all of it is getting through my head. How is one microsecond enough settling time when it takes Teensy 6 microseconds to read properly? Is "analogReadResolution" how you control how fast you are reading analog values times per second. 10 bit being faster than 12 bit? Lower resolution having lower number of numbers in the entire analog range? If I can use one 500 ohm pot on an MUX board does that help reduce the read speed due to lower capacitance? Which is more important, Teensey analog read speed or capacitance versus resistance timing? No one who helps a novice ever goes unpunished for being so helpful.
 
Few things that need explanation:

1. 500 ohm is too low for potentiometer as it will cause large current flowing thru it (power consumption). You should rather use 10K pots (sweet spot)
2. The impedance is not the same as resistance. You can lower impedance (and thus ability to source current to ADC) without lowering resistance by adding capacitor right between ground and potentiometer wiper. Say 100nF would provide plenty of charge reservoir for ADC to sample without worry and effectively lowering impedance (not resistance) to just 15 ohm at 100kHz (assuming your sampling rate is <10us). This way you have best of two worlds - current driving capacity without constant high current draw.
1782637827222.png

3. You should power pots directly from 3.3V power line. Not from PNP transistor. Why ? Because pots will give you full range GND to VCC unlike Hall sensors that do not drive their output rail to rail. 3.3V is the only secure option for pots that are meant to be read by 3.3V ADC input of microcontroller
4. Settling time is NOT time to measure (sample). Settling time is time required BEFORE sampling for voltage to stabilise, before sampling process begins. So this ADDS to sampling time. You switch mux channel, ADC circuitry samples the voltage (internally using capacitor) waits settling time and then starts actual conversion.
5. analogReadResolution changes the number of bits reported by analogRead. If you set 10 bits, analogRead would return values 0..1023. If you set to 12 bits, analogRead would return values 0..4095. Full resolution is required if you want to handle aftertouch (as fluctuations with different pressure are small).
 
Last edited:
I forgot that digital switching frequency is similar to sinusoidal frequency as far as reactance is concerned. You have convinced me that STM32F103 is the better way to go, but that is a lot more new information, programming and circuity to absorb. It would be impolite to impose on you to teach me something I know nothing about. I have a much better grasp on your Teensy data acquisition method. If I use 12 bit (2^12), the Teensy needs ~6μs for each analog read to sample after 1μs settling. If I use 10 bit, the Teensy has a theoretical speed of 1μs for each analog read after 1μs settling. If I use 10 bit I can't have after touch due to reduced sensitivity.

I have 3 61 note keyboards, 32 note pedal and 3 pots for a total of 218 inputs. That will require 14 mux boards.

If I use 12 bit the scanning, speed is 1/(16 * 0.000001+218 * 0.000006) = 755 samples per second.

I noticed, your 500-700 SPS for only three keyboards (183 inputs) is more conservative.

If I use 10 bit the scanning, speed is possibly 1/(16 * 0.000001+218 * 0.000001) = 4273 samples per second.

Please let me know where my thinking might be wrong.
 
Teensy is fine for the job too. I wouldn't worry about scan rate as much. 700-750 SPS is plenty.
There is a difference between ordinary (on-off switch based) keyboard and this one. This one gives you analog readings,
which means that you have different "on" and "off" levels, that enables you to have hysteresis that prevents main problem
of on-off switches which is contact bouncing. High scan rates were required to process de-bouncing (by waiting for multiple reads to stabilize). With analog reads and hysteresis the problem is gone completely. With hysteresis as soon as threshold is hit you get note on or off, there is no need for processing multiple inputs to guarantee debouncing. Therefore response (keypress -> note on/off message) is actually faster.
 
Back
Top