File Coverage

blib/lib/ChordPro/Chords.pm
Criterion Covered Total %
statement 212 348 60.9
branch 71 150 47.3
condition 55 121 45.4
subroutine 23 30 76.6
pod 0 24 0.0
total 361 673 53.6


line stmt bran cond sub pod time code
1             #! perl
2              
3             package main;
4              
5             our $config;
6             our $options;
7              
8             package ChordPro::Chords;
9              
10 79     79   1045 use v5.26;
  79         331  
11 79     79   441 use utf8;
  79         233  
  79         493  
12 79     79   1870 use Carp;
  79         180  
  79         4436  
13 79     79   532 use feature qw( signatures );
  79         209  
  79         7442  
14 79     79   620 no warnings "experimental::signatures";
  79         239  
  79         3368  
15              
16 79     79   42530 use ChordPro::Chords::Parser;
  79         274  
  79         435487  
17              
18             # Chords defined by the configs.
19             my %config_chords;
20              
21             # Names of chords loaded from configs.
22             my @chordnames;
23              
24             # Additional chords, defined by the user.
25             my %song_chords;
26              
27             # Current tuning.
28             my @tuning;
29              
30             # Assert that an instrument is loaded.
31 223     223 0 577 sub assert_tuning () {
  223         482  
32 223 50       933 Carp::croak("FATAL: No instrument?") unless @tuning;
33             }
34              
35             ################ Section Dumping Chords ################
36              
37             # API: Returns a list of all chord names in a nice order.
38             # Used by: ChordPro, Output/ChordPro.
39 0     0 0 0 sub chordnames () {
  0         0  
40 0         0 assert_tuning();
41 0         0 [ sort chordcompare @chordnames ];
42             }
43              
44             # Chord order ordinals, for sorting.
45             my %chordorderkey; {
46             my $ord = 0;
47             for ( split( ' ', "C C# Db D D# Eb E F F# Gb G G# Ab A A# Bb B" ) ) {
48             $chordorderkey{$_} = $ord;
49             $ord += 2;
50             }
51             }
52              
53             # Compare routine for chord names.
54             # API: Used by: Songbook.
55 0     0 0 0 sub chordcompare ( $chorda, $chordb ) {
  0         0  
  0         0  
  0         0  
56 0         0 my ( $a0, $arest ) = $chorda =~ /^([A-G][b#]?)(.*)/;
57 0         0 my ( $b0, $brest ) = $chordb =~ /^([A-G][b#]?)(.*)/;
58 0   0     0 $a0 = $chordorderkey{$a0//"\x{ff}"}//return 0;
      0        
59 0   0     0 $b0 = $chordorderkey{$b0//"\x{ff}"}//return 0;
      0        
60 0 0       0 return $a0 <=> $b0 if $a0 != $b0;
61 0 0       0 $a0++ if $arest =~ /^m(?:in)?(?!aj)/;
62 0 0       0 $b0++ if $brest =~ /^m(?:in)?(?!aj)/;
63 0         0 for ( $arest, $brest ) {
64 0         0 s/11/:/; # sort 11 after 9
65 0         0 s/13/;/; # sort 13 after 11
66 0         0 s/\((.*?)\)/$1/g; # ignore parens
67 0         0 s/\+/aug/; # sort + as aug
68             }
69 0 0       0 $a0 <=> $b0 || $arest cmp $brest;
70             }
71             # Dump a textual list of chord definitions.
72             # Should be handled by the ChordPro backend?
73              
74 26     26 0 61 sub list_chords ( $chords, $origin, $hdr ) {
  26         50  
  26         56  
  26         46  
  26         40  
75 26         102 assert_tuning();
76 26         77 my @s;
77 26 50       99 if ( $hdr ) {
78 0         0 my $t = "-" x (((@tuning - 1) * 4) + 1);
79 0         0 substr( $t, (length($t)-7)/2, 7, "strings" );
80             push( @s,
81             "# CHORD CHART",
82             "# Generated by ChordPro " . $ChordPro::VERSION,
83             "# https://www.chordpro.org",
84             "#",
85             "# " . ( " " x 35 ) . $t,
86             "# Chord" . ( " " x 35 ) .
87             join("",
88 0         0 map { sprintf("%-4s", $_) }
  0         0  
89             @tuning ),
90             );
91             }
92              
93 26         100 foreach my $chord ( @$chords ) {
94 77         130 my $info;
95 77 100       115 if ( eval{ $chord->{name} } ) {
  77 50       731  
96 2         3 $info = $chord;
97             }
98             elsif ( $origin eq "chord" ) {
99 0         0 push( @s, sprintf( "{%s: %s}", "chord", $chord ) );
100 0         0 next;
101             }
102             else {
103 75         176 $info = known_chord($chord);
104             }
105 77 100       245 next unless $info;
106             my $s = sprintf( "{%s %-15.15s base-fret %2d ".
107             "frets %s",
108             $origin eq "chord" ? "chord: " : "define:",
109             $info->{name}, $info->{base},
110 69   50     248 @{ $info->{frets} // [] }
111             ? join("",
112 414         1178 map { sprintf("%-4s", $_) }
113 414 100       792 map { $_ < 0 ? "X" : $_ }
114 69 100       239 @{ $info->{frets} } )
  69 50       144  
115             : (" " x strings() ));
116             $s .= join("", " fingers ",
117 6         17 map { sprintf("%-4s", $_) }
118 6 50       13 map { $_ < 0 ? "X" : $_ }
119 1         3 @{ $info->{fingers} } )
120 69 100 100     266 if $info->{fingers} && @{ $info->{fingers} };
  51         159  
121             $s .= join("", " keys ",
122 0         0 map { sprintf("%2d", $_) }
123 0         0 @{ $info->{keys} } )
124 69 50 66     189 if $info->{keys} && @{ $info->{keys} };
  50         132  
125 69         173 $s .= "}";
126 69         192 push( @s, $s );
127             }
128 26         168 \@s;
129             }
130              
131 0     0 0 0 sub dump_chords ( $mode ) {
  0         0  
  0         0  
132 0         0 assert_tuning();
133             print( join( "\n",
134             $mode && $mode == 2
135 0         0 ? @{ json_chords(\@chordnames ) }
136 0 0 0     0 : @{ list_chords(\@chordnames, "__CLI__", 1) } ), "\n" );
  0         0  
137             }
138              
139 0     0 0 0 sub json_chords ( $chords ) {
  0         0  
  0         0  
140 0         0 assert_tuning();
141 0         0 my @s;
142              
143             push( @s, "// ChordPro instrument definition.",
144             "",
145             qq<{ "instrument" : "> .
146             ($::config->{instrument} || "Guitar, 6 strings, standard tuning") .
147             qq<",>,
148             "",
149             qq< "tuning" : [ > .
150 0   0     0 join(", ", map { qq{"$_"} } @tuning) . " ],",
  0         0  
151             "",
152             qq{ "chords" : [},
153             "",
154             );
155              
156 0         0 my $maxl = -1;
157 0         0 foreach my $chord ( @$chords ) {
158 0         0 my $t = length( $chord );
159 0 0       0 $maxl < $t and $maxl = $t;
160             }
161 0         0 $maxl += 2;
162              
163 0         0 foreach my $chord ( @$chords ) {
164 0         0 my $info;
165 0 0       0 if ( eval{ $chord->{name} } ) {
  0         0  
166 0         0 $info = $chord;
167             }
168             else {
169 0         0 $info = known_chord($chord);
170             }
171 0 0       0 next unless $info;
172              
173 0         0 my $name = '"' . $info->{name} . '"';
174             my $s = sprintf( qq[ { "name" : %-${maxl}.${maxl}s,] .
175             qq[ "base" : %2d,],
176 0         0 $name, $info->{base} );
177 0 0       0 if ( @{ $info->{frets} } ) {
  0         0  
178             $s .= qq{ "frets" : [ } .
179 0         0 join( ", ", map { sprintf("%2s", $_) } @{ $info->{frets} } ) .
  0         0  
  0         0  
180             qq{ ],};
181             }
182 0 0 0     0 if ( $info->{fingers} && @{ $info->{fingers} } ) {
  0         0  
183             $s .= qq{ "fingers" : [ } .
184 0         0 join( ", ", map { sprintf("%2s", $_) } @{ $info->{fingers} } ) .
  0         0  
  0         0  
185             qq{ ],};
186             }
187 0 0 0     0 if ( $info->{keys} && @{ $info->{keys} } ) {
  0         0  
188             $s .= qq{ "keys" : [ } .
189 0         0 join( ", ", map { sprintf("%2d", $_) } @{ $info->{keys} } ) .
  0         0  
  0         0  
190             qq{ ],};
191             }
192 0         0 chop($s);
193 0         0 $s .= " },";
194 0         0 push( @s, $s );
195             }
196 0         0 chop( $s[-1] );
197 0         0 push( @s, "", " ]," );
198 0 0       0 if ( $::config->{pdf}->{diagrams}->{vcells} ) {
199             push( @s, qq< "pdf" : { "diagrams" : { "vcells" : > .
200 0         0 $::config->{pdf}->{diagrams}->{vcells} . qq< } },> );
201             }
202 0         0 chop( $s[-1] );
203 0         0 push( @s, "}" );
204 0         0 \@s;
205             }
206              
207             ################ Section Tuning ################
208              
209             # API: Return the number of strings supported.
210             # Used by: Songbook, Output::PDF.
211 50110     50110 0 73352 sub strings () {
  50110         67694  
212 50110         139137 scalar(@tuning);
213             }
214              
215             my $parser;# = ChordPro::Chords::Parser->default;
216              
217             # API: Set tuning, discarding chords.
218             # Used by: Config.
219 197     197 0 485 sub set_tuning ( $cfg ) {
  197         495  
  197         400  
220 197   50     1013 my $t = $cfg->{tuning} // [];
221 197 50       1084 return "Invalid tuning (not array)" unless ref($t) eq "ARRAY";
222 197   50     1065 $options //= { verbose => 0 };
223              
224 197 100       978 if ( @tuning ) {
225 118         1685 ( my $t1 = "@$t" ) =~ s/\d//g;
226 118         1095 ( my $t2 = "@tuning" ) =~ s/\d//g;
227 118 50       612 if ( $t1 ne $t2 ) {
228             warn("Tuning changed, chords flushed\n")
229 0 0       0 if $options->{verbose} > 1;
230 0         0 @chordnames = ();
231 0         0 %config_chords = ();
232             }
233             }
234             else {
235 79         320 @chordnames = ();
236 79         288 %config_chords = ();
237             }
238 197         1228 @tuning = @$t; # need more checks
239 197         1169 assert_tuning();
240 197         737 return;
241              
242             }
243              
244             # API: Get tuning.
245             # Used by: String substitution.
246 2115     2115 0 3463 sub get_tuning () {
  2115         2928  
247 2115         3065 @{[@tuning]};
  2115         17254  
248             }
249              
250             # API: Set target parser.
251             # Used by: ChordPro.
252 7     7 0 12562 sub set_parser ( $p ) {
  7         22  
  7         14  
253              
254 7 50 33     73 $p = ChordPro::Chords::Parser->get_parser($p)
255             unless ref($p) && $p->isa('ChordPro::Chords::Parser');
256 7         21 $parser = $p;
257             warn( "Parser: ", $parser->{system}, "\n" )
258 7 50       28 if $options->{verbose} > 1;
259              
260 7         21 return;
261             }
262              
263             # Parser stack.
264              
265             my @parsers;
266              
267             # API: Reset current parser.
268             # Used by: Config.
269 208     208 0 516 sub reset_parser () {
  208         512  
270 208         625 undef $parser;
271 208         643 @parsers = ();
272             }
273              
274 72     72 0 171 sub get_parser () {
  72         115  
275 72         259 $parser;
276             }
277              
278 197     197 0 491 sub push_parser ( $p ) {
  197         729  
  197         425  
279 197 50 33     2025 $p = ChordPro::Chords::Parser->get_parser($p)
280             unless ref($p) && $p->isa('ChordPro::Chords::Parser');
281 197         710 push( @parsers, $p );
282 197         689 $parser = $p;
283             }
284              
285 197     197 0 535 sub pop_parser () {
  197         383  
286 197 50       725 Carp::croak("Parser stack underflow") unless @parsers;
287 197         5582 $parser = pop(@parsers);
288             }
289              
290             ################ Section Config & User Chords ################
291              
292 2392     2392 0 3519 sub known_chord ( $name ) {
  2392         3822  
  2392         3081  
293 2392         3271 my $info;
294 2392 100       7511 if ( ref($name) =~ /^ChordPro::Chord::/ ) {
295 1132         1941 $info = $name;
296 1132         3495 $name = $info->name;
297             }
298 2392   100     9036 my $ret = $song_chords{$name} // $config_chords{$name};
299 2392 100       10014 $ret->{_via} = $ret->{origin} . " chords", return $ret if $ret;
300 488 100       1310 return unless $info;
301              
302             # Retry agnostic. Not all can do that.
303 248         462 $name = eval { $info->agnostic };
  248         835  
304 248 100       760 return unless $name;
305 230   66     866 $ret = $song_chords{$name} // $config_chords{$name};
306 230 100       553 if ( $ret ) {
307 9         24 $ret = $info->new($ret);
308 9         46 for ( qw( name display
309             root root_canon root_mod
310             bass bass_canon
311             system parser ) ) {
312 81 100       171 next unless defined $info->{$_};
313 63         118 $ret->{$_} = $info->{$_};
314             }
315 9         57 $ret->{_via} = "agnostic" . " " . $ret->{origin} . " chords";
316             }
317 230         1171 $ret;
318             }
319              
320 49883     49883 0 68768 sub check_chord ( $ii ) {
  49883         69347  
  49883         66343  
321             my ( $name, $base, $frets, $fingers, $keys )
322 49883         120345 = @$ii{qw(name base frets fingers keys)};
323 49883 50 66     206102 if ( $frets && @$frets && @$frets != strings() ) {
      66        
324 0         0 return scalar(@$frets) . " strings";
325             }
326 49883 50 100     144304 if ( $fingers && @$fingers && @$fingers != strings() ) {
      66        
327 0         0 return scalar(@$fingers) . " strings for fingers";
328             }
329 49883 50 33     154157 unless ( $base > 0 && $base < 24 ) {
330 0         0 return "base-fret $base out of range";
331             }
332 49883 100 100     128747 if ( $keys && @$keys ) {
333 8         18 for ( @$keys ) {
334 24 50 33     120 return "invalid key \"$_\"" unless /^\d+$/ && $_ < 24;
335             }
336             }
337 49883         96197 return;
338             }
339              
340             # API: Add a config defined chord.
341             # Used by: Config.
342 49821     49821 0 71690 sub add_config_chord ( $def ) {
  49821         78510  
  49821         66685  
343              
344 49821         80039 my $res;
345             my $name;
346              
347 49821         103957 my @extprops = qw( display format );
348              
349             # Handle alternatives.
350 49821         67922 my @names;
351 49821 50       163331 if ( $def->{name} =~ /.\|./ ) {
352 0         0 $def->{name} = [ split( /\|/, $def->{name} ) ];
353             }
354 49821 100       217237 if ( UNIVERSAL::isa( $def->{name}, 'ARRAY' ) ) {
355 1         3 $name = shift( @{ $def->{name} } );
  1         4  
356 1         3 push( @names, @{ $def->{name} } );
  1         4  
357             }
358             else {
359 49820         92685 $name = $def->{name};
360             }
361              
362             # For derived chords.
363 49821 100 66     142687 if ( $def->{copy} || $def->{"copyall"} ) {
364 30940         53646 my $src = $def->{copy};
365 30940 50       64498 if ( $def->{copyall} ) {
366 0 0       0 return "Cannot copy and copyall at the same time"
367             if $src;
368 0         0 $src = $def->{copyall};
369             }
370 30940         53949 $res = $config_chords{$src};
371 30940 50       61465 return "Cannot copy $src" unless $res;
372 30940         346424 $def = bless { %$res, %$def } => ref($res);
373 30940 50       90431 if ( $def->{copy} ) {
374 30940         86216 delete $def->{$_} for @extprops;
375             }
376             else {
377 0         0 $def->{copy} = $def->{copyall};
378             }
379             }
380 49821         100770 delete $def->{name};
381 49821   100     104911 $def->{base} ||= 1;
382              
383             my ( $base, $frets, $fingers, $keys ) =
384 49821         114493 ( $def->{base}, $def->{frets}, $def->{fingers}, $def->{keys} );
385 49821         96159 $res = check_chord($def);
386 49821 50       91888 return $res if $res;
387              
388 49821         68710 my $dpinfo;
389 49821 50       98032 if ( $def->{display} ) {
390 0         0 $dpinfo = parse_chord($def->{display});
391 0 0       0 if ( $dpinfo ) {
392 0         0 $def->{display} = $dpinfo;
393             }
394             else {
395 0         0 delete $def->{display};
396             }
397             }
398 49821         87570 for $name ( $name, @names ) {
399 49822   66     94986 my $info = parse_chord($name)
400             // ChordPro::Chord::Common->new({ name => $name });
401              
402 49822 100 100     143971 if ( $info->is_chord && $def->{copy} && $def->is_chord ) {
      66        
403 19873         41911 for ( qw( root bass ext qual ) ) {
404 79492         131633 delete $def->{$_};
405 79492         136250 delete $def->{$_."_mod"};
406 79492         148746 delete $def->{$_."_canon"};
407             }
408 19873         32084 for ( qw( ext qual ) ) {
409 39746         57669 delete $def->{$_};
410 39746         71228 delete $def->{$_."_canon"};
411             }
412             }
413 49822 50       121965 Carp::confess(::dump($parser)) unless $parser->{target};
414             $config_chords{$name} = bless
415             { origin => "config",
416             system => $parser->{system},
417             %$info,
418             %$def,
419             base => $base,
420             baselabeloffset => $def->{baselabeloffset}||0,
421             frets => [ $frets && @$frets ? @$frets : () ],
422             fingers => [ $fingers && @$fingers ? @$fingers : () ],
423             keys => [ $keys && @$keys ? @$keys : () ]
424 49822 50 50     1302261 } => $parser->{target};
    50 33        
    50 66        
      66        
425 49822         161663 push( @chordnames, $name );
426              
427             # Also store the chord info under a neutral name so it can be
428             # found when other note name systems are used.
429 49822         75361 my $i;
430 49822 100       123554 if ( $info->is_chord ) {
431 38318         100232 $i = $info->agnostic;
432             }
433             else {
434             # Retry with default parser.
435 11504         32793 $i = ChordPro::Chords::Parser->default->parse($name);
436 11504 50 33     31740 if ( $i && $i->is_chord ) {
437 0         0 $info->{root_ord} = $i->{root_ord};
438             $config_chords{$name}->{$_} = $i->{$_}
439 0         0 for qw( root_ord root_mod ext_canon qual_canon );
440 0         0 $i = $i->agnostic;
441             }
442             }
443 49822 100       129003 if ( $info->is_chord ) {
444 38318         290593 $config_chords{$i} = $config_chords{$name};
445 38318         155986 $config_chords{$i}->{origin} = "config";
446             }
447             }
448 49821         195103 return;
449             }
450              
451             # API: Add a user defined chord.
452             # Used by: Song.
453 62     62 0 108 sub add_song_chord ( $ii ) {
  62         105  
  62         81  
454              
455 62         151 my $res = check_chord($ii);
456 62 50       171 return $res if $res;
457              
458             # Need a parser anyway.
459 62   33     174 $parser //= ChordPro::Chords::Parser->get_parser;
460              
461             my $c =
462             { system => $parser->{system},
463 62         789 parser => $parser,
464             %$ii,
465             };
466 62   50     240 $c->{origin} //= "user";
467              
468             # Cleanup.
469 62         135 for ( qw( display ) ) {
470 62 100       210 delete $c->{$_} unless defined $c->{$_};
471             }
472 62         148 for ( qw( frets fingers keys ) ) {
473 186 100 100     464 delete $c->{$_} unless $c->{$_} && @{ $c->{$_} };
  104         521  
474             }
475              
476 62         283 $song_chords{$c->{name}} = bless $c => $parser->{target};
477 62         159 return;
478             }
479              
480             # API: Add an unknown chord.
481             # Used by: Song.
482 0     0 0 0 sub add_unknown_chord ( $name ) {
  0         0  
  0         0  
483 0   0     0 $parser //= ChordPro::Chords::Parser->get_parser;
484             $song_chords{$name} = bless
485             { origin => "user",
486             name => $name,
487             base => 0,
488             frets => [],
489             fingers => [],
490             keys => []
491 0         0 } => $parser->{target};
492             }
493              
494             # API: Reset user defined songs. Should be done for each new song.
495             # Used by: Songbook, Output::PDF.
496 171     171 0 368 sub reset_song_chords () {
  171         293  
497 171         1270 %song_chords = ();
498             }
499              
500             # API: Return some chord statistics.
501 0     0 0 0 sub chord_stats () {
  0         0  
502 0         0 my $res = sprintf( "%d config chords", scalar(keys(%config_chords)) );
503 0 0       0 $res .= sprintf( ", %d song chords", scalar(keys(%song_chords)) )
504             if %song_chords;
505 0         0 return $res;
506             }
507              
508             ################ Section Chords Parser ################
509              
510 53307     53307 0 3886662 sub parse_chord ( $chord ) {
  53307         83823  
  53307         72456  
511              
512 53307   66     108475 $parser //= ChordPro::Chords::Parser->get_parser;
513 53307         147007 return $parser->parse($chord);
514             }
515              
516             ################ Section Keyboard keys ################
517              
518             my %keys =
519             ( "" => [ 0, 4, 7 ], # major
520             "-" => [ 0, 3, 7 ], # minor
521             "7" => [ 0, 4, 7, 10 ], # dominant 7th
522             "-7" => [ 0, 3, 7, 10 ], # minor seventh
523             "maj7" => [ 0, 4, 7, 11 ], # major 7th
524             "-maj7" => [ 0, 3, 7, 11 ], # minor major 7th
525             "6" => [ 0, 4, 7, 9 ], # 6th
526             "-6" => [ 0, 3, 7, 9 ], # minor 6th
527             "6add9" => [ 0, 4, 7, 9, 14], # 6/9
528             "5" => [ 0, 7 ], # 6th
529             "9" => [ 0, 4, 7, 10, 14 ], # 9th
530             "-9" => [ 0, 3, 7, 10, 14 ], # minor 9th
531             "maj9" => [ 0, 4, 7, 11, 14 ], # major 9th
532             "11" => [ 0, 4, 7, 10, 14, 17 ], # 11th
533             "-11" => [ 0, 3, 7, 10, 14, 17 ], # minor 11th
534             "13" => [ 0, 4, 7, 10, 14, 17, 21 ], # 13th
535             "-13" => [ 0, 3, 7, 10, 14, 17, 21 ], # minor 13th
536             "maj13" => [ 0, 4, 7, 11, 14, 21 ], # major 13th
537             "add2" => [ 0, 2, 4, 7 ], # add 2
538             "add9" => [ 0, 4, 7, 14 ], # add 9
539             "-add2" => [ 0, 2, 3, 7 ], # minor add 2
540             "-add9" => [ 0, 2, 3, 7, 11 ], # minor add 9
541             "-add11" => [ 0, 3, 5, 7, 11 ], # minor add 11
542             "7-5" => [ 0, 4, 6, 10 ], # 7 flat 5 altered chord
543             "7+5" => [ 0, 4, 8, 10 ], # 7 sharp 5 altered chord
544             "sus4" => [ 0, 5, 7 ], # sus 4
545             "sus2" => [ 0, 2, 7 ], # sus 2
546             "7sus2" => [ 0, 2, 7, 10 ], # 7 sus 2
547             "7sus4" => [ 0, 5, 7, 10 ], # 7 sus 4
548             "-7sus2" => [ 0, 2, 3, 7, 10 ], # minor 7 sus 2
549             "-7sus4" => [ 0, 3, 5, 7, 10 ], # minor 7 sus 4
550             "0" => [ 0, 3, 6 ], # diminished
551             "07" => [ 0, 3, 6, 9 ], # diminished 7
552             "-7b5" => [ 0, 3, 6, 10 ], # minor 7 flat 5
553             "+" => [ 0, 4, 8 ], # augmented
554             "+7" => [ 0, 4, 8, 10 ], # augmented 7
555             "h" => [ 0, 3, 6, 10 ], # half-diminished seventh
556             );
557              
558 0     0 0 0 sub get_keys ( $info ) {
  0         0  
  0         0  
559             # ::dump( { %$info, parser => ref($info->{parser}) });
560             # Has keys defined.
561 0 0 0     0 return $info->{keys} if $info->{keys} && @{$info->{keys}};
  0         0  
562              
563             # Known chords.
564             return $keys{$info->{qual_canon}.$info->{ext_canon}}
565             if defined $info->{qual_canon}
566             && defined $info->{ext_canon}
567 0 0 0     0 && defined $keys{$info->{qual_canon}.$info->{ext_canon}};
      0        
568              
569             # Try to derive from guitar chords.
570 0 0 0     0 return [] unless $info->{frets} && @{$info->{frets}};
  0         0  
571 0         0 my @tuning = ( 4, 9, 2, 7, 11, 4 );
572 0         0 my %keys;
573 0         0 my $i = -1;
574 0         0 my $base = $info->{base} - 1;
575 0 0       0 $base = 0 if $base < 0;
576 0         0 for ( @{ $info->{frets} } ) {
  0         0  
577 0         0 $i++;
578 0 0       0 next if $_ < 0;
579 0         0 my $c = $tuning[$i] + $_ + $base;
580 0 0       0 if ( $info->{root_ord} ) {
581 0 0       0 $c += 12 if $c < $info->{root_ord};
582 0         0 $c -= $info->{root_ord};
583             }
584 0         0 $keys{ $c % 12 }++;
585             }
586 0         0 return [ keys %keys ];
587             }
588              
589             ################ Section Transposition ################
590              
591             # API: Transpose a chord.
592             # Used by: Songbook.
593 123     123 0 200 sub transpose ( $c, $xpose, $xcode = "" ) {
  123         231  
  123         186  
  123         228  
  123         169  
594 123 50 33     319 return $c unless $xpose || $xcode;
595 123 50       279 return $c if $c =~ /^ .+/;
596 123         274 my $info = parse_chord($c);
597 123 50       382 unless ( $info ) {
598 0         0 assert_tuning();
599 0         0 for ( \%song_chords, \%config_chords ) {
600             # Not sure what this is for...
601             # Anyway, it causes unknown but {defined} chords to silently
602             # bypass the trans* warnings.
603             # return if exists($_->{$c});
604             }
605             $xpose
606 0 0       0 ? warn("Cannot transpose $c\n")
607             : warn("Cannot transcode $c\n");
608 0         0 return;
609             }
610              
611 123         418 my $res = $info->transcode($xcode)->transpose($xpose)->canonical;
612              
613             # Carp::cluck("__XPOSE = ", $xpose, " __XCODE = $xcode, chord $c => $res\n");
614              
615 123         3540 return $res;
616             }
617              
618             1;