PerlでReverbを実装する
といっても、後部残響音の方。
シュレーダーによる生成アルゴリズムを実装してみた。
やることは、4つのコムフィルタを並列で処理して、
その出力を2回オールパスフィルタに入力するだけ。
use v5.14;
use strict;
use warnings;
use constant X_MAX => 1024;
use constant Y_MAX => 256;
use constant Y_MIN => -256;
use constant VALUE_MAX => 256;
use constant VALUE_MIN => -256;
use constant X_STEP => 2;
use Imager;
use List::Util qw/sum/;
use Math::Trig qw/pi/;
my $margin = 10;
my $width = $margin + (X_MAX + 1) + $margin;
my $height = $margin + (Y_MAX - Y_MIN + 1) + $margin;
my ( $x0, $y0 ) = ( $margin, $margin + (Y_MAX + 1) );
my $f1 = comb_filters(
[ 0.5, 31 ],
[ 0.5, 37 ],
[ 0.5, 41 ],
[ 0.5, 43 ]
);
my $f2 = all_pass_filter( 0.7, 17 );
my $f3 = all_pass_filter( 0.7, 5 );
my $f = sub {
my $in = shift;
return $f3->( $f2->($f1->($in)) );
};
=pod
my $f = comb_filters(
[ 0.5, 31 ],
[ 0.5, 37 ],
[ 0.5, 41 ],
[ 0.5, 43 ]
);
=cut
my $log = {
in => [],
out => []
};
my $in = VALUE_MAX;
foreach ( 0..(X_MAX / X_STEP) ) {
push @{$log->{in}}, $in;
push @{$log->{out}}, $f->( $in );
$in = 0;
}
write_graph( 'aaa.png', {
red => $log->{in},
blue => $log->{out}
} );
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) );
foreach my $color ( keys %{$waves} ) {
my $x = 0;
my @tmp = ();
foreach ( @{$waves->{$color}} ) {
push @tmp, [ $x, $_ ] if 0 != $_; # <<< 0は無視!
$x += X_STEP;
}
draw_points_with_bar( $img, \@tmp, $color );
}
$img->write( file => $dst_file ) or die $img->errstr;
}
sub calc_points {
my ( $fa, $fb ) = @_;
my @src = ();
for (my $i=0; $i<=X_MAX; $i+=8) {
my $t = $i / X_MAX;
my $y = ($fa * $t) + ($fb * $t ** 5.0);
push @src, [
$i,
int( ($y * 128) )
];
}
return \@src;
}
sub draw_graduation {
my ( $img, $color ) = @_;
{
my $gray = Imager::Color->new( 192, 192, 192 );
my $x = 128;#($w / 4);
while ( $x <= X_MAX ) {
$img->line( color => $gray,
x1 => $x0 + $x, y1 => $y0 + Y_MIN,
x2 => $x0 + $x, y2 => $y0 + Y_MAX );
$x += 128;#($w / 4);
}
my $y = 128;#($h / 4);
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 += 128;#($h / 4);
}
}
{
$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 ) = @_;
my $n = 1;
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->box( xmin => $x0 + $x - $n, ymin => $y0 - $y - $n,
xmax => $x0 + $x + $n, ymax => $y0 - $y + $n,
color => $color, filled => 0 );
}
}
sub draw_points {
my ( $img, $data, $color ) = @_;
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->setpixel( x => $x0 + $x, y => $y0 - $y, color => $color );
#$img->box( xmin => $x0 + $x - $n, ymin => $y0 - $y - $n,
# xmax => $x0 + $x + $n, ymax => $y0 - $y + $n,
# color => $color, filled => 1 );
}
}
sub draw_polyline {
my ( $img, $data, $color ) = @_;
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->polyline( points => \@points, color => $color );
}
# 棒グラフっぽく
sub draw_points_with_bar {
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 );
}
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 ( $img != $img_dst ) {
$img->compose(
src => $img_dst, opacity => $opacity, combine => 'add' );
}
plot_points( $img, $data, $color );
}
sub all_pass_filter {
my ( $aa, $n, $fill ) = @_;
$n = 1 if not defined($n);
$fill = 0 if not defined($fill);
my @za = map { $fill; } 1..$n;
my @zb = map { $fill; } 1..$n;
return sub {
my $in = shift;
unshift @za, $in;
my $out = (pop(@zb) * $aa) + pop(@za) - ($aa * $in);
unshift @zb, $out;
return $out;
};
}
sub comb_filter {
my ( $aa, $n, $fill ) = @_;
$n = 1 if not defined($n);
$fill = 0 if not defined($fill);
my @za = map { $fill; } 1..$n;
return sub {
my $in = shift;
my $out = pop @za;
unshift @za, ($in + ($aa * $out));
return $out;
};
}
sub comb_filters {
my @args = @_;
my @filters = ();
foreach ( @args ) {
push @filters, comb_filter( @{$_} );
}
return sub {
my $in = shift;
return sum( map {
$_->( $in );
} @filters );
};
}
コムフィルタだけ通すと、
インパルス応答はディレイが4つみたいな感じになる。
それをオールパスフィルタで拡散する。
つまり、乱反射を再現してるんだと思われる。
という訳で、本日のネタ元はこちら。
あと、このページの方が分かりやすいかも。
(このページ以外も必見!)
リバーブ2/4 リバーブレーター(残響装置)
http://www.ari-web.com/service/soft/reverb-2.htm
次のアプリに、Reverbが搭載される訳ではないので悪しからず。
おしまい。


Leave a Comment