tttapa / Control-Surface

Arduino library for creating MIDI controllers and other MIDI devices.
GNU General Public License v3.0
1.19k stars 134 forks source link

Is it possible to have 40 banks with 40 controls? #177

Open Sylvan72 opened 4 years ago

Sylvan72 commented 4 years ago

Hi. Thank you very much for your work.

I am researching for the design of a midi controller to connect to the CLA MixHub. Is it possible to create 40 tracks (banks) with 25 potentiometers, 1 fader and 14 buttons? The controls would be multiplexed by three 74hc4067 and each track would be enabled sequentially through five 74hc595.

Thank you.

tttapa commented 4 years ago

It'll depend on how you lay out the addresses. There are only 120 control change addresses in each MIDI channel. The default banks will only allow you to change either the address or the channel, not both at the same time (you can combine banks of course, but this is probably needlessly more complicated).

I'd definitely recommend starting small, and make sure you understand the addressing and banking systems in detail before scaling up.

You'll also need to check if your hardware can handle it. This many controls requires a lot of memory (especially RAM), and the refresh rate may be too low to be usable. The ADC of most Arduinos is relatively slow, so reading this many potentiometers and faders could be a problem. Do some timing experiments with a smaller controller first.

It's not entirely clear why you would need 5 74HC595 shift registers to enable three multiplexers though. Or do you plan to have 1600 hardware inputs?

Sylvan72 commented 4 years ago

Thanks for your attention.

It may be crazy, but the idea would be exactly that: 1600 independent inputs.

There would be 40 common reading lines (3x74HC4067). The 74HC595 outputs would enable transistors that would power one column at a time. The microcontroller would switch the input from the multiplexers, record the values and switch to the next column. For that, each control would need to have a unique identifier.

I don't know if the MIDI specifications allow to define 3 tracks of 40 controls per channel (120 addresses), because, if it is, 120 addresses x 16 channels are more than enough.

As for the refresh rate, I would have to test to the limit to see how many columns a microcontroller would support reading. If so, it would divide the 40 tracks into smaller modules, each with its own control board.

I'm just at the beginning, learning along the way.

Thank you.

tttapa commented 4 years ago

There would be 40 common reading lines (3x74HC4067). The 74HC595 outputs would enable transistors that would power one column at a time.

This may work for buttons, but it won't work for potentiometers. You can only use the transistor to disconnect either ground or Vcc to the potentiometers, but you cannot remove them from the circuit. Even with one of the two power pins connected, the "inactive" potentiometers will still influence the active potentiometer. You need a multiplexer.

I don't know if the MIDI specifications allow to define 3 tracks of 40 controls per channel (120 addresses), because, if it is, 120 addresses x 16 channels are more than enough.

That should work if you can map arbitrary addresses in the software you wish to control.

As for the refresh rate, I would have to test to the limit to see how many columns a microcontroller would support reading.

The Arduino UNO has a sampling rate of 9600 Sps. This is under ideal circumstances in free running mode, without doing anything else. If you're using multiplexers, you'll most definitely have to do a dummy read after switching mux channels. That means reading 26×40 analog inputs takes at least 2×26×40 S / 9600 Sps = 220 ms. That's completely unacceptable.

In a real scenario, you need to apply some filtering to the inputs as well, otherwise, it'll be sending MIDI messages all the time, completely flooding the MIDI link. This adds another couple of samples of latency, so you'll be happy with an effective latency of over a second.

Also, before you buy any hardware (potentiometers etc.), carefully read the datasheet of the multiplexers and the microcontroller or ADC you plan on using. Most microcontroller ADCs aren't happy with analog signal sources that have an output impedance of more than 10 kΩ. So let's say you're using 10 kΩ pots, which will give you an effective output impedance of around 5 kΩ in the center, you're looking at a quiescent power consumption of ≃ 0.5 A or 2.5 Watts for the potentiometers alone.

A project of this scale is impossible without very careful planning and testing.

Sylvan72 commented 4 years ago

In my idea a terminal of the potentiometers would be connected to the VCC line provided by the transistor, the central terminal at the inputs of two multiplexers and the third terminal would be open or connected to the central pin. The output pins of these two 74HC4067 would be connected to a pulldown resistor and to the A0 pin of an Arduino (or its equivalent in an ESP-32). In this way, I believe, the potentiometers would remain disconnected from the circuit when their input to the mux was disabled.

The third multiplexer would read the state of the buttons and be connected to pin A1.

The multiplexers would be configured like this:

CD74HC67 mux1 = {A0, {3, 4, 5, 6}, 7} // 0 to 15 CD74HC67 mux2 = {A0, {3, 4, 5, 6}, 8} // 16 to 25 CD74HC67 mux3 = {A1, {3, 4, 5, 6}, 9} // 14 buttons

I intend to assemble a control board and only a bank of 25 potentiometers, 1 fader and 14 buttons for now. With that I can carry out all the tests because it is enough to read the same bank 40 times with a different address...

Excuse me if I'm bothering you. It's the fault of the COVID-19 quarantine... Too much time at home to think...

Greetings from Brazil and thank you very much.

Sylvan72 commented 4 years ago

I can use a microcontroller to manage, read and filter the 1600 controls. Only the changes would be sent to another microcontroller with the Controller-Surface running independently.

I am researching and learning. Instead of limiting myself to using the 120 MIDI addresses of the 16 channels, I will focus on NRPN although nurpins are slower.

tttapa commented 4 years ago

The output pins of these two 74HC4067 would be connected to a pulldown resistor and to the A0 pin of an Arduino (or its equivalent in an ESP-32). In this way, I believe, the potentiometers would remain disconnected from the circuit when their input to the mux was disabled.

Yes, but the ratio between the potentiometer position and the output voltage will be non-linear, you'll lose a large fraction of your ADC's range and precision, both from the voltage drop across the transistors and because of the way your voltage divider is configured.

I can use a microcontroller to manage, read and filter the 1600 controls. Only the changes would be sent to another microcontroller with the Controller-Surface running independently.

You could do that, or you could run Control Surface on all of the microcontrollers, and merge them in a tree structure or linearly, using the MIDI routing functions:

I am researching and learning. Instead of limiting myself to using the 120 MIDI addresses of the 16 channels, I will focus on NRPN although nurpins are slower.

This is not supported out of the box, the standard MIDIAddress type that's used by all MIDI-related classes (CCPotentiometer, CCButton, etc.) can only store 16 virtual cables, 16 channels and 128 addresses. You could of course edit its definition to support 14-bit addresses.

You'll also have to be careful when merging NRPN messages: each NRPN message consists of multiple MIDI messages. You have to finish the entire NRPN transaction before interleaving it with other MIDI messages. The MIDI routing functions support this scenario, but you have to tell the MIDI MIDI parsers to handle NRPN differently, and keep the MIDI route locked. Most of the development for this feature happens on the midi-pipes branch, but it's not finished yet.

Sylvan72 commented 4 years ago

Thank you very much for all the attention, you have already offered me enough information to start a prototype. I will try to inform you if I have news.

Sylvan72 commented 4 years ago

Hi Pieter.

Regarding the non-linearity of the voltage divider in the rheostat configuration (2 terminals) with a pull-down resistor, I deduced how to make it linear and I will try to implement it. I know little about programming, but, if I understand correctly, I must perform the conversion inside "FilteredAnalog.hpp", which is responsible for reading the analog port. Am I wrong?

tttapa commented 4 years ago

You shouldn't edit FilteredAnalog. It will affect all analog inputs. Use the MIDIFilteredAnalog::map function instead.

For example:
https://tttapa.github.io/Control-Surface-doc/Doxygen/d3/d26/CCPotentiometer-Map_8ino-example.html

The mapping function has a signature analog_t (analog_t), the input is (if the default ADC settings are used) a 14-bit number: when the input is at 0V, the input value will be 0, when the input is at 5V (or 3.3V on newer boards), the input value will be 16383. The output should be in the same range as the input.

A simple example that inverts the input:

analog_t mapping_function(analog_t in) {
  return 16383 - in;
}
Sylvan72 commented 4 years ago

The method I deduced to perform the linearization of the rheostat is to calculate and use the resistance momentary value of the potentiometer by the output voltage.

R = (Pull-down resistor * (Vin - Vout)) / Vout

The pull-down resistor would be fixed at 10% of the potentiometer value (in my case 1000 :: 10% of 10000).

I would like to try to implement a function that receives the maximum value of the potentiometer (negative if you want to invert the direction of rotation), the input voltage Vin (if different from the default, type the 4.3v by using a diode or transistor) and the output voltage Vout. That is, for the function to work, it must be inserted at a point where the Vout value is available.

Analyzing the mapping function: 1) Vout will be obtained with getValue () or getRawValue ().

2) If maximum potentiometer value for the "rheostat" is informed, the function will understand that linearization must be applied. This value will be used to obtain the pull-down resistor value (10%) and map the output to the range 0 to 16383.

3) Optionally, inform an input voltage different from the default (valid only for linearization).

4) Sender will return the mapped value (linearized or not).

tttapa commented 4 years ago

Vout will be obtained with getValue () or getRawValue ().

You get Vout as the argument to the mapping function. You don't have to (and can't) call getValue.

Sylvan72 commented 4 years ago

Excuse me. I think I should have written "use the value of getValue () or getRawValue ()".

tttapa commented 4 years ago

I don't understand what you mean. The mapping function is applied before the value is available through getValue. If you use the standard CCPotentiometer classes, there's no need to call getValue yourself.

Sylvan72 commented 4 years ago

CCPotentiometer only provides for the use of potentiometers in the configuration of a voltage divider with three terminals.

The point is: create an array of multiplexed potentiometers in the simplest possible way. By connecting the potentiometers in the rheostat configuration (2 terminals) it is possible to isolate each potentiometer with a diode in central pin, connect it at the input of the multiplexer and to a pull-down resistor at the output. As you mentioned, the value will not be linear. In some cases this can even be useful as it would simulate a logarithmic potentiometer, however, in most cases (Pan for example) linearity is necessary.

For now this is just a study and, to proceed, I needed to find a way to get to a linear exit. I searched the internet and found no reference on how to do it, so I deduced from the voltage divider formula how to calculate the value of the potentiometer, as it is linear and does not require other calculations.

So, I would have to find a point where I could optionally calculate and change the non-linear to linear value (as I said, a non-linear potentiometer can also be useful).

tttapa commented 4 years ago

CCPotentiometer only provides for the use of potentiometers in the configuration of a voltage divider with three terminals.

No it doesn't, you just have to use the mapping function to undo the non-linearity.

Sylvan72 commented 4 years ago

Would I need to enter an equation for this? My knowledge of trigonometry should be less than C++ ... \o/

tttapa commented 4 years ago
#include <Control_Surface.h>

USBDebugMIDI_Interface midi;

CCPotentiometer potentiometer = {
  A0, {MIDI_CC::Channel_Volume, CHANNEL_1}
};

analog_t mappingFunction(analog_t raw) {
    constexpr float R_pd = 1e4;  // Pull-down resistance
    constexpr float R_pot = 5e3; // Total potentiometer resistance
    constexpr analog_t min = 16383 * R_pd / (R_pd + R_pot);
    constexpr uint32_t ratio = 16383 * (R_pd / R_pot);
    if (raw < min) raw = min;
    analog_t lin = ratio * (16383 - raw) / raw;
    if (lin > 16383) lin = 16383;
    return 16383 - lin;
}

void setup() {
    // Add the mapping function to the potentiometer
    potentiometer.map(mappingFunction);
    Control_Surface.begin();
}

void loop() {
    Control_Surface.loop();
}
Sylvan72 commented 4 years ago

Thank you so much.

The conversion algorithm works very well and is exactly what I needed ... You saved me a big headache!

I changed the values of R_pd (1e3) and R_pot (1e4) and tested it with a double potentiometer connected to an Arduino Uno with USB-Midi MocoLufa firmware, one as a rheostat in A0 and the other as a normal voltage divider in A1. The value were close.

Unfortunately, "Life is not a bed of roses"... The A0 entrance is unstable, unlike the A1. I believe that the noise is being amplified and passing through the filtering routines.

Thank you very much in advance.

Sylvan72 commented 4 years ago

I did some tests on PureData and MIDIOX to try to find out where the instability comes from. Just touch the potentiometer for the values to fluctuate.

TIMESTAMP IN PORT STATUS DATA1 DATA2 CHAN NOTE EVENT               
 00000934   1  --     B0    07    51    1  ---  CC: Volume            
 000010F6   1  --     B0    07    50    1  ---  CC: Volume            
 00001110   1  --     B0    07    51    1  ---  CC: Volume            
 0000111D   1  --     B0    07    50    1  ---  CC: Volume            
 00001137   1  --     B0    07    4F    1  ---  CC: Volume            
 0000114C   1  --     B0    07    4E    1  ---  CC: Volume            
 00001155   1  --     B0    07    4F    1  ---  CC: Volume            
 0000115A   1  --     B0    07    50    1  ---  CC: Volume            
 00001160   1  --     B0    07    4F    1  ---  CC: Volume            
 00001170   1  --     B0    07    4E    1  ---  CC: Volume            
 00001174   1  --     B0    07    4F    1  ---  CC: Volume            
 00001174   1  --     B0    07    50    1  ---  CC: Volume            
 00001179   1  --     B0    07    4F    1  ---  CC: Volume            
 0000119D   1  --     B0    07    50    1  ---  CC: Volume            
 000011A4   1  --     B0    07    4F    1  ---  CC: Volume            
 00001217   1  --     B0    07    50    1  ---  CC: Volume            
 00001252   1  --     B0    07    4F    1  ---  CC: Volume            
 0000126A   1  --     B0    07    50    1  ---  CC: Volume            
 000012CE   1  --     B0    07    4F    1  ---  CC: Volume            
 000013A6   1  --     B0    07    50    1  ---  CC: Volume            
 0000162D   1  --     B0    07    51    1  ---  CC: Volume            
 00001B46   1  --     B0    07    50    1  ---  CC: Volume            
 00001B83   1  --     B0    07    51    1  ---  CC: Volume            
 00001DD3   1  --     B0    07    50    1  ---  CC: Volume            
 000020FD   1  --     B0    07    4F    1  ---  CC: Volume            
 00002126   1  --     B0    07    50    1  ---  CC: Volume            
 0000214D   1  --     B0    07    4F    1  ---  CC: Volume            
 0000217A   1  --     B0    07    4E    1  ---  CC: Volume            
 000021A8   1  --     B0    07    4F    1  ---  CC: Volume            
 00002367   1  --     B0    07    50    1  ---  CC: Volume 

Just as a comment. In another test, I ran the program below (without the linearization function):

#include <Control_Surface.h>

USBMIDI_Interface midi;

CCPotentiometer potentiometer = {A0, {MIDI_CC::Channel_Volume, CHANNEL_1}};
CCPotentiometer potentiometer1 = {A1, {MIDI_CC::Channel_Volume, CHANNEL_2}};

void setup() { Control_Surface.begin();}
void loop() { Control_Surface.loop();}

As expected, the result is stable. However, I noticed something on PureData when I disconnected the wire from potentiometer A1: input A0 that should only control Channel_Volume of Channel 1 also affected the same control as Channel 2.

I turned everything off, redo the connections and the same thing happened when switching to A1, because the control of A0 varied together.

I'm only going over what happened because it may be of interest to you.

tttapa commented 4 years ago

The linearization function only uses a part of the ADC range, so you lose some precision, and noise is amplified.

You can experiment with the different parameters of the FilteredAnalog class:

Note that the maximum value used in your mapping function will change depending on the settings you use for the FilteredAnalog. You can use FilteredAnalog::getMaxRawValue to replace the hard-coded 16383. (This function was added just now, so you'll have to update to the latest master version of the library.)