3 # Fluffy Ball Signal Analyzer (c) 2011 Petr Baudis <pasky@ucw.cz>
6 # http://brmlab.cz/project/fluffyball
8 # Example: fb.pl -r sound hw:1,0
9 # ...will show rectangle labels instead of frequency, and take input
10 # from the second soundcard instead of the first.
16 use List
::Util
qw(min max sum);
17 use List
::MoreUtils
qw(pairwise);
21 our ($render, $label_recti, $delay, $debug);
23 if ($ARGV[0] eq '-n') {
27 if ($ARGV[0] eq '-r') {
31 if ($ARGV[0] eq '-s') {
35 if ($ARGV[0] eq '-d') {
40 my $mode = shift @ARGV;
41 my ($devname, $playfile);
42 if (not $mode or $mode eq 'sound') {
44 $devname ||= 'default';
45 } elsif ($mode eq 'replay') {
47 $playfile or die "no file specified";
49 print STDERR
"Usage: $0 [-n] [-r] [-s USEC] [-d] [sound [DEVNAME]] | replay FILENAME\n";
54 our ($mw, $canvas, $text);
56 our ($sampleno, $sampling);
58 our ($chan, $fmt, $rate, $sps) = (1, 16, 16384, 8);
59 our $bps = ($chan * $fmt * $rate) / 8;
60 our $buf = $bps / $sps;
62 our @avgsteps = (2, 4);
64 # How much to amplify the spectrum
67 # Rectangles cover averages in various bands. They also include averages
68 # of surrounding of the rectangle picture. E.g. with step 20, over 10,
69 # the average includes average of 20 of the covered area + 10 on each side.
73 our $draw_lines = 0; # this makes for real pretty graphs but it is a huge slowdown
82 # Jingle inside of our ball. If we did not have it,
83 # alternative detection methods could be possible too.
84 my ($jingle) = $finfo->{rect_2
}->[29];
86 # Look at mid-frequency average level over longer time
87 # and check that it is universal in that range.
88 my @midrange = 25..50;
89 my ($fidgetl, $fidgetm);
90 $fidgetl = sum
(@
{$finfo->{rect_2
}}[@midrange]) / @midrange;
91 $fidgetm = min
(@
{$finfo->{rect_2
}}[@midrange]);
93 # Look at dynamics of the sound; too much change means
94 # this is more likely buch fallout.
96 my (@mid_rect_now) = @
{$finfo->{rect_now
}}[@midrange];
97 my (@mid_rect_2) = @
{$finfo->{rect_4
}}[@midrange];
98 $dynamics = sqrt(sum
(pairwise
{ ($a - $b) * ($a - $b) } @mid_rect_now, @mid_rect_2)) / @midrange;
101 print "(jingle $jingle) (fidget $fidgetl $fidgetm) (dynamics $dynamics)\n";
102 #print join(' ', @{$finfo->{rect_4}}[29, 46, 47, 48])."\n";
105 my $newstate = $state;
106 if ($state ne 'buch' and $jingle > 0.15) {
109 # buch -> hlazeni not allowed
110 } elsif ($state eq 'ticho' and ($fidgetl > 0.03 and $fidgetm > 0.02 and $dynamics < 0.005)) {
111 $newstate = 'hlazeni';
113 } elsif ($state eq 'hlazeni' and ($fidgetl < 0.02 or $fidgetm < 0.015 or $dynamics > 0.02)) {
116 } elsif ($state eq 'buch' and $jingle < 0.05) {
120 $newstate ne $state and print "$newstate\n";
124 $text->delete('all');
125 if ($newstate ne 'ticho') {
126 $text->createText(0, 20, -anchor
=> 'nw', -font
=> 'Small', -text
=> "[$newstate]", -fill
=> 'blue');
131 sub render_spectrum
{
132 my ($finfo, $label, $reflabel, $fir, $srows, $xofs, $yofs, $w, $h, $hs) = @_;
134 my @f = @
{$finfo->{"freq_$label"}};
135 my @fref; defined $reflabel and @fref = @
{$finfo->{"freq_$reflabel"}};
136 my @r = @
{$finfo->{"rect_$label"}};
137 my @rref; defined $reflabel and @rref = @
{$finfo->{"rect_$reflabel"}};
138 for my $y (0..($srows-1)) {
139 for my $x (0..($w-1)) {
140 my $hb = $yofs + $h * ($y + 1) + $hs * $y;
141 my $i = $w * $y + $x;
144 my $barh = $h * ($draw_rect ?
2/3 : 1);
145 my $bar = $h * $f[$i] * $amplifier;
146 $bar = min
($barh, $bar);
147 $canvas->createLine($xofs + $x, $hb, $xofs + $x, $hb - $bar);
148 if (defined $reflabel) {
149 my $refbar = $h * $fref[$i] * $amplifier;
150 $refbar = min
($barh, $refbar);
151 if ($refbar > $bar) {
152 $canvas->createLine($xofs + $x, $hb - $bar, $xofs + $x, $hb - $refbar, -fill
=> 'darkred');
154 $canvas->createLine($xofs + $x, $hb - $refbar, $xofs + $x, $hb - $bar, -fill
=> 'red');
159 if ($draw_rect and !($i % $rect_step) and $i >= $rect_over and $i <= @f - $rect_step - $rect_over) {
160 my $recth = $h * ($draw_lines ?
2/3 : 1);
161 my $avg = $recth * $amplifier * $r[$i / $rect_step];
162 $avg = min
($h, $avg);
164 $canvas->createRectangle($xofs + $x, $rhb, $xofs + $x + $rect_step, $rhb + $avg, -fill
=> 'black');
165 if (defined $reflabel) {
166 my $refavg = $recth * $amplifier * $rref[$i / $rect_step];
167 $refavg = min
($h, $refavg);
168 if ($refavg > $avg) {
169 $canvas->createRectangle($xofs + $x, $rhb + $avg, $xofs + $x + $rect_step, $rhb + $refavg, -fill
=> 'darkred', -outline
=> 'darkred');
171 $canvas->createRectangle($xofs + $x, $rhb + $refavg, $xofs + $x + $rect_step, $rhb + $avg, -fill
=> 'red', -outline
=> 'red');
176 if (!($x % ($w/4))) {
177 $canvas->createLine($xofs + $x, $hb + 0, $xofs + $x, $hb + 5, -fill
=> 'blue');
178 $canvas->createText($xofs + $x, $hb + 15, -fill
=> 'blue', -font
=> 'small', -text
=> $label_recti ?
$i/$rect_step : $fi[$i]);
182 $canvas->createText($xofs, $yofs, -fill
=> 'darkgreen', -font
=> 'small', -text
=> $label);
187 my @fi = @
{$finfo->{freqi
}};
190 my $srows = 32/$sps; # spectrum rows; "density" of data depends on sps
191 my $w = @fi / $srows; # width
192 my $ws = 20; # spacing
193 my $h = 150; # height
194 my $hs = 20; # spacing
196 my $htot = $hs + ($h + $hs) * $srows;
200 my $arows = @avgsteps / $acols; # rows of plots; each plot is further divided to $srows rows of spectrum
202 my $ahtot = ($htot - 3 * $hs) / $arows;
203 my $ah = $ahtot / $srows - $hs;
206 $canvas = $mw->Canvas(-width
=> $ws + $w + $ws*3 + ($aw + $ws) * $acols + $ws, -height
=> $htot);
209 $canvas->delete('all');
211 render_spectrum
($finfo, 'now', $avgsteps[0], $finfo->{freqi
}, $srows, $ws, $hs, $w, $h, $hs);
212 for my $i (0..$#avgsteps) {
213 my $ax = $i % $acols;
214 my $ay = sprintf('%d', $i / $acols);
215 my $su = $avgsteps[$i];
216 render_spectrum
($finfo, $su, $i < $#avgsteps ?
$avgsteps[$i+1] : undef, $finfo->{freqi
}, $srows,
217 $ws + $w + $ws*3 + ($ws + $aw) * $ax,
218 ($ahtot + $hs*2) * $ay + $hs,
227 read $dsp, $w, $buf or die "read: $!";
231 # Array with frequencies corresponding to elements of fftsig
234 my $dft_size = $rate / $sps;
235 for (my $i = 0; $i < $dft_size / 2; $i++) {
236 $freqs[$i] = $i / $dft_size * $rate;
246 while (length($bytes) > 0) {
247 my $sample = unpack('c', substr($bytes, 0, 1, ''));
248 push(@samples, $sample);
250 } elsif ($fmt == 16) {
251 while (length($bytes) > 0) {
252 my $sample = unpack('s<', substr($bytes, 0, 2, ''));
253 push(@samples, $sample);
256 die "unsupported $fmt bits per sample\n";
259 # Magic from Audio::Analyze:
260 my $fft = Math
::FFT
->new(\
@samples);
261 my $coeff = $fft->rdft;
262 my $size = scalar(@
$coeff);
266 $mag[$k] = sqrt($coeff->[$k*2]**2);
267 for($k = 1; $k < $size / 2; $k++) {
268 $mag[$k] = sqrt(($coeff->[$k * 2] ** 2) + ($coeff->[$k * 2 + 1] ** 2));
272 my $avgmag = sum
(@mag) / @mag; #[1000..$#mag];
273 @mag = map { $_ / $avgmag * 0.3 } @mag;
278 my ($finfo, $l) = @_;
279 my @f = @
{$finfo->{"freq_$l"}};
281 for (my ($fi, $ri) = ($rect_step, 1); $fi <= $#f - $rect_step - $rect_over; $fi += $rect_step, $ri++) {
282 $r[$ri] = sum
(@f[$fi - $rect_over .. $fi + $rect_step + $rect_over]) / ($rect_step + 2 * $rect_over);
284 $finfo->{"rect_$l"} = \
@r;
293 defined $sampling and $sampling .= $w;
294 $finfo->{freqi
} = [fftfreq
()];
295 $finfo->{freq_now
} = [fftsig
($w)];
297 # Create averaged rects
298 rectsof
($finfo, 'now');
300 # Update floating freq. averages
301 for my $su (@avgsteps) {
302 for my $i (0..$#{$finfo->{freqi}}) {
303 my $fn = $finfo->{freq_now
}->[$i];
304 my $fs = $finfo->{"freq_$su"}->[$i];
306 $finfo->{"freq_$su"}->[$i] = ($fs * ($su - 1) + $fn) / $su;
308 rectsof
($finfo, $su);
312 $render and render
($finfo);
315 use Time
::HiRes
qw(usleep);
319 $render and $mw->idletasks();
327 if (defined $sampling) {
333 $text->delete('all');
334 $text->createText(0, 20, -anchor
=> 'nw', -font
=> 'Small', -text
=> "Recording $sampleno", -fill
=> 'red');
340 $text->delete('all');
344 open my $fh, '>'.sprintf('sample-%04d.raw', $sampleno) or die "$!";
348 $text->delete('all');
352 if ($mode eq 'replay') {
353 open ($dsp, $playfile) or die "$playfile: $!";
356 print join(' ', 'arecord', '-D', $devname, '-t', 'raw', '-r', $rate, '-f', 'S'.$fmt)."\n";
357 open ($dsp, '-|', 'arecord', '-D', $devname, '-t', 'raw', '-r', $rate, '-f', 'S'.$fmt) or die "arecord: $!";
363 $mw = MainWindow
->new;
364 $mw->bind('<space>' => \
&recStart
);
365 $mw->bind('<Escape>' => \
&recCancel
);
366 $mw->bind('<Return>' => \
&recSave
);
367 $text = $mw->Canvas(-width
=> 400, -height
=> 40);
370 $mw->after(1, \
&ticks
);