2016/12/09 @ YAPC::Hokkaido 2016 SAPPORO前夜祭
Created by @techno_neko
時間経過と共に音程が高くなって、
聴き取れる周波数を超えるので聴こえなくなるはず
外部入力(マイクによる入力など)をサンプリングする例
あらかじめ波形テーブルを用意してサンプリングする例
「サンプリング間隔」と「音の高さ」の関係
波形テーブルが1秒間に何回出力されるか?
周波数が分かれば、波形テーブル1回あたりの出力時間が分かる
16個のデータに対して、
波形テーブルを何回出現させるか?
16個のデータに対して、波形テーブルを1回出現させる
波形テーブル1周あたり16個
16個のデータに対して、波形テーブルを2回出現させる
波形テーブル1周あたり8個
16個のデータに対して、波形テーブルを5回出現させる
波形テーブル1周あたり3.2個
32個のデータに対して、波形テーブルを5回出現する
波形テーブル1周あたり6.4個
「正規化周波数」とは、出力したい周波数を
出力フォーマットにおける「サンプリング周波数」で割ったもの
出力フォーマットが16Hzの時に、
1.4Hzを表現するためのサンプリング間隔は?
波形テーブルの端から端までを0.0以上1.0未満とすると、
1.4 / 16.0 = 0.0875(正規化周波数の計算方法と同じ)
正規化周波数が0.35なら、
正規化周波数が0.5になると、
どの波形テーブルをサンプリングしたとしても、
出力されるデータは同じ
元の波形は、(サイン|三角|矩形)波かも知れない
周波数(サンプリング間隔)の変更によって、
サンプリング開始位置がずれた場合
サンプリング開始位置によっては0が出力される
正規化周波数が0.65の場合
あれ?これ、さっき見たような・・・?
(気付いた人いる???)
出力されるデータが同じ!?
正規化周波数が0.875と0.125の場合でも、
サンプリング間隔が同じ
ヒント:0.875と0.125を足すと1.0になる
以降は、この現象を「折り返しが生じた」と表現する
Cassis
主な用途
440Hzのサイン波を、WAVファイルに出力する
use v5.16;
use strict;
use warnings;
use Math::Trig qw/pi/;
use Cassis;
my $osc = sub {
return sin( 2.0 * pi() * $_[0] );
};
my $t = 0.0;
my $freq = 440.0;
my @samples = ();
foreach ( 1..65536 ) {
push @samples, $osc->($t);
$t += ( $freq / 44100.0 );
}
Cassis::File::write(
file => 'sample.wav',
fs => 44100,
channels => [ \@samples ]
);
以降は、波形出力の処理のみ抜粋します
my $osc = sub {
return sin( 2.0 * pi() * $_[0] );
};
my $t = 0.0;
my $freq = 440.0;
my @samples = ();
foreach ( 1..65536 ) {
push @samples, $osc->($t);
$t += ( $freq / 44100.0 );
}
DFT(離散的フーリエ変換)
FFT(高速フーリエ変換)
用意するもの
周波数成分とは、
波形データに含まれているサイン波のこと?
この波形データのスペクトルは?
my $t = 0.0;
my $freq = 440.0;
my @samples = ();
foreach ( 1..65536 ) {
push @samples, (
($osc->($t * 1.0) / 2.0) # => 440Hz
+ ($osc->($t * 3.0) / 4.0) # => 1,320Hz
+ ($osc->($t * 6.0) / 8.0) # => 2,640Hz
);
$t += ( $freq / 44100.0 );
}
大きさも、だいたい合ってる
FFT実行時の入力データの「サイズ」について
周期関数の例
あらゆる周期関数はsinとcosで表現できる!?
例)矩形波のフーリエ級数展開
f(t)=4π(sint+13sin3t+15sin5t+17sin7t+⋯)
例)矩形波のフーリエ級数展開
これで、ほんとに矩形波になるの!?
f(t)=4π(sint+13sin3t+15sin5t+17sin7t+⋯)
sub get_osc {
my $k = shift;
my @wk = map { (2 * $_) - 1; } 1..$k;
return sub {
my $t = shift;
return (4.0 / pi()) * sum( map {
(1.0 / $_) * sin(2.0 * pi() * $t * $_);
} @wk );
};
}
オリジナルコードは下のページ
矩形波のグラフとWAVファイルを出力するスクリプト
use v5.16;
use strict;
use warnings;
use constant X_MAX => 2048;
use constant Y_MAX => 512;
use constant Y_MIN => -512;
use constant VALUE_MAX => 256;
use constant VALUE_MIN => -256;
use constant X_STEP => 4;
use Cassis;
use Imager;
use List::Util qw/sum/;
use Math::Trig qw/pi/;
my $marginX = 20;
my $marginY = 120;
my $width = $marginX + (X_MAX + 1) + $marginX;
my $height = $marginY + (Y_MAX - Y_MIN + 1) + $marginY;
my ( $x0, $y0 ) = ( $marginX, $marginY + (Y_MAX + 1) );
my $fs = X_MAX / X_STEP;
my $freq = 1.0;
my $dt = $freq / $fs;
write_graph( 'pulse.png', [
{ color => 'orange' , samples => gen_samples($fs + 1, $dt, 2) },
{ color => 'purple' , samples => gen_samples($fs + 1, $dt, 4) },
{ color => 'green' , samples => gen_samples($fs + 1, $dt, 12) },
{ color => 'red' , samples => gen_samples($fs + 1, $dt,128) }
] );
$dt = 0.005;
Cassis::File::write(
file => 'pulse.wav',
fs => 44100,
channels => [ gen_samples(44100 * 5, $dt, 50, 1.0) ]
);
sub get_osc {
my $k = shift;
my @wk = map { (2 * $_) - 1; } 1..$k;
return sub {
my $t = shift;
return (4.0 / pi()) * sum( map {
(1.0 / $_) * sin(2.0 * pi() * $t * $_);
} @wk );
};
}
sub gen_samples {
my ( $n, $dt, $k, $gain ) = @_;
$gain //= VALUE_MAX;
say 'max dt = ', $dt * ((2 * $k) - 1);
my $osc = get_osc( $k );
my @samples = map {
$gain * $osc->($_ * $dt);
} 0..($n - 1);
return \@samples;
}
sub write_graph {
my ( $dst_file, $waves ) = @_;
my $img = Imager->new(
xsize => $width, ysize => $height, channels => 4 );
$img->box( filled => 1, color => 'white' );
draw_graduation( $img, Imager::Color->new(192, 192, 192) );
my %pallete = (
orange => { hue => 45, v => 1.0, s => 1.0, opacity => 1.0 },
purple => { hue => 300, v => 1.0, s => 1.0, opacity => 1.0 },
green => { hue => 130, v => 0.7, s => 1.0, opacity => 1.0 },
red => { hue => 0, v => 1.0, s => 1.0, opacity => 1.0 }
);
foreach my $wave ( @{$waves} ) {
my $x = 0;
my @tmp = ();
foreach ( @{$wave->{samples}} ) {
push @tmp, [ $x, $_ ];
$x += X_STEP;
}
my $color = $pallete{$wave->{color}};
if ( not $color ) {
warn 'not found! -> ', $wave->{color};
next;
}
my $c = Imager::Color->new( %{$color} );
my ( $rr, $gg, $bb, undef ) = $c->rgba();
say 'Color = ', sprintf('#%02X%02X%02X', $rr, $gg, $bb);
my $o = $color->{opacity};
draw_polyline( $img, \@tmp, $c, $o * 0.7 );
plot_points( $img, \@tmp, $c, $o * 0.3, 1 );
plot_points( $img, \@tmp, $c, $o * 0.5 );
}
$img->write( file => $dst_file ) or die $img->errstr;
}
sub draw_graduation {
my ( $img, $color ) = @_;
{
my $gray = Imager::Color->new( 192, 192, 192 );
my $x = (X_MAX / 8);
while ( $x <= X_MAX ) {
$img->line( color => $gray,
x1 => $x0 + $x, y1 => $y0 + Y_MIN,
x2 => $x0 + $x, y2 => $y0 + Y_MAX );
$x += (X_MAX / 8);
}
my $y = (Y_MAX / 2);
while ( $y <= Y_MAX ) {
$img->line( color => $gray,
x1 => $x0 + 0, y1 => $y0 - $y,
x2 => $x0 + X_MAX, y2 => $y0 - $y );
$img->line( color => $gray,
x1 => $x0 + 0, y1 => $y0 + $y,
x2 => $x0 + X_MAX, y2 => $y0 + $y );
$y += (Y_MAX / 2);
}
}
{
$img->line( color => 'black',
x1 => $x0, y1 => $y0 + Y_MIN,
x2 => $x0, y2 => $y0 + Y_MAX );
$img->line( color => 'black',
x1 => $x0 + 0, y1 => $y0,
x2 => $x0 + X_MAX, y2 => $y0 );
}
}
sub plot_points {
my ( $img, $data, $color, $opacity, $filled ) = @_;
$filled //= 0;
my $n = 2;
my $img_dst = $img;
if ( defined($opacity) and $opacity < 1.0 ) {
$img_dst = Imager->new(
xsize => $img->getwidth(), ysize => $img->getheight(), channels => 4 );
}
foreach my $pt ( @{$data} ) {
my ( $x, $y ) = ( $pt->[0], $pt->[1] );
$y /= (VALUE_MAX / Y_MAX);
$y = ( $y < .0 ) ? int($y - .5) : int($y + .5);
#printf( "%6.3f, %6.3f\n", $x, $y );
$img_dst->box(
xmin => $x0 + $x - $n, ymin => $y0 - $y - $n,
xmax => $x0 + $x + $n, ymax => $y0 - $y + $n,
color => $color, filled => $filled );
}
if ( $img != $img_dst ) {
$img->compose(
src => $img_dst, opacity => $opacity );
}
}
sub draw_polyline {
my ( $img, $data, $color, $opacity ) = @_;
my $img_dst = $img;
if ( defined($opacity) and $opacity < 1.0 ) {
$img_dst = Imager->new(
xsize => $img->getwidth(), ysize => $img->getheight(), channels => 4 );
}
my @points = map {
my ( $x, $y ) = ( $_->[0], $_->[1] );
$y /= (VALUE_MAX / Y_MAX);
$y = ( $y < .0 ) ? int($y - .5) : int($y + .5);
[ $x0 + $x, $y0 - $y ];
} @{$data};
$img_dst->polyline( points => \@points, color => $color );
if ( $img != $img_dst ) {
$img->compose(
src => $img_dst, opacity => $opacity );
}
}
512個で1Hzを表現
橙 → 紫 → 緑 → 赤 の順に矩形波に近づいている
1, 1/3, 1/5, 1/7, ... 1/23, ... 1/255
1, 1/3, 1/5, ... 1/255, ... 1/439, ... 1/5999 まで計算すると、
紫のように矩形波に収束する
これで、440Hzを出力すると?
f(t)=4π(sint+13sin3t+15sin5t+17sin7t+19sin9t)
my $freq = 440.0;
# gain を指定してクリップされないようにすること!
my $samples = gen_samples(
44100 * 5, $freq / 44100, 5, 0.8 );
オリジナルコードは下のページ
矩形波のWAVファイルを出力するスクリプト
use v5.16;
use strict;
use warnings;
use List::Util qw/sum/;
use Math::Trig qw/pi/;
use Cassis;
my $freq = 440.0;
# gain を指定してクリップされないようにすること!
my $samples = gen_samples(
44100 * 5, $freq / 44100, 5, 0.8 );
Cassis::File::write(
file => 'sample3.wav',
fs => 44100,
channels => [ $samples ]
);
sub get_osc {
my $k = shift;
my @wk = map { (2 * $_) - 1; } 1..$k;
return sub {
my $t = shift;
return (4.0 / pi()) * sum( map {
(1.0 / $_) * sin(2.0 * pi() * $t * $_);
} @wk );
};
}
sub gen_samples {
my ( $n, $dt, $k, $gain ) = @_;
say 'max dt = ', $dt * ((2 * $k) - 1);
my $osc = get_osc( $k );
my @samples = map {
$gain * $osc->($_ * $dt);
} 0..($n - 1);
return \@samples;
}
予想通りですよね?
音程が高くなると?
今までナイキスト周波数を超えていなかった周波数成分が、
ナイキスト周波数を超えてしまう
より矩形波に近づくと?
-1.0と+1.0を繰り返すような矩形波は、
限りなく高い周波数成分を含んでいるため、
ナイキスト周波数を超えた分は折り返しが生じる
エイリアスノイズが出力されるのを防ぐには?
結論から言うと、
ナイキスト周波数を超えてサンプリングしたデータを、
出力しなければ良い
「サイン波」と「矩形波」で共通
「矩形波」の場合
フーリエ級数展開を使って矩形波を出力する場合
音程が上がる分には計算量が減る
フーリエ級数展開を使って矩形波を出力する場合
折り返しが生じるのを防ぐのが目的のローパスフィルタを、
特に「アンチエイリアシング・フィルタ」と呼ぶ
この関数を使って見繕う
f(ω)=1√1+(ω/ωc)2M
ちゃんと知りたい人は、「バタワース特性」で調べてね!
バタワース特性を描画するスクリプト
use v5.16;
use strict;
use warnings;
use constant X_MAX => 480;
use constant Y_MAX => 240;
use constant Y_MIN => 0;
use constant VALUE_MAX => 256;
use constant VALUE_MIN => 0;
use constant X_STEP => 3;
use Imager;
use List::Util qw/sum/;
use Math::Trig qw/pi/;
my $marginX = 16;
my $marginY = 16;
my $width = $marginX + (X_MAX + 1) + $marginX;
my $height = $marginY + (Y_MAX - Y_MIN + 1) + $marginY;
my ( $x0, $y0 ) = ( $marginX, $marginY + (Y_MAX + 1) );
my $fc = 0.35;
write_graph( 'alias-filter.png', [
{ color => 'blue', func => get_func(22.0, $fc) }
] );
sub get_func {
my ( $m, $fc ) = @_;
return sub {
my $t = shift;
return 1.0 / sqrt(1.0 + (($t / $fc) ** (2.0 * $m)));
};
}
sub write_graph {
my ( $dst_file, $filters ) = @_;
my $img = Imager->new(
xsize => $width, ysize => $height, channels => 4 );
$img->box( filled => 1, color => 'white' );
draw_graduation( $img, Imager::Color->new(192, 192, 192) );
my %pallete = (
orange => { hue => 45, v => 1.0, s => 1.0, opacity => 1.0 },
purple => { hue => 300, v => 1.0, s => 1.0, opacity => 1.0 },
green => { hue => 130, v => 0.7, s => 1.0, opacity => 1.0 },
red => { hue => 0, v => 1.0, s => 1.0, opacity => 1.0 },
blue => { hue => 240, v => 1.0, s => 1.0, opacity => 1.0 },
);
foreach my $f ( @{$filters} ) {
my $func = $f->{func};
my @tmp = ();
for (my $i=0; $i<=X_MAX; $i+=X_STEP) {
my $x = $i / X_MAX;
push @tmp, [ $i, $func->($x) * VALUE_MAX ];
}
my $color = $pallete{$f->{color}};
if ( not $color ) {
warn 'not found! -> ', $f->{color};
next;
}
my $c = Imager::Color->new( %{$color} );
my ( $rr, $gg, $bb, undef ) = $c->rgba();
say 'Color = ', sprintf('#%02X%02X%02X', $rr, $gg, $bb);
my $o = $color->{opacity};
draw_spectrum( $img, \@tmp, $c, $o * 0.3, 1 );
#draw_polyline( $img, \@tmp, $c, $o * 0.7 );
#plot_points( $img, \@tmp, $c, $o * 0.3, 1 );
plot_points( $img, \@tmp, $c, $o * 0.5 );
}
$img->write( file => $dst_file ) or die $img->errstr;
}
sub draw_graduation {
my ( $img, $color ) = @_;
{
my $gray = Imager::Color->new( 192, 192, 192 );
my $x = (X_MAX / 4);
while ( $x <= X_MAX ) {
$img->line( color => $gray,
x1 => $x0 + $x, y1 => $y0 - Y_MIN,
x2 => $x0 + $x, y2 => $y0 - Y_MAX );
$x += (X_MAX / 4);
}
my $y = Y_MIN;
while ( $y <= Y_MAX ) {
$img->line( color => $gray,
x1 => $x0 + 0, y1 => $y0 - $y,
x2 => $x0 + X_MAX, y2 => $y0 - $y );
$y += (Y_MAX / 2);
}
}
# 原点じゃない側の軸線を端まで伸ばすバージョン
{
$img->line( color => 'black',
x1 => $x0, y1 => $y0 - Y_MIN,
#x2 => $x0, y2 => $y0 - Y_MAX );
x2 => $x0, y2 => 0 );
$img->line( color => 'black',
x1 => $x0 + 0, y1 => $y0,
#x2 => $x0 + X_MAX, y2 => $y0 );
x2 => $img->getwidth() - 1, y2 => $y0 );
}
}
sub plot_points {
my ( $img, $data, $color, $opacity, $filled ) = @_;
$filled //= 0;
my $n = 1;
my $img_dst = $img;
if ( defined($opacity) and $opacity < 1.0 ) {
$img_dst = Imager->new(
xsize => $img->getwidth(), ysize => $img->getheight(), channels => 4 );
}
foreach my $pt ( @{$data} ) {
my ( $x, $y ) = ( $pt->[0], $pt->[1] );
$y /= (VALUE_MAX / Y_MAX);
$y = ( $y < .0 ) ? int($y - .5) : int($y + .5);
#printf( "%6.3f, %6.3f\n", $x, $y );
$img_dst->box(
xmin => $x0 + $x - $n, ymin => $y0 - $y - $n,
xmax => $x0 + $x + $n, ymax => $y0 - $y + $n,
color => $color, filled => $filled );
}
if ( $img != $img_dst ) {
$img->compose(
src => $img_dst, opacity => $opacity );
}
}
sub draw_polyline {
my ( $img, $data, $color, $opacity ) = @_;
my $img_dst = $img;
if ( defined($opacity) and $opacity < 1.0 ) {
$img_dst = Imager->new(
xsize => $img->getwidth(), ysize => $img->getheight(), channels => 4 );
}
my @points = map {
my ( $x, $y ) = ( $_->[0], $_->[1] );
$y /= (VALUE_MAX / Y_MAX);
$y = ( $y < .0 ) ? int($y - .5) : int($y + .5);
[ $x0 + $x, $y0 - $y ];
} @{$data};
$img_dst->polyline( points => \@points, color => $color );
if ( $img != $img_dst ) {
$img->compose(
src => $img_dst, opacity => $opacity );
}
}
sub draw_spectrum {
my ( $img, $data, $color, $opacity, $with_box ) = @_;
$with_box //= 0;
my $n = 1;
my $img_dst = $img;
if ( defined($opacity) and $opacity < 1.0 ) {
$img_dst = Imager->new(
xsize => $img->getwidth(), ysize => $img->getheight(), channels => 4 );
}
foreach my $pt ( @{$data} ) {
my ( $x, $y ) = ( $pt->[0], $pt->[1] );
$y /= (VALUE_MAX / Y_MAX);
$y = ( $y < .0 ) ? int($y - .5) : int($y + .5);
$img_dst->line(
x1 => ($x0 + $x), y1 => $y0,
x2 => ($x0 + $x), y2 => ($y0 - $y),
color => $color );
if ( $with_box ) {
$img_dst->box(
xmin => $x0 + $x - $n, ymin => $y0 - $y - $n,
xmax => $x0 + $x + $n, ymax => $y0 - $y + $n,
color => $color, filled => 1 );
}
}
if ( $img != $img_dst ) {
$img->compose(
src => $img_dst, opacity => $opacity );
}
}
f(ω)=1√1+(ω/ωc)2M
ωc の値を変えると、 1√2 を通る時の ω の値が変わる
f(ω)=1√1+(ω/ωc)2M
M の値を変えると、 ωc を通る時の角度が変わる
振幅特性の参照方法
正規化周波数を引数に、 f(ω)=1√1+(ω/ωc)2M を参照して、
波形テーブルから取得した値に掛ける
ループ回数を抑える方針
こんな感じでグルーピングして、
グループ単位でサイン波の積算を済ませておく
グループ単位の積算
単純に足し合わせるのではなく、
三角形の傾きに合わせて配合する
振幅特性の参照方法
右端のグループで折り返しが生じているが、
「傾きによる配合」と「振幅特性」によって、
エイリアスノイズが軽減されている
調整項目
矩形波のデモ