fftwでいろいろ実験
by K.I
2014/08/05〜2014/08/29
Index
さくらインターネットのスタンダードプランで、Webサーバを運用中
高速フーリエ変換用ライブラリのfftwが使いたくなったので、さくらのサーバにインストールしてみた
ここはFreeBSDだけど、まぁ、普通のLinuxでも同じだと思う
fftwは、サンプル数を自由に選べて、とにかく簡単に周波数成分を調べられる
ということで、fftwを使って、いろいろ遊んでみました。
主に、デジタルフィルタの勉強用かな。
[top]
以下のファイルを、カレントディレクトリにコピーする
.libs/libfftw3.a
api/fftw3.h
$ gcc fftw.c -o fftw libfftw3.a -lm
configureの時、prefixでユーザのHOMEを指定することで、
% gcc fftw.c -o fftw -lm -lfftw3 -L/home/username/lib -I/home/username/include
この方が、気分が良い。やってることは同じなんだけどね。。
コンパイルしたfftwは、pathの通っているディレクトリ、例えばHomeの/binとかに入れておく。
管理者権限があれば、--prefixとかいらないので、念のため。。
fftwを使って、fftを行う簡単なフィルタプログラムを組んでみた
入力データとして、改行コード区切りの数値データを用意すれば良い
サンプル数は、任意で構わない(行数がサンプル数となる)
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#include "fftw3.h"
#define SIZE 1000000000
main( int argc, char *argv[])
{
FILE *fp;
int i;
int N;
double *Y;
double re,im,mag,ang;
fftw_complex *in, *out;
fftw_plan p;
// データ読込み用のバッファを確保
if (!(Y=(double *)malloc(SIZE))) exit(-1);
// データ読込み(データ個数を調べる)
for (N=0; scanf("%lf",&Y[N])!=EOF && N<SIZE ;N++) ;
//N個のFFTW用の複素数配列を確保する
in = (fftw_complex*)fftw_malloc(sizeof(fftw_complex)*N);
out = (fftw_complex*)fftw_malloc(sizeof(fftw_complex)*N);
// 複素数配列に入力データをセット
for(i=0; i<N; i++) { // t=0〜1secのデータ
in[i][0]=Y[i]; // 実数部
in[i][1]=0.0; // 虚数部
}
// 1次元のフーリエ変換を実行
p = fftw_plan_dft_1d(N,in,out,FFTW_FORWARD,FFTW_ESTIMATE);
fftw_execute(p);
// 結果を出力する
for(i=0; i<N; i++) { // f=0〜N(サンプリング周波数)のデータ
re = out[i][0]; // 複素数の実数部
im = out[i][1]; // 複素数の虚数部
mag = sqrt(re*re + im*im); // 大きさを計算
ang = atan2(im,re); // 位相角を計算
printf("%d \t %.5lf \t %.5lf \t %.5lf \t %.5lf \n", i,re,im,mag,ang);
}
// 後始末(使用した配列等を廃棄)
fftw_destroy_plan(p);
fftw_free(in);
fftw_free(out);
free(Y);
}
位相角の計算式がimとreが逆になっていたので修正しました。(160920)
このプログラムは、N個のデータ(改行コードで区切る)を読み込んで、
周波数0〜Nの範囲のFFT結果を出力する
周波数、実数部、虚数部、絶対値、位相角の順で出力される
fftというと、通常サンプル数が2の倍数である必要があったりするが、
fftwは、2の倍数じゃなくても大丈夫なアルゴリズムらしい
[top]
fftwの結果を見るために、gnuplotもインストールする
% gunzip -c gnuplot-4.6.5.tar.gz | tar -xvf -
% cd gnuplot-4.6.5
% ./configure --prefix=/home/username
% make
% make install
set terminal png
set nokey
set xlabel "f[kHz]"
set ylabel "|F(w)|"
set logscale x
set logscale y
plot [:][:] 'fftw.dat' using ($1/1000):4 with impulses linewidth 1
fftw.datファイルを入力として、
png形式で出力
X,Y軸はLogスケールで
X軸は、1項め/1000($を付ければ値を計算して出力できる)
Y軸は、4項めをライン幅1のインパルスで出力
つまりX軸に周波数(kHz)、Y軸にFFT結果の絶対値を出力する
スクリプトの実行は、スクリプト名を gp_outとすると
$ gnuplot gp_out > hoge.png
gnuplotはファイル名を'-'にすると標準入力に出来るが、スクリプトファイルの場合はうまくいかない。
でも、 こちら で、うまくやっていたので、真似させてもらう1 。
#!/home/username/bin/gnuplot
set terminal png
set nokey
set xlabel "f[kHz]"
set ylabel "|F(w)|"
set logscale x
set logscale y
plot [:][:] '< cat -' using ($1/1000):4 with impulses linewidth 1
これで、中間ファイル無しで、直接プロットファイルを作成できる
$ singen 1000 | fftw | gp_fft > hoge.png
gnuplot表示用スクリプトは、いくつか作っておいて使い分ける
fftw出力の表示用のスクリプト(周波数リニアスケール版)(gp_fftl)
#!/home/username/bin/gnuplot
set terminal png
set nokey
set xlabel "f[kHz]"
set ylabel "|F(w)|"
set logscale y
plot [:][:] '< cat -' using ($1/1000):4 with impulses linewidth 1
fftw出力の表示用のスクリプト(ゲイン・周波数リニアスケール版)(gp_fftll)
#!/home/username/bin/gnuplot
set terminal png
set nokey
set xlabel "f[kHz]"
set ylabel "|F(w)|"
plot [:][:] '< cat -' using ($1/1000):4 with impulses linewidth 1
#!/home/username/bin/gnuplot
set terminal png
set nokey
set xlabel "f[kHz]"
set ylabel "Angle"
set logscale x
plot [:][:] '< cat -' using ($1/1000):5 with line linewidth 2
#!/home/username/bin/gnuplot
set terminal png
set nokey
plot [:][:] '< cat -' with line linewidth 5
波形表示用のスクリプト(pointのみ表示)(gp_wavp)
#!/home/username/bin/gnuplot
set terminal png
set nokey
set style line 1 pointtype 3
plot [:][:] '< cat -' linestyle 1
波形表示用のスクリプト(impulse表示)(gp_pulse)
#!/home/username/bin/gnuplot
set terminal png
set nokey
plot [:][:] '< cat -' with impulses linewidth 5
位相を表示すると、-π〜πの範囲に入らない波形の場合、不連続なグラフになってしまう。
これだと、見にくいので、繋げて表示するルーチンを作成(fftw_unwrap.c)
#include <stdio.h>
#include <math.h>
double last;
double unwrap(double in)
{
double div;
while (in-last < -2*M_PI)
in += 2*M_PI;
while (in-last > 2*M_PI)
in -= 2*M_PI;
div = in - last;
if (div < -M_PI)
in += 2*M_PI;
else if (div > M_PI)
in -= 2*M_PI;
last = in;
return in;
}
int main(int argc, char *argv[])
{
int i;
double re,im,mag,ang;
while (scanf("%d %lf %lf %lf %lf",&i,&re,&im,&mag,&ang) != EOF) {
printf("%d \t %.5lf \t %.5lf \t %.5lf \t %.5lf\n",
i,re,im,mag,unwrap(ang));
}
return 0;
}
とりあえず、fftwの出力の位相成分をunwrapするようにした
雑音を含んでいたり、サンプリングが荒い場合は、位相ジャンプが起きてしまうので注意
1 最初の行は自分の環境に合わせて、gnuplotのパスを指定する。
[top]
信号源として、正弦波を発生させるプログラム(singen.c)
サンプリング周波数1MHzで、1秒分のデータ(1000000point)
#include<stdio.h>
#include<math.h>
#define SAMPLE 1000000
#define MAX 32767
#define POINT 1000000
main( int argc, char *argv[] )
{
double d,f;
double out;
int t;
f = atoi(argv[1]);
d = 2*M_PI*f/SAMPLE;
for (t=0; t<POINT ;t++) {
out = sin(t*d);
printf("%d\n",(int)(out*MAX/2));
}
}
singenの出力を最初の5000pointだけgnuplotで出力してみる
% singen 1000 | head -5000 | gp_wav > singen.png
1kHzのsingen出力を、fftwを通してgnuplotで出力
% singen 1000 | fftw | gp_fft > singen_fft.png
% singen 1000 | head -999999 | fftw | gp_fft > singenx_fft.png
実際のサンプリングでは、周波数をピッタリ合わせることは出来ないので、
別の方法として、テーブルを使った正弦波の生成を考えてみる
まず、1周期分の正弦波のデータをテーブルにしておく
データを間引きして出力することで周波数を変えることが出来る
テーブルの大きさをN、サンプリング周波数をSとして、
生成可能な周波数fは、(サンプリング周波数S/テーブルの大きさN)の整数倍となる
例えば、 サンプリング周波数S=1MHz、N=4000の場合、250Hzの整数倍の周波数の生成が可能になる
1/4周期分のテーブルを用意して、象限毎にデータを出力すれば良い
実際には、0〜N/4で、N/4+1個のテーブルを用意して、以下のようにプログラム(dds.c)
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
//1秒分のデータを生成
#define FREQ (1000) //生成周波数
#define SAMPLE (1000000) //サンプリング周波数
#define N (1000*4) //サンプル数
double sin_table[N/4+1];
void make_table(void)
{
int x;
for (x=0; x<=N/4 ;x++)
sin_table[x] = sin(2*M_PI/N*x);
}
double sin_x(int x)
{
int xx;
xx = x%(N/4);
if (x < N/4*1) return sin_table[xx];
else if (x < N/4*2) return sin_table[N/4-xx];
else if (x < N/4*3) return -sin_table[xx];
else return -sin_table[N/4-xx];
}
main( int argc, char *argv[] )
{
int i, d, t=0;
double f, s;
f = FREQ;
s = SAMPLE;
make_table();
if (argc>1)
f = atof(argv[1]);
if (argc>2)
s = atof(argv[2]);
d = f*N/s;
for (i=0; i<s ;i++) {
printf("%f\n",sin_x(t));
t = (t+d)%N;
}
}
一応、最初の5000ポイントを出力して、波形を確認する
% dds 1000 | head -5000 | gp_wav > dds.png
% dds 1000 | fftw | gp_fft > dds_fft.png
奇数倍の高調波はあるが、単純にsin関数を使った場合より高調波が少なくなっている
おそらく、ddsの方は完全に周期的なデータなので、計算上誤差が小さくなるんだろうと思う
これなら、実験には十分使えそうだ。
とりあえず正弦波の生成には、ddsのルーチンを使うことにする
矩形波の生成は、DDSのプログラムをちょっと弄っただけで簡単に作れる(square.c)
象限毎に、1, 1, -1, -1 と変化させるだけ
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
//1秒分のデータを生成
#define FREQ (1000) //生成周波数
#define SAMPLE (1000000) //サンプリング周波数
#define N (1000*4) //サンプル数
int square_x(int x)
{
int xx;
if (x < N/4*1) return 1;
else if (x < N/4*2) return 1;
else if (x < N/4*3) return -1;
else return -1;
}
main( int argc, char *argv[] )
{
int i, d, t=0;
double f, s;
f = FREQ;
s = SAMPLE;
if (argc>1)
f = atof(argv[1]);
if (argc>2)
s = atof(argv[2]);
d = f*N/s;
for (i=0; i<s ;i++) {
printf("%d\n",square_x(t));
t = (t+d)%N;
}
}
矩形波の出力を最初の5000pointだけgnuplotで出力してみる
% square 1000 | head -5000 | gp_wav > square.png
% square 1000 | fftw | gp_fft > square_fft.png
Dutyを変えたらどうなるか、とりあえず矩形波の第3象限も1にして、Dutyを変えてみる(150608追記)
% squarex 1000 | fftw | gp_fft > squarex_fft.png
矩形波のDutyが50%の時は、奇数倍の高調波のみ含まれているが、
Dutyが変わると、偶数倍の高調波も含まれる様になることが分かる
三角波の生成も、DDSのテーブルを変えるだけなので簡単!(triangle.c)
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
//1秒分のデータを生成
#define FREQ (1000) //生成周波数
#define SAMPLE (1000000) //サンプリング周波数
#define N (1000*4) //サンプル数
double tri_table[N/4+1];
void make_table(void)
{
int x;
for (x=0; x<=N/4 ;x++)
tri_table[x] = 4.0/N*x;
}
double tri_x(int x)
{
int xx;
xx = x%(N/4);
if (x < N/4*1) return tri_table[xx];
else if (x < N/4*2) return tri_table[N/4-xx];
else if (x < N/4*3) return -tri_table[xx];
else return -tri_table[N/4-xx];
}
main( int argc, char *argv[] )
{
int i, d, t=0;
double f, s;
f = FREQ;
s = SAMPLE;
make_table();
if (argc>1)
f = atof(argv[1]);
if (argc>2)
s = atof(argv[2]);
d = f*N/s;
for (i=0; i<s ;i++) {
printf("%f\n",tri_x(t));
t = (t+d)%N;
}
}
三角波の出力を最初の5000pointだけgnuplotで出力してみる
% triangle 1000 | head -5000 | gp_wav > triangle.png
% triangle 1000 | fftw | gp_fft > triangle_fft.png
パッと見た感じでは、矩形波と、ほとんど区別がつかない
でも奇数倍の高調波のみ含まれているが、周波数の高い成分が急激に少なく2 なっている
ノコギリ波の生成も、テーブルを2つに分けるようにして生成できる(saw.c)
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
//1秒分のデータを生成
#define FREQ (1000) //生成周波数
#define SAMPLE (1000000) //サンプリング周波数
#define N (1000*4) //サンプル数
double saw_table[N/2+1];
void make_table(void)
{
int x;
for (x=0; x<=N/2 ;x++)
saw_table[x] = 2.0/N*x;
}
double saw_x(int x)
{
int xx;
xx = x%(N/2);
if (x < N/2*1) return saw_table[xx];
else return -saw_table[N/2-xx];
}
main( int argc, char *argv[] )
{
int i, d, t=0;
double f, s;
f = FREQ;
s = SAMPLE;
make_table();
if (argc>1)
f = atof(argv[1]);
if (argc>2)
s = atof(argv[2]);
d = f*N/s;
for (i=0; i<s ;i++) {
printf("%f\n",saw_x(t));
t = (t+d)%N;
}
}
ノコギリ波の出力を最初の5000pointだけgnuplotで出力してみる
% saw 1000 | head -5000 | gp_wav > saw.png
% saw 1000 | fftw | gp_fft > saw_fft.png
三角波と違って、奇数倍だけでなく、偶数倍の高調波も含まれていることが分かる
rand関数で、ノイズ出力するプログラムを書いてみた(rand.c)
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
//1秒分のデータを生成
#define SAMPLE (1000000) //サンプリング周波数
double Uniform( void ){
return ((double)rand()+1.0)/((double)RAND_MAX+2.0);
}
main( int argc, char *argv[] )
{
int i;
srand(777);
for (i=0; i<SAMPLE ;i++) {
printf("%f\n",Uniform());
}
}
rand関数によるノイズ出力を最初の100pointだけgnuplotで出力してみる
% rand | head -100 | gp_wav > rand.png
% rand | fftw | gp_fft > rand_fft.png
randによる乱数は偏りがあるらしいが、FFTでは全然分からないなぁ。
PN符号は、シフトレジスタと加算だけで作成できる擬似乱数
16bitのPN符号によるノイズ生成プログラム(pngen.c)
周期が最大になるように、タップを設定する必要がある →擬似ノイズジェネレータ
#include<stdio.h>
#include<math.h>
//1秒分のデータを生成
#define SAMPLE (1000000) //サンプリング周波数
int stage = 1023;
int pn16(data,tap)
int data,tap;
{
int mask,line,i;
data = data & stage;
mask = data & tap;
line = 0;
for (i=0; i<24 ;i++) {
line += mask & 0x01;
mask = mask>>1;
}
data = data<<1;
if (line & 0x01) {
data = data|1;
}
return(data&stage);
}
main( int argc, char *argv[] )
{
int pn=0x3ff; // 初期値(全ビット1)
int i;
for (i=0; i<SAMPLE ;i++) {
pn = pn16(pn,0x204);
printf("%d\n",pn&0x3ff); // 乱数出力
}
}
PN符号によるノイズ(最初の100個のデータを出力)
% pngen | head -100 | gp_wav > pngen.png
→意外と変な波形だ。
基本、シフトレジスタだから、こんな波形になるのか。。
% pngen | fftw | gp_fft > pngen_fft.png
PN符号によるノイズはFFTを掛けると、低周波では、けっこうクセがありそうだが、
周波数をリニアスケールにすると、まぁそれなりで、用途によっては問題ないかも
PN符号はシフトレジスタをフィードバックして作っているので、1ビットだけ取り出す方が良いのかも。
bit0だけ取り出すフィルタを作る(bit0.c)
#include<stdio.h>
#include<math.h>
main( int argc, char *argv[] )
{
unsigned int in;
while (scanf("%d", &in) != EOF) {
printf("%d\n",in&0x01);
}
}
pngen出力を、このフィルタbit0を通して、最初の100個のデータ出力
% pngen | bit0 | head -100 | gp_wav > pngen0.png
FFTを掛けてみると、意外と特性に大きな変化は無い
矩形波だと、高調波が大きいので、ノイズとしては却って良いのか?
2 矩形波と比較すると、縦方向のスケールが全然違うことが分かる。
[top]
2つの信号の組み合わせ、例えば足し算とか、掛け算を考える
本当は、別々に作成した正弦波を足し算するようなフィルタを作れたら良いんだけど
シェルで、2つの標準入力を動的に指定することは出来ない。
まぁ、これが出来たら、多分マルチスレッドになっちゃう気がするので、無理なんだろう
片方をスタティックなファイルにして、標準入力と演算するか、
或いはプログラムで生成したデータを、標準入力と組み合わせるしかないかな〜
中間ファイルが必要になるけど、ファイルの内容と標準入力を加算するフィルタを作ってみる(add.c)
ファイルが開けない場合は、固定の数値として計算するようにしている
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
main( int argc, char *argv[] )
{
FILE *fp;
double in1,in2;
fp = fopen(argv[1],"r");
if (fp) {
while((fscanf(stdin,"%lf",&in1)!=EOF)&&(fscanf(fp,"%lf",&in2)!=EOF)) {
printf("%f\n",in1+in2);
}
}
else {
in2 = atof(argv[1]);
while((fscanf(stdin,"%lf",&in1)!=EOF)) {
printf("%f\n",in1+in2);
}
}
}
% dds 2000 | add dds1000.dat | fftw | gp_fft > add_1000_2000_fft.png
和の場合は、単純に2つの周波数が合成されているだけだ
加算と全く同じだけど、ファイルの内容と標準入力を積算するフィルタを作ってみる(mul.c)
やはり、ファイルが開けない場合は、固定の数値として計算するようにした
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
main( int argc, char *argv[] )
{
FILE *fp;
double in1,in2;
fp = fopen(argv[1],"r");
if (fp) {
while((fscanf(stdin,"%lf",&in1)!=EOF)&&(fscanf(fp,"%lf",&in2)!=EOF)) {
printf("%f\n",in1*in2);
}
}
else {
in2 = atof(argv[1]);
while((fscanf(stdin,"%lf",&in1)!=EOF)) {
printf("%f\n",in1*in2);
}
}
}
1kHzと2kHzの、和である3kHzと、差分の1kHzが出力されている
なんか分かり難いので、1kHzと1.25kHzで、同様に積算してみた
% dds 1250 | mul dds1000.dat | head -5000 | gp_wav > mul_1000_1250.png
% dds 1250 | mul dds1000.dat | fftw | gp_fft > mul_1000_1250_fft.png
1kHzと1.25kHzの、和である2.25kHzと、差分の0.25kHzが出力される
積算では、2つの周波数の和と差分が出てくるので、
掛け算によって、信号が周波数変換されていることが分かる
同様に、ファイルの内容と標準入力を比較するフィルタを作ってみる(comp.c)
標準入力の方が大きかったら1、それ以外は0とする
ファイルが開けない場合は、固定数値として比較する。所謂コンパレータってやつ
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
main( int argc, char *argv[] )
{
FILE *fp;
double in1,in2;
fp = fopen(argv[1],"r");
if (fp) {
while((fscanf(stdin,"%lf",&in1)!=EOF)&&(fscanf(fp,"%lf",&in2)!=EOF)) {
printf("%d\n",in1>in2);
}
}
else {
in2 = atof(argv[1]);
while((fscanf(stdin,"%lf",&in1)!=EOF)) {
printf("%d\n",in1>in2);
}
}
}
試しに、三角波に対して、0.5の閾値で比較してみた
2つの信号の組み合わせじゃないんだけど、
最小値、最大値を求めるフィルタのプログラム(minmax.c)
#include<stdio.h>
#include<stdlib.h>
#include<float.h>
#include<math.h>
main( int argc, char *argv[] )
{
FILE *fp;
double in, min=DBL_MAX, max=-DBL_MAX;
while((fscanf(stdin,"%lf",&in)!=EOF)) {
if (min>in) min = in;
if (max<in) max = in;
}
printf("%f, %f\n",min,max);
}
% triangle 1000 | minmax
-0.997000, 0.997000
[top]
波形を見ていても、なかなか違いが分からない。
音で聞けば、分かりやすいかもということで、サウンドファイルに変換する方法を考える
調べてみると、生成されたサウンドファイルのヘッダが44byteより大きくなってエラーになっている
どうも、64bit環境でコンパイルしたので、ヘッダが大きくなりすぎてしまう様だ
試しに、32bitでコンパイルしてみたら変換できるようになった
# gcc -m32 txt2wav.c -o txt2wav -lm
試しに、1kHzの正弦波のwavファイルを生成。サンプリング周波数は50kHz
どんなブラウザでも再生できるようにするには、 audio要素 を使うべきだろうか
でも、IEだとWAVは再生できないので、MP3にしなきゃダメみたい。。。あぁ面倒だ。。。
作成したサウンドファイルはモノラルなので、ステレオにしてみる。
といっても、単に同じデータを並べるだけなので、本当のステレオではないんだけど(stereo.c)
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
main( int argc, char *argv[] )
{
double in;
while((fscanf(stdin,"%lf",&in)!=EOF)) {
printf("%d\t%d\n",(int)floor(in),(int)floor(in));
}
}
正弦波データを並べたデータを作って、txt2wavに掛ける
% dumpwave dds1000x.wav > dds1000x.txt
これで、逆にwavファイルから、データファイルを作って、
Linuxで動くフリーのMP3エンコーダに、 lame (Lame Ain't an MP3 Encoder)というのがあるらしい
% lame dds1000x.wav dds1000x.mp3
Audio要素は、ちょっと大げさだけど、まぁいいか。。
コントロールの表示方法は変えられるかもしれないけど、まぁそれが目的じゃないから。
sox (Sound eXchange)というのもあるらしい。(140902追記)
sox-14.4.1をダウンロードして、インストール
% gunzip -c sox-14.4.1.tar.gz | tar -xvf -
% cd sox-14.4.1
% ./configure --prefix=/home/username
% make
% make install
option description
-s signed
-u unsigned
-b 8bit
-w 16bit
-l 32bit
-d 64bit
-r rate Sampling Rate
-c ch Channel(ch:1,2)
-x exchange endian
-t type format type
# sox dds1000x.wav dds1000x.mp3
ファイル形式は、拡張子で指定出来る(typeで指定も可能)
.datはテキスト形式みたいだが、余計なものが付いているので、ちょっと使いづらいかな
[top]
可聴周波数は20kHzぐらいまでなので、サンプリング周波数は1MHzではオーバースペックだ
例えばCDだと44.1kHzなので、キリの良いところでサンプリング周波数は50kHzとして計算してみる
% dds 1000 50000 | add 1 | mul 128 | int > dds1000x.dat
% txt2wav -50000 dds1000x.dat
% lame dds1000x.wav dds1000x.mp3
% dds 1000 50000 | add 1 | mul 128 | int | head -250 | gp_wav > dds1000x.png
% dds 1000 50000 | add 1 | mul 128 | int | fftw | gp_fft > dds1000x_fft.png
8bitなので、少し高調波もあるが、十分キレイな音に聞こえる
% triangle 1000 50000 | add 1 | mul 128 | int > tri1000x.dat
% txt2wav -50000 tri1000x.dat
% lame tri1000x.wav tri1000x.mp3
% triangle 1000 50000 | add 1 | mul 128 | int | head -250 | gp_wav > tri1000x.png
% triangle 1000 50000 | add 1 | mul 128 | int | fftw | gp_fft > tri1000x_fft.png
正弦波に近い音だが、聞き比べると高調波が多いことが分かる
こもった感が少ないので、正弦波よりはっきりした感じ
三角波に、偶数次の高調波が含まれているのが気になるなぁ
% triangle 1000 50000 | mul 128 | int | add 128> tri1000xxx.dat
% txt2wav -50000 tri1000xxx.dat
% lame tri1000xxx.wav tri1000xxx.mp3
% triangle 1000 50000 | mul 128 | int | add 128 | head -250 | gp_wav > tri1000xxx.png
% triangle 1000 50000 | mul 128 | int | add 128 | fftw | gp_fft > tri1000xxx_fft.png
% saw 1000 50000 | add 1 | mul 128 | int > saw1000x.dat
% txt2wav -50000 saw1000x.dat
% lame saw1000x.wav saw1000x.mp3
% saw 1000 50000 | add 1 | mul 128 | int | head -250 | gp_wav > saw1000x.png
% saw 1000 50000 | add 1 | mul 128 | int | fftw | gp_fft > saw1000x_fft.png
% rand | head -50000 | add 1 | mul 128 | int > randx.dat
% txt2wav -50000 randx.dat
% lame randx.wav randx.mp3
% rand | head -50000 | add 1 | mul 128 | int | head -100 | gp_wav > randx.png
% rand | head -50000 | add 1 | mul 128 | int | fftw | gp_fft > randx_fft.png
% pngen | bit0 | head -50000 | mul 255 > pngen0x.dat
% txt2wav -50000 pngen0x.dat
% lame pngen0x.wav pngen0x.mp3
% pngen | bit0 | head -50000 | mul 255 | head -100 | gp_wav > pngen0x.png
% pngen | bit0 | head -50000 | mul 255 | fftw | gp_fft > pngen0x_fft.png
うぁっ、これは、ちょっと機械のノイズっぽい感じ。全然違うなぁ
WAVファイルを作るために、サンプリング周波数を50kHzにしていたが、
考えてみると、サンプリング周波数1MHzのままで、変換できるかもしれない。
ということで、1MHzサンプリングでWAVファイルを作って、
元のサンプリング周波数が大きくても、高調波はあまり改善されているようには見えない。
高調波的には、うーん。むしろ悪くなってるかも3
非対称ということより、全体的な歪の影響の方が大きいのかな
まぁ、元のサンプリング周波数が大きくても、特に音質が良くなるわけでも悪くなるわけでも無い様だ。
でも、サンプリング周波数に関係なく、WAVファイルは作れるし、MP3変換時にリサンプリングされる
わざわざ、CDで使われる44.1kHzとか、DVD規格の48kHzに近い、50kHzサンプリングで生成していたが、
周波数を全く気にせずに、いきなり変換しても全然問題なさそうだ。
3 音で聞く限りは、全然わかんないけれども。
[top]
単に、コンパレータを通すことで信号を2値化する(150608追記)
実際には、偶数倍の高調波もかなり含まれている
これは、2値化の際にDutyがずれるのが理由かもしれないが、全部同じ大きさなのは何故だろう?
考えてみると、全周波数に均等に分布しているということから、これは量子化誤差だろう。
2値化対象の正弦波が計算値なので、キレイに均等になっているのが分かるということか。。
量子化誤差が、これほどハッキリと見えるものだとは思わなかった。
でも逆に言えば、他のどのような2値化の方法でも、この量子化誤差の影響はあるということだろう。
% dds 1000 | comp 0 | mul 255 > comp1000.dat
% txt2wav -1000000 comp1000.dat
% lame comp1000.wav comp1000.mp3
音としては、普通の矩形波と区別はつかないなぁ。。量子化誤差はレベルが小さいので、ほとんど影響していない。
% dds 1000 | comp tri10000.dat | fftw | gp_fft > pwm1000_fft.png
肝心の1kHzと同じぐらいのレベルの雑音が含まれるので、このままではマズイ
10kHzの三角波でPWM化しているので、その半分の5kHz以下を使えば良さそうだ
% dds 1000 | comp tri10000.dat | mul 255 > pwm1000.dat
% txt2wav -1000000 pwm1000.dat
% lame pwm1000.wav pwm1000.mp3
FFTの結果を見ると、ローパスフィルタを通して使う必要がありそうだけど、
2値なのに、意外とちゃんと聞ける。まぁ、ノイズは結構高い周波数だからかな
でも、音質はかなり変わっちゃってる感じだ
フィルタレスのD級アンプの音を聞いて、PWMの周波数を上げれば音質は良くなりそうなので試す
雑音の周波数は少し上がっている。大きなのは20kHz以上だし、ちょっと良くなったかも。
% dds 1000 | comp tri40000.dat | mul 255 > pwm1000x.dat
% txt2wav -1000000 pwm1000x.dat
% lame pwm1000x.wav pwm1000x.mp3
fftの結果では良さそうだけど、音質はあまり良くないなぁ。。
大きな雑音は、高い周波数になったけど、全体的に見るとむしろ悪くなった4 ような。。
% dds 1000 | comp tri300000.dat | mul 255 > pwm1000xx.dat
% txt2wav -1000000 pwm1000xx.dat
% lame pwm1000xx.wav pwm1000xx.mp3
あれ、ノイズがほとんど聞こえなくなった。。。
fftで見ると雑音だらけなんだけど、人間の耳には聞こえないのか。。
スピーカー自体が、ローバスフィルタの役目をしているのかもしれないなぁ。
でも、20kHz以上の雑音はあまり影響ないと思っていたんだけど、やっぱり影響するのかな
まぁ、いずれにしてもPWMの周波数は、けっこう高めじゃないとダメらしい。
PWMの周波数が上がると、三角波自体の品質というか解像度も影響ありそう
同様に、乱数と比較することで、信号をPWM化する
やはり1kHzの正弦波を、乱数と比較してPWM化してみた
% dds 1000 | comp rand.dat | fftw | gp_fft > rpwm1000_fft.png
ノイズだらけなんだけど、肝心の1kHzの信号以外のレベルは小さく抑えられている
但し、ノイズは全ての周波数に渡っているので、フィルタとかで削るのは難しい
でも意外と悪くない。そのままでもそれなりに使えるかもしれない。
% dds 1000 | comp rand.dat | mul 255 > rpwm1000.dat
% txt2wav -1000000 rpwm1000.dat
% lame rpwm1000.wav rpwm1000.mp3
うーん、音質自体はそんなに悪くないけど、ホワイトノイズは、そのまま残ってるなぁ。。
これをそのままプログラムにすると、こんな感じかな(ds1.c)
面倒なので、入力は-1〜1を想定、積分器はdoubleにしました
#include <stdio.h>
int main(int argc, char *argv[])
{
int out;
double in, delta, sigma=0;
while (scanf("%lf", &in) != EOF) {
delta = in - out;
sigma += delta;
out = (sigma > 0) ? 1 : -1;
printf("%d\n",out);
}
return 0;
}
% dds 1000 | ds1 | head -2000 | gp_wav > ds1_1000.png
% dds 1000 | ds1 | fftw | gp_fft > ds1_1000_fft.png
いわゆるノイズシェーピングにより、ノイズ成分が高周波に押し上げられていることが分かる
% dds 1000 | ds1 | add 1 | mul 127 | int > ds1_1000.dat
% txt2wav -1000000 ds1_1000.dat
% lame ds1_1000.wav ds1_1000.mp3
ノイズシェーピングの効果は絶大! ほとんどのノイズが可聴周波数外になっているらしい
とても2値の信号とは思えないほど、ノイズが少なく感じる
ひょっとすると、MP3へのリサンプリング時にLPFが掛かっている可能性もあるけど
4 良く見ると縦軸の目盛り変わってるので、それほど悪くはなってない。
[top]
前述の項目で、rand関数を使ったホワイトノイズを作ったんだけど、
やはり乱数だけに、周波数毎の大きさに結構バラツキがある。
特に、デジタルフィルタの特性を見たい時には、バラツキが無いほうが良いなぁ。
ということで、fftwの実験用の擬似ホワイトノイズを作ってみる
理屈で言えば、全ての周波数の正弦波を足していけば、ホワイトノイズになる
ddsで正弦波を生成して、addで加算すれば良いが、とても面倒なのでプログラムを作る
DDSで、1Hzおきに正弦波を生成したい。
DDSで生成可能な周波数は、サンプリング周波数/波形テーブルの大きさの整数倍になるので、
波形テーブルは、少なくともサンプリング周波数分だけ必要になる
最初は、試しにサンプリング周波数1kHzで、0〜999Hzを加算してみた
すると、結果が常に0になってしまった。プログラムを何度見直してもゼロになる。
よく考えてみると、ナイキスト周波数で折り返しになっているので、全部足せば0なのか。。
ということで、サンプリング周波数の半分まで加算することにした。(white.c)
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#define MAX_SAMPLE (1000000)
#define SAMPLE (1000)
double sin_table[MAX_SAMPLE/4+1];
int t[MAX_SAMPLE];
int N, f1, f2;
void make_table(void)
{
int x;
for (x=0; x<=N/4 ;x++)
sin_table[x] = sin(2*M_PI/N*x);
}
double sin_x(int x)
{
int xx;
xx = x%(N/4);
if (x < N/4*1) return sin_table[xx];
else if (x < N/4*2) return sin_table[N/4-xx];
else if (x < N/4*3) return -sin_table[xx];
else return -sin_table[N/4-xx];
}
main( int argc, char *argv[] )
{
int i, j, d;
double out;
N = SAMPLE;
if (argc>1)
N = atoi(argv[1]);
f1 = 0;
f2 = N/2-1;
if (argc>2)
f2 = atoi(argv[2]);
if (argc>3)
f1 = atoi(argv[3]);
make_table();
srand(777);
for (i=0; i<N ;i++) t[i] = rand()%N; // 位相の初期値を0ではなく、バラつかせる
for (i=0; i<N ;i++) {
out = 0;
for (j=f1; j<=f2 ;j++) {
out += sin_x(t[j]);
d = j;
t[j] = (t[j]+d)%N;
}
printf("%f\n",out);
}
}
サンプリング周波数 Sと、生成する周波数範囲 f1〜f2 の指定も可能
パラメータは、S, f2, f1の順に指定する
周波数範囲を省略すると、1Hz間隔で 0〜S/2の範囲で生成する
サンプリング周波数1kHzで、加算した結果をFFTで見てみる
% white0 1000 | fftw | gp_fft > white01000_fft.png
よしよし、思った以上に、ちゃんと加算されてるなぁ。
% white0 1000 | gp_wav > white01000.png
うーん。fftwの実験用には、これでも良いんだけど。。
ホワイトノイズには程遠いなぁ。。それにちょっと使いづらい。。。
これは、ひょっとすると δ関数5 を作ってるようなものなのかもしれない。。
上で作成した変な波形も、位相が揃っているので、デジタルフィルタの位相特性を見るためには使える(140905追記)
ナイキスト周波数まで、一定の位相(π、或いは-π)になっている。
でも、ナイキスト周波数以上は、含まれないので、位相はいきなり0となる(不連続になっている)
これは、FFTではそういうものなのか、fftwの仕様なのか分からないが、ナイキスト周波数以上は使えない様だ
% white0 1000 | rcf 0.5 | fftw |fftw_unwrap | gp_ang > rcf_ang.png
RCフィルタの位相特性が見える(でも±が逆のような)7
まぁ、いずれにしても何かの役に立つかもしれないので、
擬似ホワイトノイズ0として、使えるようにしておく(white0.c)
多分、正弦波を合成する時に、スタート位置が一緒なので、
ということで、各周波数の位相の初期値を0じゃなくて、
では、改めて先頭の100ポイントのプロットと、FFT結果を見てみよう
% white 1000 | head -100 | gp_wav > white1000_100.png
% white 1000 | fftw | gp_fft > white1000_fft.png
% white0 1000 | minmax
-318.308839, 318.308839
% white 1000 | minmax
-51.121728, 45.900439
では、音で聞いてみることを前提に、サンプリング周波数を48kHzにして
やはり先頭の100ポイントのプロットと、FFT結果
% white 48000 | head -100 | gp_wav > white48000_100.png
% white 48000 | fftw | gp_fft > white48000_fft.png
素晴らしい!少なくともfftwでみる限りは、全周波数について同レベルの信号になっている
あくまで計算上なので、実際の波形にする場合は、そんなにうまく行くわけない8 と思うけど
% white 48000 | minmax
-451.917214, 435.368105
16bitのWAVファイルは、-32768〜32767のデータ範囲なので、
32768/452≒72.5ぐらいだから、x70ぐらいでいいか。
% white 48000 | mul 70 > white48000.dat
% txt2wav -48000 white48000.dat
% lame white48000.wav white48000.mp3
まぁ、ほんとにホワイトノイズかどうかは別にしても
fftwの実験用には、乱数によるノイズよりも、ずっと使いやすいと思う
指定範囲だけ、生成できる様にしてみた(140902追記)
% white 1000 100 10 | fftw | gp_fft > white1000_100_10_fft.png
サンプリング周波数1kHzで、10〜100Hzの範囲だけ生成してみた
少しノイズもあるけど、レベルは十分小さいので、全然問題なし9
乱数だと、こんなことは無理だけど、これは加算してるだけなので簡単だ
実験用のデータとしては、結構使い道があるんじゃないかと思う
% white 1000 100 10 | mul 5 | add 128 > white1000_100_10.dat
% txt2wav -1000 white1000_100_10.dat
% lame white1000_100_10.wav white1000_100_10.mp3
はっきりしなくて、音とは言えない感じだけど、こもったような感じでちょっと面白い。
5 δ関数は、全ての周波数を均等に含んでいるらしいので。
6 π、或いは−πで暴れるので、unwrapを掛けている。だから、値は元の計算値とは違っている。
7 これについては、fftwプログラムの位相角計算が間違っていた。プログラムは直したけど、図は後で差換えないと。。
8 だいたい実際は窓をどう取るかによって、全然変わってくるよね。
9 ちゃんと、ナイキスト周波数で、折り返しがあることが分かる。
[top]
プログラムで波形を作っても、波形を見ただけでは違いが分からなかったりするけど、
fftwがあれば、なんでもFFTを掛けて、すぐに含まれる周波数を確認できる
サンプル数の制限も無いので、いろいろ試せて、とっても便利
個人的には、デジタル系の処理を考えるときに、fftwは無くてはならない道具になっている。
[top]
[プログラムの部屋に戻る]
⇒ Disqusの広告がうるさすぎるので基本は非表示にしました