File Coverage

blib/lib/Music/Tension/Counterpoint.pm
Criterion Covered Total %
statement 81 81 100.0
branch 54 54 100.0
condition 15 15 100.0
subroutine 11 11 100.0
pod 4 4 100.0
total 165 165 100.0


line stmt bran cond sub pod time code
1             # -*- Perl -*-
2             #
3             # Counterpoint tension calculation, which here means a boolean pass or
4             # fail depending on the interval(s) involved
5              
6             package Music::Tension::Counterpoint;
7              
8             our $VERSION = '1.03';
9              
10 2     2   124544 use 5.008;
  2         17  
11 2     2   11 use strict;
  2         5  
  2         50  
12 2     2   12 use warnings;
  2         3  
  2         56  
13 2     2   9 use Carp qw/croak/;
  2         4  
  2         100  
14 2     2   13 use Scalar::Util qw/looks_like_number/;
  2         4  
  2         86  
15              
16 2     2   11 use constant { DISS => 0, CONS => 1 };
  2         4  
  2         179  
17              
18 2     2   483 use parent qw(Music::Tension);
  2         287  
  2         12  
19              
20             my $DEG_IN_SCALE = 12;
21              
22             ########################################################################
23             #
24             # METHODS
25              
26             sub new {
27 9     9 1 3879 my ( $class, %param ) = @_;
28 9         39 my $self = $class->SUPER::new(%param);
29              
30 9 100       21 if ( exists $param{big_dissonance} ) {
31 1         3 $self->{_big_dissonance} = $param{big_dissonance};
32             } else {
33             # various counterpoint texts allow for dissonant intervals
34             # larger than an octave, so this is the default
35 8         18 $self->{_big_dissonance} = 1;
36             }
37              
38 9 100       17 if ( exists $param{octave_allow} ) {
39 1         3 $self->{_octave_allow} = $param{octave_allow};
40             } else {
41 8         15 $self->{_octave_allow} = 1;
42             }
43              
44             # special lookup table for the "vertical" method when considering
45             # interior voices to any voices above
46 9 100       15 if ( exists $param{interior} ) {
47             croak "interior must be a hash reference"
48 4 100 100     36 unless defined $param{interior} and ref $param{interior} eq 'HASH';
49 2         7 for my $i ( 0 .. 11 ) {
50             croak "interior must include all intervals from 0 through 11"
51 13 100       32 if !exists $param{interior}->{$i};
52 12 100       28 $self->{_interior}{$i} = $param{interior}->{$i} ? CONS : DISS;
53             }
54             } else {
55             # following Norden 1969
56             $self->{_interior} = {
57 5         36 0 => CONS, # unison
58             1 => DISS, # minor 2nd
59             2 => DISS, # major 2nd
60             3 => CONS, # minor 3rd
61             4 => CONS, # major 3rd
62             5 => CONS, # perfect fourth
63             6 => CONS, # augmented fourth
64             7 => CONS, # fifth
65             8 => CONS, # minor 6th
66             9 => CONS, # major 6th
67             10 => DISS, # minor 7th
68             11 => DISS, # major 7th
69             };
70             }
71 6 100       14 if ( exists $param{tensions} ) {
72             croak "tensions must be a hash reference"
73 4 100 100     60 unless defined $param{tensions} and ref $param{tensions} eq 'HASH';
74 2         6 for my $i ( -11 .. 11 ) {
75             croak "tensions must include all intervals from -11 through 11"
76 24 100       53 if !exists $param{tensions}->{$i};
77 23 100       49 $self->{_tensions}{$i} = $param{tensions}->{$i} ? CONS : DISS;
78             }
79             } else {
80             # these are typical values for vertical intervals. the otherwise
81             # redundant negative intervals are to support melodic checks
82             # that may need to know whether a leap is ascending or
83             # descending
84             $self->{_tensions} = {
85 2         25 0 => CONS, # unison
86             1 => DISS, # minor 2nd
87             2 => DISS, # major 2nd
88             3 => CONS, # minor 3rd
89             4 => CONS, # major 3rd
90             5 => DISS, # fourth
91             6 => DISS, # the evil, evil tritone
92             7 => CONS, # fifth
93             8 => CONS, # minor 6th
94             9 => CONS, # major 6th
95             10 => DISS, # minor 7th
96             11 => DISS, # major 7th
97             -1 => DISS, # minor 2nd
98             -2 => DISS, # major 2nd
99             -3 => CONS, # minor 3rd
100             -4 => CONS, # major 3rd
101             -5 => DISS, # fourth
102             -6 => DISS, # the evil, evil tritone
103             -7 => CONS, # fifth
104             -8 => CONS, # minor 6th
105             -9 => CONS, # major 6th
106             -10 => DISS, # minor 7th
107             -11 => DISS, # major 7th
108             };
109             }
110              
111 3         7 bless $self, $class;
112 3         9 return $self;
113             }
114              
115             sub pitches {
116 65     65 1 246 my ( $self, $p1, $p2 ) = @_;
117 65 100 100     236 croak "two pitches required" if !defined $p1 or !defined $p2;
118 63 100 100     372 croak "pitches must be integers"
119             if $p1 !~ m/^-?[0-9]+$/
120             or $p2 !~ m/^-?[0-9]+$/;
121              
122 61         117 my $interval = $p2 - $p1;
123 61         95 my $mod = $interval % $DEG_IN_SCALE;
124              
125 61 100       117 if ( abs($interval) >= $DEG_IN_SCALE ) {
126 18 100       34 if ( $mod == 0 ) {
127             # exclusive test of octave intervals so that they can be
128             # treated differently from the unison
129 5 100       21 return $self->{_octave_allow} ? CONS : DISS;
130             } else {
131             # anything above an octave is okay by default; otherwise
132             # falls through to the _tensions lookup below
133 13 100       44 return CONS if $self->{_big_dissonance};
134             }
135             }
136              
137 46 100       87 my $neg = $interval < 0 ? $DEG_IN_SCALE : 0;
138 46         191 return $self->{_tensions}->{ $mod - $neg };
139             }
140              
141             # why not hide former synopsis code in a method?
142             sub usable_offsets {
143 2     2 1 2211 my $self = shift;
144 2         8 my @ret = $self->offset_tensions(@_);
145 2         5 my @ok;
146 2         5 OFFS: for my $i ( 1 .. $#ret ) {
147 5         7 for my $consonant ( @{ $ret[$i] } ) {
  5         10  
148 7 100       17 next OFFS unless $consonant;
149             }
150 2         6 push @ok, $i;
151             }
152 2         11 return @ok;
153             }
154              
155             sub vertical {
156 14     14 1 124 my ( $self, $pset ) = @_;
157 14 100 100     85 croak "pitch set must be array ref"
158             unless defined $pset and ref $pset eq 'ARRAY';
159 12 100       35 croak "pitch set must contain multiple elements" if @$pset < 2;
160              
161             # the root (or lowest) pitch is from where special checks are done;
162             # that pitch must be first in the list. also the sort simplifies the
163             # interior interval checks (always non-negative intervals)
164 11         42 my @pcs = sort { $a <=> $b } @$pset;
  40         83  
165              
166 11         31 for my $i ( 1 .. $#pcs ) {
167 22 100       49 return DISS if $self->pitches( $pcs[0], $pcs[$i] ) == DISS;
168              
169             # interior voice interval checks may vary; Norden 1969 (p.84)
170             # allows for interior perfect and augmented fourths in a 3-part
171             # texture but presumably not 2nds nor 7ths. so, new lookup table
172 19 100       44 if ( $i < $#pcs ) {
173 13         29 for my $j ( $i + 1 .. $#pcs ) {
174 18         32 my $interval = $pcs[$j] - $pcs[$i];
175 18         23 my $mod = $interval % $DEG_IN_SCALE;
176 18 100       38 if ( $interval >= $DEG_IN_SCALE ) {
177 5 100       12 if ( $mod == 0 ) {
178 2 100       6 if ( $self->{_octave_allow} ) {
179 1         3 next;
180             } else {
181 1         5 return DISS;
182             }
183             } else {
184 3 100       9 next if $self->{_big_dissonance};
185             }
186             }
187 14 100       41 return DISS if $self->{_interior}{$mod} == DISS;
188             }
189             }
190             }
191              
192 6         26 return CONS;
193             }
194              
195             1;
196             __END__