File Coverage

lib/ChordPro/Output/MMA.pm
Criterion Covered Total %
statement 134 278 48.2
branch 62 176 35.2
condition 37 80 46.2
subroutine 9 10 90.0
pod 0 7 0.0
total 242 551 43.9


line stmt bran cond sub pod time code
1             #! perl
2              
3             package main;
4              
5             our $options;
6             our $config;
7              
8             package ChordPro::Output::MMA;
9              
10 1     1   9 use ChordPro::Output::Common;
  1         3  
  1         66  
11              
12 1     1   7 use strict;
  1         2  
  1         44  
13 1     1   7 use warnings;
  1         13  
  1         3741  
14              
15             sub generate_songbook {
16 7     7 0 21 my ( $self, $sb ) = @_;
17 7         21 my @book;
18              
19             die("MMA generation requires a single song\n")
20 7 50       14 if @{$sb->{songs}} > 1;
  7         40  
21              
22 7         16 foreach my $song ( @{$sb->{songs}} ) {
  7         40  
23 7 50       19 if ( @book ) {
24 0 0       0 push(@book, "") if $options->{'backend-option'}->{tidy};
25 0         0 push(@book, "-- New song");
26             }
27 7         16 push(@book, @{generate_song($song)});
  7         30  
28             }
29              
30 7         23 push( @book, "");
31 7         30 \@book;
32             }
33              
34             my $groove; # groove to use
35             my $single_space = 0; # suppress chords line when empty
36             my $chords_under = 0; # chords under lyrics
37              
38             sub safemeta {
39 14     14 0 42 my ( $s, $meta, $default ) = @_;
40 14 50 33     70 return $default undef unless defined $meta && defined $s->{meta}->{$meta};
41 14         170 return $s->{meta}->{$meta}->[0];
42             }
43              
44             sub generate_song {
45 7     7 0 28 my ( $s ) = @_;
46              
47 7         19 my $st = 0; # current MMA statement number
48 7         13 my $cur = ''; # MMA statement under construction
49 7         16 my $prev = ''; # previous MMA statement
50 7         12 my $did = 0; # preamble was emitted
51 7         16 my $pchord = '.'; # last real chord
52              
53 7         32 $groove = $options->{'backend-option'}->{groove};
54 7         20 my $tidy = $options->{'backend-option'}->{tidy};
55              
56             # Normally a counting beat is 1 quarter. deCoda uses 1/8th.
57 7   66     32 my $decoda = $options->{'backend-option'}->{decoda} || $options->{'backend-option'}->{deCoda};
58              
59 7         17 $single_space = $options->{'single-space'};
60 7         19 $chords_under = $config->{settings}->{'chords-under'};
61              
62             $s->structurize
63 7 50 50     39 if ( $options->{'backend-option'}->{structure} // '' ) eq 'structured';
64              
65 7         14 my @s;
66              
67             # Preamble.
68 7 50       45 push( @s, "// title: " . $s->{title}, "" ) if defined $s->{title};
69              
70             # Select a groove.
71 7         15 my $bpm = 4;
72 7         16 my $q = 4;
73 7 50       39 ( $bpm, $q ) = ( $1, $2 ) if safemeta( $s, "time", "4/4" ) =~ /^(\d+)\/(\d+)/;
74 7 50       32 unless ( $groove ) {
75 0 0       0 if ( $bpm == 3 ) {
    0          
76 0 0       0 $q = 4 unless $q == 8;
77 0         0 $groove = "Neutral$bpm$q";
78             }
79             elsif ( $bpm == 6 ) {
80 0 0       0 warn("Time 6/$q set to 6/8\n") unless $q == 8;
81 0         0 $q = 8;
82 0         0 $groove = "Neutral$bpm$q";
83             }
84             else {
85 0         0 warn("Time $bpm/$q set to 4/4\n");
86 0         0 $q = $bpm = 4;
87 0         0 $groove = "Neutral44";
88             }
89             }
90              
91 7         65 push( @s, sprintf( "Time %d/%d", $bpm, $q ) );
92 7         44 push( @s, makegroove( $bpm, $q ) );
93              
94             # When deCoda decodes a song in 6/8 at 100bpm, it gets interpreted as 2/4.
95             # When the time signature is manually fixed to 6/8, the song becomes
96             # twice as long. So we must double the tempo.
97 7 100 100     33 push( @s, sprintf( "Tempo %d",
98             safemeta( $s, "tempo", 60 ) * (( $q == 8 && $decoda ) ? 2 : 1 )
99             ) );
100              
101 7         28 push( @s, "", "/**** End of Preamble ****/", "" );
102              
103 7         17 my $ctx = "";
104 7         14 my $line;
105              
106 7         18 foreach my $elt ( @{$s->{body}} ) {
  7         35  
107 70         178 my $line = sprintf( "%3d", $elt->{line} );
108              
109 70 100       176 if ( $elt->{context} ne $ctx ) {
110 14 100       127 push(@s, "// $line End of $ctx") if $ctx;
111 14 100       78 push(@s, "// $line Start of $ctx") if $ctx = $elt->{context};
112             }
113              
114 70 100       158 if ( $elt->{type} eq "empty" ) {
115             push(@s, "***SHOULD NOT HAPPEN***")
116 14 50       37 if $s->{structure} eq 'structured';
117 14         31 push(@s, "");
118 14         42 next;
119             }
120              
121 56 50       114 if ( $elt->{type} eq "colb" ) {
122 0         0 push(@s, "// $line Column break");
123 0         0 next;
124             }
125              
126 56 50       131 if ( $elt->{type} eq "newpage" ) {
127 0         0 push(@s, "// $line New page");
128 0         0 next;
129             }
130              
131 56 100       139 if ( $elt->{type} eq "gridline" ) {
132 21         69 my @a = @{ $elt->{tokens} };
  21         121  
133             # Reduce the elements (objects) to simple chords or symbols.
134 21         62 @a = map { $_->{class} eq 'chord'
135             ? $_->{chord}->key
136 405 100       1032 : $_->{symbol} } @a;
137              
138 21         485 push( @s, "// $line @a" );
139              
140             # Remove label and initial bar symbol.
141 21         40 my $firstbar;
142 21         72 do { } until is_bar( $firstbar = shift(@a) );
143              
144 21 100 100     76 if ( $decoda && $q == 4 ) {
145             # deCoda always uses a beat step of 8. For x/4 times we must reduce.
146 9         28 @a = reduce( \@a, $bpm, $line, \@s);
147 9         55 push( @s, "// $line $firstbar " .
148             join(" ", @a) );
149             }
150              
151             # Bars must be full.
152 21 50       87 if ( @a % ( $bpm + 1 ) ) {
153 0         0 push( @s, "// $line $bpm $q ".scalar(@a)." OOPS?" );
154 0         0 next;
155             }
156              
157 21         48 my $rept = 0;
158 21         31 my $bar = 0;
159              
160             # Process the elements.
161 21         52 while ( @a ) {
162             # Increment bar number and mma statement number.
163 56         84 $bar++;
164 56         77 $st++;
165 56         82 my $c = ''; # mma statement being constructed
166              
167             # Reuse last chord if we have none.
168 56 100 66     159 if ( $a[0] eq '.' && $a[1] eq '.' ) {
169 7         29 $a[0] = $pchord;
170             }
171              
172             # Process the beats.
173 56         153 for ( my $b = 1; $b <= $bpm; $b++ ) {
174              
175             # Get a chord.
176 240         350 $cur = shift(@a);
177              
178             # Append to statement.
179 240 100       460 $c .= $cur eq '.' ? "/ " : "$cur ";
180 240 100       616 $pchord = $cur unless $cur eq '.';
181             }
182              
183             # Remove trailing slashes.
184 56         266 $c =~ s;[\s/]+$;;;
185 56 50       165 $c = $prev unless $c =~ /\S/;
186              
187             # Print MMA statement.
188 56 100 100     270 if ( $prev eq $c || $st == 1 ) {
189 14         29 $rept++;
190             }
191             else {
192 42 50       208 push( @s,
    100          
193             sprintf( "%3d %s%s", $st-$rept, $prev,
194             $rept > 1 ? " * $rept" : "" )
195             ) if $rept;
196 42         76 $rept = 1;
197             }
198 56         84 $prev = $c;
199              
200             # Check for trailing barline.
201 56 50       110 unless ( is_bar(shift(@a)) ) {
202 0         0 push( @s, "// bar $bar: Missing final barline?" );
203 0         0 warn("line $., bar $bar: Missing final barline?\n");
204             }
205             }
206 21 50       113 push( @s,
207             sprintf( "%3d %s%s", $st-$rept+1, $prev,
208             $rept > 1 ? " * $rept" : "" ) );
209 21         34 $rept = 0;
210              
211 21         44 next;
212             }
213              
214 35 100       113 if ( $elt->{type} =~ /^comment(?:_italic|_box)?$/ ) {
215 7 50       18 push(@s, "") if $tidy;
216 7         33 my $text = $elt->{text};
217 7 50       26 if ( $elt->{chords} ) {
218 0         0 $text = "";
219 0         0 for ( 0..$#{ $elt->{chords} } ) {
  0         0  
220             $text .= "[" . $elt->{chords}->[$_] . "]"
221 0 0       0 if $elt->{chords}->[$_] ne "";
222 0         0 $text .= $elt->{phrases}->[$_];
223             }
224             }
225 7         41 $text = fmt_subst( $s, $text );
226 7         594 push(@s, "// $line comment: $text");
227 7 50       32 push(@s, "") if $tidy;
228 7         24 next;
229             }
230              
231 28         50 next;
232              
233 0 0       0 if ( $elt->{type} eq "songline" ) {
234 0         0 push(@s, songline($elt));
235 0         0 next;
236             }
237              
238 0 0       0 if ( $elt->{type} eq "tabline" ) {
239 0         0 push(@s, $elt->{text});
240 0         0 next;
241             }
242              
243 0 0       0 if ( $elt->{type} eq "chorus" ) {
244 0 0       0 push(@s, "") if $tidy;
245 0         0 push(@s, "// $line Start of chorus*");
246 0         0 foreach my $e ( @{$elt->{body}} ) {
  0         0  
247 0 0       0 if ( $e->{type} eq "empty" ) {
248 0         0 push(@s, "");
249 0         0 next;
250             }
251 0 0       0 if ( $e->{type} eq "songline" ) {
252 0         0 push(@s, songline($e));
253 0         0 next;
254             }
255             }
256 0         0 push(@s, "// $line End of chorus*");
257 0 0       0 push(@s, "") if $tidy;
258 0         0 next;
259             }
260              
261 0 0       0 if ( $elt->{type} eq "tab" ) {
262 0 0       0 push(@s, "") if $tidy;
263 0         0 push(@s, "// $line Start of tab");
264 0         0 push(@s, map { "// " . $_->{text} } @{$elt->{body}} );
  0         0  
  0         0  
265 0         0 push(@s, "// $line End of tab");
266 0 0       0 push(@s, "") if $tidy;
267 0         0 next;
268             }
269              
270 0 0       0 if ( $elt->{type} eq "verse" ) {
271 0 0       0 push(@s, "") if $tidy;
272 0         0 push(@s, "// $line Start of verse");
273 0         0 foreach my $e ( @{$elt->{body}} ) {
  0         0  
274 0 0       0 if ( $e->{type} eq "empty" ) {
275             push(@s, "***SHOULD NOT HAPPEN***")
276 0 0       0 if $s->{structure} eq 'structured';
277 0         0 next;
278             }
279 0 0       0 if ( $e->{type} eq "songline" ) {
280 0         0 push(@s, songline($e));
281 0         0 next;
282             }
283 0 0       0 if ( $e->{type} eq "comment" ) {
284 0         0 push(@s, "-c- " . $e->{text});
285 0         0 next;
286             }
287 0 0       0 if ( $e->{type} eq "comment_italic" ) {
288 0         0 push(@s, "-i- " . $e->{text});
289 0         0 next;
290             }
291             }
292 0         0 push(@s, "// $line End of verse");
293 0 0       0 push(@s, "") if $tidy;
294 0         0 next;
295             }
296              
297 0 0       0 if ( $elt->{type} eq "image" ) {
298 0         0 my @args = ( "image:", $elt->{uri} );
299 0         0 while ( my($k,$v) = each( %{ $elt->{opts} } ) ) {
  0         0  
300 0         0 push( @args, "$k=$v" );
301             }
302 0         0 foreach ( @args ) {
303 0 0       0 next unless /\s/;
304 0         0 $_ = '"' . $_ . '"';
305             }
306 0         0 push( @s, "// $line @args" );
307 0         0 next;
308             }
309              
310 0 0       0 if ( $elt->{type} eq "set" ) {
311 0         0 next;
312             }
313              
314 0 0       0 if ( $elt->{type} eq "control" ) {
315 0         0 next;
316             }
317              
318             # Ignore everyting else.
319              
320             }
321 7 50       48 push(@s, "// $line End of $ctx") if $ctx;
322              
323 7         72 \@s;
324             }
325              
326             sub songline {
327 0     0 0 0 my ($elt) = @_;
328              
329 0         0 my $t_line = "";
330              
331 0 0 0     0 if ( $single_space && ! ( $elt->{chords} && join( "", @{ $elt->{chords} } ) =~ /\S/ )
      0        
332             ) {
333 0         0 $t_line = join( "", @{ $elt->{phrases} } );
  0         0  
334 0         0 $t_line =~ s/\s+$//;
335 0         0 return $t_line;
336             }
337              
338 0 0       0 unless ( $elt->{chords} ) {
339 0         0 return ( "", join( " ", @{ $elt->{phrases} } ) );
  0         0  
340             }
341              
342 0 0       0 if ( my $f = $::config->{settings}->{'inline-chords'} ) {
343 0 0       0 $f = '[%s]' unless $f =~ /^[^%]*\%s[^%]*$/;
344 0         0 $f .= '%s';
345 0         0 foreach ( 0..$#{$elt->{chords}} ) {
  0         0  
346             $t_line .= sprintf( $f,
347             $elt->{chords}->[$_]->key,
348 0         0 $elt->{phrases}->[$_] );
349             }
350 0         0 return ( $t_line );
351             }
352              
353 0         0 my $c_line = "";
354 0         0 foreach ( 0..$#{$elt->{chords}} ) {
  0         0  
355 0         0 $c_line .= $elt->{chords}->[$_]->key . " ";
356 0         0 $t_line .= $elt->{phrases}->[$_];
357 0         0 my $d = length($c_line) - length($t_line);
358 0 0       0 $t_line .= "-" x $d if $d > 0;
359 0 0       0 $c_line .= " " x -$d if $d < 0;
360             }
361 0         0 s/\s+$// for ( $t_line, $c_line );
362 0 0       0 return $chords_under
363             ? ( $t_line, $c_line )
364             : ( $c_line, $t_line )
365             }
366              
367             sub is_bar {
368 102     102 0 174 for ( $_[0] ) {
369 102 50 33     1050 return 1
      33        
      33        
      33        
      33        
      66        
      66        
      66        
370             if $_ eq "|:" || $_ eq "{"
371             || $_ eq ":|" || $_ eq "}"
372             || $_ eq ":|:" || $_ eq "}{"
373             || $_ eq "|" || $_ eq "||" || $_ eq "|.";
374             }
375 1         28 return;
376             }
377              
378             sub reduce {
379 9     9 0 31 my ( $a, $bpm, $line, $s ) = @_;
380 9         50 my @a = @$a;
381 9 50       32 warn("R: ", join(' ',@a), "\n") if $config->{debug}->{mma};
382 9         14 my @reduced;
383 9         15 my $bar = 0;
384 9         12 my $carry;
385              
386 9         27 while ( @a ) {
387 24         35 $bar++;
388 24 50       45 if ( $carry ) {
389 0 0       0 if ( $a[0] eq '.' ) {
390 0         0 $a[0] = $carry;
391             }
392             else {
393 0         0 push( @$s,
394             sprintf( "// line %d, bar %d, cannot resolve %s (from previous line)",
395             $line, $bar, $carry ) );
396             }
397 0         0 $carry = '';
398             }
399 24         55 for ( my $b = 1; $b <= $bpm; $b++ ) {
400 88         133 my $a0 = shift(@a);
401 88         120 my $a1 = shift(@a);
402             # Check for clash.
403 88 100 100     205 if ( $a0 ne '.' && $a1 ne '.' ) {
404 1 50 33     34 if ( @a > 1 && $a[0] eq '.' && $a[1] eq '.' ) {
      33        
405             # X Y . . => X . Y .
406 1         3 $a[0] = $a1;
407 1         9 push( @$s,
408             sprintf("// line %d, bar %d, beat %d: shifting %s to beat %d",
409             $line, $bar, $b, $a[0], $b+1) );
410             }
411             else {
412             # Cannot resolve.
413 0         0 push( @$s,
414             sprintf( "// line %d, bar %d, beat %d: too many chords",
415             $line, $bar, $b ) );
416             }
417             }
418            
419             # Check for clash and try to resolve.
420             # . X => X .
421 88 100 100     259 if ( $a0 eq '.' && $a1 ne '.' ) {
    100          
422 1         2 $a0 = $a1;
423 1         10 push( @$s,
424             sprintf( "// line %d, bar %d, beat %d: move back %s",
425             $line, $bar, $b, $a1) );
426             }
427             # x X . => x . X
428             elsif ( $a1 ne '.' ) {
429 1 50 33     11 if ( @a > 1 && is_bar($a[0]) && $a[1] eq '.' ) {
    50 33        
    50 33        
430 0         0 $a[1] = $a1;
431 0         0 push( @$s,
432             sprintf( "// line %d, bar %d, beat %d: advancing %s",
433             $line, $bar, $b, $a1) );
434             }
435             elsif ( @a > 0 && $a[0] eq '.' ) {
436 0         0 $a[0] = $a1;
437 0         0 push( @$s,
438             sprintf( "// line %d, bar %d, beat %d: advancing %s",
439             $line, $bar, $b, $a1) );
440             }
441             elsif ( !@a ) {
442 0         0 $carry = $a1;
443 0         0 push( @$s,
444             sprintf( "// line %d, bar %d, beat %d: carry %s to next line",
445             $line, $bar, $b, $a1) );
446             }
447             }
448 88         193 push( @reduced, $a0 );
449             }
450 24 50       51 if ( is_bar($a[0]) ) {
451 24         70 push( @reduced, shift(@a) );
452             }
453             else {
454 0         0 push( @$s,
455             sprintf( "// line %d, bar %d, missing bar line?", $line, $bar ) )
456             }
457             }
458 9         61 return @reduced;
459             }
460              
461             sub makegroove {
462 7     7 0 22 my ( $bpm, $q ) = @_;
463              
464 7 50       46 return ( "Groove $groove" ) if $groove;
465              
466 0           my @s;
467 0 0         if ( $bpm == 3 ) {
    0          
468 0 0         $q = 4 unless $q == 8;
469 0           $groove = "Neutral$bpm$q";
470             }
471             elsif ( $bpm == 6 ) {
472 0 0         $q = 4 unless $q == 8;
473 0           $groove = "Neutral$bpm$q";
474             }
475             else {
476 0           $groove = "Neutral44";
477             }
478              
479 0           my $seq;
480             my $whole;
481              
482 0 0 0       if ( $bpm == 3 && $q == 4 ) {
    0 0        
    0 0        
483 0           $whole = "2.";
484 0           $seq = "{ 1 0 90; 2 0 30; 3 0 30 }";
485             }
486             elsif ( $bpm == 3 && $q == 8 ) {
487 0           $whole = "4.";
488 0           $seq = "{ 1 0 90; 1.67 0 30; 2.33 0 30 }";
489             }
490             elsif ( $bpm == 6 && $q == 8 ) {
491 0           $whole = "1.";
492 0           $seq = "{ 1 0 90; 2 0 30; 3 0 30; 4 0 80; 5 0 30; 6 0 30 }";
493             }
494             else { # assume 4/4
495 0           $whole = "1";
496 0           $seq = "{ 1 0 90; 2 0 30; 3 0 50; 4 0 30 }";
497             }
498              
499 0           return split( /\n/, <
500             SeqClear
501             SeqSize 1
502             Time $bpm/$q
503              
504             Begin Drum-Side
505             Tone KickDrum1
506             Sequence { 1.0 0 60 }
507             Volume 30
508             End
509              
510             Begin Drum-CHH
511             Tone ClosedHiHat
512             Sequence $seq
513             Volume 30
514             End
515              
516             Begin Chord
517             Channel 2
518             Voice ReedOrgan
519             Sequence { 1 $whole 50 }
520             Volume 30
521             Articulate 100
522             End
523              
524             DefGroove $groove
525              
526             Groove $groove
527              
528             EOD
529              
530             }
531              
532             1;
533              
534             unless ( caller) {
535             my $bpm = 4;
536             my @s = ();
537             unless ( join( ' ',
538             reduce( [split(' ','C . . . . . . . | C . . . . . . . | C . . . . . . . | C . . . . . . . |')], $bpm, 1, \@s) )
539             eq 'C . . . | C . . . | C . . . | C . . . |'
540             ) {
541             warn("reduce error\n");
542             print "$_\n" for @s;
543             }
544             while ( <> ) {
545             @s = ();
546             chomp;
547             print("=> ", join(' ',reduce([split(' ',$_)], $bpm, 1, \@s)), "\n");
548             print "$_\n" for @s;
549             }
550             }