File Coverage

lib/ChordPro/Chords.pm
Criterion Covered Total %
statement 192 369 52.0
branch 60 166 36.1
condition 50 127 39.3
subroutine 22 30 73.3
pod 0 24 0.0
total 324 716 45.2


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 81     81   1044 use v5.26;
  81         291  
11 81     81   529 use utf8;
  81         190  
  81         470  
12 81     81   1848 use Carp;
  81         170  
  81         4496  
13 81     81   508 use feature qw( signatures );
  81         240  
  81         6663  
14 81     81   588 no warnings "experimental::signatures";
  81         173  
  81         3352  
15              
16 81     81   44463 use ChordPro::Chords::Parser;
  81         260  
  81         493906  
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; # names, e.g. E2
29             my @t_ord; # ordinals, e.g. 4
30             my $t_oct; # octave, usually 12
31              
32             # Assert that an instrument is loaded.
33 199     199 0 497 sub assert_tuning () {
  199         408  
34 199 50       829 Carp::croak("FATAL: No instrument?") unless @tuning;
35             }
36              
37             ################ Section Dumping Chords ################
38              
39             # API: Returns a list of all chord names in a nice order.
40             # Used by: ChordPro, Output/ChordPro.
41 0     0 0 0 sub chordnames () {
  0         0  
42 0         0 assert_tuning();
43 0         0 [ sort chordcompare @chordnames ];
44             }
45              
46             # Chord order ordinals, for sorting.
47             my %chordorderkey; {
48             my $ord = 0;
49             for ( split( ' ', "C C# Db D D# Eb E F F# Gb G G# Ab A A# Bb B" ) ) {
50             $chordorderkey{$_} = $ord;
51             $ord += 2;
52             }
53             }
54              
55             # Compare routine for chord names.
56             # API: Used by: Song.
57             sub chordcompare {
58 0     0 0 0 my ( $a0, $arest ) = $a =~ /^([A-G][b#]?)(.*)/;
59 0         0 my ( $b0, $brest ) = $b =~ /^([A-G][b#]?)(.*)/;
60 0   0     0 $a0 = $chordorderkey{$a0//"\x{ff}"}//return 0;
      0        
61 0   0     0 $b0 = $chordorderkey{$b0//"\x{ff}"}//return 0;
      0        
62 0 0       0 return $a0 <=> $b0 if $a0 != $b0;
63 0 0       0 $a0++ if $arest =~ /^m(?:in)?(?!aj)/;
64 0 0       0 $b0++ if $brest =~ /^m(?:in)?(?!aj)/;
65 0         0 for ( $arest, $brest ) {
66 0         0 s/11/:/; # sort 11 after 9
67 0         0 s/13/;/; # sort 13 after 11
68 0         0 s/\((.*?)\)/$1/g; # ignore parens
69 0         0 s/\+/aug/; # sort + as aug
70             }
71 0 0       0 $a0 <=> $b0 || $arest cmp $brest;
72             }
73             # Dump a textual list of chord definitions.
74             # Should be handled by the ChordPro backend?
75              
76 0     0 0 0 sub list_chords ( $chords, $origin, $hdr ) {
  0         0  
  0         0  
  0         0  
  0         0  
77 0         0 assert_tuning();
78 0         0 my @s;
79 0 0       0 if ( $hdr ) {
80 0         0 my $t = "-" x (((@tuning - 1) * 4) + 1);
81 0         0 substr( $t, (length($t)-7)/2, 7, "strings" );
82             push( @s,
83             "# CHORD CHART",
84             "# Generated by ChordPro " . $ChordPro::VERSION,
85             "# https://www.chordpro.org",
86             "#",
87             "# " . ( " " x 35 ) . $t,
88             "# Chord" . ( " " x 35 ) .
89             join("",
90 0         0 map { sprintf("%-4s", $_) }
  0         0  
91             @tuning ),
92             );
93             }
94              
95 0         0 foreach my $chord ( @$chords ) {
96 0         0 my $info;
97 0 0       0 if ( eval{ $chord->{name} } ) {
  0 0       0  
98 0         0 $info = $chord;
99             }
100             elsif ( $origin eq "chord" ) {
101 0         0 push( @s, sprintf( "{%s: %s}", "chord", $chord ) );
102 0         0 next;
103             }
104             else {
105 0         0 $info = known_chord($chord);
106             }
107 0 0       0 next unless $info;
108             my $s = sprintf( "{%s %-15.15s base-fret %2d ".
109             "frets %s",
110             $origin eq "chord" ? "chord: " : "define:",
111             $info->{name}, $info->{base},
112 0   0     0 @{ $info->{frets} // [] }
113             ? join("",
114 0         0 map { sprintf("%-4s", $_) }
115 0 0       0 map { $_ < 0 ? "X" : $_ }
116 0 0       0 @{ $info->{frets} } )
  0 0       0  
117             : (" " x strings() ));
118             $s .= join("", " fingers ",
119 0         0 map { sprintf("%-4s", $_) }
120 0 0       0 map { $_ < 0 ? "X" : $_ }
121 0         0 @{ $info->{fingers} } )
122 0 0 0     0 if $info->{fingers} && @{ $info->{fingers} };
  0         0  
123             $s .= join("", " keys ",
124 0         0 map { sprintf("%2d", $_) }
125 0         0 @{ $info->{keys} } )
126 0 0 0     0 if $info->{keys} && @{ $info->{keys} };
  0         0  
127 0         0 $s .= "}";
128 0         0 push( @s, $s );
129             }
130 0         0 \@s;
131             }
132              
133 0     0 0 0 sub dump_chords ( $mode ) {
  0         0  
  0         0  
134 0         0 assert_tuning();
135             print( join( "\n",
136             $mode && $mode == 2
137 0         0 ? @{ json_chords(\@chordnames ) }
138 0 0 0     0 : @{ list_chords(\@chordnames, "__CLI__", 1) } ), "\n" );
  0         0  
139             }
140              
141 0     0 0 0 sub json_chords ( $chords ) {
  0         0  
  0         0  
142 0         0 assert_tuning();
143 0         0 my @s;
144              
145             push( @s, "// ChordPro instrument definition.",
146             "",
147             qq<{ "instrument" : "> .
148             ($::config->{instrument} || "Guitar, 6 strings, standard tuning") .
149             qq<",>,
150             "",
151             qq< "tuning" : [ > .
152 0   0     0 join(", ", map { qq{"$_"} } @tuning) . " ],",
  0         0  
153             "",
154             qq{ "chords" : [},
155             "",
156             );
157              
158 0         0 my $maxl = -1;
159 0         0 foreach my $chord ( @$chords ) {
160 0         0 my $t = length( $chord );
161 0 0       0 $maxl < $t and $maxl = $t;
162             }
163 0         0 $maxl += 2;
164              
165 0         0 foreach my $chord ( @$chords ) {
166 0         0 my $info;
167 0 0       0 if ( eval{ $chord->{name} } ) {
  0         0  
168 0         0 $info = $chord;
169             }
170             else {
171 0         0 $info = known_chord($chord);
172             }
173 0 0       0 next unless $info;
174              
175 0         0 my $name = '"' . $info->{name} . '"';
176             my $s = sprintf( qq[ { "name" : %-${maxl}.${maxl}s,] .
177             qq[ "base" : %2d,],
178 0         0 $name, $info->{base} );
179 0 0       0 if ( @{ $info->{frets} } ) {
  0         0  
180             $s .= qq{ "frets" : [ } .
181 0         0 join( ", ", map { sprintf("%2s", $_) } @{ $info->{frets} } ) .
  0         0  
  0         0  
182             qq{ ],};
183             }
184 0 0 0     0 if ( $info->{fingers} && @{ $info->{fingers} } ) {
  0         0  
185             $s .= qq{ "fingers" : [ } .
186 0         0 join( ", ", map { sprintf("%2s", $_) } @{ $info->{fingers} } ) .
  0         0  
  0         0  
187             qq{ ],};
188             }
189 0 0 0     0 if ( $info->{keys} && @{ $info->{keys} } ) {
  0         0  
190             $s .= qq{ "keys" : [ } .
191 0         0 join( ", ", map { sprintf("%2d", $_) } @{ $info->{keys} } ) .
  0         0  
  0         0  
192             qq{ ],};
193             }
194 0         0 chop($s);
195 0         0 $s .= " },";
196 0         0 push( @s, $s );
197             }
198 0         0 chop( $s[-1] );
199 0         0 push( @s, "", " ]," );
200 0 0       0 if ( $::config->{pdf}->{diagrams}->{vcells} ) {
201             push( @s, qq< "pdf" : { "diagrams" : { "vcells" : > .
202 0         0 $::config->{pdf}->{diagrams}->{vcells} . qq< } },> );
203             }
204 0         0 chop( $s[-1] );
205 0         0 push( @s, "}" );
206 0         0 \@s;
207             }
208              
209             ################ Section Tuning ################
210              
211             # API: Return the number of strings supported.
212             # Used by: Songbook, Output::PDF.
213 50112     50112 0 67979 sub strings () {
  50112         65783  
214 50112         133888 scalar(@tuning);
215             }
216              
217             my $parser;# = ChordPro::Chords::Parser->default;
218              
219             # API: Set tuning, discarding chords.
220             # Used by: Config.
221 199     199 0 673 sub set_tuning ( $cfg ) {
  199         533  
  199         486  
222 199   50     1185 my $t = $cfg->{tuning} // [];
223 199 50       1287 return "Invalid tuning (not array)" unless ref($t) eq "ARRAY";
224 199   50     1289 $options //= { verbose => 0 };
225              
226 199 100       1095 if ( @tuning ) {
227 118         1822 ( my $t1 = "@$t" ) =~ s/\d//g;
228 118         1184 ( my $t2 = "@tuning" ) =~ s/\d//g;
229 118 50       667 if ( $t1 ne $t2 ) {
230             warn("Tuning changed, chords flushed\n")
231 0 0       0 if $options->{verbose} > 1;
232 0         0 @chordnames = ();
233 0         0 %config_chords = ();
234             }
235             }
236             else {
237 81         344 @chordnames = ();
238 81         313 %config_chords = ();
239             }
240 199         1209 @tuning = @$t;
241              
242             # Get ordinals for tuning.
243 199         3047 my $p = ChordPro::Chords::Parser->get_parser("common");
244 199         583 $t_oct = keys %{ $p->{ns_tbl} };
  199         1034  
245 199         761 for ( @tuning ) {
246 1194 50       5385 return "Invalid tuning (should be note + octave): $_"
247             unless /(^.*?)(\d+)$/; # split off octave
248 1194   33     4365 my $n = $p->{ns_tbl}->{$1} // $p->{nf_tbl}->{$1};
249 1194 50       2897 return "Invalid tuning (unknown note): $1" unless defined $n;
250 1194         3902 push( @t_ord, $2 * $t_oct + $n );
251             }
252              
253 199         1713 assert_tuning();
254 199         797 return;
255              
256             }
257              
258             # API: Get tuning.
259             # Used by: String substitution.
260 2139     2139 0 3364 sub get_tuning () {
  2139         2959  
261 2139         2945 @{[@tuning]};
  2139         15328  
262             }
263              
264             # API: Set target parser.
265             # Used by: ChordPro.
266 7     7 0 12437 sub set_parser ( $p ) {
  7         21  
  7         12  
267              
268 7 50 33     68 $p = ChordPro::Chords::Parser->get_parser($p)
269             unless ref($p) && $p->isa('ChordPro::Chords::Parser');
270 7         22 $parser = $p;
271             warn( "Parser: ", $parser->{system}, "\n" )
272 7 50       30 if $options->{verbose} > 1;
273              
274 7         23 return;
275             }
276              
277             # Parser stack.
278              
279             my @parsers;
280              
281             # API: Reset current parser.
282             # Used by: Config.
283 210     210 0 559 sub reset_parser () {
  210         398  
284 210         622 undef $parser;
285 210         767 @parsers = ();
286             }
287              
288 72     72 0 117 sub get_parser () {
  72         113  
289 72         248 $parser;
290             }
291              
292 199     199 0 577 sub push_parser ( $p ) {
  199         757  
  199         469  
293 199 50 33     1706 $p = ChordPro::Chords::Parser->get_parser($p)
294             unless ref($p) && $p->isa('ChordPro::Chords::Parser');
295 199         968 push( @parsers, $p );
296 199         729 $parser = $p;
297             }
298              
299 199     199 0 463 sub pop_parser () {
  199         458  
300 199 50       737 Carp::croak("Parser stack underflow") unless @parsers;
301 199         5463 $parser = pop(@parsers);
302             }
303              
304             ################ Section Config & User Chords ################
305              
306 2317     2317 0 3271 sub known_chord ( $name ) {
  2317         3640  
  2317         3005  
307 2317         3207 my $info;
308 2317 100       7149 if ( ref($name) =~ /^ChordPro::Chord::/ ) {
309 1132         1963 $info = $name;
310 1132         3216 $name = $info->name;
311             }
312 2317   100     8827 my $ret = $song_chords{$name} // $config_chords{$name};
313 2317 100       9275 $ret->{_via} = $ret->{origin} . " chords", return $ret if $ret;
314 480 100       1216 return unless $info;
315              
316             # Retry agnostic. Not all can do that.
317 248         459 $name = eval { $info->agnostic };
  248         803  
318 248 100       731 return unless $name;
319 230   66     842 $ret = $song_chords{$name} // $config_chords{$name};
320 230 100       537 if ( $ret ) {
321 9         28 $ret = $info->new($ret);
322 9         37 for ( qw( name display
323             root root_canon root_mod
324             qual qual_canon ext ext_canon
325             bass bass_canon
326             system parser ) ) {
327 117 100       229 next unless defined $info->{$_};
328 99         172 $ret->{$_} = $info->{$_};
329             }
330 9         34 $ret->{_via} = "agnostic" . " " . $ret->{origin} . " chords";
331             }
332 230         1157 $ret;
333             }
334              
335 49885     49885 0 68403 sub check_chord ( $ii ) {
  49885         67754  
  49885         65226  
336             my ( $name, $base, $frets, $fingers, $keys )
337 49885         114052 = @$ii{qw(name base frets fingers keys)};
338 49885 50 66     205007 if ( $frets && @$frets && @$frets != strings() ) {
      66        
339 0         0 return scalar(@$frets) . " strings";
340             }
341 49885 50 100     146055 if ( $fingers && @$fingers && @$fingers != strings() ) {
      66        
342 0         0 return scalar(@$fingers) . " strings for fingers";
343             }
344 49885 50 33     160031 unless ( $base > 0 && $base < 24 ) {
345 0         0 return "base-fret $base out of range";
346             }
347 49885 100 100     132939 if ( $keys && @$keys ) {
348 8         19 for ( @$keys ) {
349 24 50 33     106 return "invalid key \"$_\"" unless /^\d+$/ && $_ < 24;
350             }
351             }
352 49885         93374 return;
353             }
354              
355             # API: Add a config defined chord.
356             # Used by: Config.
357 49823     49823 0 75160 sub add_config_chord ( $def ) {
  49823         73488  
  49823         64956  
358              
359 49823         76993 my $res;
360             my $name;
361              
362 49823         106837 my @extprops = qw( display format );
363              
364             # Handle alternatives.
365 49823         69531 my @names;
366 49823 50       152147 if ( $def->{name} =~ /.\|./ ) {
367 0         0 $def->{name} = [ split( /\|/, $def->{name} ) ];
368             }
369 49823 100       226446 if ( UNIVERSAL::isa( $def->{name}, 'ARRAY' ) ) {
370 1         3 $name = shift( @{ $def->{name} } );
  1         4  
371 1         2 push( @names, @{ $def->{name} } );
  1         4  
372             }
373             else {
374 49822         91264 $name = $def->{name};
375             }
376              
377             # For derived chords.
378 49823 100 66     147924 if ( $def->{copy} || $def->{"copyall"} ) {
379 30940         54620 my $src = $def->{copy};
380 30940 50       65481 if ( $def->{copyall} ) {
381 0 0       0 return "Cannot copy and copyall at the same time"
382             if $src;
383 0         0 $src = $def->{copyall};
384             }
385 30940         52918 $res = $config_chords{$src};
386 30940 50       60409 return "Cannot copy $src" unless $res;
387 30940         347178 $def = bless { %$res, %$def } => ref($res);
388 30940 50       89721 if ( $def->{copy} ) {
389 30940         85295 delete $def->{$_} for @extprops;
390             }
391             else {
392 0         0 $def->{copy} = $def->{copyall};
393             }
394             }
395 49823         99769 delete $def->{name};
396 49823   100     99830 $def->{base} ||= 1;
397              
398             my ( $base, $frets, $fingers, $keys ) =
399 49823         116208 ( $def->{base}, $def->{frets}, $def->{fingers}, $def->{keys} );
400 49823         95549 $res = check_chord($def);
401 49823 50       88378 return $res if $res;
402              
403 49823         68528 my $dpinfo;
404 49823 50       95719 if ( $def->{display} ) {
405 0         0 $dpinfo = parse_chord($def->{display});
406 0 0       0 if ( $dpinfo ) {
407 0         0 $def->{display} = $dpinfo;
408             }
409             else {
410 0         0 delete $def->{display};
411             }
412             }
413 49823         87780 for $name ( $name, @names ) {
414 49824 50       199106 next if $name =~ /^(\||\s*)$/;
415 49824   66     99467 my $info = parse_chord($name)
416             // ChordPro::Chord::Common->new({ name => $name });
417              
418 49824 100 100     138477 if ( $info->is_chord && $def->{copy} && $def->is_chord ) {
      66        
419 19873         41975 for ( qw( root bass ext qual ) ) {
420 79492         129162 delete $def->{$_};
421 79492         133293 delete $def->{$_."_mod"};
422 79492         145102 delete $def->{$_."_canon"};
423             }
424 19873         32916 for ( qw( ext qual ) ) {
425 39746         55653 delete $def->{$_};
426 39746         68302 delete $def->{$_."_canon"};
427             }
428             }
429 49824 50       120303 Carp::confess(::dump($parser)) unless $parser->{target};
430             $config_chords{$name} = bless
431             { origin => "config",
432             system => $parser->{system},
433             %$info,
434             %$def,
435             base => $base,
436             baselabeloffset => $def->{baselabeloffset}||0,
437             frets => [ $frets && @$frets ? @$frets : () ],
438             fingers => [ $fingers && @$fingers ? @$fingers : () ],
439             keys => [ $keys && @$keys ? @$keys : () ]
440 49824 50 50     1376348 } => $parser->{target};
    50 33        
    50 66        
      66        
441 49824         169160 push( @chordnames, $name );
442              
443             # Also store the chord info under a neutral name so it can be
444             # found when other note name systems are used.
445 49824         75004 my $i;
446 49824 100       120577 if ( $info->is_chord ) {
447 38318         94518 $i = $info->agnostic;
448             }
449             else {
450             # Retry with default parser.
451 11506         33596 $i = ChordPro::Chords::Parser->default->parse($name);
452 11506 50 33     31420 if ( $i && $i->is_chord ) {
453 0         0 $info->{root_ord} = $i->{root_ord};
454             $config_chords{$name}->{$_} = $i->{$_}
455 0         0 for qw( root_ord root_mod ext_canon qual_canon );
456 0         0 $i = $i->agnostic;
457             }
458             }
459 49824 100       125076 if ( $info->is_chord ) {
460 38318         340870 $config_chords{$i} = $config_chords{$name};
461 38318         155940 $config_chords{$i}->{origin} = "config";
462             }
463             }
464 49823         195625 return;
465             }
466              
467             # API: Add a user defined chord.
468             # Used by: Song.
469 62     62 0 101 sub add_song_chord ( $ii ) {
  62         109  
  62         89  
470              
471 62 50       185 return if $ii->name =~ /^(\||\s*)$/;
472              
473 62         202 my $res = check_chord($ii);
474 62 50       199 return $res if $res;
475              
476             # Need a parser anyway.
477 62   33     163 $parser //= ChordPro::Chords::Parser->get_parser;
478              
479             my $c =
480             { system => $parser->{system},
481 62         773 parser => $parser,
482             %$ii,
483             };
484 62   50     229 $c->{origin} //= "user";
485              
486             # Cleanup.
487 62         154 for ( qw( display ) ) {
488 62 100       199 delete $c->{$_} unless defined $c->{$_};
489             }
490 62         135 for ( qw( frets fingers keys ) ) {
491 186 100 100     457 delete $c->{$_} unless $c->{$_} && @{ $c->{$_} };
  104         340  
492             }
493              
494 62         230 $song_chords{$c->{name}} = bless $c => $parser->{target};
495 62         185 return;
496             }
497              
498             # API: Add an unknown chord.
499             # Used by: Song.
500 0     0 0 0 sub add_unknown_chord ( $name ) {
  0         0  
  0         0  
501 0   0     0 $parser //= ChordPro::Chords::Parser->get_parser;
502             $song_chords{$name} = bless
503             { origin => "user",
504             name => $name,
505             base => 0,
506             frets => [],
507             fingers => [],
508             keys => []
509 0         0 } => $parser->{target};
510             }
511              
512             # API: Reset user defined songs. Should be done for each new song.
513             # Used by: Songbook, Output::PDF.
514 172     172 0 432 sub reset_song_chords () {
  172         312  
515 172         1290 %song_chords = ();
516             }
517              
518             # API: Return some chord statistics.
519 0     0 0 0 sub chord_stats () {
  0         0  
520 0         0 my $res = sprintf( "%d config chords", scalar(keys(%config_chords)) );
521 0 0       0 $res .= sprintf( ", %d song chords", scalar(keys(%song_chords)) )
522             if %song_chords;
523 0         0 return $res;
524             }
525              
526             ################ Section Chords Parser ################
527              
528 53309     53309 0 4043861 sub parse_chord ( $chord ) {
  53309         84251  
  53309         67621  
529              
530 53309   66     104063 $parser //= ChordPro::Chords::Parser->get_parser;
531 53309         148684 return $parser->parse($chord);
532             }
533              
534             ################ Section Keyboard keys ################
535              
536             my %keys =
537             ( "" => [ 0, 4, 7 ], # major
538             "-" => [ 0, 3, 7 ], # minor
539             "7" => [ 0, 4, 7, 10 ], # dominant 7th
540             "-7" => [ 0, 3, 7, 10 ], # minor seventh
541             "maj7" => [ 0, 4, 7, 11 ], # major 7th
542             "-maj7" => [ 0, 3, 7, 11 ], # minor major 7th
543             "6" => [ 0, 4, 7, 9 ], # 6th
544             "-6" => [ 0, 3, 7, 9 ], # minor 6th
545             "6add9" => [ 0, 4, 7, 9, 14], # 6/9
546             "5" => [ 0, 7 ], # 6th
547             "9" => [ 0, 4, 7, 10, 14 ], # 9th
548             "-9" => [ 0, 3, 7, 10, 14 ], # minor 9th
549             "maj9" => [ 0, 4, 7, 11, 14 ], # major 9th
550             "11" => [ 0, 4, 7, 10, 14, 17 ], # 11th
551             "-11" => [ 0, 3, 7, 10, 14, 17 ], # minor 11th
552             "13" => [ 0, 4, 7, 10, 14, 17, 21 ], # 13th
553             "-13" => [ 0, 3, 7, 10, 14, 17, 21 ], # minor 13th
554             "maj13" => [ 0, 4, 7, 11, 14, 21 ], # major 13th
555             "add2" => [ 0, 2, 4, 7 ], # add 2
556             "add9" => [ 0, 4, 7, 14 ], # add 9
557             "-add2" => [ 0, 2, 3, 7 ], # minor add 2
558             "-add9" => [ 0, 2, 3, 7, 11 ], # minor add 9
559             "-add11" => [ 0, 3, 5, 7, 11 ], # minor add 11
560             "7-5" => [ 0, 4, 6, 10 ], # 7 flat 5 altered chord
561             "7+5" => [ 0, 4, 8, 10 ], # 7 sharp 5 altered chord
562             "sus4" => [ 0, 5, 7 ], # sus 4
563             "sus2" => [ 0, 2, 7 ], # sus 2
564             "7sus2" => [ 0, 2, 7, 10 ], # 7 sus 2
565             "7sus4" => [ 0, 5, 7, 10 ], # 7 sus 4
566             "-7sus2" => [ 0, 2, 3, 7, 10 ], # minor 7 sus 2
567             "-7sus4" => [ 0, 3, 5, 7, 10 ], # minor 7 sus 4
568             "0" => [ 0, 3, 6 ], # diminished
569             "07" => [ 0, 3, 6, 9 ], # diminished 7
570             "-7b5" => [ 0, 3, 6, 10 ], # minor 7 flat 5
571             "+" => [ 0, 4, 8 ], # augmented
572             "+7" => [ 0, 4, 8, 10 ], # augmented 7
573             "h" => [ 0, 3, 6, 10 ], # half-diminished seventh
574             );
575              
576 0     0 0 0 sub get_keys ( $info ) {
  0         0  
  0         0  
577              
578             # Has keys defined.
579 0 0 0     0 return $info->{keys} if $info->{keys} && @{$info->{keys}};
  0         0  
580              
581 0         0 my @keys;
582              
583 0 0 0     0 if ( defined $info->{qual_canon}
      0        
584             && defined $info->{ext_canon}
585             && defined $keys{$info->{qual_canon}.$info->{ext_canon}} ) {
586             # Known chord extension.
587 0         0 @keys = @{ $keys{$info->{qual_canon}.$info->{ext_canon}} };
  0         0  
588             }
589             else {
590             # Try to derive from guitar chord.
591 0 0 0     0 return [] unless $info->{frets} && @{$info->{frets}};
  0         0  
592              
593             # Get ordinals for tuning.
594 0         0 my @t_ord = map { $_ % $t_oct } @t_ord;
  0         0  
595              
596 0         0 my %keys;
597 0         0 my $i = -1;
598 0         0 my $base = $info->{base} - 1;
599 0 0       0 $base = 0 if $base < 0;
600 0         0 for ( @{ $info->{frets} } ) {
  0         0  
601 0         0 $i++;
602 0 0       0 next if $_ < 0;
603 0         0 my $c = $t_ord[$i] + $_ + $base;
604 0 0       0 if ( $info->{root_ord} ) {
605 0 0       0 $c += $t_oct if $c < $info->{root_ord};
606 0         0 $c -= $info->{root_ord};
607             }
608 0         0 $keys{ $c % $t_oct }++;
609             }
610 0         0 @keys = sort keys %keys;
611             }
612              
613 0 0 0     0 if ( defined $info->{bass} && $info->{bass} ne '' ) {
614             # Handle inversions.
615 0         0 my @k;
616 0         0 my $bass = $info->{bass_ord} - $info->{root_ord};
617 0         0 my $oct = 12; # yes
618 0 0       0 $bass += $oct if $bass < 0;
619 0         0 for ( @keys ) {
620 0 0       0 next if $_ == $bass;
621 0 0       0 push( @k, $_ < $bass ? $_+$oct : $_ );
622             }
623 0         0 unshift( @k, $bass );
624 0         0 @keys = @k;
625             }
626 0         0 \@keys;
627             }
628              
629             ################ Section Transposition ################
630              
631             # API: Transpose a chord.
632             # Used by: Songbook.
633 123     123 0 191 sub transpose ( $c, $xpose, $xcode = "" ) {
  123         222  
  123         184  
  123         206  
  123         169  
634 123 50 33     310 return $c unless $xpose || $xcode;
635 123 50       274 return $c if $c =~ /^ .+/;
636 123         270 my $info = parse_chord($c);
637 123 50       363 unless ( $info ) {
638 0         0 assert_tuning();
639 0         0 for ( \%song_chords, \%config_chords ) {
640             # Not sure what this is for...
641             # Anyway, it causes unknown but {defined} chords to silently
642             # bypass the trans* warnings.
643             # return if exists($_->{$c});
644             }
645             $xpose
646 0 0       0 ? warn("Cannot transpose $c\n")
647             : warn("Cannot transcode $c\n");
648 0         0 return;
649             }
650              
651 123         394 my $res = $info->transcode($xcode)->transpose($xpose)->canonical;
652              
653             # Carp::cluck("__XPOSE = ", $xpose, " __XCODE = $xcode, chord $c => $res\n");
654              
655 123         3605 return $res;
656             }
657              
658             1;