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