PerlでReverbを実装する(続き)

ちゃんとやるには、コムフィルタのフィードバックゲインを調整して、
例えば0.001になるタイミングを揃える必要があるらしい。

という訳で、リバーブタイムを決めて、
サンプリング周波数からサンプル数を決めて、
そこからフィードバックゲインを求めることにした。

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 =>  1.0;
use constant VALUE_MIN => -1.0;
use constant X_STEP => 8;

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 $sf = (X_MAX / X_STEP) / 2;
my @m = ( 2, 8, 5 );
my $reverb_time = 2.0; # sec

my @g = map {
    10 ** ((-3.0 * $_) / ($reverb_time * $sf));
    #0.5;
} @m;

my @f = map {
    comb_filter( $g[$_], $m[$_] );
} 0..2;

say 'Sampling Freq: ', $sf;
say 'Reverb Time: ', $reverb_time, '(sec)';
foreach my $i ( 0..2 ) {
    printf( "f[%d]: %.3f, %2d\n", $i, $g[$i], $m[$i] );
}

my $log = {
    in => [],
    out1 => [],
    out2 => [],
    out3 => []
};

my $in = VALUE_MAX;
foreach ( 0..(X_MAX / X_STEP) ) {
    push @{$log->{in}}, $in;
    push @{$log->{out1}}, $f[0]->( $in );
    push @{$log->{out2}}, $f[1]->( $in );
    push @{$log->{out3}}, $f[2]->( $in );
    $in = 0;
}

write_graph( 'bbb.png', {
    red     => $log->{in},
    blue    => $log->{out1},
    green   => $log->{out2},
    purple  => $log->{out3},
} );

foreach my $i ( 0..16 ) {
    my @tmp = (
        $f[0]->( 0 ),
        $f[1]->( 0 ),
        $f[2]->( 0 )
    );
    say sprintf('[%2d] : ', $i), join( ',', map { sprintf('%6.4f', $_); } @tmp );
}

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, 0.4 ); } $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 = 2;

    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 );
    }

    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 );
    };
}

$ perl bbb.pl
Sampling Freq: 64
Reverb Time: 2(sec)
f[0]: 0.898, 2
f[1]: 0.649, 8
f[2]: 0.764, 5
[ 0] : 0.0000,0.0000,0.0000
[ 1] : 0.0010,0.0000,0.0012
[ 2] : 0.0000,0.0000,0.0000
[ 3] : 0.0009,0.0000,0.0000
[ 4] : 0.0000,0.0000,0.0000
[ 5] : 0.0008,0.0000,0.0000
[ 6] : 0.0000,0.0000,0.0009
[ 7] : 0.0007,0.0010,0.0000
[ 8] : 0.0000,0.0000,0.0000
[ 9] : 0.0006,0.0000,0.0000
[10] : 0.0000,0.0000,0.0000
[11] : 0.0006,0.0000,0.0007
[12] : 0.0000,0.0000,0.0000
[13] : 0.0005,0.0000,0.0000
[14] : 0.0000,0.0000,0.0000
[15] : 0.0005,0.0006,0.0000
[16] : 0.0000,0.0000,0.0005

なんで、0.001(10の-3乗)辺りで揃えるのかは分からないけど、
ディレイタイム(サンプル数)を素数にするのは、
各コムフィルタの出力を被らないようにするため。

で、グラフはこんな感じ。
20161008-1

ところで、コムフィルタのディレイタム(サンプル数)が十分にある場合でも、
フィードバックゲインを調整した方が良い結果になるんですかね?
その辺りは、おいおいということで。

おしまい。

Leave a Comment