File Coverage

blib/lib/Data/BiaB.pm
Criterion Covered Total %
statement 18 188 9.5
branch 0 62 0.0
condition 0 12 0.0
subroutine 6 15 40.0
pod 0 6 0.0
total 24 283 8.4


line stmt bran cond sub pod time code
1             #! perl
2              
3             package Data::BiaB;
4              
5             =head1 NAME
6              
7             Data::BiaB - Analyze Band-in-a-Box data files
8              
9             =cut
10              
11             our $VERSION = 0.11;
12              
13             =head1 SYNOPSIS
14              
15             This module provides methods to read Band-in-a-Box data files and
16             extract some useful information from them.
17              
18             Band-in-a-Box is an excellent tool for creating professional music and
19             accompanying tracks. I've been using it for many years but had to
20             abandon it when I phased out Microsoft Windows PCs.
21              
22             Example:
23              
24             use Data::BiaB;
25              
26             # Load an existing song.
27             my $biab = Data::BiaB->new();
28             $biab->load("Vaya_Con_Dios.mgu");
29              
30             # This will show what was gathered.
31             use Data::Dumper;
32             print Dumper($biab);
33              
34             =head1 NOTE
35              
36             Many BiaB files fail loading and parsing. If you have a recent version
37             of Band-in-a-Box its MusicXML export feature will be a much better
38             alternative.
39              
40             This is a hobby project. It is pre-alpha, under development, works for
41             me, caveat emptor and so on. Have fun!
42              
43             =cut
44              
45 1     1   88712 use warnings;
  1         2  
  1         38  
46 1     1   5 use strict;
  1         2  
  1         22  
47 1     1   5 use Carp qw( carp croak );
  1         2  
  1         49  
48 1     1   648 use Data::Dumper;
  1         6824  
  1         58  
49 1     1   10 use Data::Hexify;
  1         2  
  1         2187  
50              
51             $Data::Dumper::Indent = 1;
52             $Data::Dumper::Sortkeys = 1;
53              
54             sub new {
55 0     0 0   my ( $pkg, %opts ) = @_;
56 0           bless { %opts }, $pkg;
57             }
58              
59             sub load {
60 0     0 0   my ( $self, $file ) = @_;
61 0           $self->{_file} = $file;
62 0           $self->{_size} = -s $file;
63 0 0         open( my $fh, '<:raw', $file )
64             or croak("$file: $!");
65 0           $self->{_raw} = do { local $/; <$fh> };
  0            
  0            
66 0           close($fh);
67              
68 0           $self;
69             }
70              
71             sub parse {
72 0     0 0   my ( $self ) = @_;
73              
74 0           my $data = $self->{_raw};
75 0           my $inx = 0;
76 0           my $i;
77             my $val;
78              
79             my $dd1 = sub {
80             warn(Hexify( $data, { start => $_[0], length => $_[1] } ))
81 0 0   0     if $self->{debug} >= 1;
82 0           };
83             my $dd2 = sub {
84             warn(Hexify( $data, { start => $_[0], length => $_[1] } ))
85 0 0   0     if $self->{debug} >= 2;
86 0           };
87 0     0     my $gb = sub { unpack( "C", substr($data, $inx++, 1) ) };
  0            
88              
89             # Skip 1.
90 0           $dd1->( $inx, 1 );
91 0           $inx++;
92              
93             # Ttitle.
94 0           $val = $gb->();
95 0 0         warn("Title length = $val\n") if $self->{debug} > 2;
96 0           $dd2->( $inx-1, 1+$val );
97 0           $self->{title} = substr($data, $inx, $val );
98 0           warn("Title = $self->{title}\n");
99 0           $inx += $val;
100              
101             # Skip 2.
102 0           $dd1->( $inx, 2 );
103 0           $inx += 2;
104              
105             # Style/Key/BPM.
106 0           $dd2->( $inx, 3 );
107 0           $self->{basic_style} = $gb->();
108 0           $self->{key_nr} = $gb->();
109 0           $self->{bpm} = $gb->();
110              
111             # Styles;
112 0           $i = 0;
113 0           my $tally = 0;
114 0           my $first = 0;
115 0           $self->{stylemap} = {};
116 0           while ( $i < 256 ) {
117 0           $val = $gb->();
118 0 0         if ( $val ) {
119 0           $dd2->( $inx-1, 1 );
120 0           $self->{stylemap}->{$i-1} = $val;
121 0 0         warn("Style: $val @ $i\n") if $self->{debug} > 2;
122 0           $tally++;
123 0           $i++;
124             }
125             else {
126 0           $dd2->( $inx-1, 2 );
127 0           $val = $gb->();
128 0 0         croak("Format error (zero offset) in styles") unless $val;
129 0           $i += $val;
130             }
131             }
132 0 0         if ( $i > 256 ) {
133 0           croak("Format error (offset $i mismatch) in styles");
134             }
135 0           warn("Read: $tally styles\n");
136              
137             # Chord types.
138 0           $i = 1;
139 0           $self->{ctypes} = [];
140 0           $tally = 0;
141             # 1021 = 4 * 255 + 1
142             # 255 measures of 4 chords.
143 0           while ( $i < 1021 ) {
144 0           $val = $gb->();
145 0 0         if ( $val ) {
146 0           $dd2->( $inx-1, 1 );
147 0           $self->{ctypes}->[$i-1] = $val;
148 0   0       $first //= $i-1;
149 0 0         warn("Ctype: [", $inx-1, "] $val @ $i\n") if $self->{debug} > 1;
150 0           $tally++;
151 0           $i++;
152             }
153             else {
154 0           $dd2->( $inx-1, 2 );
155 0           $val = $gb->();
156 0 0         croak("Format error (zero offset) in ctypes") unless $val;
157 0           $i += $val;
158             }
159             }
160             # The sequence ends with 00 ff 00 ff 00 nn to sum up to 1021.
161 0 0         if ( $i > 1021 ) {
162 0           croak("Format error (offset $i mismatch) in ctypes");
163             }
164 0           $first++;
165 0           warn("Read: $tally ctypes, first @ $first, last @ ", scalar(@{$self->{ctypes}}), "\n");
  0            
166              
167             # Chord names.
168 0           $i = 1;
169 0           $self->{cnames} = [];
170 0           $tally = 0;
171 0           $first = undef;
172 0           while ( $i < 1022 ) {
173 0           $val = $gb->();
174 0 0         if ( $val ) {
175 0           $dd2->( $inx-1, 1 );
176 0           $self->{cnames}->[$i-1] = $val;
177 0   0       $first //= $i-1;
178 0 0         warn("Cname: [", $inx-1, "] $val @ $i\n") if $self->{debug} > 1;
179 0           $tally++;
180 0           $i++;
181             }
182             else {
183 0           $dd2->( $inx-1, 2 );
184 0           $val = $gb->();
185 0 0         croak("Format error (zero offset) in cnames") unless $val;
186 0           $i += $val;
187             }
188             }
189             # The sequence ends with 00 ff 00 ff 00 nn to sum up to 1022.
190             # Yes, really...???
191 0 0         if ( $i > 1022 ) {
192 0           croak("Format error (offset $i mismatch) in cnames");
193             }
194 0           $first++;
195 0           warn("Read: $tally cnames, first @ $first, last @ ", scalar(@{$self->{cnames}}), "\n");
  0            
196              
197 0           $dd2->( $inx, 3 );
198              
199             # A song consists of lead-in (bar 0), intro, chorus, and coda.
200             # The chorus is repeated a number of times.
201 0           $self->{start_chorus_bar} = $gb->(); # chorus start
202 0           $self->{end_chorus_bar} = $gb->(); # chorus ends
203 0           $self->{number_of_repeats} = $gb->();
204              
205             #$dd1->($inx, 1024);
206              
207 0 0         if ( substr($data, $inx, $inx+2560)
208             =~ /^(.*?\x{42})((?:\x{5}.|\x{6}..|\x{7}...|\x{8}....|\x{9}.....|\x{a}......|\x{b}.......|\x{c}........)\.STY)/ ) {
209 0           $val = substr($2,1);
210 0           $self->{stylefile} = $val;
211 0           warn("Style $val @ ", $inx+length($1), " ($inx+", length($1), ")\n");
212             }
213             # Although the length is flexible, it seems to be filled to the max
214             # with garbage (or a default XXXXXXXX.STY).
215 0           $inx += length($1);
216 0           $inx += 13;
217              
218 0 0         if ( substr($data, $inx, $inx+256) =~ /^(.*?)\x{00}\x{ff}\x{00}\x{0d}(..)/ ) {
219 0           $val = unpack("v", $2);
220 0           warn("NumNotes $val @ ", $inx+length($1), " ($inx+", length($1), ")\n");
221 0           $self->{numnotes} = $val;
222 0           $inx += length($1) + 6;
223             }
224              
225 0           my ( $onset, $chan, $pitch, $velo, $dur, $unk );
226 0           my @m;
227             RETRY:
228 0 0         warn("Search for melody from $inx...\n") if $self->{debug};
229 0 0         if ( substr($data, $inx) =~ /^(.*?)\x{a0}\x{b0}(\x{c0}|\x{c1})/s ) {
230 0           $inx += 3 + length($1);
231             warn( sprintf("melody %02x @ %d, %d notes\n",
232 0           ord($2), $inx, $self->{numnotes}) );
233 0           while ( $inx < length($data)-12 ) {
234 0           $dd2->($inx,12);
235 0           ( $onset, $unk, $pitch, $velo, $chan, $dur ) =
236             unpack("VCCCCV", substr($data, $inx, 12));
237              
238 0 0 0       if ( @m == 0
      0        
239             && ( $pitch > 100 || $velo > 127 || $chan > 15
240             || $dur > 7200 || $onset > 7200 ) ) {
241 0           $dd1->($inx,12);
242 0           warn("insane values in melody -- retrying...\n");
243 0           goto RETRY;
244             }
245             # $pitch = pitchname($pitch);
246 0           push( @m, [ $onset, $chan, $pitch, $velo, $unk, $dur ] );
247 0           $inx += 12;
248 0 0         if ( @m == $self->{numnotes} - 1) {
249 0           last;
250             }
251 0 0         if ( $inx >= length($data)-12 ) {
252 0           warn("Oops");
253 0           last;
254             }
255             }
256             }
257             else {
258 0           warn("No melody found\n");
259             }
260 0 0         if ( @m != $self->{numnotes} ) {
261             warn("Missing or incomplete melody (",
262             scalar(@m), " notes, should have been ",
263 0           $self->{numnotes}, ")\n");
264             }
265 0           $self->{melody} = \@m;
266              
267 0 0         if ( $inx < length($data) ) {
268 0           $dd1->( $inx, length($data) - $inx );
269             }
270              
271 0           $self;
272             }
273              
274             sub pitchname {
275 0     0 0   my ( $p ) = @_;
276 0           my $n = [ "C", "C#", "D", "D#", "E", "F", "F#",
277             "G", "G#", "A", "A#", "B" ]->[$p % 12];
278             # BiaB pitch is 1 octave low.
279 0           $n . int($p/12);
280             }
281              
282             my %ctypes =
283             ( "0" => "",
284             "1" => "",
285             "2" => "maj",
286             "3" => "5b",
287             "4" => "aug",
288             "5" => "6",
289             "6" => "maj7",
290             "7" => "maj9",
291             "8" => "maj9#11",
292             "9" => "maj13#11",
293             "10" => "maj13",
294             "12" => "+",
295             "13" => "maj7#5",
296             "14" => "69",
297             "15" => "2",
298             "16" => "m",
299             "17" => "maug",
300             "18" => "mM7",
301             "19" => "m7",
302             "20" => "m9",
303             "21" => "m11",
304             "22" => "m13",
305             "23" => "m6",
306             "24" => "m#5",
307             "25" => "m7#5",
308             "26" => "m69",
309             "32" => "m7b5",
310             "33" => "dim",
311             "34" => "m9b5",
312             "40" => "5",
313             "56" => "7+",
314             "57" => "+",
315             "58" => "13+",
316             "64" => "7",
317             "65" => "13",
318             "66" => "7b13",
319             "67" => "7#11",
320             "70" => "9",
321             # "70" => "9b13",
322             "73" => "9#11",
323             "74" => "13#11",
324             "76" => "7b9",
325             "77" => "13b9",
326             "79" => "7b9#11",
327             "82" => "7#9",
328             "83" => "13#9",
329             "84" => "7#9b13",
330             "85" => "9#11",
331             "88" => "7b5",
332             "89" => "13b5",
333             "91" => "9b5",
334             "93" => "7b5b9",
335             "96" => "7b5#9",
336             "99" => "7#5",
337             "103" => "9#5",
338             "105" => "7#5b9",
339             "109" => "7#5#9",
340             "113" => "7alt",
341             "128" => "7sus",
342             "129" => "13sus",
343             "134" => "11",
344             "140" => "7susb9",
345             "146" => "7sus#9",
346             "163" => "7sus#5",
347             "177" => "4",
348             "184" => "sus",
349             );
350              
351             sub chordroot {
352 0     0 0   my ( $nr ) = @_;
353             # Convert the byte for chord root to a string.
354 0           my @roots = ( '/','C','Db','D','Eb','E','F','Gb','G',
355             'Ab','A','Bb','B','C#','D#','F#','G#','A#');
356 0           my @bassflat = ('B','C','Db','D','Eb','E','F','Gb','G','Ab','A','Bb');
357 0           my @basssharp = ('B','C','C#','D','D#','E','F','F#','G','G#','A','A#');
358              
359 0           my $root = $roots[$nr % 18];
360 0 0         if ( $nr > 18 ) {
361 0           my $bass = "";
362 0 0         if ( $root =~ /b/ ) {
363 0           $bass = $bassflat[(int $nr / 18 + $nr % 18) % 12]; #flat slash
364             }
365             else {
366 0           $bass = $basssharp[(int $nr / 18 + $nr % 18) % 12]; #sharp slash
367             }
368 0           $root .= "/" . $bass;
369             }
370 0           return $root;
371             }
372              
373             sub makechords {
374 0     0 0   my ( $self ) = @_;
375 0           my @cn = @{ $self->{cnames} };
  0            
376 0           my @ct = @{ $self->{ctypes} };
  0            
377 0           my @c;
378 0 0         carp("Expecting same number of chord names " . scalar(@cn) .
379             " and chord types " . scalar(@ct))
380             unless @cn == @ct;
381 0           for ( my $i = 0; $i < @cn; $i++ ) {
382 0 0         if ( defined $cn[$i] ) {
383 0 0         if ( defined $ct[$i] ) {
384             push( @c,
385             sprintf("%3d %3d %s %s",
386             $cn[$i], $ct[$i],
387 0           chordroot($cn[$i]), $ctypes{"".$ct[$i]}));
388             }
389             else {
390 0           warn("Chord ", 1+$i, ": name = $cn[$i], no type\n");
391             }
392             }
393             else {
394 0 0         if ( defined $ct[$i] ) {
395 0           warn("Chord ", 1+$i, ": no name, type = $ct[$i]\n");
396             }
397             else {
398 0           push( @c, undef );
399             }
400             }
401             }
402              
403 0           $self->{chords} = \@c;
404             }
405              
406             =head1 AUTHOR
407              
408             Johan Vromans, C<< >>
409              
410             =head1 BUGS
411              
412             Please report any bugs or feature requests to C, or through
413             the web interface at L. I will be notified, and then you'll
414             automatically be notified of progress on your bug as I make changes.
415              
416             =head1 SUPPORT
417              
418             You can find documentation for this module with the perldoc command.
419              
420             perldoc Data::BiaB
421              
422             You can also look for information at:
423              
424             =over 4
425              
426             =item * RT: CPAN's request tracker
427              
428             L
429              
430             =item * Search CPAN
431              
432             L
433              
434             =back
435              
436             =head1 ACKNOWLEDGEMENTS
437              
438             PG Music inc., for making Band-in-a-Box. I've used Band-in-a-Box for
439             several years with great pleasure.
440              
441             The ancient and abandoned Band-In-A-Box File Converter 'biabconverter'
442             by Alain Brenzikofer inspired me to write this.
443              
444             =head1 COPYRIGHT & LICENSE
445              
446             Copyright 2016 Johan Vromans, all rights reserved.
447              
448             This program is free software; you can redistribute it and/or modify it
449             under the same terms as Perl itself.
450              
451             =cut
452              
453             1; # End of Data::BiaB
454              
455             package main;
456              
457             unless ( caller ) {
458 1     1   9 use Data::Dumper;
  1         2  
  1         198  
459             my $b = Data::BiaB->new( debug => 1 )->load (shift )->parse;
460             $b->makechords;
461              
462             if ( 1 ) {
463             for ( qw( _raw stylemap ctypes cnames ) ) {
464             delete $b->{$_};
465             }
466             $b->{melody} =
467             [ map { $_->[2] = Data::BiaB::pitchname($_->[2]); $_ }
468             @{$b->{melody}} ];
469             warn(Dumper($b));
470             }
471             }