Maryland, USA
Offline
Jr. Member
Karma: 0
Posts: 79
|
 |
« on: November 29, 2012, 01:43:34 pm » |
Hello All, I am not sure if my problem has a hardware or software solution but any help will be appreciated. I am experimenting with the Modern Devices Wind Sensor: http://shop.moderndevice.com/products/wind-sensorin the attempt to make a midi wind controller with Arduino. The sensor is very responsive and has a nice resolution BUT, a very slow decay time. In other words, when you stop blowing, the output takes several second to reach baseline. If I take the output at face value, my instrument would keep playing for several seconds after I stop blowing. I am attaching a graph I obtained by plotting analog read vs. time. In the upper graph I gave four quick "pulse" breaths, in the lower one I did a sudden start, steady blow for a few seconds and sudden stop. As you can see, in either case it takes up to 10-15 seconds to go back to baseline. The topic has been discussed a few month ago @ modern devices forum but without a real solution, and my fiddling with software (since I am a total newbie when it comes to electronics) didn't carry me very far. Since I suspect this is hardly a unique problem, I wonder if somebody can point me to a software algorithm or an hardware solution to obtain nice square waves (sharp attack, sustain and, what's missing here, a sharp release). Thank you
|
|
|
|
|
Logged
|
There are three kind of people in the world: Those who can count, and those who can't
|
|
|
|
Netherlands
Offline
Tesla Member
Karma: 90
Posts: 9401
In theory there is no difference between theory and practice, however in practice there are many...
|
 |
« Reply #1 on: November 29, 2012, 02:12:12 pm » |
You must determine the first derivative (steepness and direction of the graph) and use that to make your decisions in the code. That way you do not need to wait until the baseline is reached, you can wait until a certain value is reached. assuming the signal is analog, give this code a try int oldval = 0; int newval = 0;
void setup() { Serial.begin(115200); Serial.println("Square breathing 0.1"); }
void loop() { newval = analogRead(A0); // measure the wind int derivative = (newval - oldval); // first derivative of the signal oldval = newval; // remember previous value
// OUTPUT CAN BE COPIED TO EXCEL Serial.print(millis()); Serial.print(";"); Serial.print(oldval); Serial.print(";"); Serial.print(newval); Serial.print(";"); Serial.print(derivative); Serial.print(";");
// SQUARE OUTPUT (% as example) if (derivative > 0) Serial.println(100); else Serial.println(0); }
Please post the graph ...
|
|
|
|
« Last Edit: November 29, 2012, 02:15:23 pm by robtillaart »
|
Logged
|
|
|
|
|
Netherlands
Offline
Tesla Member
Karma: 90
Posts: 9401
In theory there is no difference between theory and practice, however in practice there are many...
|
 |
« Reply #2 on: November 29, 2012, 02:21:30 pm » |
if (derivative > 0) Serial.println(100); else Serial.println(0); you can of course also use newval in this comparison to suppress some noise. if (derivative > 0 && newval > 40) Serial.println(100); else Serial.println(0); Multiple analog reads and averaging them also suppresses some noise of the ADC (==analogRead(). I often average 4, 8 or 16 reads as this divides fast.
|
|
|
|
|
Logged
|
|
|
|
|
Maryland, USA
Offline
Jr. Member
Karma: 0
Posts: 79
|
 |
« Reply #3 on: November 29, 2012, 02:33:33 pm » |
Thanks for the quick and thorough reply.
I'll test your code and post the results!
I have the results.
The graph did not show well because there are two bands at 0 and 100 but it looks like we are getting something. The 16 values running average of the derivative in particular seems to pick up nicely when the signal is increasing or decreasing or staying relatively steady. I am attaching the spreadsheet itself so you can take a look.
Thanks for your help
|
|
|
|
« Last Edit: November 29, 2012, 05:28:52 pm by Thot »
|
Logged
|
There are three kind of people in the world: Those who can count, and those who can't
|
|
|
|
Maryland, USA
Offline
Jr. Member
Karma: 0
Posts: 79
|
 |
« Reply #4 on: November 29, 2012, 09:39:19 pm » |
This is another test. The 16 values moving average of the derivative was calculated on the fly by the Arduino. It appears that it stays 0 when there is no activity, picks up immediately when I blow on the sensor and drops immediately when I stop blowing, remaining negative or 0 for most of the decay.
The interval between the time th signal begins fading and the moving average reaches 0 (at about 3.7 seconds) is 111 ms.
So far, it's the most reliable signal of sensor activation I found.
Thanks again!
P.S. On the plot I multiplied the value of the moving average by 50 so to be able to superimpose it to the actual signal.
|
|
|
|
« Last Edit: November 29, 2012, 09:45:10 pm by Thot »
|
Logged
|
There are three kind of people in the world: Those who can count, and those who can't
|
|
|
|
Netherlands
Offline
Tesla Member
Karma: 90
Posts: 9401
In theory there is no difference between theory and practice, however in practice there are many...
|
 |
« Reply #5 on: November 30, 2012, 05:14:21 am » |
So far, it's the most reliable signal of sensor activation I found. Does it solve your problem now? Otherwise you might need the second derivative too. Missing in the graph is the square wave, derived from the first derivative. Would make the picture complete. Maybe smoothing the original signal with a moving average is better than smoothing the derivative (should become smoother automatically). seen this class - http://arduino.cc/playground/Main/RunningAverage - ?
|
|
|
|
|
Logged
|
|
|
|
|
Offline
Edison Member
Karma: 114
Posts: 2205
|
 |
« Reply #6 on: November 30, 2012, 07:16:13 am » |
I wonder if somebody can point me to a software algorithm or an hardware solution to obtain nice square waves (sharp attack, sustain and, what's missing here, a sharp release). Fundamentally, there is no good solution. Alternatively, you can introduce some derivative gain, similar to a PID controller, for this.
|
|
|
|
|
Logged
|
|
|
|
|
Maryland, USA
Offline
Jr. Member
Karma: 0
Posts: 79
|
 |
« Reply #7 on: November 30, 2012, 10:52:40 am » |
Rob, >>> Does it solve your problem now? I have to try in practice in my application see if the result is acceptable. In a "perfect" wind controller, I should be able to detect a gradual decrescendo, that is the player blowing softer and softer to make the sound fade away gradually but I don't know how I would differentiate that from the slow decay that I see when I stop blowing altogether. >>> Otherwise you might need the second derivative too. By second derivative you mean something like: newderivative = oldvalue - newvalue ; secondderivative = oldderivative - newderivative ; oldderivative = newderivative ; // Remembers the previous one
>>> Missing in the graph is the square wave, derived from the first derivative. Would make the picture complete. Sorry, I did not include it because it does not show very well, But I am attaching it now. I am also attaching the actual Excel in case you are interested (the 8 periods moving average was calculated in the spreadsheeti itself). >>> Maybe smoothing the original signal with a moving average is better than smoothing the derivative (should become smoother automatically). I did not think about that because the actual signal seems clean enough but maybe a 2 or 4 period moving average of the signal will yield a clean derivative as well and be faster to show a true change >>> seen this class - http://arduino.cc/playground/Main/RunningAverage - ? No, I haven't. It does look very useful.  My approach is very similar to yours in: long runningAverage(int M)
which avoids the need for floating math. with the difference that I use two distinct routines, one for initializing the array during setup() and one to update the actual running average from loop(). That has the advantage of eliminating if (count < LMSIZE) count++;
so that the division is always by 16 (in my case) which could be replaced by a rightward 4 bit shift (though if I understand correctly, the compiler is smart enough to make the replacement). Also, is: index = index % LMSIZE;
More efficient than: index++ ; if (index >= LMSIZE) index = 0 ; // If LMSIZE is 16 index cannot be more than 15
I can post the code for my little functions if of interest (and if you'd like to comment on it) Thanks again
|
|
|
|
« Last Edit: November 30, 2012, 12:26:47 pm by Thot »
|
Logged
|
There are three kind of people in the world: Those who can count, and those who can't
|
|
|
|
Netherlands
Offline
Tesla Member
Karma: 90
Posts: 9401
In theory there is no difference between theory and practice, however in practice there are many...
|
 |
« Reply #8 on: November 30, 2012, 12:31:40 pm » |
newderivative = oldvalue - newvalue ; secondderivative = oldderivative - newderivative ; oldderivative = newderivative ; // Remembers the previous one Yes that should work. Also, is: index = index % LMSIZE; More efficient than: index++ ; if (index >= LMSIZE) index = 0 ; // If LMSIZE is 16 index cannot be more than 15 No, shorter source code does not always mean shorter exe or faster runtime. The modulo is a division which is one of the most expensive basic math operations. Yes, please post the code, you want to share, I'll comment (maybe others too
|
|
|
|
|
Logged
|
|
|
|
|
Netherlands
Offline
Tesla Member
Karma: 90
Posts: 9401
In theory there is no difference between theory and practice, however in practice there are many...
|
 |
« Reply #9 on: November 30, 2012, 12:37:08 pm » |
In the image I see quite some HIGH LOW fluctuations, e.g between [2.2 , 3.6] smoothing the source signal should really help here. You can make the square wave higher of course by writing 400 iso 100 // SQUARE OUTPUT (% as example) if (derivative > 0) Serial.println(400); else Serial.println(0);
|
|
|
|
|
Logged
|
|
|
|
|
Maryland, USA
Offline
Jr. Member
Karma: 0
Posts: 79
|
 |
« Reply #10 on: November 30, 2012, 12:50:14 pm » |
Alternatively, you can introduce some derivative gain, similar to a PID controller, for this.
I find your suggestion interesting. I found that: http://en.wikipedia.org/wiki/PID_controller#PseudocodeWhile the math is way above my head, in the bottom they put a simple pseudocode, which they said is suited for MCUs: previous_error = 0 integral = 0 start: error = setpoint - measured_value integral = integral + error*dt derivative = (error - previous_error)/dt output = Kp*error + Ki*integral + Kd*derivative previous_error = error wait(dt) goto start but I am not sure about the whole idea behind it though it resembles the derivative idea of Rob. Also, how to determine the dt (time between measurements) given that I need the fastest response possible. Should I eliminate the "wait" statement and make dt just = millis() - last_millis() ? Kp, Ki and Kd are tuning constants and that, I suspect, is more a matter of trial and errors. How could I apply it to my problem (slow decay curve)? Thanks
|
|
|
|
|
Logged
|
There are three kind of people in the world: Those who can count, and those who can't
|
|
|
|
Netherlands
Offline
Tesla Member
Karma: 90
Posts: 9401
In theory there is no difference between theory and practice, however in practice there are many...
|
 |
« Reply #11 on: November 30, 2012, 01:16:14 pm » |
|
|
|
|
|
Logged
|
|
|
|
|
Maryland, USA
Offline
Jr. Member
Karma: 0
Posts: 79
|
 |
« Reply #12 on: November 30, 2012, 03:36:00 pm » |
Boy, I definitely have to spend more time in the playground  . Though I still don't know how PID helps me. In other words, I don't know what the note should be, the input value is supposed to tell me that. Am I missing something? Thanks
|
|
|
|
|
Logged
|
There are three kind of people in the world: Those who can count, and those who can't
|
|
|
|
Maryland, USA
Offline
Jr. Member
Karma: 0
Posts: 79
|
 |
« Reply #13 on: December 03, 2012, 05:58:11 pm » |
Yes, please post the code, you want to share, I'll comment (maybe others too Here it is (I wanted to triple check it and test it before posting): /* Breath Sensor and Moving average By Luca Brigatti Adapted from an algorithm by robtillaart Calculates the derivative (difference) between two subsequent sensor readings and calculates a moving average of the derivative signal Optimized for a running average of 2^n elements ( 2, 4, 8 , 16 etc.) Sensor used for testing is breath sensor by Modern Devices: http://shop.moderndevice.com/products/wind-sensor */
const boolean DEBUG = true ; // true, to print data on the serial port. false to do something else const int MAVSIZE = 16 ; // Elements (size) of the moving average
// Signal variables int oldval ; int newval ; int derivative ; // will hold oldval - newval
// Moving Average variables int MavValues [MAVSIZE] ; // Holds the last MAVSIZE values of the moving average int mavtot = 0 ; // total of the moving average (before the division) int mav = 0 ; // Moving average = mavtot / MAVSIZE int mavindex = 0 ; // next value to use for the moving average int sizemask ; // Mask to use for the index counter
void setup() {
Serial.begin(9600); // <--- Low speed: easier to collect data from the serial port, High Speed: smoother curve and tighter moving average sizemask = MAVSIZE -1 ; // If MAVSIZE is B10000 (16), sizemask is B01111 (15) setupmav (MAVSIZE) ; // Initialize the Moving Average with the first MAVSIZE values if (DEBUG) Serial.println("Square breathing 0.1");
}
void loop() {
newval = analogRead(A2); // measure the wind derivative = (newval - oldval); // first derivative of the signal updatemav (MAVSIZE) ; // Updates the moving average, a new mav value is assigned. if (DEBUG) serialdump() ; else ; // Do something with mav , the moving average of the derivative or newval, the actual reading oldval = newval ; // Remember previous value for next derivative
}
void serialdump () {
// OUTPUT CAN BE COPIED TO EXCEL // Put here what you want to print Serial.print(millis()); Serial.print(";"); Serial.print(oldval); Serial.print(";"); Serial.print(newval); Serial.print(";"); Serial.print(derivative); Serial.print(";"); Serial.print(mav); Serial.print(";");
// SQUARE OUTPUT (% as example) if (derivative > 0 && newval > 40) Serial.println(100); else Serial.println(0);
}
void setupmav(int mavsize) {
mav = 0 ; mavtot = 0 ; oldval = analogRead(A2) ; // Starting point, change with Pin actually used for (int i = 0 ; i < mavsize; i++ ) {
newval = analogRead(A2); // measure the wind, change with pin used derivative = (newval - oldval); // first derivative of the signal MavValues [i] = derivative ; // Remember this value mavtot += derivative; // Update the running total oldval = newval ; // New Value is now old value
}
mav = mavtot / mavsize ; // First moving average
}
void updatemav (int mavsize) {
mavtot += derivative ; // Add latest value mavtot -= MavValues [mavindex] ; // Subtract oldest value in the array MavValues [mavindex] = derivative ; // Newest value in array, replaces oldest value mav = mavtot / mavsize ; // New moving average mavindex++ ; // Point to the next element in the array, now the oldest element. mavindex &= sizemask ; // Circle back to 0 if reached size of the moving average (e.g. 10000 & 01111 (16 & 15) = 00000 (0) )
}
My algorithm is not as abstracted and nearly as useful as your library but it is optimized for a situation like the one at hand: Putting the initial loading of the moving average array in setup and the main moving average calculation in the loop function eliminates the need for an "if" statement in the main loop to keep the count on how many values are in the array. Also, this algorithm uses only integer math. Finally mavindex++ ; // Point to the next element in the array, now the oldest element. mavindex &= sizemask ; // Circle back to 0 if reached size of the moving average (e.g. 10000 & 01111 (16 & 15) = 00000 (0) )
Replaces your modulus and my if (mavindex=16) mavindex = 15 ;
As I feel it may be faster than both (but I really don't know). Your comments are welcome.
|
|
|
|
|
Logged
|
There are three kind of people in the world: Those who can count, and those who can't
|
|
|
|
Offline
Edison Member
Karma: 114
Posts: 2205
|
 |
« Reply #14 on: December 03, 2012, 09:04:13 pm » |
A fast moving average would be something like this:
mav = (1-alpha)*mav + alpha * new_data;
where 0<alpha<1 as a weight for new observations. The smaller alpha is, the longer memory mav has of historical data.
If you pick alpha to be 1/2^n, the math gets considerably easier as you can simply shift. For example, for alpha = 1/16:
mav = mav + (new_data - mav) / 16.
essentially, 1 subtraction, 1 shift, 1 increment.
|
|
|
|
|
Logged
|
|
|
|
|
|