Goertzel algorithm

酒飲みの為の、酒呑みによる、 「日本酒カレンダー」 を教えてもらったので謹んでメモしておく。

probe goertzel

前回やったCW信号のデコード。この核心は、goertzelというアルゴリズムだった。 単一周波数が存在するか否かを効率よく検出できるとな。これでもう、FFTとはおさらばできる 。

内部でどんな計算がおこなわれているか、プローブもとえgdbをあててみたくなった。そこで困るのが、信号がon/offしてる事。(CWなんであたりまえ)

そんな訳で、連続波が欲くなったんだ。確かsoxでも発生できたはず。sox 正弦波 発生 とか 叫んでみたよ。

SoX で音を作る

$ sox -r 10000 -b 16 -c 1 -n sweep.wav synth 10 sine 100-3000 gain -10

こんな風にすると、100から3000Hzまで10秒かけて掃引しながら正弦波を発生してくれるとな。なんだか、これってGNU RadioにVCOって名前で登録されてたな。

       gain   A power gain in dB.  Zero gives no gain; less than zero gives an
              attenuation.

gainの-10って何かなと調べてみた。ここには説明ないけど、絶対基準は、CDのフルスケールの音量みたい。大体こんな大音量だとクリッピングしちゃうので、-3dB (~70%)が実用的らしい。

ああ、検索結果に、こんな楽しいコンテンツも出てきた。

fftwでいろいろ実験

作者さんは、オイラーと同郷のsakura村の方。オイラーよりは富豪な方で、FreeBSDをバリバリ使っておられた。

こうなると、同じような事を、やってみたくなるのが人情というものです。

あれ? Goertzelはどうした?

The Goertzel Algorithm

とりあえず、ワシントン大学の資料をみましょ。

work bench

どうも、上の掃引波形と言い、gnuplotによる表示機構といい、ネットワークアナライザーを作って実習しろと言う圧力があるな。

#include <stdio.h>
#include <math.h>

float sampling_freq=10000.0;
float target_freq=    600.0;
#define N 100                 //  BW = sampling_freq / N
short int Data[N];
float omega,coeff,mag2;
float Q0,Q1,Q2,sine,cosine;
int rc,i,k;

int main (){
  k = (int) (0.5 + ((N * target_freq) / sampling_freq));
  omega = (2.0 * M_PI * k) / N;
  cosine = cos(omega);
  coeff = 2.0 * cosine;

  for (;;){
    rc = fread(&Data, sizeof(short int), N, stdin);
    if (rc < 1)
      break;
    Q1 = 0; Q2 = 0;
    for (i = 0; i < N; i++){
      Q0 = coeff * Q1 - Q2 + (float) Data[i];
      Q2 = Q1; Q1 = Q0;
    }
    mag2 = (Q1*Q1) + (Q2*Q2) - Q1*Q2*coeff;
    printf("%f\n", sqrt(mag2));
  }
}

cwdecoder.cから核心部分を抜き出してきました。これ、中心周波数600Hz,帯域幅100Hzのバンドパスフィルターとして機能するのね。FFTみたいに、データ個数を2のべき乗にする必要なしってのが、便利でよさそう。

$ cat pg.out
#! /usr/local/bin/gnuplot

#set terminal png
set nokey
set grid
#set logscale y
plot '< cat -' with line
pause 10

こちらは、gnuplot script です。pauseで10秒待つようにしました。こうしておかないと、 表示してすぐにグラフが消えてしまうからです。10の代りに -1 とすると改行入力がされるまで、停止するはずなんですが、機能しませんでした。まあ、スクリプトにチョッカイができない のは、あたり前の事だわな。

下記のようにパイプの終端で使っているので、セッションリーダーはcatが担当。catが終了すると、パイプの後段も全て死ぬので、悠長にキー入力なんて待っていられない? そう思って、単独で動かしてみたけど、やっぱり無視されてましたよ。(-pなんてオプションも機能しなかった)

$ cat sweep.raw | ./dut | ./pg.out

dutてのは、上のプログラムね。信号源から被試験デバイスのフィルターに信号を入力。その応答を可視化するために、オシロスコープを使う。これってネットワークアナライザーの原型ですよ。

現役時代は、そのオシロにポラロイドカメラを取り付けて、写真にとる。オシロの輝度やら露光を間違えると、白けた写真になっちゃう。今なら、pg.outスクリプトのset terminal pngを有効にするだけで、綺麗なpngが得られる。( .. | ./pg.out > picture.png 等とする)

signal generator

soxに作らせた掃引音源が、どうも周波数をリニアに発生していないようだ。低い周波数の時は長時間、高い周波数になるにつれて、短時間になってるっぽい。

予想するに、発生波形の個数を一定になるようにしてないかな。そうすると、(ある)周波数の持続時間は、周波数に反比例するな。正確に調べるならsoxのソース嫁。

面倒なんで、信号発生器を自作しちゃえ。

ob$ cat sg.c
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define SAMPLE_FREQ (10 * 1000)
#define LEN  100

void gen(int freq){
  double out;
  int t;
  short int b16;

  for (t=0; t<LEN; t++){
     out =  sin( 2 * M_PI * freq * t / SAMPLE_FREQ );
     b16 = (short int) (out * 3276);
     fwrite(&b16, sizeof(short int), 1, stdout);
  }
}

int main( int argc, char *argv[] ){
  int f;

  //a = atof(argv[1]);
  for (f=100; f <= 3000; f += 10){
    gen(f);
  }
}

arduinoのA/Dの変換スピードに敬意を表して、サンプリング周波数は、10KHzにした。なんてのは、表向きの理由で、単に暗算が楽だからです。

サンプリングの個数と言うか長さは100個。時間にすると、10ミリ秒だけの期間で各周波数を発生。

発生させる周波数は、100Hzから3000Hzまで、10Hzステップという、プリセット版のスィープジェネレーターです。信号レベルは、ほぼ -10dB です。( 3276 / 32768 )

DUTに直結できる様に、信号出力は、バイナリーなrow形式にしています。

goertzel の Q

ob$ ./sg | ./dut | ./pg.out > scan.png
ob$ gpicview scan.png

scan.png

ピークが500Hzにあって、あれって思わないでください。X軸の最左が100Hzになってますんで、あしからず。

このフィルターのQはどれぐらいになるんですかね? センター周波数の信号レベルから3dB落ちた所の帯域幅との比って定義でしたっけ?

  Freq    BW=100           BW=50           BW=25
------  --------        --------        --------
    41         0               0               0
    42      1769            1639            1203
    43      3667            2724             878
    44      5536            3047            1041
    45      7345            2339            1794
    46      9151               0               0
    47     10967            3585            2783
    48     12668            7460            2336
    49     14021           11162            3546
    50     14823           14025           11257
    51     14999           14999           14999  <== Center
    52     14633           13956           11403
    53     13880           11453            3512
    54     12853            7752            2301
    55     11550            3520            2929
    56      9910               0               0
    57      7930            2264            1940
    58      5732            3338            1009
    59      3540            3017             840
    60      1585            1576            1350
    61         0               0               0

BWを変えて(N=100,200,400)中心周波数+-100Hzの範囲(10Hz step)のレスポンスを表にしてみた。

サンプル数を変えると、値も変わるので、Nで除して正規化してある。また、入力振幅によって値が変わるのは常識なんで(線形応答)、見易い数値になるように、sgの振幅係数を、30000に設定しておいた。

上記表のグラフ

sox synth

冒頭の所であげたsoxのsynth機能での掃引周波数発生。低い周波数はゆったりと、高い周波数はせわしなくってのが、どんな具合になってるか確認。どうやって?

無線家の常識、F = 1 / T という定義を思いだす。周期Tの逆数が、周波数Fだよってあれね。 周期なら、ちょっとの工夫で測定できるだろう。

#include <stdio.h>

#define SAMPLE_FREQ (10 * 1000)
#define N (SAMPLE_FREQ / 10)
short int Data[N];

int m2p(int offset){
  int t;
  for(t = offset; t<N; t++)
    if (Data[t] < 0) break;
  for(t; t<N; t++)
    if (Data[t] > 0) break;
  return (t);
}

void decide(){
  int t,t0,t1;
  t0 = m2p(0);
  t1 = m2p(t0 + 1);
  t = t1 - t0;
  if (t)
    printf("%d\n", SAMPLE_FREQ / t);
  else
    printf("0\n");
}

int main (){
  int rc;
  for (;;){
    rc = fread(&Data, sizeof(short int), N, stdin);
    if (rc < 1)
      break;
    decide();
  }
}

0.1秒ごとにデータの塊を切り出す。その塊(配列)の中をスキャンして、マイナスからプラスに転じるポイントを2点みつける。その差が、周期になる。ポイント間は、1/SAMPLE_FREQという時間になってるからね。

すなわち、0.1秒間隔で、その時の周波数を求めているんだ。X軸は、発生開始からの経過時間(相当)、Y軸は周波数だ。Y軸を対数表記にしたら、直線になったよ。すなわち、経過時間に対して、周波数を対数的に発生させてるんですな。

soxsg.png

音を扱う世界の対面には、人間がいる。物理的な刺激量と知覚量は、対数関係にあるっていう、ウェーバーヘィヒナの法則ってのが良く知られている。周波数検知も同様なの?

世の中には、絶対音感と言う特技の持ち主がいるそうだ。そういう人って、人間の格好をした周波数カウンターと思うんだけど、どうよ。

sin wave gen in golang

package main

import (
    "encoding/binary"
    "math"
    "os"
)

const PI = 3.1415926

func main() {
    var (
        data      float64
        frequency float64 = 440 * math.Pow(2, 3/12)  // 523 Hz
        rate      float64 = 44100
        duration  float64 = 3                        // 3 secs
    )
    for n := 0; n < int(rate*duration); n++ {
        // Calculate amplitude at the n / rate phase.
        radius := frequency * 2 * PI * float64(n) / rate
        data = math.Sin(radius)

        // Render audio data to STDOUT.
        binary.Write(os.Stdout, binary.LittleEndian, data)
    }
    return
}
go run main.go | play -r 44100 -c 1 -t f64 -

SoX - Sound eXchange | Documentation