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