File Coverage

blib/lib/Music/Cadence.pm
Criterion Covered Total %
statement 147 147 100.0
branch 63 68 92.6
condition 31 38 81.5
subroutine 14 14 100.0
pod 2 2 100.0
total 257 269 95.5


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.1505';
7              
8 2     2   1264 use List::Util 'any';
  2         4  
  2         174  
9 2     2   693 use Music::Chord::Note;
  2         1677  
  2         54  
10 2     2   728 use Music::Chord::Positions;
  2         2879  
  2         54  
11 2     2   694 use Music::Note;
  2         2458  
  2         55  
12 2     2   696 use Music::Scales;
  2         7689  
  2         164  
13 2     2   889 use Music::ToRoman;
  2         104462  
  2         66  
14              
15 2     2   15 use Moo;
  2         5  
  2         7  
16 2     2   636 use strictures 2;
  2         20  
  2         75  
17 2     2   400 use namespace::clean;
  2         4  
  2         15  
18              
19             with('Music::PitchNum');
20              
21              
22             has key => (
23             is => 'ro',
24             default => sub { 'C' },
25             );
26              
27              
28             has scale => (
29             is => 'ro',
30             default => sub { 'major' },
31             );
32              
33              
34             has octave => (
35             is => 'ro',
36             default => sub { 0 },
37             );
38              
39              
40             has format => (
41             is => 'ro',
42             default => sub { 'isobase' },
43             );
44              
45              
46             has seven => (
47             is => 'ro',
48             default => sub { 0 },
49             );
50              
51              
52             has picardy => (
53             is => 'ro',
54             default => sub { 0 },
55             );
56              
57              
58             sub cadence {
59 60     60 1 40871 my ( $self, %args ) = @_;
60              
61 60         136 my $cadence = [];
62              
63 60   66     362 my $key = $args{key} || $self->key;
64 60   66     317 my $scale = $args{scale} || $self->scale;
65 60   100     291 my $octave = $args{octave} // $self->octave;
66 60   66     257 my $picardy = $args{picardy} || $self->picardy;
67 60   100     166 my $type = $args{type} || 'perfect';
68 60   100     166 my $leading = $args{leading} || 1;
69 60   100     179 my $variation = $args{variation} || 1;
70 60   100     174 my $inversion = $args{inversion} || 0;
71              
72 60 100 66     230 die 'unknown leader' if $leading < 1 or $leading > 7;
73              
74 59         197 my @scale_notes = get_scale_notes( $key, $scale );
75              
76 59 100 100     8622 if ( $type eq 'perfect' ) {
    100 66        
    100          
    100          
    100          
    100          
    100          
77 15         55 my $chord = $self->_generate_chord( $key, $scale, $scale_notes[4], $octave );
78 12         28 push @$cadence, $chord;
79              
80 12         33 $chord = $self->_generate_chord( $key, $scale, $scale_notes[0], $octave );
81             # Add another top note, but an octave above
82 12         26 my $top = $chord->[0];
83 12 100       38 if ( $self->format eq 'midinum' ) {
84 6         15 $top += 12;
85             }
86             else {
87 6 100       28 if ( $top =~ /^(.+?)(\d+)$/ ) {
88 4         11 my $note = $1;
89 4         11 my $octave = $2;
90 4         11 $top = $note . ++$octave;
91             }
92             }
93 12         29 push @$chord, $top;
94 12         23 push @$cadence, $chord;
95             }
96             elsif ( $type eq 'imperfect' && $inversion ) {
97 16         61 my $chord = $self->_generate_chord( $key, $scale, $scale_notes[4], $octave );
98             $chord = $self->_invert_chord( $chord, $inversion->{1}, $octave )
99 16 50       89 if $inversion->{1};
100 16         36 push @$cadence, $chord;
101              
102 16         37 $chord = $self->_generate_chord( $key, $scale, $scale_notes[0], $octave );
103             $chord = $self->_invert_chord( $chord, $inversion->{2}, $octave )
104 16 100       66 if $inversion->{2};
105 16         37 push @$cadence, $chord;
106             }
107             elsif ( $type eq 'imperfect' ) {
108 4 100       16 my $note = $variation == 1 ? $scale_notes[4] : $scale_notes[6];
109 4         15 my $chord = $self->_generate_chord( $key, $scale, $note, $octave );
110 4         10 push @$cadence, $chord;
111              
112 4         12 $chord = $self->_generate_chord( $key, $scale, $scale_notes[0], $octave );
113 4         9 push @$cadence, $chord;
114             }
115             elsif ( $type eq 'evaded' && $self->seven ) {
116 2 100       8 if ( $inversion ) {
117             $inversion->{1} = 3
118 1 50       3 unless defined $inversion->{1};
119             $inversion->{2} = 1
120 1 50       3 unless defined $inversion->{2};
121             }
122             else {
123 1         3 $inversion = { 1 => 3, 2 => 1 };
124             }
125              
126 2         9 my $chord = $self->_generate_chord( $key, $scale, $scale_notes[4], $octave );
127 2         8 $chord = $self->_invert_chord( $chord, $inversion->{1}, $octave );
128 2         5 push @$cadence, $chord;
129              
130 2         6 $chord = $self->_generate_chord( $key, $scale, $scale_notes[0], $octave );
131 2         7 $chord = $self->_invert_chord( $chord, $inversion->{2}, $octave );
132 2         5 push @$cadence, $chord;
133             }
134             elsif ( $type eq 'plagal' ) {
135 4         17 my $chord = $self->_generate_chord( $key, $scale, $scale_notes[3], $octave );
136 4         11 push @$cadence, $chord;
137              
138 4         11 $chord = $self->_generate_chord( $key, $scale, $scale_notes[0], $octave );
139 4         10 push @$cadence, $chord;
140             }
141             elsif ( $type eq 'half' ) {
142 13         51 my $chord = $self->_generate_chord( $key, $scale, $scale_notes[ $leading - 1 ], $octave );
143             $chord = $self->_invert_chord( $chord, $inversion->{1}, $octave )
144 13 50 66     45 if $inversion && $inversion->{1};
145 13         29 push @$cadence, $chord;
146              
147 13         31 $chord = $self->_generate_chord( $key, $scale, $scale_notes[4], $octave );
148             $chord = $self->_invert_chord( $chord, $inversion->{2}, $octave )
149 13 50 66     46 if $inversion && $inversion->{2};
150 13         28 push @$cadence, $chord;
151             }
152             elsif ( $type eq 'deceptive' ) {
153 4         15 my $chord = $self->_generate_chord( $key, $scale, $scale_notes[4], $octave );
154 4         8 push @$cadence, $chord;
155              
156 4 100       31 my $note = $variation == 1 ? $scale_notes[5] : $scale_notes[3];
157 4         13 $chord = $self->_generate_chord( $key, $scale, $note, $octave );
158 4         9 push @$cadence, $chord;
159             }
160             else {
161 1         8 die 'unknown cadence';
162             }
163              
164 55 100       138 if ( $picardy ) {
165 3 100       12 if ( $self->format eq 'midinum' ) {
166 1         3 $cadence->[1][1]++;
167             }
168             else {
169 2         18 my $note = Music::Note->new( $cadence->[1][1], $self->format );
170 2         84 my $num = $note->format('midinum');
171 2         74 $num++;
172 2         7 $note = Music::Note->new( $num, 'midinum' );
173 2         65 $cadence->[1][1] = $note->format( $self->format );
174             }
175             }
176              
177 55         315 return $cadence;
178             }
179              
180             sub _invert_chord {
181 32     32   73 my ( $self, $chord, $inversion, $octave ) = @_;
182              
183 32         171 my $mcp = Music::Chord::Positions->new;
184              
185 32 100       387 if ( $self->format eq 'midinum' ) {
186 2         6 $chord = $mcp->chord_inv( $chord, inv_num => $inversion );
187             }
188             else { # Perform these gymnastics to convert named notes to inverted named notes:
189             # Strip the octave if present
190 30 100       79 $chord = [ map { s/\d+//; $_ } @$chord ]
  64         120  
  64         106  
191             if $octave;
192              
193             # Convert the chord into pitch-class representation
194 30         61 my $pitches = [ map { $self->pitchnum( $_ . -1 ) } @$chord ];
  106         3105  
195              
196             # Do the inversion!
197 30         1178 $pitches = $mcp->chord_inv( $pitches, inv_num => $inversion );
198              
199             # Convert the pitch-classes back to named notes
200 30         1231 $chord = [ map { $self->pitchname($_) } @$pitches ];
  106         983  
201              
202             # Clean-up the chord
203 30         266 for ( @$chord ) {
204 106 100       133 if ( $octave ) {
205 64         99 s/-1/$octave/;
206 64         98 s/0/$octave + 1/e;
  23         45  
207             }
208             else {
209 42         75 s/-1//;
210 42         66 s/0//;
211             }
212              
213 106 100       205 if ( $self->format eq 'midi' ) {
214 12         20 s/#/s/;
215 12         16 s/b/f/;
216             }
217             }
218             }
219              
220 32         165 return $chord;
221             }
222              
223             sub _generate_chord {
224 113     113   241 my ( $self, $key, $scale, $note, $octave ) = @_;
225              
226             # Know what chords should be diminished
227 113         500 my %diminished = (
228             ionian => 'vii',
229             major => 'vii',
230             dorian => 'vi',
231             phrygian => 'v',
232             lydian => 'iv',
233             mixolydian => 'iii',
234             aeolian => 'ii',
235             minor => 'ii',
236             locrian => 'i',
237             );
238              
239 113 100       243 die 'unknown scale' unless exists $diminished{$scale};
240              
241 112         2017 my $mtr = Music::ToRoman->new(
242             scale_note => $key,
243             scale_name => $scale,
244             chords => 0,
245             );
246              
247             # Figure out if the chord is diminished, minor, or major
248 111         24590 my $roman = $mtr->parse($note);
249 111 100       23120 my $type = $roman =~ /^$diminished{$scale}$/ ? 'dim' : $roman =~ /^[a-z]/ ? 'm' : '';
    100          
250              
251 111 100       285 $type .= 7
252             if $self->seven;
253              
254 111         350 my $mcn = Music::Chord::Note->new;
255              
256             # Get the notes of the chord (without an octave)
257 111         583 my @notes = $mcn->chord( $note . $type );
258              
259 111 100       3547 if ( $self->format eq 'midi' ) {
    100          
    100          
260             # Convert the sharps and flats
261 8         15 for ( @notes ) {
262 26         39 s/#/s/;
263 26         40 s/b/f/;
264             }
265             }
266             elsif ( $self->format eq 'midinum' ) {
267             # Convert the notes to midinum format
268 14         23 @notes = map { $self->pitchnum( $_ . $octave ) } @notes;
  48         1259  
269             }
270             elsif ( $self->format ne 'isobase' ) {
271 1         19 die 'unknown format';
272             }
273              
274             # Append the octave if defined and the format is not midinum
275 110 100 100     799 @notes = map { $_ . $octave } @notes
  92         151  
276             if $octave && $self->format ne 'midinum';
277              
278 110         416 return \@notes;
279             }
280              
281              
282             sub remove_notes {
283 4     4 1 1942 my ($self, $indices, $chord) = @_;
284 4         6 my @chord;
285 4         10 for my $n (0 .. @$chord - 1) {
286 12 100   11   34 next if any { $n == $_ } @$indices;
  11         22  
287 9         19 push @chord, $chord->[$n];
288             }
289 4         9 return \@chord;
290             }
291              
292             1;
293              
294             __END__