File Coverage

blib/lib/Music/Cadence.pm
Criterion Covered Total %
statement 162 162 100.0
branch 75 92 81.5
condition 31 38 81.5
subroutine 15 15 100.0
pod 2 2 100.0
total 285 309 92.2


line stmt bran cond sub pod time code
1             package Music::Cadence;
2             our $AUTHORITY = 'cpan:GENE';
3              
4             # ABSTRACT: Generate musical cadence chords
5              
6             our $VERSION = '0.1511';
7              
8 3     3   1082016 use Moo;
  3         30165  
  3         21  
9 3     3   7652 use strictures 2;
  3         6302  
  3         141  
10 3     3   3564 use Data::Dumper::Compact qw(ddc);
  3         56508  
  3         20  
11 3     3   440 use List::Util qw(any);
  3         9  
  3         307  
12 3     3   2021 use Music::Chord::Note ();
  3         6203  
  3         92  
13 3     3   1541 use Music::Chord::Positions ();
  3         6475  
  3         74  
14 3     3   1438 use Music::Note ();
  3         5674  
  3         93  
15 3     3   1414 use Music::Scales qw(get_scale_notes);
  3         15805  
  3         240  
16 3     3   1604 use Music::ToRoman ();
  3         172811  
  3         125  
17 3     3   25 use namespace::clean;
  3         6  
  3         26  
18              
19             with('Music::PitchNum');
20              
21              
22             has verbose => (
23             is => 'ro',
24             default => sub { 0 },
25             );
26              
27              
28             has key => (
29             is => 'ro',
30             default => sub { 'C' },
31             );
32              
33              
34             has scale => (
35             is => 'ro',
36             default => sub { 'major' },
37             );
38              
39              
40             has octave => (
41             is => 'ro',
42             default => sub { 0 },
43             );
44              
45              
46             has format => (
47             is => 'ro',
48             default => sub { 'isobase' },
49             );
50              
51              
52             has seven => (
53             is => 'ro',
54             default => sub { 0 },
55             );
56              
57              
58             has picardy => (
59             is => 'ro',
60             default => sub { 0 },
61             );
62              
63              
64             sub cadence {
65 60     60 1 87705 my ( $self, %args ) = @_;
66              
67 60         190 my $cadence = [];
68              
69 60   66     426 my $key = $args{key} || $self->key;
70 60   66     324 my $scale = $args{scale} || $self->scale;
71 60   100     281 my $octave = $args{octave} // $self->octave;
72 60   66     250 my $picardy = $args{picardy} || $self->picardy;
73 60   100     207 my $type = $args{type} || 'perfect';
74 60   100     251 my $leading = $args{leading} || 1;
75 60   100     231 my $variation = $args{variation} || 1;
76 60   100     206 my $inversion = $args{inversion} || 0;
77              
78 60 100 66     273 die 'unknown leader' if $leading < 1 or $leading > 7;
79              
80 59         316 my @scale_notes = get_scale_notes( $key, $scale );
81              
82 59 100 100     14074 if ( $type eq 'perfect' ) {
    100 66        
    100          
    100          
    100          
    100          
    100          
83 15         66 my $chord = $self->_generate_chord( $key, $scale, $scale_notes[4], $octave );
84 12         36 push @$cadence, $chord;
85              
86 12         41 $chord = $self->_generate_chord( $key, $scale, $scale_notes[0], $octave );
87             # Add another top note, but an octave above
88 12         35 my $top = $chord->[0];
89 12 100       42 if ( $self->format eq 'midinum' ) {
90 6         33 $top += 12;
91             }
92             else {
93 6 100       36 if ( $top =~ /^(.+?)(\d+)$/ ) {
94 4         20 my $note = $1;
95 4         13 my $octave = $2;
96 4         13 $top = $note . ++$octave;
97             }
98             }
99 12 50       72 print ddc($top) if $self->verbose;
100 12         32 push @$chord, $top;
101 12 50       36 print ddc($chord) if $self->verbose;
102 12         28 push @$cadence, $chord;
103             }
104             elsif ( $type eq 'imperfect' && $inversion ) {
105 16         72 my $chord = $self->_generate_chord( $key, $scale, $scale_notes[4], $octave );
106             $chord = $self->_invert_chord( $chord, $inversion->{1}, $octave )
107 16 50       141 if $inversion->{1};
108 16         52 push @$cadence, $chord;
109              
110 16         52 $chord = $self->_generate_chord( $key, $scale, $scale_notes[0], $octave );
111             $chord = $self->_invert_chord( $chord, $inversion->{2}, $octave )
112 16 100       82 if $inversion->{2};
113 16         40 push @$cadence, $chord;
114             }
115             elsif ( $type eq 'imperfect' ) {
116 4 100       17 my $note = $variation == 1 ? $scale_notes[4] : $scale_notes[6];
117 4         17 my $chord = $self->_generate_chord( $key, $scale, $note, $octave );
118 4 50       21 print ddc($chord) if $self->verbose;
119 4         10 push @$cadence, $chord;
120              
121 4         17 $chord = $self->_generate_chord( $key, $scale, $scale_notes[0], $octave );
122 4 50       19 print ddc($chord) if $self->verbose;
123 4         14 push @$cadence, $chord;
124             }
125             elsif ( $type eq 'evaded' && $self->seven ) {
126 2 100       7 if ( $inversion ) {
127             $inversion->{1} = 3
128 1 50       4 unless defined $inversion->{1};
129             $inversion->{2} = 1
130 1 50       3 unless defined $inversion->{2};
131             }
132             else {
133 1         4 $inversion = { 1 => 3, 2 => 1 };
134             }
135              
136 2         8 my $chord = $self->_generate_chord( $key, $scale, $scale_notes[4], $octave );
137 2         8 $chord = $self->_invert_chord( $chord, $inversion->{1}, $octave );
138 2 50       11 print ddc($chord) if $self->verbose;
139 2         5 push @$cadence, $chord;
140              
141 2         7 $chord = $self->_generate_chord( $key, $scale, $scale_notes[0], $octave );
142 2         6 $chord = $self->_invert_chord( $chord, $inversion->{2}, $octave );
143 2 50       10 print ddc($chord) if $self->verbose;
144 2         4 push @$cadence, $chord;
145             }
146             elsif ( $type eq 'plagal' ) {
147 4         19 my $chord = $self->_generate_chord( $key, $scale, $scale_notes[3], $octave );
148 4 50       17 print ddc($chord) if $self->verbose;
149 4         12 push @$cadence, $chord;
150              
151 4         16 $chord = $self->_generate_chord( $key, $scale, $scale_notes[0], $octave );
152 4 50       21 print ddc($chord) if $self->verbose;
153 4         11 push @$cadence, $chord;
154             }
155             elsif ( $type eq 'half' ) {
156 13         58 my $chord = $self->_generate_chord( $key, $scale, $scale_notes[ $leading - 1 ], $octave );
157             $chord = $self->_invert_chord( $chord, $inversion->{1}, $octave )
158 13 50 66     65 if $inversion && $inversion->{1};
159 13 50       46 print ddc($chord) if $self->verbose;
160 13         29 push @$cadence, $chord;
161              
162 13         61 $chord = $self->_generate_chord( $key, $scale, $scale_notes[4], $octave );
163             $chord = $self->_invert_chord( $chord, $inversion->{2}, $octave )
164 13 50 66     51 if $inversion && $inversion->{2};
165 13 50       72 print ddc($chord) if $self->verbose;
166 13         37 push @$cadence, $chord;
167             }
168             elsif ( $type eq 'deceptive' ) {
169 4         21 my $chord = $self->_generate_chord( $key, $scale, $scale_notes[4], $octave );
170 4 50       23 print ddc($chord) if $self->verbose;
171 4         14 push @$cadence, $chord;
172              
173 4 100       16 my $note = $variation == 1 ? $scale_notes[5] : $scale_notes[3];
174 4         17 $chord = $self->_generate_chord( $key, $scale, $note, $octave );
175 4 50       54 print ddc($chord) if $self->verbose;
176 4         14 push @$cadence, $chord;
177             }
178             else {
179 1         40 die 'unknown cadence';
180             }
181              
182 55 100       153 if ( $picardy ) {
183 3 100       13 if ( $self->format eq 'midinum' ) {
184 1         4 $cadence->[1][1]++;
185             }
186             else {
187 2         21 my $note = Music::Note->new( $cadence->[1][1], $self->format );
188 2         119 my $num = $note->format('midinum');
189 2         111 $num++;
190 2         8 $note = Music::Note->new( $num, 'midinum' );
191 2         98 $cadence->[1][1] = $note->format( $self->format );
192             }
193             }
194              
195 55         508 return $cadence;
196             }
197              
198             sub _invert_chord {
199 32     32   88 my ( $self, $chord, $inversion, $octave ) = @_;
200              
201 32         149 my $mcp = Music::Chord::Positions->new;
202              
203 32 100       564 if ( $self->format eq 'midinum' ) {
204 2         6 $chord = $mcp->chord_inv( $chord, inv_num => $inversion );
205             }
206             else { # Perform these gymnastics to convert named notes to inverted named notes:
207             # Strip the octave if present
208 30 100       105 $chord = [ map { s/\d+//; $_ } @$chord ]
  64         207  
  64         156  
209             if $octave;
210              
211             # Convert the chord into pitch-class representation
212 30         99 my $pitches = [ map { $self->pitchnum( $_ . -1 ) } @$chord ];
  106         5033  
213              
214             # Do the inversion!
215 30         1891 $pitches = $mcp->chord_inv( $pitches, inv_num => $inversion );
216              
217             # Convert the pitch-classes back to named notes
218 30         1760 $chord = [ map { $self->pitchname($_) } @$pitches ];
  106         1213  
219              
220             # Clean-up the chord
221 30         447 for ( @$chord ) {
222 106 100       203 if ( $octave ) {
223 64         173 s/-1/$octave/;
224 64         150 s/0/$octave + 1/e;
  23         67  
225             }
226             else {
227 42         112 s/-1//;
228 42         97 s/0//;
229             }
230              
231 106 100       330 if ( $self->format eq 'midi' ) {
232 12         26 s/#/s/;
233 12         31 s/b/f/;
234             }
235             }
236             }
237              
238 32         256 return $chord;
239             }
240              
241             sub _generate_chord {
242 113     113   361 my ( $self, $key, $scale, $note, $octave ) = @_;
243              
244             # Know what chords should be diminished
245 113         829 my %diminished = (
246             ionian => 'vii',
247             major => 'vii',
248             dorian => 'vi',
249             phrygian => 'v',
250             lydian => 'iv',
251             mixolydian => 'iii',
252             aeolian => 'ii',
253             minor => 'ii',
254             locrian => 'i',
255             );
256              
257 113 100       393 die 'unknown scale' unless exists $diminished{$scale};
258              
259 112         4405 my $mtr = Music::ToRoman->new(
260             scale_note => $key,
261             scale_name => $scale,
262             chords => 0,
263             );
264              
265             # Figure out if the chord is diminished, minor, or major
266 111         49437 my $roman = $mtr->parse($note);
267 111 100       44986 my $type = $roman =~ /^$diminished{$scale}$/ ? 'dim' : $roman =~ /^[a-z]/ ? 'm' : '';
    100          
268              
269 111 100       485 $type .= 7
270             if $self->seven;
271              
272 111         537 my $mcn = Music::Chord::Note->new;
273              
274             # Get the notes of the chord (without an octave)
275 111         1044 my @notes = $mcn->chord( $note . $type );
276              
277 111 100       5646 if ( $self->format eq 'midi' ) {
    100          
    100          
278             # Convert the sharps and flats
279 8         21 for ( @notes ) {
280 26         69 s/#/s/;
281 26         50 s/b/f/;
282             }
283             }
284             elsif ( $self->format eq 'midinum' ) {
285             # Convert the notes to midinum format
286 14         38 @notes = map { $self->pitchnum( $_ . $octave ) } @notes;
  48         2018  
287             }
288             elsif ( $self->format ne 'isobase' ) {
289 1         49 die 'unknown format';
290             }
291              
292             # Append the octave if defined and the format is not midinum
293 110 100 100     1155 @notes = map { $_ . $octave } @notes
  92         267  
294             if $octave && $self->format ne 'midinum';
295              
296 110         865 return \@notes;
297             }
298              
299              
300             sub remove_notes {
301 4     4 1 3224 my ($self, $indices, $chord) = @_;
302 4         9 my @chord;
303 4         18 for my $n (0 .. @$chord - 1) {
304 12 100   11   60 next if any { $n == $_ } @$indices;
  11         32  
305 9         38 push @chord, $chord->[$n];
306             }
307 4         14 return \@chord;
308             }
309              
310             1;
311              
312             __END__