File Coverage

blib/lib/Music/Tension/Cope.pm
Criterion Covered Total %
statement 82 83 98.8
branch 38 54 70.3
condition 10 30 33.3
subroutine 13 13 100.0
pod 7 7 100.0
total 150 187 80.2


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