File Coverage

blib/lib/Music/Tension/Cope.pm
Criterion Covered Total %
statement 81 81 100.0
branch 54 54 100.0
condition 45 45 100.0
subroutine 12 12 100.0
pod 7 7 100.0
total 199 199 100.0


line stmt bran cond sub pod time code
1             # -*- Perl -*-
2             #
3             # "Copian" tension analysis for 12-pitch material in equal temperament
4              
5             package Music::Tension::Cope;
6              
7             our $VERSION = '1.03';
8              
9 2     2   129977 use strict;
  2         17  
  2         60  
10 2     2   10 use warnings;
  2         4  
  2         58  
11 2     2   12 use Carp qw/croak/;
  2         4  
  2         86  
12 2     2   10 use Scalar::Util qw/looks_like_number/;
  2         4  
  2         81  
13              
14 2     2   1047 use parent qw(Music::Tension);
  2         610  
  2         12  
15              
16             my $DEG_IN_SCALE = 12;
17              
18             ########################################################################
19             #
20             # METHODS
21              
22             sub new {
23 9     9 1 313 my ( $class, %param ) = @_;
24 9         39 my $self = $class->SUPER::new(%param);
25              
26 9 100       20 if ( exists $param{duration_weight} ) {
27             croak "duration_weight must be a number"
28 2 100       18 if !looks_like_number $param{duration_weight};
29 1         3 $self->{_duration_weight} = $param{duration_weight};
30             } else {
31 7         22 $self->{_duration_weight} = 0.1;
32             }
33              
34 8 100       14 if ( exists $param{metric_weight} ) {
35             croak "metric_weight must be a number"
36 2 100       15 if !looks_like_number $param{metric_weight};
37 1         3 $self->{_metric_weight} = $param{metric_weight};
38             } else {
39 6         14 $self->{_metric_weight} = 0.1;
40             }
41              
42 7 100       20 if ( exists $param{octave_adjust} ) {
43             croak "octave_adjust must be a number"
44 3 100       22 if !looks_like_number $param{octave_adjust};
45 2         5 $self->{_octave_adjust} = $param{octave_adjust};
46             } else {
47 4         20 $self->{_octave_adjust} = -0.02;
48             }
49              
50 6 100       13 if ( exists $param{tensions} ) {
51             croak "tensions must be a hash reference"
52 5 100 100     40 unless defined $param{tensions} and ref $param{tensions} eq 'HASH';
53 3         11 for my $i ( 0 .. 11 ) {
54             croak "tensions must include all intervals from 0 through 11"
55 31 100       65 if !exists $param{tensions}->{$i};
56             }
57 2         5 $self->{_tensions} = $param{tensions};
58             } else {
59             # default interval tensions taken from "Computer Models of
60             # Musical Creativity", Cope, p.229-230, from least tension (0.0)
61             # to greatest (1.0), less if greater than an octave
62             $self->{_tensions} = {
63 1         12 0 => 0.0,
64             1 => 1.0,
65             2 => 0.8,
66             3 => 0.225,
67             4 => 0.2,
68             5 => 0.55,
69             6 => 0.65,
70             7 => 0.1,
71             8 => 0.275,
72             9 => 0.25,
73             10 => 0.7,
74             11 => 0.9,
75             };
76             }
77              
78 3         7 bless $self, $class;
79 3         9 return $self;
80             }
81              
82             # approach tension - horizontal tension, I'm assuming harmonic function,
83             # therefore limit to intervals in same register.
84             sub approach {
85 4     4 1 69 my ( $self, $p1 ) = @_;
86 4 100       22 croak "pitch is required" if !defined $p1;
87 3 100       25 croak "pitch must be integer" if $p1 !~ m/^-?[0-9]+$/;
88              
89 2         8 $self->pitches( 0, abs($p1) % $DEG_IN_SCALE );
90             }
91              
92             # tension over durations
93             sub duration {
94 7     7 1 144 my ( $self, $input, $duration ) = @_;
95              
96 7 100 100     75 croak "duration must be a positive value"
      100        
97             if !defined $duration
98             or !looks_like_number($duration)
99             or $duration <= 0;
100              
101 4         7 my $tension;
102 4 100       16 if ( ref $input eq 'ARRAY' ) {
    100          
103 1         3 $tension = $self->vertical($input);
104             } elsif ( looks_like_number($input) ) {
105 2         6 $tension = $input;
106             } else {
107 1         9 croak "unknown pitch set or prior tension value";
108             }
109              
110             # p.232-233 [Cope 2005] - this result "is then added to any grouping's
111             # accumulated tension weighting"
112             return $self->{_duration_weight} * $duration +
113 3         16 $self->{_duration_weight} * $tension;
114             }
115              
116             # KLUGE things into whatever is closest equal temperament for now
117             sub frequencies {
118 7     7 1 208 my ( $self, $f1, $f2 ) = @_;
119 7 100 100     47 croak "two frequencies required" if !defined $f1 or !defined $f2;
120 5 100 100     65 croak "frequencies must be positive numbers"
      100        
      100        
121             if !looks_like_number $f1
122             or !looks_like_number $f2
123             or $f1 < 0
124             or $f2 < 0;
125              
126 1         9 $self->pitches( map $self->freq2pitch($_), $f1, $f2 );
127             }
128              
129             # tension based on where note is within measure p.232 [Cope 2005]
130             sub metric {
131 11     11 1 223 my ( $self, $b, $v ) = @_;
132 11 100 100     178 croak "input must be positive numeric"
      100        
      100        
      100        
      100        
133             if !defined $b
134             or !looks_like_number($b)
135             or $b <= 0
136             or !defined $v
137             or !looks_like_number($v)
138             or $v <= 0;
139              
140 5         39 return ( $b * $self->{_metric_weight} ) / $v;
141             }
142              
143             # tension for two pitches
144             sub pitches {
145 37     37 1 227 my ( $self, $p1, $p2 ) = @_;
146 37 100 100     148 croak "two pitches required" if !defined $p1 or !defined $p2;
147 34 100 100     221 croak "pitches must be integers"
148             if $p1 !~ m/^-?[0-9]+$/
149             or $p2 !~ m/^-?[0-9]+$/;
150              
151 32         59 my $interval = abs( $p2 - $p1 );
152 32         66 my $octave = int( $interval / $DEG_IN_SCALE );
153             my $tension =
154             $self->{_tensions}->{ $interval % $DEG_IN_SCALE } +
155 32 100       91 ( $octave > 0 ? $self->{_octave_adjust} : 0 );
156 32 100       66 $tension = 0 if $tension < 0;
157              
158 32         93 return $tension;
159             }
160              
161             # tension from first note to all others above it in a passed pitch set.
162             # returns sum, min, max, and an array ref of tensions, unless just the
163             # sum is desired by context
164             sub vertical {
165 7     7 1 115 my ( $self, $pset ) = @_;
166 7 100 100     50 croak "pitch set must be array ref"
167             unless defined $pset and ref $pset eq 'ARRAY';
168 5 100       22 croak "pitch set must contain multiple elements" if @$pset < 2;
169 4         13 my @pcs = @$pset;
170              
171             # reposition pitches upwards if subsequent lower than the
172             # initial pitch
173 4         11 for my $i ( 1 .. $#pcs ) {
174 12 100       29 if ( $pcs[$i] < $pcs[0] ) {
175 8         23 $pcs[$i] += $DEG_IN_SCALE +
176             ( int( ( $pcs[0] - $pcs[$i] - 1 ) / $DEG_IN_SCALE ) ) * $DEG_IN_SCALE;
177             }
178             }
179              
180 4         8 my $min = ~0;
181 4         7 my $max = 0;
182 4         5 my ( @tensions, $sum );
183 4         9 for my $j ( 1 .. $#pcs ) {
184 12         26 my $t = $self->pitches( $pcs[0], $pcs[$j] );
185 12         18 $sum += $t;
186 12 100       24 $min = $t if $t < $min;
187 12 100       24 $max = $t if $t > $max;
188 12         23 push @tensions, $t;
189             }
190              
191 4 100       50 return wantarray ? ( $sum, $min, $max, \@tensions ) : $sum;
192             }
193              
194             1;
195             __END__