File Coverage

blib/lib/ChordPro/Chords.pm
Criterion Covered Total %
statement 184 346 53.1
branch 58 154 37.6
condition 49 121 40.5
subroutine 22 30 73.3
pod 0 24 0.0
total 313 675 46.3


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   1043 use v5.26;
  79         318  
11 79     79   522 use utf8;
  79         164  
  79         450  
12 79     79   1754 use Carp;
  79         184  
  79         4360  
13 79     79   524 use feature qw( signatures );
  79         160  
  79         5993  
14 79     79   503 no warnings "experimental::signatures";
  79         166  
  79         3508  
15              
16 79     79   41949 use ChordPro::Chords::Parser;
  79         259  
  79         442201  
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 197     197 0 548 sub assert_tuning () {
  197         398  
32 197 50       922 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: Song.
55             sub chordcompare {
56 0     0 0 0 my ( $a0, $arest ) = $a =~ /^([A-G][b#]?)(.*)/;
57 0         0 my ( $b0, $brest ) = $b =~ /^([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 0     0 0 0 sub list_chords ( $chords, $origin, $hdr ) {
  0         0  
  0         0  
  0         0  
  0         0  
75 0         0 assert_tuning();
76 0         0 my @s;
77 0 0       0 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 0         0 foreach my $chord ( @$chords ) {
94 0         0 my $info;
95 0 0       0 if ( eval{ $chord->{name} } ) {
  0 0       0  
96 0         0 $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 0         0 $info = known_chord($chord);
104             }
105 0 0       0 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 0   0     0 @{ $info->{frets} // [] }
111             ? join("",
112 0         0 map { sprintf("%-4s", $_) }
113 0 0       0 map { $_ < 0 ? "X" : $_ }
114 0 0       0 @{ $info->{frets} } )
  0 0       0  
115             : (" " x strings() ));
116             $s .= join("", " fingers ",
117 0         0 map { sprintf("%-4s", $_) }
118 0 0       0 map { $_ < 0 ? "X" : $_ }
119 0         0 @{ $info->{fingers} } )
120 0 0 0     0 if $info->{fingers} && @{ $info->{fingers} };
  0         0  
121             $s .= join("", " keys ",
122 0         0 map { sprintf("%2d", $_) }
123 0         0 @{ $info->{keys} } )
124 0 0 0     0 if $info->{keys} && @{ $info->{keys} };
  0         0  
125 0         0 $s .= "}";
126 0         0 push( @s, $s );
127             }
128 0         0 \@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 73605 sub strings () {
  50110         64484  
212 50110         141523 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 490 sub set_tuning ( $cfg ) {
  197         470  
  197         510  
220 197   50     971 my $t = $cfg->{tuning} // [];
221 197 50       1019 return "Invalid tuning (not array)" unless ref($t) eq "ARRAY";
222 197   50     1388 $options //= { verbose => 0 };
223              
224 197 100       1015 if ( @tuning ) {
225 118         1760 ( my $t1 = "@$t" ) =~ s/\d//g;
226 118         1076 ( my $t2 = "@tuning" ) =~ s/\d//g;
227 118 50       589 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         317 @chordnames = ();
236 79         365 %config_chords = ();
237             }
238 197         1327 @tuning = @$t; # need more checks
239 197         1188 assert_tuning();
240 197         718 return;
241              
242             }
243              
244             # API: Get tuning.
245             # Used by: String substitution.
246 2115     2115 0 3346 sub get_tuning () {
  2115         2877  
247 2115         2924 @{[@tuning]};
  2115         17128  
248             }
249              
250             # API: Set target parser.
251             # Used by: ChordPro.
252 7     7 0 11858 sub set_parser ( $p ) {
  7         27  
  7         15  
253              
254 7 50 33     70 $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       33 if $options->{verbose} > 1;
259              
260 7         24 return;
261             }
262              
263             # Parser stack.
264              
265             my @parsers;
266              
267             # API: Reset current parser.
268             # Used by: Config.
269 208     208 0 494 sub reset_parser () {
  208         464  
270 208         627 undef $parser;
271 208         648 @parsers = ();
272             }
273              
274 72     72 0 129 sub get_parser () {
  72         104  
275 72         234 $parser;
276             }
277              
278 197     197 0 575 sub push_parser ( $p ) {
  197         685  
  197         402  
279 197 50 33     2110 $p = ChordPro::Chords::Parser->get_parser($p)
280             unless ref($p) && $p->isa('ChordPro::Chords::Parser');
281 197         724 push( @parsers, $p );
282 197         613 $parser = $p;
283             }
284              
285 197     197 0 548 sub pop_parser () {
  197         402  
286 197 50       748 Carp::croak("Parser stack underflow") unless @parsers;
287 197         5501 $parser = pop(@parsers);
288             }
289              
290             ################ Section Config & User Chords ################
291              
292 2317     2317 0 3289 sub known_chord ( $name ) {
  2317         3589  
  2317         3396  
293 2317         3105 my $info;
294 2317 100       7181 if ( ref($name) =~ /^ChordPro::Chord::/ ) {
295 1132         1942 $info = $name;
296 1132         3234 $name = $info->name;
297             }
298 2317   100     9163 my $ret = $song_chords{$name} // $config_chords{$name};
299 2317 100       9357 $ret->{_via} = $ret->{origin} . " chords", return $ret if $ret;
300 480 100       1207 return unless $info;
301              
302             # Retry agnostic. Not all can do that.
303 248         438 $name = eval { $info->agnostic };
  248         840  
304 248 100       711 return unless $name;
305 230   66     854 $ret = $song_chords{$name} // $config_chords{$name};
306 230 100       527 if ( $ret ) {
307 9         27 $ret = $info->new($ret);
308 9         37 for ( qw( name display
309             root root_canon root_mod
310             bass bass_canon
311             system parser ) ) {
312 81 100       164 next unless defined $info->{$_};
313 63         125 $ret->{$_} = $info->{$_};
314             }
315 9         36 $ret->{_via} = "agnostic" . " " . $ret->{origin} . " chords";
316             }
317 230         1189 $ret;
318             }
319              
320 49883     49883 0 70445 sub check_chord ( $ii ) {
  49883         72959  
  49883         64751  
321             my ( $name, $base, $frets, $fingers, $keys )
322 49883         112974 = @$ii{qw(name base frets fingers keys)};
323 49883 50 66     206940 if ( $frets && @$frets && @$frets != strings() ) {
      66        
324 0         0 return scalar(@$frets) . " strings";
325             }
326 49883 50 100     142133 if ( $fingers && @$fingers && @$fingers != strings() ) {
      66        
327 0         0 return scalar(@$fingers) . " strings for fingers";
328             }
329 49883 50 33     156880 unless ( $base > 0 && $base < 24 ) {
330 0         0 return "base-fret $base out of range";
331             }
332 49883 100 100     131260 if ( $keys && @$keys ) {
333 8         20 for ( @$keys ) {
334 24 50 33     112 return "invalid key \"$_\"" unless /^\d+$/ && $_ < 24;
335             }
336             }
337 49883         92288 return;
338             }
339              
340             # API: Add a config defined chord.
341             # Used by: Config.
342 49821     49821 0 71511 sub add_config_chord ( $def ) {
  49821         73447  
  49821         64803  
343              
344 49821         78058 my $res;
345             my $name;
346              
347 49821         109948 my @extprops = qw( display format );
348              
349             # Handle alternatives.
350 49821         69034 my @names;
351 49821 50       155344 if ( $def->{name} =~ /.\|./ ) {
352 0         0 $def->{name} = [ split( /\|/, $def->{name} ) ];
353             }
354 49821 100       219329 if ( UNIVERSAL::isa( $def->{name}, 'ARRAY' ) ) {
355 1         2 $name = shift( @{ $def->{name} } );
  1         4  
356 1         3 push( @names, @{ $def->{name} } );
  1         4  
357             }
358             else {
359 49820         90925 $name = $def->{name};
360             }
361              
362             # For derived chords.
363 49821 100 66     147382 if ( $def->{copy} || $def->{"copyall"} ) {
364 30940         53179 my $src = $def->{copy};
365 30940 50       63659 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         54278 $res = $config_chords{$src};
371 30940 50       60418 return "Cannot copy $src" unless $res;
372 30940         342090 $def = bless { %$res, %$def } => ref($res);
373 30940 50       89325 if ( $def->{copy} ) {
374 30940         85152 delete $def->{$_} for @extprops;
375             }
376             else {
377 0         0 $def->{copy} = $def->{copyall};
378             }
379             }
380 49821         96695 delete $def->{name};
381 49821   100     100601 $def->{base} ||= 1;
382              
383             my ( $base, $frets, $fingers, $keys ) =
384 49821         113661 ( $def->{base}, $def->{frets}, $def->{fingers}, $def->{keys} );
385 49821         96795 $res = check_chord($def);
386 49821 50       91747 return $res if $res;
387              
388 49821         68340 my $dpinfo;
389 49821 50       95115 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         91613 for $name ( $name, @names ) {
399 49822 50       202554 next if $name =~ /^(\||\s*)$/;
400 49822   66     102524 my $info = parse_chord($name)
401             // ChordPro::Chord::Common->new({ name => $name });
402              
403 49822 100 100     142745 if ( $info->is_chord && $def->{copy} && $def->is_chord ) {
      66        
404 19873         41462 for ( qw( root bass ext qual ) ) {
405 79492         128889 delete $def->{$_};
406 79492         134591 delete $def->{$_."_mod"};
407 79492         145227 delete $def->{$_."_canon"};
408             }
409 19873         32203 for ( qw( ext qual ) ) {
410 39746         56408 delete $def->{$_};
411 39746         67238 delete $def->{$_."_canon"};
412             }
413             }
414 49822 50       120945 Carp::confess(::dump($parser)) unless $parser->{target};
415             $config_chords{$name} = bless
416             { origin => "config",
417             system => $parser->{system},
418             %$info,
419             %$def,
420             base => $base,
421             baselabeloffset => $def->{baselabeloffset}||0,
422             frets => [ $frets && @$frets ? @$frets : () ],
423             fingers => [ $fingers && @$fingers ? @$fingers : () ],
424             keys => [ $keys && @$keys ? @$keys : () ]
425 49822 50 50     1304362 } => $parser->{target};
    50 33        
    50 66        
      66        
426 49822         164136 push( @chordnames, $name );
427              
428             # Also store the chord info under a neutral name so it can be
429             # found when other note name systems are used.
430 49822         74766 my $i;
431 49822 100       123686 if ( $info->is_chord ) {
432 38318         94200 $i = $info->agnostic;
433             }
434             else {
435             # Retry with default parser.
436 11504         31694 $i = ChordPro::Chords::Parser->default->parse($name);
437 11504 50 33     30981 if ( $i && $i->is_chord ) {
438 0         0 $info->{root_ord} = $i->{root_ord};
439             $config_chords{$name}->{$_} = $i->{$_}
440 0         0 for qw( root_ord root_mod ext_canon qual_canon );
441 0         0 $i = $i->agnostic;
442             }
443             }
444 49822 100       127121 if ( $info->is_chord ) {
445 38318         305633 $config_chords{$i} = $config_chords{$name};
446 38318         158556 $config_chords{$i}->{origin} = "config";
447             }
448             }
449 49821         195187 return;
450             }
451              
452             # API: Add a user defined chord.
453             # Used by: Song.
454 62     62 0 96 sub add_song_chord ( $ii ) {
  62         104  
  62         89  
455              
456 62 50       155 return if $ii->name =~ /^(\||\s*)$/;
457              
458 62         197 my $res = check_chord($ii);
459 62 50       152 return $res if $res;
460              
461             # Need a parser anyway.
462 62   33     151 $parser //= ChordPro::Chords::Parser->get_parser;
463              
464             my $c =
465             { system => $parser->{system},
466 62         731 parser => $parser,
467             %$ii,
468             };
469 62   50     233 $c->{origin} //= "user";
470              
471             # Cleanup.
472 62         140 for ( qw( display ) ) {
473 62 100       213 delete $c->{$_} unless defined $c->{$_};
474             }
475 62         130 for ( qw( frets fingers keys ) ) {
476 186 100 100     473 delete $c->{$_} unless $c->{$_} && @{ $c->{$_} };
  104         330  
477             }
478              
479 62         244 $song_chords{$c->{name}} = bless $c => $parser->{target};
480 62         175 return;
481             }
482              
483             # API: Add an unknown chord.
484             # Used by: Song.
485 0     0 0 0 sub add_unknown_chord ( $name ) {
  0         0  
  0         0  
486 0   0     0 $parser //= ChordPro::Chords::Parser->get_parser;
487             $song_chords{$name} = bless
488             { origin => "user",
489             name => $name,
490             base => 0,
491             frets => [],
492             fingers => [],
493             keys => []
494 0         0 } => $parser->{target};
495             }
496              
497             # API: Reset user defined songs. Should be done for each new song.
498             # Used by: Songbook, Output::PDF.
499 171     171 0 392 sub reset_song_chords () {
  171         309  
500 171         1312 %song_chords = ();
501             }
502              
503             # API: Return some chord statistics.
504 0     0 0 0 sub chord_stats () {
  0         0  
505 0         0 my $res = sprintf( "%d config chords", scalar(keys(%config_chords)) );
506 0 0       0 $res .= sprintf( ", %d song chords", scalar(keys(%song_chords)) )
507             if %song_chords;
508 0         0 return $res;
509             }
510              
511             ################ Section Chords Parser ################
512              
513 53307     53307 0 3911443 sub parse_chord ( $chord ) {
  53307         84869  
  53307         68359  
514              
515 53307   66     107450 $parser //= ChordPro::Chords::Parser->get_parser;
516 53307         151341 return $parser->parse($chord);
517             }
518              
519             ################ Section Keyboard keys ################
520              
521             my %keys =
522             ( "" => [ 0, 4, 7 ], # major
523             "-" => [ 0, 3, 7 ], # minor
524             "7" => [ 0, 4, 7, 10 ], # dominant 7th
525             "-7" => [ 0, 3, 7, 10 ], # minor seventh
526             "maj7" => [ 0, 4, 7, 11 ], # major 7th
527             "-maj7" => [ 0, 3, 7, 11 ], # minor major 7th
528             "6" => [ 0, 4, 7, 9 ], # 6th
529             "-6" => [ 0, 3, 7, 9 ], # minor 6th
530             "6add9" => [ 0, 4, 7, 9, 14], # 6/9
531             "5" => [ 0, 7 ], # 6th
532             "9" => [ 0, 4, 7, 10, 14 ], # 9th
533             "-9" => [ 0, 3, 7, 10, 14 ], # minor 9th
534             "maj9" => [ 0, 4, 7, 11, 14 ], # major 9th
535             "11" => [ 0, 4, 7, 10, 14, 17 ], # 11th
536             "-11" => [ 0, 3, 7, 10, 14, 17 ], # minor 11th
537             "13" => [ 0, 4, 7, 10, 14, 17, 21 ], # 13th
538             "-13" => [ 0, 3, 7, 10, 14, 17, 21 ], # minor 13th
539             "maj13" => [ 0, 4, 7, 11, 14, 21 ], # major 13th
540             "add2" => [ 0, 2, 4, 7 ], # add 2
541             "add9" => [ 0, 4, 7, 14 ], # add 9
542             "-add2" => [ 0, 2, 3, 7 ], # minor add 2
543             "-add9" => [ 0, 2, 3, 7, 11 ], # minor add 9
544             "-add11" => [ 0, 3, 5, 7, 11 ], # minor add 11
545             "7-5" => [ 0, 4, 6, 10 ], # 7 flat 5 altered chord
546             "7+5" => [ 0, 4, 8, 10 ], # 7 sharp 5 altered chord
547             "sus4" => [ 0, 5, 7 ], # sus 4
548             "sus2" => [ 0, 2, 7 ], # sus 2
549             "7sus2" => [ 0, 2, 7, 10 ], # 7 sus 2
550             "7sus4" => [ 0, 5, 7, 10 ], # 7 sus 4
551             "-7sus2" => [ 0, 2, 3, 7, 10 ], # minor 7 sus 2
552             "-7sus4" => [ 0, 3, 5, 7, 10 ], # minor 7 sus 4
553             "0" => [ 0, 3, 6 ], # diminished
554             "07" => [ 0, 3, 6, 9 ], # diminished 7
555             "-7b5" => [ 0, 3, 6, 10 ], # minor 7 flat 5
556             "+" => [ 0, 4, 8 ], # augmented
557             "+7" => [ 0, 4, 8, 10 ], # augmented 7
558             "h" => [ 0, 3, 6, 10 ], # half-diminished seventh
559             );
560              
561 0     0 0 0 sub get_keys ( $info ) {
  0         0  
  0         0  
562             # ::dump( { %$info, parser => ref($info->{parser}) });
563             # Has keys defined.
564 0 0 0     0 return $info->{keys} if $info->{keys} && @{$info->{keys}};
  0         0  
565              
566             # Known chords.
567             return $keys{$info->{qual_canon}.$info->{ext_canon}}
568             if defined $info->{qual_canon}
569             && defined $info->{ext_canon}
570 0 0 0     0 && defined $keys{$info->{qual_canon}.$info->{ext_canon}};
      0        
571              
572             # Try to derive from guitar chords.
573 0 0 0     0 return [] unless $info->{frets} && @{$info->{frets}};
  0         0  
574 0         0 my @tuning = ( 4, 9, 2, 7, 11, 4 );
575 0         0 my %keys;
576 0         0 my $i = -1;
577 0         0 my $base = $info->{base} - 1;
578 0 0       0 $base = 0 if $base < 0;
579 0         0 for ( @{ $info->{frets} } ) {
  0         0  
580 0         0 $i++;
581 0 0       0 next if $_ < 0;
582 0         0 my $c = $tuning[$i] + $_ + $base;
583 0 0       0 if ( $info->{root_ord} ) {
584 0 0       0 $c += 12 if $c < $info->{root_ord};
585 0         0 $c -= $info->{root_ord};
586             }
587 0         0 $keys{ $c % 12 }++;
588             }
589 0         0 return [ keys %keys ];
590             }
591              
592             ################ Section Transposition ################
593              
594             # API: Transpose a chord.
595             # Used by: Songbook.
596 123     123 0 197 sub transpose ( $c, $xpose, $xcode = "" ) {
  123         218  
  123         190  
  123         204  
  123         194  
597 123 50 33     299 return $c unless $xpose || $xcode;
598 123 50       273 return $c if $c =~ /^ .+/;
599 123         462 my $info = parse_chord($c);
600 123 50       336 unless ( $info ) {
601 0         0 assert_tuning();
602 0         0 for ( \%song_chords, \%config_chords ) {
603             # Not sure what this is for...
604             # Anyway, it causes unknown but {defined} chords to silently
605             # bypass the trans* warnings.
606             # return if exists($_->{$c});
607             }
608             $xpose
609 0 0       0 ? warn("Cannot transpose $c\n")
610             : warn("Cannot transcode $c\n");
611 0         0 return;
612             }
613              
614 123         406 my $res = $info->transcode($xcode)->transpose($xpose)->canonical;
615              
616             # Carp::cluck("__XPOSE = ", $xpose, " __XCODE = $xcode, chord $c => $res\n");
617              
618 123         3422 return $res;
619             }
620              
621             1;