Open giubu opened 4 years ago
Hi,
Could you post a link to the other issue as well as the code you're using?
There's a MappingFunction example here, which admittedly doesn't do a great job at explaining how to actually use the MappingFunction.
The FilteredAnalog class reads an analog value from an analog pin, and converts it to a value in a range from 0 to 2¹⁴-1 or [0, 16383]. Then you can specify a mapping function that maps this analog value from the range [0, 16383] to any other value in the same range.
The simplest example is just inverting the value, i.e. mapping the range [0→16383] to the range [16383→0]:
// Create a filtered analog object on pin A0, with the default settings:
FilteredAnalog<> analog = A0;
CCPotentiometer potentiometer = {A0, MIDI_CC::Channel_Volume};
// Define a mapping function that inverts the analog value
analog_t myMappingFunction(analog_t raw) {
return 16383 - raw;
}
void setup() {
// other setup
analog.map(myMappingFunction); // tell the analog input to apply your mapping function to all measurements
potentiometer.map(myMappingFunction); // you can do the same for analog MIDI elements
}
hi thanks for replying so quickly the link to other issue https://github.com/tttapa/Control-Surface/issues/55#
code
#include <Control_Surface.h>
USBMIDI_Interface midi;
CD74HC4067 mux1 = {
A0, // analog pin
{7, 15, 14, 16}, // Address pins S0, S1, S2, S3
};
class NotePot : public MIDIOutputElementPotentiometer {
public:
NotePot(pin_t analogPin, MIDICNChannelAddress midinote)
: analog(analogPin), midinote(midinote) {}
void begin() override {}
void update() override {
if (analog.update()) {
uint8_t newValue = analog.getValue();
if (newValue > 10) {
if (noteOnSent) {
midi.sendKP(midinote, newValue); // MIDI Key Pressure
} else {
midi.sendNoteOn(midinote, newValue);
noteOnSent = true;
}
} else {
midi.sendNoteOff(midinote, 0x7F);
noteOnSent = false;
}
}
}
void map(MappingFunction fn) { analog.map(fn); }
void invert() { analog.invert(); }
private:
FilteredAnalog<7> analog;
MIDICNChannelAddress midinote;
bool noteOnSent = false;
};
using namespace MIDI_Notes;
NotePot notepots[] = {
{mux1.pin(0), {note(C, 4), CHANNEL_1}},
{mux1.pin(1), {note(Db, 4), CHANNEL_1}},
{mux1.pin(2), {note(D, 4), CHANNEL_1}},
{mux1.pin(3), {note(Eb, 4), CHANNEL_1}},
// Etc.
};
void setup() {
Control_Surface.begin();
// mux1.begin(); // Initialize multiplexer
}
void loop() {
Control_Surface.loop();
}
this works for the pads, only the range I get from analogread is 0-400, so the volume is low and the dynamic range is too small to be useful.
You could try something like this:
#include <Control_Surface.h>
USBMIDI_Interface midi;
CD74HC4067 mux1 = {
A0, // analog pin
{7, 15, 14, 16}, // Address pins S0, S1, S2, S3
};
class NotePot : public MIDIOutputElementPotentiometer {
// ...
};
using namespace MIDI_Notes;
NotePot notepots[] = {
{mux1.pin(0), {note(C, 4), CHANNEL_1}},
{mux1.pin(1), {note(Db, 4), CHANNEL_1}},
{mux1.pin(2), {note(D, 4), CHANNEL_1}},
{mux1.pin(3), {note(Eb, 4), CHANNEL_1}},
// Etc.
};
// Specify the pressure value that you want to map to the maximum velocity.
// This is the `analogRead` value in 10-bit range [0, 1023].
const uint16_t maxPressureValue = 400;
// Convert this value to the 14-bit range used by FilteredAnalog.
const analog_t maxRawFilteredAnalogValue =
increaseBitDepth<14, 10, analog_t>(maxPressureValue);
analog_t myMappingFunction(analog_t raw) {
// First constrain the value
if (raw > maxRawFilteredAnalogValue)
raw = maxRawFilteredAnalogValue;
// Then map it from the range [0, maxRawFilteredAnalogValue]
// to the range [0, 16383].
return (uint32_t)raw * 16383 / maxRawFilteredAnalogValue;
// (Be careful for overflow, raw is a 14-bit number, and so is
// 16383, so multiplying them results in a 28-bit number.
// That's why you need a 32-bit intermediate result.
}
void setup() {
Control_Surface.begin();
for (auto ¬epot : notepots) // for every element of the `notepots` array
notepot.map(myMappingFunction); // apply the mapping function to it
}
void loop() {
Control_Surface.loop();
}
Hi there and thanks for the code.
I really wish I knew how to do this myself so I don't have to bother you so much,
but my programming skills are very limited, the best I can do is read code and try to understand what is happening in order to modify it to suit my needs...
So, a few points about the code, and I really hope this can help other people trying to do something similar to what I am trying to do.
The mapping function seems to work, in the sense that I can now play notes with louder velocities.
However, it will only play at the velocity specified in
if (newValue > 10) {
part of the code +1, so in this case it will only play notes at velocity 11.
On the MIDI monitor( I am on Ubuntu), it shows a constant flow of notes off coming in when I am not pressing anything, and a bunch of Key pressure change (aftertouch) without a value assigned to them when I do press a pad.
If I am pressing a pad, the note plays at the specific velocity dictated by that value and turns off when I release the pad, however if I press anothe pad ( in polyphonic mode), the new note will be triggered constantly on and off.
what I think needs to happen in the code (again, I really wish I could do this myself without bothering you), is the following: 1 Detect pad being touched ( there should be a threshold value to avoid noise around 0, the pads are very sensitive) 2 Wait a very short time ( say between 2 and 5 millis) and read the value again. 3 Calculate the integral of the value over time and map that to the velocity of the note. 4 read the actual value of the pressure again and assign it to the aftertouch so that the volume can be modulated by changing the pressure of the pad ( say like a wind instrument). 5 send a note off when the value goes below a specified threshold.
Could you recommend a starting point to learn how to code that, or is that something not achievable for somebody new to this world? thank you again for your time and attention, I am sending you nothing but good Karma since I found out about this...
I think you should be able to fix the constant stream of note offs by adding an extra if statement:
if (analog.update()) {
uint8_t newValue = analog.getValue();
if (newValue > 10) {
if (noteOnSent) {
midi.sendKP(midinote, newValue); // MIDI Key Pressure
} else {
midi.sendNoteOn(midinote, newValue);
noteOnSent = true;
}
} else if(noteOnSent) {
midi.sendNoteOff(midinote, 0x7F);
noteOnSent = false;
}
}
I'm not sure what you mean by "Key pressure change (aftertouch) without a value assigned to them"? What MIDI monitor and synthesizer are you using?
If I am pressing a pad, the note plays at the specific velocity dictated by that value and turns off when I release the pad, however if I press anothe pad ( in polyphonic mode), the new note will be triggered constantly on and off.
I don't really know why this happens. Does the Arduino send note off and on events for the note, or is it the synth that turns on and off the note?
what I think needs to happen in the code (again, I really wish I could do this myself without bothering you), is the following: 1 Detect pad being touched ( there should be a threshold value to avoid noise around 0, the pads are very sensitive) 2 Wait a very short time ( say between 2 and 5 millis) and read the value again. 3 Calculate the integral of the value over time and map that to the velocity of the note. 4 read the actual value of the pressure again and assign it to the aftertouch so that the volume can be modulated by changing the pressure of the pad ( say like a wind instrument). 5 send a note off when the value goes below a specified threshold.
Could you recommend a starting point to learn how to code that, or is that something not achievable for somebody new to this world?
That could work.
I don't really have the time or hardware to write code for it right now, but it's definitely doable, even if you're new.
For timing 2 to 5 milliseconds, you can either use the millis
or micros
functions (google for "blink without delay"), or you can use the Timer<micros>
class. It'll return true after the given number of microseconds (unlike the delay
function, the Timer
class doesn't block the execution). You can reset the timer by using timer.begin()
. Here's an example.
If you don't explicitly reset the timer, it'll just start counting again. For example Timer<micros> timer = 5000
is a timer that returns true every 5 ms.
It's also useful to know what analog.update()
does exactly:
It reads the analog input (using analogRead
internally), it applies the filters and the mapping function, and then it converts the raw 14-bit value to a 7-bit value (because MIDI velocities are 7-bit).
If the 7-bit value is the same as the previous value, the update
function will return false. If it's different from the value that was measured during the previous call to update
, the function will return true. You use analog.getValue()
to get this 7-bit value to use it in your code.
Feel free to post your attempts when you get stuck, good luck!
Hi, thanks again, your code fixed the note off problem I am using GMIDImonitor on ALSA and Yoshimi as a synth and things didn't go too well. I tried in Reaper, and things worked much better. I changed sendKP to sendCC volume and It's usable in terms of dynamics. I modified the code this way using your suggestions ( or rather what I can understand from it at this point)
void begin() override {}
void update() override {
if (analog.update()) {
uint8_t newValue = analog.getValue();
uint8_t velocity = analog.getValue();
if (newValue > 20) {
if (noteOnSent) {
midi.sendKP(midinote, newValue); // MIDI Key Pressure
} else {
Timer<micros> timer = 5000;
timer.begin();
if (timer){
uint8_t velocity = analog.getValue();
midi.sendNoteOn(midinote, velocity);
noteOnSent = true;
}
}
} else if(noteOnSent) {
midi.sendNoteOff(midinote, 0x7F);
noteOnSent = false;
}
}
}
in this way I can get a velocity of 40/45 if I really slam on the pad, not really what I want to do but at least I know something is happening in the velocity. I think I am not implementing the timer right, because no matter how much I increase it the behaviour doesn't change.
I then tried to create a function to return the difference between the velocity before and after the delay and map it to a value between 0 and 127 ` if(noteOnSent) {
midi.sendKP(midinote, newValue); // MIDI Key Pressure
} else {
Timer<micros> timer = 5000;
timer.begin();
if (timer){
uint8_t velocity = analog.getValue();
uint8_t mapvel= velocity-newValue;
mapvel=map(mapvel,0,20,0,127);
midi.sendNoteOn(midinote, velocity);
noteOnSent = true;
}
}
} else if(noteOnSent) {
midi.sendNoteOff(midinote, 0x7F);
noteOnSent = false;
` but it seems like it won't let me map a value the usual way. it returns
exit status 1 no matching function for call to 'NotePot::map(uint8_t&, int, int, int, int)'
and I have no idea what that even means.... I guess I'll wait until my brain cools off a little and I'll give it another go. of course any suggestions are more that welcome. thank you again
Timer<micros> timer = 5000;
timer.begin();
if (timer){
This won't work because the timer is a local variable, it's created and deleted every time the update
function runs, which is not what you want, you want to remember the time across updates.
timer
should be a member variable of the NotePot
class.
Secondly, you're initializing/resetting the timer to 0, and then you immediately check if it has fired yet, which is impossible, because there will never be 5 milliseconds between timer.begin()
and if (timer)
.
hmm, I thought I would introduce some sort of delay between reading the 2 values, but I guess not. I don't understand why it still sends a note though, even if timer is not true. I'll keep trying and let you know
I don't understand why it still sends a note though, even if timer is not true.
That's a good question.
Is it because I declare velocity 2 times maybe?
The problem with the map function is that there are two map functions at play here: the NotePot::map
member function, and the built-in Arduino map
function. You have to use mapvel = ::map(mapvel,0,20,0,127)
to specifically select the Arduino map in the global namespace (::
), not the one in the NotePot::
namespace.
Is it because I declare velocity 2 times maybe?
The second velocity is unnecessary, but it shouldn't be an issue, as far as I can see.
ok, I will work on this code until I get it working or get ultimately stuck...
meanwhile, sendKP sends a monophonic aftertouch, is it possible to make it polyphonic.
I looked in the modules, but cannot find any sender for this,
I tried
midi.sendOnCable(0xA0, MIDICNChannelAddress, midinote, newValue);
but get
expected primary-expression before ',' token
Most probably the syntax is wrong.
meanwhile, sendKP sends a monophonic aftertouch, is it possible to make it polyphonic.
sendKP
does send a polyphonic aftertouch. sendCP
(send Channel Pressure) sends a monophonic aftertouch.
You can check the implementation of sendKP
to see that it does in fact send a key pressure event, i.e. a status byte of 0xA*.
sendOnCable
expects 5 bytes, the message type m
(as a the high nibble), the MIDI channel c
(from 1 to 16), two data bytes d1
and d2
, and the cable number cn
.
sorry friend, I think the soldering fumes are getting to my head...
Hi there, so I have finally put everything together and I'm back to struggling writing this code now. I am making some progress, but I would like to ask you a question. it seems to me that analog.update() has to be called with an if statement otherwise that piece of code is ignored. Am I wrong in this assumption? If so, could you explain how to work with it? many thanks
analog.update()
does a couple of things:
analogRead(pin)
analog.update()
was called, and returns true if it changed, false if it didn'tUsually, you don't need to take any action if the analog value didn't change, so that's why if (analog.update()) { /* do something */ }
is often used.
You can find an example here: https://tttapa.github.io/Arduino-Helpers/Doxygen/d3/dbe/1_8FilteredAnalog_8ino-example.html
ok, so does analog.getValue() retrieve the stored value from analog.update or does it analogRead it at the time it is called in the code?
getValue
only retrieves the value that was read when update
was last called. getValue
doesn't call analogRead
.
that explains a lot, thank you so much!
Still something strange is happening and I cannot explain why...
class NotePot : public MIDIOutputElementPotentiometer {
public:
NotePot(pin_t analogPin, MIDICNChannelAddress midinote)
: analog(analogPin), midinote(midinote) {}
int PadCState = 0; // Current state of the Pad
int PadPState = 0; // Previous state of the Pad
int PadVar = 0; // Difference between the current and previous state of the Pad
const int SenseThresh = 1000; //* Amount of time the Pad will be read after it exceeds the varThreshold
const int PadThreshold = 20; //* Threshold for the Pad signal variation
bool PadMoving = false; // If the Pad is moving
unsigned long PTime = 0; // Previously stored time
unsigned long timer = 0; // Stores the time that has elapsed since the timer was reset
bool noteOnSent = false;
void begin() override {}
void update() override {
if (analog.update()) {
PadCState = analog.getValue(); // Mapping
PadCState = 127 - PadCState; //
PadCState =::map(PadCState, 5, 35, 0, 127); //
PadCState = constrain(PadCState, 0, 127); //
PadVar = abs(PadCState - PadPState); // Calculates the absolute value between the difference between the current and previous state of the Pad
if (PadVar > PadThreshold) {// Opens the gate if the Pad variation is greater than the threshold
// Serial.print(PadVar);
// Serial.println();
PTime= millis();
}
timer= millis()-PTime; // zero the timer
if (timer>SenseThresh){
Serial.print("pad pressed");
Serial.println();
PadMoving = true;
}
}
}
private:
FilteredAnalog<7> analog;
MIDICNChannelAddress midinote;
};
this should either not work or print pad pressed when I hold it down for a second. instead it prints pad pressed as soon as I touch the pad.
Could it be that PadVar > PadThreshold
never happens, so PTime
is still zero, and therefore millis()-PTime>SenseThres
will always be true?
if I uncomment
// Serial.print(PadVar);
// Serial.println();
then it responds as it should, giving me a value for PadVar always greater than PadThreshold
So, this is what I have so far
#include <Control_Surface.h>
USBMIDI_Interface midi;
// Instantiate a multiplexer
CD74HC4067 mux1 = {
A0, // analog pin
{7, 3, 16, 2}, // Address pins S0, S1, S2, S3
// 7, // Optionally, specify the enable pin
};
class Pad : public MIDIOutputElementPotentiometer {
public:
Pad(pin_t analogPin, MIDICNChannelAddress midinote)
: analog(analogPin), midinote(midinote) {}
uint8_t PadCState = 0; // Current value of the Pad
uint8_t PadPState = 0; // Previous value of the Pad
int CPadVar = 0; // Current Variation between the current and previous value of the Pad
int PPadVar = 0; // Previous Variation between the current and previous value of the Pad
int PadVarDiff = 0; // Difference between the current and previous value of the Pad Variation
int velocity = 0; // Pad Velocity
const int PadThreshold = 20; // Threshold for sending note
bool PadPressed = false; // If the Pad is pressed
bool noteOnSent = false; // If noteOn is sent
void begin() override {}
void update() override {
if (analog.update()) {
PadCState = analog.getValue();
PadCState = 127 - PadCState; //invert
// Serial.print( PadCState);
// Serial.println();
PadCState =::map(PadCState, 0, 40, 0, 127); //map
// Serial.print( PadCState);
// Serial.println();
PadCState = constrain(PadCState, 0, 127); //constrain
// Serial.print( PadCState);
// Serial.println();
CPadVar = (PadCState - PadPState); //Current Variation in Pad Value
PadPState = PadCState; //Set Current Pad Value to Previous Pad Value
PadVarDiff = CPadVar - PPadVar; //Difference in Pad Variation
PPadVar = CPadVar; //Set Current Pad Variation to Previous Pad Variation
// Serial.print( PadVarDiff);
// Serial.println();
velocity =::map(CPadVar, 0, 10, 0, 127); //map velocity from Pad variation (stronger hit==bigger variation)
velocity = constrain(velocity, 0, 127); //constrain velocity
// Serial.print(velocity);
// Serial.println();
if (CPadVar > PadThreshold ) { // If Pad variation is greater than the threshold
if ( PadPressed = false ) {
PadPressed = true; // Pad is pressed
}
}
if ( PadPressed = true) { // If Pad is pressed and
if (PadVarDiff < 0) { // if the Difference in Variation of Signal is negative (reached the peak)
PadPressed = false;
if (noteOnSent) { // if noteOn is already sent
midi.sendKP(midinote, velocity); // send MIDI Key Pressure
} else {
midi.sendNoteOn(midinote, velocity); // otherwise send Noteon
noteOnSent = true;
//
//Serial.print("note on");
//Serial.print(velocity);
//Serial.println();
}
}
}
if (PadCState < PadThreshold && PadPressed) { // If pad is being released
if (noteOnSent) { // and a noteOn has been sent
midi.sendNoteOff(midinote, 0x7F); // send a NoteOff
noteOnSent = false; // Reset NoteOnSent
}
}
}
}
private:
FilteredAnalog<7> analog;
MIDICNChannelAddress midinote;
};
using namespace MIDI_Notes;
Pad pads[] = {
{mux1.pin(0), {note(C, 4), CHANNEL_1}},
{mux1.pin(1), {note(Db, 4), CHANNEL_1}},
{mux1.pin(2), {note(D, 4), CHANNEL_1}},
{mux1.pin(3), {note(Eb, 4), CHANNEL_1}},
{mux1.pin(4), {note(E, 4), CHANNEL_1}},
{mux1.pin(5), {note(F, 4), CHANNEL_1}},
{mux1.pin(6), {note(Gb, 4), CHANNEL_1}},
{mux1.pin(7), {note(G, 4), CHANNEL_1}},
{mux1.pin(8), {note(Ab, 4), CHANNEL_1}},
{mux1.pin(9), {note(A, 4), CHANNEL_1}},
{mux1.pin(10), {note(Bb, 4), CHANNEL_1}},
{mux1.pin(11), {note(B, 4), CHANNEL_1}},
{mux1.pin(12), {note(C, 5), CHANNEL_1}},
{mux1.pin(13), {note(Db, 5), CHANNEL_1}},
{mux1.pin(14), {note(D, 5), CHANNEL_1}},
{mux1.pin(15), {note(Eb, 5), CHANNEL_1}},
// Etc.
};
void setup() {
Control_Surface.begin();
}
void loop() {
Control_Surface.loop();
}
It works pretty good, very responsive and sensitive to the touch. a few things I have to do, but I still have to figure out how to them right. I am experiencing double hits, I think it's because I cannot cutoff the bottom end of the values using the arduino mapping method. If I try to map PadCState from say 5, I get very weird results, I don't know what is happening there, I think it might have something to do with what type of integer I am using. Maybe doing all the maths on the raw value and then map it 0-127 would be more effective. Also because the readings happen very fast, the signal variation has a small range, therefore the mapped velocity has 4 or 5 discrete steps in value. I should try and implement a timer to get a value every millisecond, but I have to figure out how to make it work. I'm very curious to see what you think of it, of course when you have the time. Meanwhile I will enjoy it as it is, and improve little by little... it is really a pleasure to jam on it. thank you so much for the library and the help you give out so generously.
Thanks for posting your code. Unfortunately, I don't have the hardware (or time) to wire it up and experiment with it myself.
I think using a timer to determine the velocity would be a good idea. E.g. start it when the pressure goes over a first threshold, wait for it to pass a second (higher) threshold, and measure the elapsed time between them. The shorter the time passed, the higher the velocity. You could also measure the value after a fixed amount of time.
To fix the precision, you could indeed map the raw value to get PadCState.
Something like this, perhaps:
void begin() override {
analog.map([](analog_t raw) -> analog_t {
analog_t PadCState = 16383 - raw; // invert
PadCState = constrain(PadCState, 0, 16383); // constrain
PadCState =::map(PadCState, 0, 40 * 128, 0, 16383); // map
return PadCState;
});
}
If you do this, you no longer need the invert, map, constrain steps in your update function, because the value returned by analog.getValue()
will already be corrected by the mapping function.
That seems to work better,
however if I
PadCState =::map(PadCState, 5*128, 40 * 128, 0, 16383);
to try and remove noise the Monitor shows values of either 0 or 255 for the pad at rest.
that's the same behaviour I had with my code, and I don't know why it's happening...
This is probably because the value becomes negative if the input is below 5*128, it's best to constrain the value before mapping.
My previous code had a mistake in the constraint as well.
This seems to work well for me:
analog.map([](analog_t raw) -> analog_t {
analog_t PadCState = 16383 - raw; // invert
PadCState = constrain(PadCState, 5*128, 40 * 128); // constrain
PadCState = ::map(PadCState, 5*128, 40 * 128, 0, 16383); // map
return PadCState;
});
So, I did this
void begin() override {
analog.map([](analog_t raw) -> analog_t {
constexpr analog_t minimumValue = 750;
constexpr analog_t maximumValue = 2500;
raw= 16383 - raw; // invert
raw = constrain(raw, minimumValue, maximumValue);
analog_t PadCState = map(raw, minimumValue, maximumValue, 0, 16383);
return PadCState;
});
and it works really well, I can fine tune the values to get maximum range and no spontaneous events. If I wanted to map a specific pad, say pad 12, how would I go about it? something like
analog.map([12](analog_t raw) -> analog_t {
???
I tried to follow your suggestion regarding the timer, but I cannot get it to work.
I do
if (PadCState > PadThreshold ) { // If Pad variation is greater than the threshold
if ( PadPressed = false ) {
PadPressed = true; // Pad is pressed
HitTime=millis();
}
}
if ( PadPressed = true) { // If Pad is pressed and
if (PadVarDiff<0) { // if the Difference in Variation of Signal is negative (reached the peak)
// PadPressed = false;
VelTime=millis()- HitTime;
but both HitTime and VelTime show on the monitor as always 0. that's weird, no?
The []
is the capture list of a lambda expression. Writing [12]
is meaningless in this context.
You cannot capture any variables in the lambda either, since it has to decay to a function pointer when passing it to the map
function.
Do you mean you want a different minimumValue
and maximumValue
for each pad?
I hadn't noticed before, but PadPressed = false
is an assignment, not a comparison. You need PadPressed == false
.
Do you mean you want a different minimumValue and maximumValue for each pad?
that would be perfect. thanks for checking the code, of course you are right, adding a = solved the timing problem. so I implemented it, but it doesn't really reflect the intended velocity. I can press the pad quickly but not with force to get a quiet note, but it would come out as a loud note, and viceversa. I think measuring the variation between the moment you press and the time you stop pressing, but still keep your finger on, will give the most accurate results in terms of playability. the way I do it now, triggering the note when the variation becomes negative, it's a little bit cheating, because that activates when you slightly release the pad. but the readings happen so fast that I cannot trigger it when it's 0 ( which would be theoretically ideal), becasue as the signal is ramping up there are also readings of 0 in between values. I have to figure out a way to return either the maximum variation or to actually read the pad at more discrete intervals.
Sorry to bother you, but have you noticed that timer doesn't function properly under 50 millis? It's very possible I am doing something wrong, but in any way I try yo use it , it will only work if I give it values greater than 50. By the way I have figured out how to map every pad independently, it seems like you only have to figure out how to work with this library, then you can do pretty much anything you want... You must be on some serious genius level...
I made the mapping function generic, so you can pass any functor to it:
#include <Control_Surface.h>
class Pad : public MIDIOutputElementPotentiometer {
public:
struct Mapper {
analog_t minimumValue;
analog_t maximumValue;
analog_t operator()(analog_t raw) {
raw= 16383 - raw; // invert
raw = constrain(raw, minimumValue, maximumValue);
analog_t mapped = ::map(raw, minimumValue, maximumValue, 0, 16383);
return mapped;
}
};
public:
Pad(pin_t analogPin, MIDIAddress midinote, Mapper mapper = {750, 2500})
: analog(analogPin, mapper), midinote(midinote) {}
void begin() override {}
void update() override {
if (analog.update()) {
analog_t PadCState = analog.getValue();
// ...
}
}
void map(Mapper mapper) { analog.map(mapper); }
private:
GenericFilteredAnalog<Mapper, 7> analog;
MIDIAddress midinote;
};
Pad testpad1 = {A0, 0x10, Pad::Mapper{750, 2500}};
Pad testpad2 = {A0, 0x10, {750, 2500}};
void setup() {
testpad1.map({700, 2600});
}
void loop() {}
Note how FilteredAnalog<7>
has been replaced with GenericFilteredAnalog<Mapper, 7>
. The difference is that FilteredAnalog
only allows using mapping functions (just the function, no additional data like minimum and maximum values), while GenericFilteredAnalog
can use anything as a mapper function, including functors (i.e. structs/classes with a call operator so they can be called like a function). Now that the mapping function is an object (including data), you can include a minimum and a maximum value for each analog object.
I haven't merged this into master yet, so you'll have to check out the generic-filtered-analog
branch. (ZIP download)
You have to remove the other version(s) of Control Surface you have in your ~/Arduino/libraries
folder.
but have you noticed that timer doesn't function properly under 50 millis? It's very possible I am doing something wrong, but in any way I try yo use it , it will only work if I give it values greater than 50.
I don't have an explanation. How did you test this?
There is a (deliberate) update rate limit for potentiometers, but this is 1 kHz, so nowhere near 50 ms.
You can disable this limit by inheriting from MIDIOutputElement
instead of MIDIOutputElementPotentiometer
, then it'll call the update
method as fast as possible.
what I did to map every pad is
analog_t MappingFunction0(analog_t raw) {
constexpr analog_t minimumValue = 75;
constexpr analog_t maximumValue = 1750;
raw = 16383 - raw; // invert
raw = constrain(raw, minimumValue, maximumValue);
analog_t PadCState = map(raw, minimumValue, maximumValue, 0, 16383);
return PadCState;
}
analog_t MappingFunction1(analog_t raw) {
constexpr analog_t minimumValue = 100;
constexpr analog_t maximumValue = 2000;
raw = 16383 - raw; // invert
raw = constrain(raw, minimumValue, maximumValue);
analog_t PadCState = map(raw, minimumValue, maximumValue, 0, 16383);
return PadCState;
}......
for every pad and then
void setup() {
Control_Surface.begin();
notepots[0].map(MappingFunction0);
notepots[1].map(MappingFunction1);
notepots[2].map(MappingFunction2);
notepots[3].map(MappingFunction3);
notepots[4].map(MappingFunction4);
notepots[5].map(MappingFunction5);
notepots[6].map(MappingFunction6);
notepots[7].map(MappingFunction7);
notepots[8].map(MappingFunction8);
notepots[9].map(MappingFunction9);
notepots[10].map(MappingFunction10);
notepots[11].map(MappingFunction11);
notepots[12].map(MappingFunction12);
notepots[13].map(MappingFunction13);
notepots[14].map(MappingFunction14);
notepots[15].map(MappingFunction15);
}
it works pretty good and it's easy to modify as I play. But I will definitely implement your solution as it is much more elegant and compact, and give you some feedback. Regarding the timer, I used your blink without delay example in my code and tried different values, under 50 it seems to just ignore the timer altogether. I also tried to implement it in the Pad class, with the same results. I think maybe that's why I am having trouble measuring the time interval, it must be definitely less than 50ms so it returns 0. changing MIDIOutputElementPotentiometer to MIDIOutputElement made things worst, as now a value of 100ms will return constant 100ms interval between readings, as I decrease the value it gets progressively skewed, like a value of 70 will give 30 or 40 ms intervals until a value of 50, where it again ignores it. but using MIDIOutputElement has made the pads even more fast and responsive to the touch.
I'm unable to reproduce this problem. The only explanation I can imagine is that your main loop takes more than 50 ms.
You can verify that Timer itself works without any problems:
#include <Control_Surface.h>
void setup() {
Serial.begin(115200);
volatile unsigned long t0 = micros();
unsigned long counter = 0;
Timer<millis> timer = 1;
while (counter <= 10000) {
if (timer)
++counter;
}
volatile unsigned long t1 = micros();
Serial.println(t1 - t0);
}
void loop() {}
I've verified that this prints 9999708, which is very close to the expected duration of 10'000'000 µs.
Yes, that does indeed work, but if you run the exact same code in loop, which is what we want I think, I experience the same behavior , going under 50 ms or in this case reducing the count to 50 or lower, gives out inconsistent results. the values returned are correct, but the timestamps show multiple results happening at the same time and the interval between outputs are not consistent with the input.
I'm unable to reproduce that. Could you post a minimal example that demonstrates this problem? I'm guessing something else is the bottleneck here, such as a slow Serial baud rate or too many print statements or too many MIDI messages at a time.
What board are you using?
As you can see, there's nothing special about the implementation of Timer
#include <Control_Surface.h>
void setup() {
}
void loop() {
Serial.begin(115200);
volatile unsigned long t0 = micros();
unsigned long counter = 0;
Timer<millis> timer = 1;
while (counter <= 50) {
if (timer)
++counter;
}
volatile unsigned long t1 = micros();
Serial.println(t1 - t0);
}
output
16:37:08.604 -> 49956
16:37:08.670 -> 49956
16:37:08.703 -> 49952
16:37:08.770 -> 48932
16:37:08.803 -> 49956
16:37:08.869 -> 49956
16:37:08.954 -> 49956
16:37:08.967 -> 49956
16:37:08.997 -> 49952
16:37:09.069 -> 48932
16:37:09.102 -> 49956
16:37:09.169 -> 49956
16:37:09.202 -> 49956
#include <Control_Surface.h>
void setup() {
}
void loop() {
Serial.begin(115200);
volatile unsigned long t0 = micros();
unsigned long counter = 0;
Timer<millis> timer = 1;
while (counter <= 20) {
if (timer)
++counter;
}
volatile unsigned long t1 = micros();
Serial.println(t1 - t0);
}
output
16:39:47.027 -> 19236
16:39:47.060 -> 20264
16:39:47.093 -> 19236
16:39:47.093 -> 20260
16:39:47.126 -> 20260
16:39:47.126 -> 19236
16:39:47.161 -> 20260
16:39:47.193 -> 19232
16:39:47.193 -> 20264
16:39:47.226 -> 19232
16:39:47.226 -> 20264
16:39:47.259 -> 19232
16:39:47.292 -> 20260
and so forth, getting worst and worst as time is reduced. I am using a pro micro, knock off. I am curious to see if you run the same code what results you get from the monitor.
I get
11:37:47.713 -> 18660
11:37:47.746 -> 19684
11:37:47.746 -> 18664
11:37:47.780 -> 19680
11:37:47.780 -> 19684
11:37:47.813 -> 18660
11:37:47.846 -> 19684
11:37:47.846 -> 18660
11:37:47.879 -> 19684
11:37:47.879 -> 18660
11:37:47.913 -> 19684
11:37:47.946 -> 18660
11:37:47.946 -> 19684
11:37:47.979 -> 18660
11:37:47.979 -> 19684
Which is about what you'd expect given the resolution of the millis
function.
What do you expect to happen differently?
Also, Serial.begin belongs in the setup, not in the loop.
I would expect events to happen at intervals of roughly 20ms, but as you can see that's clearly not happening. maybe it's just my ignorance and that's perfectly normal, but to me it's a strange behaviour.
Do you mean the timestamps in the serial monitor? These are measured by the computer, and they are not reliable. The serial monitor is slow, and data is buffered in different places before it is printed. The micros
values that are printed tell you what's actually going on on the Arduino.
You can verify this by printing the actual time when a timer fires on the Arduino:
#include <Control_Surface.h>
void setup() {
Serial.begin(115200);
}
void loop() {
char buffer[300];
char *write = buffer;
unsigned long counter = 0;
Timer<millis> timer = 1;
while (counter <= 20) {
if (timer) {
write += sprintf(write, "%8lu µs\n", micros());
++counter;
}
}
Serial.println(buffer);
Serial.flush();
for (;;);
}
Printing is done at the end, because it can interfere with the timing.
Output:
12 µs
1032 µs
2056 µs
3080 µs
4104 µs
5128 µs
6152 µs
7176 µs
8200 µs
9220 µs
10248 µs
11272 µs
12296 µs
13320 µs
14344 µs
15368 µs
16392 µs
17416 µs
18440 µs
19464 µs
20488 µs
You can see that it's accurate to about 0.5 ms. This is because the millis
function is less accurate than a millisecond. You can fix this by using a micros timer:
Timer<micros> timer = 1000;
24 µs
1020 µs
2016 µs
3020 µs
4016 µs
5020 µs
6016 µs
7020 µs
8016 µs
9020 µs
10016 µs
11016 µs
12016 µs
13016 µs
14016 µs
15016 µs
16016 µs
17016 µs
18020 µs
19020 µs
20020 µs
The resolution of the micros
function is about 4 µs on an Arduino UNO/Pro Mini. The timer is started at 20 µs, the first event fires immediately afterwards, and all subsequent events happen 1 ms ± 4 µs later.
and here is me relying on the time stamps to calculate things with accuracy.... thank you so much for yet another coding lesson, I will surely put it to good use. Also, I am very sorry if I make you waste your time, I hope in some way you can find this helpful too. thank you again, I'll post the finished code , so maybe this can help somebody else too.
It would be very cool to add a touch sensitive class to ESP32, it has 10 capacitive touch pins that can read a variable number. Such number could activate note On messages followed by velocity or pressure messages and Note OFF on release. Just like the classic Bochla Easel Synth. They can be used as pads also. No hardware is necessary, just a metal surface connected to the pin. The code is touchRead(GPIO);
This shouldn't be too hard to implement yourself, you can start from this example: https://tttapa.github.io/Control-Surface-doc/Doxygen/d4/d3b/Custom-MIDI-Output-Element_8ino-example.html
Capacitive touch support is on my todo list, but it's not that high up, and I'm afraid I won't have much time for it right now.
Okay, I'll give a try, if I come up with someting, I let you know.
Hi there, thanks so much for creating this library, it's exactly what I was looking for to build the magic sound and light box for my 2 year old son ( and me). I made some pressure sensitivity pads, they work pretty good using a code you posted on another issue. the problem I have, and it's totally due to my stupidness, is that I cannot find a way to remap the analog value, I've been banging my head on it for a few weeks now but to no avail. I know there is MappingFunction, but I cannot understand how to use it, and I cannot find any information to help me do that. It would be great if I could remap each individually, they are all hooked up on 74HC4067 multiplexer.
thank you in advance for any input you can give us.