指数関数曲線を固定小数点演算で求める

エンベロープジェネレータのプログラムですが、現状は指数関数テーブルを参照して出発点の値に時間ごとに減衰する指数関数を掛け合わせて減衰曲線を作っています。

https://github.com/naokiiwakami/vceg

これは精度をあまり気にせずにざっと計算できるので手っ取り早いのですが、プログラムはどうしても煩雑になります。また、値の更新周期は約 10ms なのですが、減衰が遅いと 10ms の間には値が動かず、時間軸を補間しないといけません。これはまだ実装していませんがプログラムはもっと煩雑になります。

ちょっと嫌だなと考え始めたのですが、そもそも指数関数の減衰は

V(i+1) = V(i) * k

where 0 <= k < 1

のような式で簡単に作れそうなものなのですが、マイクロコントローラへの実装はそう甘く行きません。例えば、1秒で半分に減衰する曲線を、10ms の更新周期で計算しようとすると、k の値は

k = 0.5 ^ (1 / 100) = 0.99309249543703590153321021688807

限りなく 1 に近いけれども 1 にしてしまったら減衰しません。変数として float を使えばいいじゃない?とも思いますが、例えば AVR で float を static 領域に持つとなんだかわからないけれどもデータがばんばん壊れてしまいます(バグかもしれませんが)。それにそもそもふつうのマイクロプロセッサでの浮動小数点演算は遅いです。ということで、固定小数点演算で曲線が求まらないか考えてみました。

やり方を調べてみました。以下の解説がとてもわかりやすかったです

https://www.allaboutcircuits.com/technical-articles/multiplication-examples-using-the-fixed-point-representation/

簡単に書くと、実数の計算の前に何らかの値をかけて小数点部分を整数にする、その後計算を行い終わった後に同じ値で割って小数点部分を戻せば良いようです。「何らかの値」が2のべき乗ならビットシフト演算でこのスケーリング操作は高速に実行できます。

以下が試しに C で実装してみたものです。減衰する値は 64 (=2^6) でスケーリングを行い、減衰率は 65536 (=2^16) でスケーリングしています。どちらの値も 16 ビット整数になるので、掛け合わせた値は 32 ビット整数の範囲に収まります。そこで 32 ビット整数で減衰の掛け算を行っています。

#include <stdio.h>
#include <stdint.h>

int main(int argc, char *argv[]) {
  // fixed point integer 10bit.6bit
  uint16_t value = (1023) << 6;
  // fixed point integer .16bit
  uint16_t decay_factor = 0.99309249543703590153321021688807 * 65536;
  for (uint16_t i = 0; i < 1000; ++i) {
    // multiplication
    uint32_t temp = value * decay_factor;
    value = temp >> 16;
    // retrieve the integer part
    uint16_t output = value >> 6;
    printf("%d %d\n", i, output, 10);
  }
  return 0;
}
C

結果は良好でした。

この方法は使えるような気がします。

Comments

No comments yet. Why don’t you start the discussion?

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください