File Coverage

lib/ChordPro/Chords.pm
Criterion Covered Total %
statement 238 374 63.6
branch 82 170 48.2
condition 66 127 51.9
subroutine 24 31 77.4
pod 0 25 0.0
total 410 727 56.4


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 90     90   1432 use v5.26;
  90         384  
11 90     90   636 use utf8;
  90         189  
  90         798  
12 90     90   3209 use Carp;
  90         183  
  90         7664  
13 90     90   675 use feature qw( signatures );
  90         188  
  90         14683  
14 90     90   644 no warnings "experimental::signatures";
  90         191  
  90         5149  
15              
16 90     90   58329 use ChordPro::Chords::Parser;
  90         433  
  90         806381  
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 208     208 0 558 sub assert_tuning () {
  208         443  
34 208 50       915 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 7 50   7 0 27 ( $a, $b ) = @_ if @_;
59 7         29 my ( $a0, $arest ) = $a =~ /^([A-G][b#]?)(.*)/;
60 7         22 my ( $b0, $brest ) = $b =~ /^([A-G][b#]?)(.*)/;
61 7   50     26 $a0 = $chordorderkey{$a0//"\x{ff}"}//return 0;
      50        
62 7   50     22 $b0 = $chordorderkey{$b0//"\x{ff}"}//return 0;
      50        
63 7 50       47 return $a0 <=> $b0 if $a0 != $b0;
64 0 0       0 $a0++ if $arest =~ /^m(?:in)?(?!aj)/;
65 0 0       0 $b0++ if $brest =~ /^m(?:in)?(?!aj)/;
66 0         0 for ( $arest, $brest ) {
67 0         0 s/11/:/; # sort 11 after 9
68 0         0 s/13/;/; # sort 13 after 11
69 0         0 s/\((.*?)\)/$1/g; # ignore parens
70 0         0 s/\+/aug/; # sort + as aug
71             }
72 0 0       0 $a0 <=> $b0 || $arest cmp $brest;
73             }
74             # Dump a textual list of chord definitions.
75             # Should be handled by the ChordPro backend?
76              
77 0     0 0 0 sub list_chords ( $chords, $origin, $hdr ) {
  0         0  
  0         0  
  0         0  
  0         0  
78 0         0 assert_tuning();
79 0         0 my @s;
80 0 0       0 if ( $hdr ) {
81 0         0 my $t = "-" x (((@tuning - 1) * 4) + 1);
82 0         0 substr( $t, (length($t)-7)/2, 7, "strings" );
83             push( @s,
84             "# CHORD CHART",
85             "# Generated by ChordPro " . $ChordPro::VERSION,
86             "# https://www.chordpro.org",
87             "#",
88             "# " . ( " " x 35 ) . $t,
89             "# Chord" . ( " " x 35 ) .
90             join("",
91 0         0 map { sprintf("%-4s", $_) }
  0         0  
92             @tuning ),
93             );
94             }
95              
96 0         0 foreach my $chord ( @$chords ) {
97 0         0 my $info;
98 0 0       0 if ( eval{ $chord->{name} } ) {
  0 0       0  
99 0         0 $info = $chord;
100             }
101             elsif ( $origin eq "chord" ) {
102 0         0 push( @s, sprintf( "{%s: %s}", "chord", $chord ) );
103 0         0 next;
104             }
105             else {
106 0         0 $info = known_chord($chord);
107             }
108 0 0       0 next unless $info;
109             my $s = sprintf( "{%s %-15.15s base-fret %2d ".
110             "frets %s",
111             $origin eq "chord" ? "chord: " : "define:",
112             $info->{name}, $info->{base},
113 0   0     0 @{ $info->{frets} // [] }
114             ? join("",
115 0         0 map { sprintf("%-4s", $_) }
116 0 0       0 map { $_ < 0 ? "X" : $_ }
117 0 0       0 @{ $info->{frets} } )
  0 0       0  
118             : (" " x strings() ));
119             $s .= join("", " fingers ",
120 0         0 map { sprintf("%-4s", $_) }
121 0 0       0 map { $_ < 0 ? "X" : $_ }
122 0         0 @{ $info->{fingers} } )
123 0 0 0     0 if $info->{fingers} && @{ $info->{fingers} };
  0         0  
124             $s .= join("", " keys ",
125 0         0 map { sprintf("%2d", $_) }
126 0         0 @{ $info->{keys} } )
127 0 0 0     0 if $info->{keys} && @{ $info->{keys} };
  0         0  
128 0         0 $s .= "}";
129 0         0 push( @s, $s );
130             }
131 0         0 \@s;
132             }
133              
134 0     0 0 0 sub dump_chords ( $mode ) {
  0         0  
  0         0  
135 0         0 assert_tuning();
136             print( join( "\n",
137             $mode && $mode == 2
138 0         0 ? @{ json_chords(\@chordnames ) }
139 0 0 0     0 : @{ list_chords(\@chordnames, "__CLI__", 1) } ), "\n" );
  0         0  
140             }
141              
142 0     0 0 0 sub json_chords ( $chords ) {
  0         0  
  0         0  
143 0         0 assert_tuning();
144 0         0 my @s;
145              
146             push( @s, "// ChordPro instrument definition.",
147             "",
148             qq<{ "instrument" : "> .
149             ($::config->{instrument} || "Guitar, 6 strings, standard tuning") .
150             qq<",>,
151             "",
152             qq< "tuning" : [ > .
153 0   0     0 join(", ", map { qq{"$_"} } @tuning) . " ],",
  0         0  
154             "",
155             qq{ "chords" : [},
156             "",
157             );
158              
159 0         0 my $maxl = -1;
160 0         0 foreach my $chord ( @$chords ) {
161 0         0 my $t = length( $chord );
162 0 0       0 $maxl < $t and $maxl = $t;
163             }
164 0         0 $maxl += 2;
165              
166 0         0 foreach my $chord ( @$chords ) {
167 0         0 my $info;
168 0 0       0 if ( eval{ $chord->{name} } ) {
  0         0  
169 0         0 $info = $chord;
170             }
171             else {
172 0         0 $info = known_chord($chord);
173             }
174 0 0       0 next unless $info;
175              
176 0         0 my $name = '"' . $info->{name} . '"';
177             my $s = sprintf( qq[ { "name" : %-${maxl}.${maxl}s,] .
178             qq[ "base" : %2d,],
179 0         0 $name, $info->{base} );
180 0 0       0 if ( @{ $info->{frets} } ) {
  0         0  
181             $s .= qq{ "frets" : [ } .
182 0         0 join( ", ", map { sprintf("%2s", $_) } @{ $info->{frets} } ) .
  0         0  
  0         0  
183             qq{ ],};
184             }
185 0 0 0     0 if ( $info->{fingers} && @{ $info->{fingers} } ) {
  0         0  
186             $s .= qq{ "fingers" : [ } .
187 0         0 join( ", ", map { sprintf("%2s", $_) } @{ $info->{fingers} } ) .
  0         0  
  0         0  
188             qq{ ],};
189             }
190 0 0 0     0 if ( $info->{keys} && @{ $info->{keys} } ) {
  0         0  
191             $s .= qq{ "keys" : [ } .
192 0         0 join( ", ", map { sprintf("%2d", $_) } @{ $info->{keys} } ) .
  0         0  
  0         0  
193             qq{ ],};
194             }
195 0         0 chop($s);
196 0         0 $s .= " },";
197 0         0 push( @s, $s );
198             }
199 0         0 chop( $s[-1] );
200 0         0 push( @s, "", " ]," );
201 0 0       0 if ( $::config->{pdf}->{diagrams}->{vcells} ) {
202             push( @s, qq< "pdf" : { "diagrams" : { "vcells" : > .
203 0         0 $::config->{pdf}->{diagrams}->{vcells} . qq< } },> );
204             }
205 0         0 chop( $s[-1] );
206 0         0 push( @s, "}" );
207 0         0 \@s;
208             }
209              
210             ################ Section Tuning ################
211              
212             # API: Return the number of strings supported.
213             # Used by: Songbook, Output::PDF.
214 50019     50019 0 77661 sub strings () {
  50019         73893  
215 50019         179046 scalar(@tuning);
216             }
217              
218             my $parser;# = ChordPro::Chords::Parser->default;
219              
220             # API: Set tuning, discarding chords.
221             # Used by: Config.
222 208     208 0 558 sub set_tuning ( $cfg ) {
  208         486  
  208         522  
223 208   50     1076 my $t = $cfg->{tuning} // [];
224 208 50       1316 return "Invalid tuning (not array)" unless ref($t) eq "ARRAY";
225 208   50     966 $options //= { verbose => 0 };
226              
227 208 100       904 if ( @tuning ) {
228 118         2278 ( my $t1 = "@$t" ) =~ s/\d//g;
229 118         1417 ( my $t2 = "@tuning" ) =~ s/\d//g;
230 118 50       546 if ( $t1 ne $t2 ) {
231             warn("Tuning changed, chords flushed\n")
232 0 0       0 if $options->{verbose} > 1;
233 0         0 @chordnames = ();
234 0         0 %config_chords = ();
235             }
236             }
237             else {
238 90         253 @chordnames = ();
239 90         293 %config_chords = ();
240             }
241 208         1234 @tuning = @$t;
242              
243             # Get ordinals for tuning.
244 208         3115 my $p = ChordPro::Chords::Parser->get_parser("common");
245 208         475 $t_oct = keys %{ $p->{ns_tbl} };
  208         1052  
246 208         906 for ( @tuning ) {
247 1248 50       5653 return "Invalid tuning (should be note + octave): $_"
248             unless /(^.*?)(\d+)$/; # split off octave
249 1248   33     6090 my $n = $p->{ns_tbl}->{$1} // $p->{nf_tbl}->{$1};
250 1248 50       2736 return "Invalid tuning (unknown note): $1" unless defined $n;
251 1248         6016 push( @t_ord, $2 * $t_oct + $n );
252             }
253              
254 208         1110 assert_tuning();
255 208         892 return;
256              
257             }
258              
259             # API: Get tuning.
260             # Used by: String substitution.
261 2406     2406 0 4657 sub get_tuning () {
  2406         3902  
262 2406         3706 @{[@tuning]};
  2406         24663  
263             }
264              
265             # API: Set target parser.
266             # Used by: ChordPro.
267 7     7 0 620919 sub set_parser ( $p ) {
  7         24  
  7         17  
268              
269 7 50 33     95 $p = ChordPro::Chords::Parser->get_parser($p)
270             unless ref($p) && $p->isa('ChordPro::Chords::Parser');
271 7         21 $parser = $p;
272             warn( "Parser: ", $parser->{system}, "\n" )
273 7 50       37 if $options->{verbose} > 1;
274              
275 7         28 return;
276             }
277              
278             # Parser stack.
279              
280             my @parsers;
281              
282             # API: Reset current parser.
283             # Used by: Config.
284 219     219 0 529 sub reset_parser () {
  219         497  
285 219         588 undef $parser;
286 219         659 @parsers = ();
287             }
288              
289 3     3 0 11 sub get_parser () {
  3         8  
290 3         15 $parser;
291             }
292              
293 208     208 0 568 sub push_parser ( $p ) {
  208         687  
  208         476  
294 208 50 33     2911 $p = ChordPro::Chords::Parser->get_parser($p)
295             unless ref($p) && $p->isa('ChordPro::Chords::Parser');
296 208         725 push( @parsers, $p );
297 208         867 $parser = $p;
298             }
299              
300 208     208 0 544 sub pop_parser () {
  208         443  
301 208 50       846 Carp::croak("Parser stack underflow") unless @parsers;
302 208         927 $parser = pop(@parsers);
303             }
304              
305             ################ Section Config & User Chords ################
306              
307 2425     2425 0 4072 sub known_chord ( $name ) {
  2425         4649  
  2425         3597  
308 2425         3808 my $info;
309 2425 100       7898 if ( ref($name) =~ /^ChordPro::Chord::/ ) {
310 1173         1990 $info = $name;
311 1173         6388 $name = $info->name;
312             }
313 2425   100     11277 my $ret = $song_chords{$name} // $config_chords{$name};
314 2425 100       10804 $ret->{_via} = $ret->{origin} . " chords", return $ret if $ret;
315 592 100       1896 return unless $info;
316              
317             # Retry agnostic. Not all can do that.
318 295         669 $name = eval { $info->agnostic };
  295         1673  
319 295 100       994 return unless $name;
320 277   66     1232 $ret = $song_chords{$name} // $config_chords{$name};
321 277 100       679 if ( $ret ) {
322 9         32 $ret = $info->new($ret);
323 9         33 for ( qw( name display
324             root root_canon root_mod
325             qual qual_canon ext ext_canon
326             bass bass_canon
327             system parser ) ) {
328 117 100       249 next unless defined $info->{$_};
329 99         200 $ret->{$_} = $info->{$_};
330             }
331 9         33 $ret->{_via} = "agnostic" . " " . $ret->{origin} . " chords";
332             }
333 277         1997 $ret;
334             }
335              
336 49895     49895 0 73631 sub check_chord ( $ii ) {
  49895         79213  
  49895         71886  
337             my ( $name, $base, $frets, $fingers, $keys )
338 49895         168946 = @$ii{qw(name base frets fingers keys)};
339 49895 50 66     262208 if ( $frets && @$frets && @$frets != strings() ) {
      66        
340 0         0 return scalar(@$frets) . " strings";
341             }
342 49895 50 100     198375 if ( $fingers && @$fingers && @$fingers != strings() ) {
      66        
343 0         0 return scalar(@$fingers) . " strings for fingers";
344             }
345 49895 50 33     198478 unless ( $base > 0 && $base < 24 ) {
346 0         0 return "base-fret $base out of range";
347             }
348 49895 100 100     166328 if ( $keys && @$keys ) {
349 12         30 for ( @$keys ) {
350 39 50 33     260 return "invalid key \"$_\"" unless /^\d+$/ && $_ < 24;
351             }
352             }
353 49895         117016 return;
354             }
355              
356             # API: Access the chords table.
357             # Used by: Utils.
358 0     0 0 0 sub config_chords { \%config_chords }
359              
360             # API: Add a config defined chord.
361             # Used by: Config.
362 49832     49832 0 83275 sub add_config_chord ( $def ) {
  49832         81001  
  49832         71804  
363              
364 49832         94997 my $res;
365             my $name;
366              
367 49832         124360 my @extprops = qw( display format );
368              
369             # Handle alternatives.
370 49832         76765 my @names;
371 49832 50       223144 if ( $def->{name} =~ /.\|./ ) {
372 0         0 $def->{name} = [ split( /\|/, $def->{name} ) ];
373             }
374 49832 100       319115 if ( UNIVERSAL::isa( $def->{name}, 'ARRAY' ) ) {
375 1         4 $name = shift( @{ $def->{name} } );
  1         4  
376 1         3 push( @names, @{ $def->{name} } );
  1         5  
377             }
378             else {
379 49831         104109 $name = $def->{name};
380             }
381              
382             # For derived chords.
383 49832 100 66     200232 if ( $def->{copy} || $def->{"copyall"} ) {
384 30940         65578 my $src = $def->{copy};
385 30940 50       89312 if ( $def->{copyall} ) {
386 0 0       0 return "Cannot copy and copyall at the same time"
387             if $src;
388 0         0 $src = $def->{copyall};
389             }
390 30940         66393 $res = $config_chords{$src};
391 30940 50       70523 return "Cannot copy $src" unless $res;
392 30940 50       71082 if ( $def->{copy} ) {
393 30940         442687 my $r = { %$res };
394 30940         139331 delete $r->{$_} for @extprops;
395 30940         484139 $def = bless { %$r, %$def } => ref($res);
396             }
397             else {
398 0         0 $def = bless { %$res, %$def } => ref($res);
399 0         0 $def->{copy} = $def->{copyall};
400             }
401             }
402 49832         148422 delete $def->{name};
403 49832   100     151021 $def->{base} ||= 1;
404              
405             my ( $base, $frets, $fingers, $keys ) =
406 49832         172141 ( $def->{base}, $def->{frets}, $def->{fingers}, $def->{keys} );
407 49832         126157 $res = check_chord($def);
408 49832 50       117985 return $res if $res;
409              
410 49832         78646 my $dpinfo;
411 49832 50       124724 if ( $def->{display} ) {
412 0         0 $dpinfo = parse_chord($def->{display});
413 0 0       0 if ( $dpinfo ) {
414 0         0 $def->{display} = $dpinfo;
415             }
416             else {
417 0         0 delete $def->{display};
418             }
419             }
420 49832         110505 for $name ( $name, @names ) {
421 49833 50       269457 next if $name =~ /^(\||\s*)$/;
422 49833   66     120864 my $info = parse_chord($name)
423             // ChordPro::Chord::Common->new({ name => $name });
424              
425 49833 100 100     174177 if ( $info->is_chord && $def->{copy} && $def->is_chord ) {
      66        
426 19873         46309 for ( qw( root bass ext qual ) ) {
427 79492         151147 delete $def->{$_};
428 79492         143687 delete $def->{$_."_mod"};
429 79492         162859 delete $def->{$_."_canon"};
430             }
431 19873         35307 for ( qw( ext qual ) ) {
432 39746         62035 delete $def->{$_};
433 39746         76000 delete $def->{$_."_canon"};
434             }
435             }
436 49833 50       153374 Carp::confess(::dump($parser)) unless $parser->{target};
437             $config_chords{$name} = bless
438             { origin => "config",
439             system => $parser->{system},
440             %$info,
441             %$def,
442             base => $base,
443             baselabeloffset => $def->{baselabeloffset}||0,
444 298998 50       2034728 frets => [ $frets && @$frets ? map { $_ eq 'x' ? -1 : $_ } @$frets : () ],
445             fingers => [ $fingers && @$fingers ? @$fingers : () ],
446             keys => [ $keys && @$keys ? @$keys : () ]
447 49833 50 50     556725 } => $parser->{target};
    50 33        
    50 66        
      66        
448 49833         230460 push( @chordnames, $name );
449              
450             # Also store the chord info under a neutral name so it can be
451             # found when other note name systems are used.
452 49833         82131 my $i;
453 49833 100       150849 if ( $info->is_chord ) {
454 38318         123830 $i = $info->agnostic;
455             }
456             else {
457             # Retry with default parser.
458 11515         44119 $i = ChordPro::Chords::Parser->default->parse($name);
459 11515 50 33     40110 if ( $i && $i->is_chord ) {
460 0         0 $info->{root_ord} = $i->{root_ord};
461             $config_chords{$name}->{$_} = $i->{$_}
462 0         0 for qw( root_ord root_mod ext_canon qual_canon );
463 0         0 $i = $i->agnostic;
464             }
465             }
466 49833 100       144599 if ( $info->is_chord ) {
467 38318         289727 $config_chords{$i} = $config_chords{$name};
468 38318         238513 $config_chords{$i}->{origin} = "config";
469             }
470             }
471 49832         306608 return;
472             }
473              
474             # API: Add a user defined chord.
475             # Used by: Song.
476 63     63 0 133 sub add_song_chord ( $ii ) {
  63         119  
  63         110  
477              
478 63 50       269 return if $ii->name =~ /^(\||\s*)$/;
479              
480 63         287 my $res = check_chord($ii);
481 63 50       182 return $res if $res;
482              
483             # Need a parser anyway.
484 63   33     193 $parser //= ChordPro::Chords::Parser->get_parser;
485              
486             my $c =
487             { system => $parser->{system},
488 63         1058 parser => $parser,
489             %$ii,
490             };
491 63   50     268 $c->{origin} //= "user";
492              
493             # Cleanup.
494 63         173 for ( qw( display ) ) {
495 63 100       250 delete $c->{$_} unless defined $c->{$_};
496             }
497 63         143 for ( qw( frets fingers keys ) ) {
498 189 100 100     540 delete $c->{$_} unless $c->{$_} && @{ $c->{$_} };
  104         369  
499             }
500              
501 63         355 $song_chords{$c->{name}} = bless $c => $parser->{target};
502 63         245 return;
503             }
504              
505             # API: Add an unknown chord.
506             # Used by: Song.
507 0     0 0 0 sub add_unknown_chord ( $name ) {
  0         0  
  0         0  
508 0   0     0 $parser //= ChordPro::Chords::Parser->get_parser;
509             $song_chords{$name} = bless
510             { origin => "user",
511             name => $name,
512             base => 0,
513             frets => [],
514             fingers => [],
515             keys => []
516 0         0 } => $parser->{target};
517             }
518              
519             # API: Reset user defined songs. Should be done for each new song.
520             # Used by: Songbook, Output::PDF.
521 216     216 0 698 sub reset_song_chords () {
  216         453  
522 216         2024 %song_chords = ();
523             }
524              
525             # API: Return some chord statistics.
526 0     0 0 0 sub chord_stats () {
  0         0  
527 0         0 my $res = sprintf( "%d config chords", scalar(keys(%config_chords)) );
528 0 0       0 $res .= sprintf( ", %d song chords", scalar(keys(%song_chords)) )
529             if %song_chords;
530 0         0 return $res;
531             }
532              
533             ################ Section Chords Parser ################
534              
535 53375     53375 0 5401602 sub parse_chord ( $chord ) {
  53375         91900  
  53375         78056  
536              
537 53375   66     134378 $parser //= ChordPro::Chords::Parser->get_parser;
538 53375         216359 return $parser->parse($chord);
539             }
540              
541             ################ Section Keyboard keys ################
542              
543             my %keys =
544             ( "" => [ 0, 4, 7 ], # major
545             "-" => [ 0, 3, 7 ], # minor
546             "7" => [ 0, 4, 7, 10 ], # dominant 7th
547             "-7" => [ 0, 3, 7, 10 ], # minor seventh
548             "maj7" => [ 0, 4, 7, 11 ], # major 7th
549             "-maj7" => [ 0, 3, 7, 11 ], # minor major 7th
550             "6" => [ 0, 4, 7, 9 ], # 6th
551             "-6" => [ 0, 3, 7, 9 ], # minor 6th
552             "6add9" => [ 0, 4, 7, 9, 14], # 6/9
553             "5" => [ 0, 7 ], # 6th
554             "9" => [ 0, 4, 7, 10, 14 ], # 9th
555             "-9" => [ 0, 3, 7, 10, 14 ], # minor 9th
556             "maj9" => [ 0, 4, 7, 11, 14 ], # major 9th
557             "11" => [ 0, 4, 7, 10, 14, 17 ], # 11th
558             "-11" => [ 0, 3, 7, 10, 14, 17 ], # minor 11th
559             "13" => [ 0, 4, 7, 10, 14, 17, 21 ], # 13th
560             "-13" => [ 0, 3, 7, 10, 14, 17, 21 ], # minor 13th
561             "maj13" => [ 0, 4, 7, 11, 14, 21 ], # major 13th
562             "add2" => [ 0, 2, 4, 7 ], # add 2
563             "add9" => [ 0, 4, 7, 14 ], # add 9
564             "-add2" => [ 0, 2, 3, 7 ], # minor add 2
565             "-add9" => [ 0, 2, 3, 7, 11 ], # minor add 9
566             "-add11" => [ 0, 3, 5, 7, 11 ], # minor add 11
567             "7-5" => [ 0, 4, 6, 10 ], # 7 flat 5 altered chord
568             "7+5" => [ 0, 4, 8, 10 ], # 7 sharp 5 altered chord
569             "sus4" => [ 0, 5, 7 ], # sus 4
570             "sus2" => [ 0, 2, 7 ], # sus 2
571             "7sus2" => [ 0, 2, 7, 10 ], # 7 sus 2
572             "7sus4" => [ 0, 5, 7, 10 ], # 7 sus 4
573             "-7sus2" => [ 0, 2, 3, 7, 10 ], # minor 7 sus 2
574             "-7sus4" => [ 0, 3, 5, 7, 10 ], # minor 7 sus 4
575             "0" => [ 0, 3, 6 ], # diminished
576             "07" => [ 0, 3, 6, 9 ], # diminished 7
577             "-7b5" => [ 0, 3, 6, 10 ], # minor 7 flat 5
578             "+" => [ 0, 4, 8 ], # augmented
579             "+7" => [ 0, 4, 8, 10 ], # augmented 7
580             "h" => [ 0, 3, 6, 10 ], # half-diminished seventh
581             );
582              
583 538     538 0 1011 sub get_keys ( $info ) {
  538         1000  
  538         841  
584              
585             # Has keys defined.
586 538 50 66     1731 return $info->{keys} if $info->{keys} && @{$info->{keys}};
  296         1033  
587              
588 538         1030 my @keys;
589              
590 538 100 66     5118 if ( defined $info->{qual_canon}
      100        
591             && defined $info->{ext_canon}
592             && defined $keys{$info->{qual_canon}.$info->{ext_canon}} ) {
593             # Known chord extension.
594 514         1028 @keys = @{ $keys{$info->{qual_canon}.$info->{ext_canon}} };
  514         2333  
595             }
596             else {
597             # Try to derive from guitar chord.
598 24 100 66     102 return [] unless $info->{frets} && @{$info->{frets}};
  22         85  
599              
600             # Get ordinals for tuning.
601 22         63 my @t_ord = map { $_ % $t_oct } @t_ord;
  600         1138  
602              
603 22         52 my %keys;
604 22         44 my $i = -1;
605 22         82 my $base = $info->{base} - 1;
606 22 50       88 $base = 0 if $base < 0;
607 22         41 for ( @{ $info->{frets} } ) {
  22         69  
608 132         221 $i++;
609 132 100       304 next if $_ < 0;
610 48         88 my $c = $t_ord[$i] + $_ + $base;
611 48 100       111 if ( $info->{root_ord} ) {
612 18 50       41 $c += $t_oct if $c < $info->{root_ord};
613 18         30 $c -= $info->{root_ord};
614             }
615 48         132 $keys{ $c % $t_oct }++;
616             }
617 22         146 @keys = sort keys %keys;
618             }
619              
620 536 100 100     2975 if ( defined $info->{bass} && $info->{bass} ne '' ) {
621             # Handle inversions.
622 10         21 my @k;
623 10         34 my $bass = $info->{bass_ord} - $info->{root_ord};
624 10         24 my $oct = 12; # yes
625 10 100       37 $bass += $oct if $bass < 0;
626 10         26 for ( @keys ) {
627 31 100       75 next if $_ == $bass;
628 21 100       67 push( @k, $_ < $bass ? $_+$oct : $_ );
629             }
630 10         24 unshift( @k, $bass );
631 10         29 @keys = @k;
632             }
633 536         2930 \@keys;
634             }
635              
636             ################ Section Transposition ################
637              
638             # API: Transpose a chord.
639             # Used by: Songbook.
640 123     123 0 257 sub transpose ( $c, $xpose, $xcode = "" ) {
  123         305  
  123         254  
  123         263  
  123         198  
641 123 50 33     511 return $c unless $xpose || $xcode;
642 123 50       409 return $c if $c =~ /^ .+/;
643 123         397 my $info = parse_chord($c);
644 123 50       418 unless ( $info ) {
645 0         0 assert_tuning();
646 0         0 for ( \%song_chords, \%config_chords ) {
647             # Not sure what this is for...
648             # Anyway, it causes unknown but {defined} chords to silently
649             # bypass the trans* warnings.
650             # return if exists($_->{$c});
651             }
652             $xpose
653 0 0       0 ? warn("Cannot transpose $c\n")
654             : warn("Cannot transcode $c\n");
655 0         0 return;
656             }
657              
658 123         574 my $res = $info->transcode($xcode)->transpose($xpose)->canonical;
659              
660             # Carp::cluck("__XPOSE = ", $xpose, " __XCODE = $xcode, chord $c => $res\n");
661              
662 123         6988 return $res;
663             }
664              
665             1;