#!/usr/local/bin/perl -w use strict; use Tk; use Tk::widgets qw(Canvas); use Audio::Data qw(solve_polynomial); use Audio::Play; use Audio::Filter; use Tk::Scope; use Carp; use File::Basename qw(dirname); use File::Spec; use Cwd; my $lcwd = getcwd(); my $scwd = $lcwd; sub BORDER () { 2 } $SIG{__DIE__} = \&Carp::confess; # $SIG{INT} = \&Carp::confess; my $FFT_SIZE = 256; my $LPC_POLES = 10; my $do_fft = 1; my $do_lpc = 1; my $mw = MainWindow->new; my $menu = $mw->menu; my ($f,$val) = create_labels($mw,[qw(0 xmax start end cursor1 cursor2)],qw(Freq Samp F0)); my $scope = $mw->Scrolled(Scope => -relief => 'ridge', -border => 2, -width => 640, -height => 129); my $over = $mw->Scope(-border => 2,-relief => 'ridge', -height => 128, -width => 320); my $voice = $mw->Scope(-relief => 'ridge', -width => 256, -height => 128); my $poles = $mw->Canvas(-width => 130, -height => 130); $poles->create(oval => [1,1,129,129]); $poles->create(line => [1,65,129,65]); $poles->create(line => [65,1,65,129]); my $xfrm = $mw->Scope(-border => BORDER, -relief => 'groove', -height => 200, -yscale => undef, -range1 => 0, -access => 'dB', # -domain => 'frequency', ); my $txt = $mw->Scrolled(Text => -scrollbars => 's', -width => 25, -height => 14, -wrap => 'none'); # Image at the back inside border my $simg = $mw->Photo(-height => 129, -width => 640); $scope->create(image => [BORDER,BORDER], -anchor => 'nw', -image => $simg); my $t_swav = $scope->trace(-fill => 'red'); my $t_owav = $over->trace(-fill => 'red'); my $t_fft = $xfrm->trace(-fill => 'orange'); my $t_lpc = $xfrm->trace(-fill => 'blue'); my $t_aux = $xfrm->trace(-fill => 'cyan'); my $t_imp = $voice->trace(-fill => 'blue'); my $t_inv = $voice->trace(-fill => 'red'); my $canv = $scope->Subwidget('scope'); $canv->Tk::bind('<2>',[\&Spectrum,Ev('x')]); $canv->Tk::bind('',[\&Spectrum,Ev('x')]); my $but = create_buttons($mw); my $row = 0; $but->grid(-row => $row, -column => 0, -columnspan => 3, -sticky => 'ew'); $mw->gridRowconfigure($row++,-weight => 0); $f->grid(-row => $row, -column => 0, -columnspan => 3, -sticky => 'ew'); $mw->gridRowconfigure($row++,-weight => 0); $scope->grid(-row => $row, -column => 0, -columnspan => 3, -sticky => 'ew'); $mw->gridRowconfigure($row++,-weight => 0); $poles->grid(-row => $row, -column => 0); $voice->grid(-row => $row, -column => 1, -sticky => 'ew'); $over->grid(-row => $row, -column => 2, -sticky => 'ew', -columnspan => 2); $mw->gridRowconfigure($row++,-weight => 0); $txt->grid( -row => $row, -column => 0, -columnspan => 1,-sticky => 'nsew'); $xfrm->grid( -row => $row, -column => 1, -columnspan => 2,-sticky => 'nsew'); $mw->gridRowconfigure($row++,-weight => 1); for my $c (0..2) { $mw->gridColumnconfigure($c,-weight => ($c) ? 1 : 0); } $scope->configure(-command => [\&stats,$scope->Subwidget('scope'),$val,$over]); $xfrm->configure(-command => [\&fstats,$xfrm,$val]); $voice->configure(-command => [\&vstats,$voice,$val]); $over->configure(-zoomcmd => [doZoom => $scope], -rangecmd => [doZoom => $scope]); if (@ARGV) { load(shift); } MainLoop; sub load { my $file = shift; open(my $fh,"$file") || die "Cannot open $file:$!"; binmode($fh); warn "Loading $file\n"; my $au = Audio::Data->new(Load => $fh); if ($au) { my $dur = $au->duration; $scope->configure(-start => 0, -xmax => $dur, -end => $dur); $over->configure(-start => 0, -xmax => $dur, -end => $dur); $scope->traceconfigure($t_swav, -data => $au); $over->traceconfigure($t_owav, -data => $au); $mw->title($file); $val->{Samp} = $au->rate; } close($fh); } sub Load { my $file = $mw->getOpenFile(-defaultextension => '.au', -filetypes => [[ "Audio Files", [".au"], [ "All Files", '*']] ], -initialdir => $lcwd); if ($file) { $lcwd = dirname($file); load($file); } } sub Save { my $t1 = $scope->cursor1; my $t2 = $scope->cursor2; if (!defined $t1) { $t1 = $scope->start; $t2 = $scope->end; $scope->configure(-cursor1 => $t1, -cursor2 => $t2); } my $file = $mw->getSaveFile(-defaultextension => '.au', -filetypes => [[ "Audio Files", [".au"], [ "All Files", '*']] ], -initialdir => $scwd); if ($file) { $scwd = dirname($file); save($file,$t1,$t2); } } sub save { my ($file,$t1,$t2) = @_; my $au = $scope->audio($t1,$t2); if ($au && $au->duration) { open(my $fh,">$file") || die "Cannot open $file:$!"; binmode($fh); warn "Saving $file\n"; $au->Save($fh); close($fh); } } sub Spectrum { my ($c,$x) = @_; my ($tr) = $scope->traces; return unless defined $tr; my $r = $scope->tracecget($tr,'-data')->rate; my $t; if (@_ > 1) { $t = $c->x2val($x); } else { $t = ($scope->cursor1+$scope->cursor2)/2; } my $dt = ($FFT_SIZE/2+1)/$r; my $t1 = $t - $dt; my $t2 = $t + $dt; $scope->cursor1($t1); $scope->cursor2($t2); my $au = $scope->audio($t1,$t2,$tr); spectrum($au,$xfrm,$t_fft,$t_lpc); } sub create_buttons { my $f = $mw->Frame(-relief => 'groove', -border => 3); my @but; push @but,$f->Button(-text => 'Spectogram', -command => sub { my $au = $scope->audio($scope->start,$scope->end); spectogram($au,$simg); }); push @but,$f->Button(-text => 'Clear', -command => sub { $simg->blank }); push @but,$f->Button(-text => 'Load', -command => \&Load); push @but,$f->Button(-text => 'Save', -command => \&Save); if (1) { push @but,$f->Label(-text => 'LPC Poles:',-justify => 'right',-anchor => 'e'); push @but,$f->Optionmenu(-variable => \$LPC_POLES, -options => [14,6..13,15..24], -command => sub { Spectrum($scope->Subwidget('scope')) } ); $LPC_POLES = 14; } if (1) { push @but,$f->Label(-text => 'FFT_size',-justify => 'right',-anchor => 'e'); push @but,$f->Optionmenu(-variable => \$FFT_SIZE, -options => [256,64,128,512,1024], -command => sub { Spectrum($scope->Subwidget('scope')) } ); $FFT_SIZE = 256; } if (1) { push @but,$f->Checkbutton(-text => "LPC", -variable => \$do_lpc, -command => sub { Spectrum($scope->Subwidget('scope')) } ); push @but,$f->Checkbutton(-text => "FFT", -variable => \$do_fft, -command => sub { Spectrum($scope->Subwidget('scope')) } ); } Tk::grid(@but,-sticky => 'nsew'); for my $i (0..$#but) { $f->gridColumnconfigure($i,-weight => 1); } return $f; } sub create_labels { my ($mw,$pairs,@other) = @_; my %values; my $f = $mw->Frame(-relief => 'groove', -border => 3); my @but; my @l1; my @l2; my @labels = @$pairs; while (@labels) { my ($start,$end) = splice(@labels,0,2); my $span = 1; foreach my $lab ($start,$end) { push(@l1,$f->Label(-text => $lab, -justify => 'center', -anchor => 'c', -relief => 'ridge')); $values{$lab} = 0; push(@l2,$f->Label(-textvariable => \$values{$lab}, -justify => 'center', -anchor => 'c',-relief => 'ridge')); } push(@but,$f->Button(-text => "$start-$end", -command => [\&Play,\$values{$start},\$values{$end}]) ); } while (@other) { my $lab = shift(@other); push(@l1,$f->Label(-text => $lab, -justify => 'center', -anchor => 'c', -relief => 'ridge')); $values{$lab} = 0; push(@l2,$f->Label(-textvariable => \$values{$lab}, -justify => 'center', -anchor => 'c',-relief => 'ridge')); } Tk::grid(@l1,-sticky => 'nsew'); Tk::grid(@l2,-sticky => 'nsew'); Tk::grid(@but,-sticky => 'nsew',-columnspan => 2); for my $i (0..$#l1) { $f->gridColumnconfigure($i,-weight => 1); } return ($f,\%values); } sub Play { my ($sp,$ep) = @_; my $svr = Audio::Play->new; if ($svr) { my $au = $scope->audio($$sp,$$ep); if ($au) { $svr->play($au); } } else { warn "Cannot open audio:$!"; } } sub fstats { my ($xfrm,$val) = @_; my $max = $xfrm->xmax; return unless defined $max; $val->{Samp} = sprintf("%5d",$max*2); my $f = $xfrm->cursor1; return unless defined $f; $val->{Freq} = sprintf("%5d",$f); } sub vstats { my ($voice,$val) = @_; my $t1 = $voice->cursor1; return unless defined $t1; my $t2 = $voice->cursor2; return unless defined $t1; return unless $t2 != $t1; $val->{F0} = sprintf("%5d",1/($t2-$t1)); } sub spectogram { my ($raw,$img) = @_; my $au = $raw->difference; my $N = ($img->cget('-height')-1)*2; my $w = $img->cget('-width')-2*BORDER; my $n = $au->samples; my $step = int($n/$w); my $s = Audio::Data->new(rate => $au->rate); my ($max,$min); my $st = 0; # warn("N=$N w=$w step=$step\n"); for (my $x = 0; $x < $w; $x++) { my $window = $au->hamming($N,$st); $window->r2_fft; my @amp = $window->dB(0,$N/2+1); foreach my $v (@amp) { $max = $v if (!defined($max) || $v > $max); $min = $v if (!defined($min) || $v < $min); } $s .= \@amp; $st += $step; } for (my $x = 0; $x < $w; $x++) { my @amp = $s->amplitude($x*($N/2+1),$N/2+1); for (my $y = 0; $y < @amp; $y++) { my $c = int(255*($amp[$y]-$max)/($min-$max)); $c = sprintf("#%02X%02X%02X",$c,$c,$c); $img->put([[$c]], -to => $x, (@amp-1-$y)); } } } sub stats { my ($scope,$val,$over) = @_; $over->cursor1($scope->start); $over->cursor2($scope->end); # warn "$scope ".$scope->Width.' '.$scope->Height."\n"; $simg->configure(-width => $scope->Width); foreach my $meth (keys %$val) { next unless ($meth && $meth =~ /^[a-z]/); if ($scope->can($meth)) { my $v = $scope->$meth(); $val->{$meth} = (defined $v) ? sprintf("%10g",$v) : (' ' x 10); } else { warn "$scope cannot $meth\n"; } } } sub spectrum { my ($raw,$xfrm,$t_fft,$t_lpc) = @_; my $au = $raw->difference; my $window = $au->hamming($FFT_SIZE,0); if ($do_fft) { my $fft = $window->fft($FFT_SIZE); $fft->length($FFT_SIZE/2); $xfrm->traceconfigure($t_fft,-data => $fft, -state => 'normal'); } else { $xfrm->traceconfigure($t_fft,-state => 'hidden'); } if ($do_lpc) { my ($auto,$ref); my $lpc; if (1) { # my ($auto,$ref); my $levinson = $window->lpc($LPC_POLES,$auto,$ref); roots(levinson => $levinson); inverse($raw,$levinson); # impulse($levinson); $levinson *= -1; $levinson->[0] = 1.0; $levinson->length($FFT_SIZE); # Take fft - gives transfer func of inverse filter (1-Sigma(An*z**-n)) # so to approx filter take reciprocal of each point my $lpc = 1.0/$levinson->fft($FFT_SIZE); $lpc->length($FFT_SIZE/2); $xfrm->traceconfigure($t_lpc,-data => $lpc, -state => 'normal'); } if (0) { my $auto = $window->autocorrelation($LPC_POLES); # $auto *= ($window->samples-$LPC_POLES); my $durbin = $auto->durbin; my $gain = $durbin->[0]; warn "gain = $gain\n"; roots(durbin => $durbin); $durbin *= -1; $durbin->[0] = 1.0; $durbin->length($FFT_SIZE); # Take fft - gives transfer func of inverse filter (1-Sigma(An*z**-n)) # so to approx filter take reciprocal of each point my $lpc = 1.0/$durbin->fft($FFT_SIZE); $lpc->length($FFT_SIZE/2); $xfrm->traceconfigure($t_aux,-data => $lpc, -state => 'normal'); } } else { $xfrm->traceconfigure($t_lpc,-state => 'hidden'); } my $d = $au->rate/2; $xfrm->configure(-xmax => $d, -end => $d, -start => 0); } sub inverse { my ($au,$lpc) = @_; my @a = map { -$_ } $lpc->data; my $rate = $lpc->rate; my $n = @a-1; $au->length($FFT_SIZE); my $filter = Audio::Filter::FIR->new(rate => $rate); $filter->data(@a); $filter->[0] = 1; $filter->length(2*$n+1); my $response = Audio::Data->new(rate => $rate); $response .= $filter->process($au); my $dur = $response->duration/2; $voice->configure(-start => 0, -end => $dur, -xmax => $dur, -yscale => undef); $voice->traceconfigure($t_inv,-data => $response->timerange($dur,2*$dur)); # my $fresp = $response->fft($FFT_SIZE); # $fresp->length($FFT_SIZE/2); # $xfrm->traceconfigure($t_aux,-data => $fresp, -state => 'normal'); } sub impulse { my $lpc = shift; my $rate = $lpc->rate; my @a = $lpc->data; my $n = @a-1; my $filter = Audio::Filter::AllPole->new(rate => $rate); $filter->data(@a); # 0'th entry is gain or error or other "junk". $filter->[0] = 1; $filter->length(2*$n+1); my $response = Audio::Data->new(rate => $rate); # No feed it an impulse i.e. ... $response .= $filter->process(1); # ... 1 at time 0 and ... for my $i (1..$FFT_SIZE-1) { $response .= $filter->process(0); # ... 0 otherwise } my $dur = $response->duration; $voice->configure(-start => 0, -end => $dur, -xmax => $dur, -yscale => undef); $voice->traceconfigure($t_imp,-data => $response); my $fresp = $response->fft($FFT_SIZE); $fresp->length($FFT_SIZE/2); $xfrm->traceconfigure($t_aux,-data => $fresp, -state => 'normal'); } my @formant; my @pole; sub pole_pair { my @colours = qw(red darkgreen blue magenta brown black); my ($rate,$k,$f,$b,$r,$i) = @_; my $d = $r*$r+$i*$i; my $fresp; # chr(0xb1) is +/- sign $txt->insert('end',sprintf("f=%5.0fHz bw=%4.0fHz %.3g %+.3g%c%.3gi\n", $f,$b,1/sqrt($d),$r,0xb1,$i),["f$k"]); my $zr = 64*$r/$d; my $zi = 64*$i/$d; unless ($pole[$k]) { my $col = $colours[$k % @colours]; $txt->tagConfigure("f$k",-foreground => $col); my $p = $poles->create(oval =>[0,0,0,0],-fill => $col, -outline => $col); my $m = $poles->create(oval =>[0,0,0,0],-fill => $col, -outline => $col); $pole[$k] =[$p,$m]; } $poles->coords($pole[$k][0],[65+$zr-1,65+$zi-1,65+$zr+1,65+$zi+1]); $poles->coords($pole[$k][1],[65+$zr-1,65-$zi-1,65+$zr+1,65-$zi+1]); $poles->itemconfigure($pole[$k][0],-state => 'normal'); $poles->itemconfigure($pole[$k][1],-state => 'normal'); if (0) { # Generate impulse response and take FFT my $filter = Audio::Filter::AllPole->new(rate => $rate); $filter->data(1,2*$r/$d,-1/$d); $filter->length(5); my $response = Audio::Data->new(rate => $rate); $response .= $filter->process(1); for my $i (1..$FFT_SIZE-1) { $response .= $filter->process(0); } $fresp = $response->fft($FFT_SIZE); } else { # Get freq response from coeficents my $coef = Audio::Data->new(rate => $rate); $coef->data(1, -2*$r/$d,1/$d); $coef->length($FFT_SIZE); $fresp = 1/$coef->fft($FFT_SIZE); } $fresp->length($FFT_SIZE/2); unless ($formant[$k]) { my $col = $colours[$k % @colours]; $formant[$k] = $xfrm->trace(-fill => $col); } my $dash = ($b > 500) ? '.' : '-'; $xfrm->traceconfigure($formant[$k],-data => $fresp, -dash => $dash, -state => 'normal'); } sub roots { my $why = shift; my $durbin = shift; my $rate = $durbin->rate; my @lpc = $durbin->data; my $n = @lpc; # warn "$LPC_POLES => $n\n"; # 1st term in @lpc is not interesting, typically a gain term my $err = shift(@lpc); # Remaining terms are A[N] terms in demominator of all-pole # filter equation H(z) = 1/(1-Sigma(1,N,A[i] * z**(-i))) # Form array of coefficients my @poly = (1.0,map { -$_ } @lpc); my @roots = solve_polynomial(@poly); # Values of roots are values of z**-1 i.e. 1/Z # So poles are at 1/root. For 1/root to be inside unit circle # roots above should be outside! i.e. abs(root) > 1 my $k = 0; my $pi2 = 2*Audio::Data::PI(); my @fmt; if (@roots) { #warn "$why ".join(',',@poly).":\n"; while (@roots) { my ($r,$i) = splice(@roots,0,2); if ($i != 0) { my ($r2,$i2) = splice(@roots,0,2); if ($r2 != $r || $i2 != -$i) { warn "Weird $r+${i}i and $r2${i2}i"; next; } my $f = $rate*atan2($i,$r)/$pi2; my $b = $rate*log($r*$r+$i*$i)/$pi2; push(@fmt,[$f,$b,$r,$i]); } else { # real pole i.e. f=0 - a lowpass term # printf " %.3g\n",$r; } } } $txt->delete('1.0','end'); foreach my $f (sort { ($a->[1] > 500) <=> ($b->[1] > 500) || $a->[0] <=> $b->[0] } @fmt) { my ($f,$b,$r,$i) = @$f; pole_pair($rate,$k++,$f,$b,$r,$i); } while ($k < @formant) { $poles->itemconfigure($pole[$k][0],-state => 'hidden'); $poles->itemconfigure($pole[$k][1],-state => 'hidden'); $xfrm->traceconfigure($formant[$k++],-state => 'hidden'); } }