File Coverage

blib/lib/Music/Chord/Positions.pm
Criterion Covered Total %
statement 137 143 95.8
branch 52 76 68.4
condition 30 63 47.6
subroutine 12 12 100.0
pod 5 6 83.3
total 236 300 78.6


line stmt bran cond sub pod time code
1             # -*- Perl -*-
2             #
3             # TODO
4             # * chord_pos
5             # - support arbitrary? voice counts (would need doubling rules?) Extra
6             # voices would likely run into either doubling or maximum pitch
7             # limits, so might need good-enough effort (or lots of doublings)?
8             # - or lower than @ps voice count, which might require new logic or
9             # priority on the pitches?
10             # - inversion through "root_any" and then select only root 3rd or
11             # whatever afterwards?
12             # - nix octave_count and pitch_max in favor of just specified
13             # semitones up? - makes sense, as interval_adj_max is a semitone
14             # thing.
15             # - doublings could use more rules beyond "no" or "anything goes",
16             # perhaps optional list of "here's pitches that can be doubled" so
17             # can 2x the root or the 5th or whatever on demand.
18             # - logic tricky, could it be simplified with a Combinations module or
19             # by using ordering results from a glob() expansion?
20             # - callbacks so caller can better control the results? (or filter
21             # results via counterpoint or other rules, e.g. to exclude || 5ths
22             # or other naughty things).
23             # - allow doubling of the third: frowned on, but sees frequent use
24             # e.g. in the Bach Chorales (if used properly, but rules would need
25             # to be enforced somehow else).
26             #
27             # * progressions
28             # - support this, instead of using mcp-prog script?
29              
30             package Music::Chord::Positions;
31              
32 2     2   60229 use 5.010;
  2         9  
  2         78  
33 2     2   11 use strict;
  2         13  
  2         81  
34 2     2   11 use warnings;
  2         11  
  2         61  
35              
36 2     2   11 use Carp qw/croak/;
  2         3  
  2         162  
37 2     2   11 use List::Util qw(max min);
  2         3  
  2         4697  
38              
39             our $VERSION = '0.64';
40              
41             my $DEG_IN_SCALE = 12;
42              
43             ########################################################################
44             #
45             # SUBROUTINES
46              
47             # TODO move back to List::MoreUtils if that module is fixed up or some
48             # replacement with fewer open critical bugs is written.
49             sub all(&@) {
50 6     6 0 11 my $test = shift;
51 6         14 for (@_) {
52 9 100       49 return 0 unless &$test;
53             }
54 3         14 return 1;
55             }
56              
57             sub new {
58 3     3 1 1026 my ( $class, %param ) = @_;
59 3         7 my $self = {};
60              
61 3   66     23 $self->{_DEG_IN_SCALE} = int( $param{DEG_IN_SCALE} // $DEG_IN_SCALE );
62 3 50       12 if ( $self->{_DEG_IN_SCALE} < 2 ) {
63 0         0 croak 'degrees in scale must be greater than one';
64             }
65              
66 3         9 bless $self, $class;
67 3         10 return $self;
68             }
69              
70             ########################################################################
71             #
72             # Methods of Music
73              
74             sub chord_inv {
75 4     4 1 2709 my ( $self, $pitch_set, %params ) = @_;
76 4 50 33     31 croak 'pitch set reference required'
77             unless defined $pitch_set and ref $pitch_set eq 'ARRAY';
78              
79 4         19 my $max_pitch = max(@$pitch_set);
80 4         15 my $next_register =
81             $max_pitch + $self->{_DEG_IN_SCALE} - $max_pitch % $self->{_DEG_IN_SCALE};
82              
83 4         3 my @inversions;
84 4         13 for my $i ( 0 .. $#$pitch_set - 1 ) {
85             # Inversions simply flip lower notes up above the highest pitch in
86             # the original pitch set.
87 25         52 push @inversions,
88             [
89             @$pitch_set[ $i + 1 .. $#$pitch_set ],
90 12         38 map { $next_register + $_ } @$pitch_set[ 0 .. $i ]
91             ];
92              
93             # Normalize to "0th" register if lowest pitch is an octave+ out
94 12 100 66     53 if ( exists $params{'pitch_norm'} and $params{'pitch_norm'} ) {
95 4         4 my $min_pitch = min( @{ $inversions[-1] } );
  4         13  
96 4 100       12 if ( $min_pitch >= $self->{_DEG_IN_SCALE} ) {
97 1         3 my $offset = $min_pitch - $min_pitch % $self->{_DEG_IN_SCALE};
98 1         3 $_ -= $offset for @{ $inversions[-1] };
  1         7  
99             }
100             }
101             }
102              
103 4 100       12 if ( exists $params{'inv_num'} ) {
104 1 50 33     17 croak 'inversion number out of range'
      33        
105             if $params{'inv_num'} !~ m/^\d+$/
106             or $params{'inv_num'} < 1
107             or $params{'inv_num'} > @inversions;
108 1         2 @inversions = @{ $inversions[ $params{'inv_num'} - 1 ] };
  1         4  
109             }
110              
111 4         17 return \@inversions;
112             }
113              
114             sub chord_pos {
115 1     1 1 13 my ( $self, $pitch_set, %params ) = @_;
116 1 50 33     10 croak 'pitch set reference required'
117             unless defined $pitch_set and ref $pitch_set eq 'ARRAY';
118              
119             my (
120 1         3 @ps, @potentials, @revoicings,
121             @voice_iters, @voice_max, %seen_intervals,
122             $min_pitch_norm, $next_register, $unique_pitch_count,
123             );
124              
125 1 50 33     8 $params{'interval_adj_max'} =
126             ( exists $params{'interval_adj_max'}
127             and defined $params{'interval_adj_max'} )
128             ? $params{'interval_adj_max'}
129             : 19;
130              
131 1 50       3 if ( exists $params{'octave_count'} ) {
132 0 0       0 $params{'octave_count'} = 2 if $params{'octave_count'} < 2;
133             } else {
134 1         5 $params{'octave_count'} = 2;
135             }
136              
137 1 50 33     7 if ( exists $params{'pitch_max'} and $params{'pitch_max'} < 1 ) {
138 1         10 $params{'pitch_max'} =
139             ( $params{'octave_count'} + 1 ) * $self->{_DEG_IN_SCALE} +
140             $params{'pitch_max'};
141             }
142              
143 1 50       4 if ( exists $params{'voice_count'} ) {
144 1 50       4 if ( @$pitch_set > $params{'voice_count'} ) {
145 0         0 die
146             "case where pitches in chord exceeds allowed voices not implemented";
147             }
148             } else {
149 0         0 $params{'voice_count'} = @$pitch_set;
150             }
151              
152 1         7 @ps = sort { $a <=> $b } @$pitch_set;
  3         10  
153              
154 1         3 $min_pitch_norm = $ps[0] % $self->{_DEG_IN_SCALE};
155 1         4 $next_register =
156             $ps[-1] + ( $self->{_DEG_IN_SCALE} - $ps[-1] % $self->{_DEG_IN_SCALE} );
157             {
158 1         1 my %seen_pitch;
  1         2  
159 1         2 @seen_pitch{ map { $_ % $self->{_DEG_IN_SCALE} } @ps } = ();
  3         10  
160 1         5 $unique_pitch_count = keys %seen_pitch;
161             }
162              
163 1 50       5 if ( $params{'voice_count'} > @ps ) {
164 1         3 my $doubled_count = $params{'voice_count'} - @ps;
165 1 50       4 die "multiple extra voices not implemented" if $doubled_count > 1;
166              
167             # Double lowest pitch in octave above highest pitch C E G -> C E G C
168 1         2 push @ps, $next_register + $ps[0];
169             }
170              
171 1         4 @potentials = @ps;
172 1         4 for my $i ( 1 .. $params{'octave_count'} ) {
173 2         4 for my $n (@ps) {
174 8         14 my $p = $n + $i * $self->{_DEG_IN_SCALE};
175 8 100 66     41 push @potentials, $p
176             unless exists $params{'pitch_max'} and $p > $params{'pitch_max'};
177             }
178             }
179 1         3 my %uniq_pots;
180 1         9 @uniq_pots{@potentials} = ();
181 1         6 @potentials = sort { $a <=> $b } keys %uniq_pots;
  20         28  
182              
183 1         6 for my $i ( 0 .. $params{'voice_count'} - 1 ) {
184 4         6 $voice_iters[$i] = $i;
185 4         11 $voice_max[$i] = $#potentials - $params{'voice_count'} + $i + 1;
186             }
187 1 50 33     21 if ( exists $params{'root_lock'} and $params{'root_lock'} ) {
188 0         0 $voice_max[0] = $voice_iters[0];
189             }
190              
191 1         6 while ( $voice_iters[0] <= $voice_max[0] ) {
192 27         53 TOPV: while ( $voice_iters[-1] <= $voice_max[-1] ) {
193 66         198 my @chord = @potentials[@voice_iters];
194 66         104 $voice_iters[-1]++;
195              
196 66         65 my %harmeq;
197 66         89 for my $p (@chord) {
198 264         681 $harmeq{ $p % $self->{_DEG_IN_SCALE} }++;
199             }
200 66 50 33     227 unless ( exists $params{'no_limit_uniq'} and $params{'no_limit_uniq'} )
201             {
202 66 100       253 next if keys %harmeq < $unique_pitch_count;
203             }
204 44 50 33     109 unless ( exists $params{'no_limit_doublings'}
205             and $params{'no_limit_doublings'} ) {
206 44         87 for my $k ( grep { $_ != $min_pitch_norm } keys %harmeq ) {
  132         357  
207 76 100       272 next TOPV if $harmeq{$k} > 1;
208             }
209             }
210              
211 22         35 my ( @intervals, %intv_by_idx );
212 22         43 for my $j ( 1 .. $#chord ) {
213 63         95 push @intervals, $chord[$j] - $chord[ $j - 1 ];
214 63 100       138 next TOPV if $intervals[-1] > $params{'interval_adj_max'};
215              
216 61 50       203 $intv_by_idx{ $j - 1 } = $intervals[-1] if @chord > 2;
217             }
218             # TODO these routines have not been tested against chords with 5+
219             # voices, so may allow pitch sets that violate the spirit of the
220             # following (3rds in the middle of otherwise open voicings would
221             # be what I would expect to see pass in 5+ voice chords).
222 20 50 33     309 if ( @chord > 2
      33        
223             and exists $params{'no_partial_closed'}
224             and $params{'no_partial_closed'} ) {
225             # Exclude 3rds near fundamental where next voice 5th+ out
226 20 100 100     98 if ( $intervals[0] < 5 and $intervals[1] > 6 ) {
227 4         20 next TOPV;
228             }
229             # Exclude 3rds at top where next lower voice 5th+ out
230 16 100 100     55 if ( $intervals[-1] < 5 and $intervals[-2] > 6 ) {
231 2         11 next TOPV;
232             }
233              
234             # Exclude cases where highest voice has wandered off by a larger
235             # interval than seen below.
236 38         81 my @ordered_intv =
237 14         80 sort { $intv_by_idx{$b} <=> $intv_by_idx{$a} } keys %intv_by_idx;
238 14 100 100     109 if ( $ordered_intv[0] > $ordered_intv[-1]
239 9     9   50 and all { $intv_by_idx{ $ordered_intv[0] } > 1 + $intv_by_idx{$_} }
240             @ordered_intv[ 1 .. $#ordered_intv ] ) {
241 3         25 next TOPV;
242             }
243             }
244              
245             # Nix any identical chord voicings (c e g == c' e' g')
246 11 50 33     61 unless ( exists $params{'allow_transpositions'}
247             and $params{'allow_transpositions'} ) {
248 0 0       0 next TOPV if $seen_intervals{"@intervals"}++;
249             }
250              
251 11         53 push @revoicings, \@chord;
252             }
253              
254             # Increment any lower voices if top voice(s) maxed out
255 27         56 for my $i ( reverse 1 .. $#voice_iters ) {
256 81 100       243 if ( $voice_iters[$i] > $voice_max[$i] ) {
257 38         70 $voice_iters[ $i - 1 ]++;
258             }
259             }
260              
261 27 50 33     222 unless ( exists $params{'root_any'} and $params{'root_any'} ) {
262 27         74 while ( $potentials[ $voice_iters[0] ] % $self->{_DEG_IN_SCALE} !=
263             $min_pitch_norm ) {
264 4         7 $voice_iters[0]++;
265 4 100       615 last if $voice_iters[0] > $voice_max[0];
266             }
267             }
268              
269             # Reset higher voices to close positions above lower voices
270 27         65 for my $i ( 1 .. $#voice_iters ) {
271 81 100       161 if ( $voice_iters[$i] > $voice_max[$i] ) {
272 38         129 $voice_iters[$i] = $voice_iters[ $i - 1 ] + 1;
273             }
274             }
275             }
276              
277 1         9 return \@revoicings;
278             }
279              
280             # Change a pitch set collection (vertical) into voices (horizontal)
281             sub chords2voices {
282 2     2 1 1271 my ( $self, $pitch_sets ) = @_;
283 2 50       9 croak 'not a list of pitch sets' unless ref $pitch_sets->[0] eq 'ARRAY';
284              
285             # Nothing to swap, change nothing
286 2 100       11 return $pitch_sets if @$pitch_sets < 2;
287              
288 1         2 my @voices;
289 1         3 for my $vi ( 0 .. $#{ $pitch_sets->[0] } ) {
  1         4  
290 3         6 for my $j ( 0 .. $#$pitch_sets ) {
291 6         8 push @{ $voices[$vi] }, $pitch_sets->[$j][$vi];
  6         17  
292             }
293             }
294              
295 1         10 return [ reverse @voices ];
296             }
297              
298             sub scale_degrees {
299 4     4 1 466 my ( $self, $dis ) = @_;
300 4 100       11 if ( defined $dis ) {
301 1 50 33     17 croak 'scale degrees value must be positive integer greater than 1'
      33        
302             if !defined $dis
303             or $dis !~ /^\d+$/
304             or $dis < 2;
305 1         4 $self->{_DEG_IN_SCALE} = $dis;
306             }
307 4         23 return $self->{_DEG_IN_SCALE};
308             }
309              
310             1;
311             __END__