Arduino(?)に繋いだ可変抵抗を本当に回した時だけ値を受けとるには?にアイディアをいただいてanalogRead()用のライブラリを作ってみたのですが、その続きです。
#もっと良いやり方があるそうな
3 SIMPLE FILTERING TECHNIQUES TO ELIMINATE NOISEにでてくる"ExponentialFilter"というやつです。
日本語では指数移動平均というらしい。
- 簡単な再帰計算で軽く、
- 配列を使わないので、メモリーもあまり使わず、
- 反応の調整範囲が広い
いいことずくめだそうな。
SmoothFuncも全然使えるのですが、単純移動平均を使っているため、
- なめらかな結果を得ようとすろと、配列が大きめになる
- 配列が大きくなると反応が遅くなる
- 結局、一番あたらしい値と配列から出ていく一番古い値に左右されがち
なんだかな...と思っていたところでした。
これは試してみないと。
#ライブラリにしてみた
前に作ったやつを元にして作ってみました。ttatsf/ExponentialSmoothFunc
#ifndef EXPONENTIAL_SMOOTH_FUNC_H
#define EXPONENTIAL_SMOOTH_FUNC_H
#include <Arduino.h>
class ExponentialSmooth{
private:
const long RATE;
long previousValueMul100;
public:
ExponentialSmooth(long reactRate);
long operator()(long currentValue);
};
class IsCHANGED{
private:
long previousValue;
public:
IsCHANGED();
boolean operator()(long currentValue);
};
class IsINCREASED{
private:
long previousValue;
public:
IsINCREASED();
boolean operator()(long currentValue);
};
class IsDECREASED{
private:
long previousValue;
public:
IsDECREASED();
boolean operator()(long currentValue);
};
#endif
#include "ExponentialSmoothFunc.h"
ExponentialSmooth::ExponentialSmooth(long reactRate): RATE(reactRate)
, previousValueMul100(0) {
}
long ExponentialSmooth::operator()(long currentValue){
const long SMOOTHED_VALUE_MUL_100 = RATE * currentValue
+ (100 - RATE) * previousValueMul100 / 100
+ 1 ;
previousValueMul100 = SMOOTHED_VALUE_MUL_100;
return SMOOTHED_VALUE_MUL_100 / 100 ;
}
IsCHANGED::IsCHANGED(): previousValue(0) {
}
boolean IsCHANGED::operator()(long currentValue) {
const boolean ANSWER = currentValue != previousValue;
previousValue = currentValue;
return ANSWER;
}
IsINCREASED::IsINCREASED(): previousValue(0) {
}
boolean IsINCREASED::operator()(long currentValue) {
const boolean ANSWER = currentValue > previousValue;
previousValue = currentValue;
return ANSWER;
}
IsDECREASED::IsDECREASED(): previousValue(0) {
}
boolean IsDECREASED::operator()(long currentValue) {
const boolean ANSWER = currentValue < previousValue;
previousValue = currentValue;
return ANSWER;
}
Arduino言語的にはもっと短かく書けるのでしょうが、何とか関数型に寄せようとして定数の初期化と変数の破壊的再代入と分けて書いています。
ExponentialSmoothは
- 内部に前回の値 $y_{t-1}$ を持っていて、
- 入力された生の値 $x_t$ と、前回の値 $y_{t-1}$ とを、決められた割合 $\alpha : 0 \leq \alpha \leq 1 $ で加重平均 $y_t = \alpha x_t + ( 1 - \alpha ) y_{t-1} $ して
- 今回の値 $y_t$ として出力する、
というような仕組みです。
お手軽に整数のままで計算したいし、精度も必要だったので、内部では値を100倍で持っていて、リターンするときに1/100にするようにしました。
+1しているのは、VRを回し切っても最大値 1023 にならなかったためです。補正として入れました。
よろしければお使いください。
使い方は前のとほぼ同じです。ただし、
- ExponentialSmooth はインスタンス化するときに必ず、引数をひとつ取ります。反応速度の割合を決める値で、範囲は0(無反応)から100(最新データそのまま)までです。
- IsCHANGED, IsINCEASED, IsDECREASED は直前の値とのみ比較します。内部に値の履歴は持ちません。(配列を使わないというポリシー?だし、それでもかなり安定して使えそうなので。)
#使用例
前のと同じ、複数のアナログ入力を受け取ってMIDIコントロールチェンジを返すスケッチです。
#include <MIDIUSB.h>
#include <ExponentialSmoothFunc.h>
const int ANAPIN[2] = {0, 1};
const byte CHANNEL = 0;
const byte CC[2] ={25, 26};
const long REACT_RATE = 20;
ExponentialSmooth expSmooth[2] = {REACT_RATE, REACT_RATE};
IsCHANGED isCHANGED[2];
void setup() {
Serial.begin(115200);
}
void loop() {
for(int i=0;i < 2; i++) {
const long SMOOTHED_DATA = expSmooth[i]( analogRead(ANAPIN[i]) >> 3 );
if( isCHANGED[i](SMOOTHED_DATA) ) {
controlChange(CHANNEL, CC[i], SMOOTHED_DATA);
MidiUSB.flush();
};
delay(10);
};
}
//controll value or velocity
// First parameter is the event type (0x0B = control change).
// Second parameter is the event type, combined with the channel.
// Third parameter is the control number number (0-119).
// Fourth parameter is the control value (0-127).
void controlChange(byte channel, byte control, byte value) {
midiEventPacket_t event = {0x0B, 0xB0 | channel, control, value};
MidiUSB.sendMIDI(event);
}}
定数REACT_RATE
を小さくするとノイズが減りますが反応は悪くなります。
Leonardoに20kBのVRを挿してざっと試した感じだと、
- ビットシフトして 0 ~ 127 の範囲で使うなら 20 くらい
- 0 ~ 1023 で使うなら 5 くらい
が使いやすいかなあ...という感じです。もちろん環境によりますが。
#比べてみた
指数移動平均と単純移動平均、比べてみました。
以下のスケッチをLeonardoに入れてVRをぐりぐりしてみました。
#include <ExponentialSmoothFunc.h>
#include <SmoothFunc.h>
const int PIN = 0 ;
const long REACT_RATE = 12;
ExponentialSmooth expSmooth( REACT_RATE );
const int HISTORY_SIZE = 20;
GetAverage getAverage( HISTORY_SIZE);
void setup() {
Serial.begin(115200);
while(!Serial);
Serial.print("Start!!\n");
}
void loop() {
const long RAW_DATA = analogRead(PIN);
const long EXP_SMOOTHED_DATA = expSmooth( RAW_DATA);
const long SIMPLE_SMOOTHED_DATA = getAverage( RAW_DATA);
Serial.print(RAW_DATA);
Serial.print(",");
Serial.print(EXP_SMOOTHED_DATA);
Serial.print(",");
Serial.print(SIMPLE_SMOOTHED_DATA);
Serial.print(",");
Serial.println();
delay(100);
}
- 青:analogRead()した生の値
- オレンジ:指数移動平均
- 赤:単純移動平均
です。
まず何度か試して、パルスの90%あたりで同じ値になるように反応速度を合せました。
指数移動平均の12%が単純移動平均の履歴20個に相当するようです。
あとは見た通りです。
単純なパルスでは良さげに見える単純移動平均ですが、ぐりぐりするとついてこれない感がありありです。
対して指数移動平均のほうは、単純なパルスではかなり鈍っているように見えますが、ぐりぐりした時にはちゃんとついてこれてます。
何よりグラフが綺麗。これでDAWのMIDIオートメーションしたい!
最後はもろコントローラー寄りの話になってしまいましたが(そもそも動機がそっち)、データ計測系の用途にも使えるんじゃないかと思います。