File Coverage

blib/lib/Music/Tension/PlompLevelt.pm
Criterion Covered Total %
statement 83 83 100.0
branch 34 34 100.0
condition 34 34 100.0
subroutine 10 10 100.0
pod 4 4 100.0
total 165 165 100.0


line stmt bran cond sub pod time code
1             # -*- Perl -*-
2             #
3             # "Plomp-Levelt consonance curve" implementation
4              
5             package Music::Tension::PlompLevelt;
6              
7             our $VERSION = '1.03';
8              
9 2     2   125622 use strict;
  2         14  
  2         62  
10 2     2   12 use warnings;
  2         3  
  2         58  
11 2     2   10 use Carp qw/croak/;
  2         5  
  2         89  
12 2     2   11 use List::Util qw/sum/;
  2         4  
  2         218  
13 2     2   31 use Scalar::Util qw/looks_like_number/;
  2         4  
  2         115  
14              
15 2     2   464 use parent qw(Music::Tension);
  2         330  
  2         11  
16              
17             # pianowire* are from [Helmholtz 1877 p.79] relative intensity of first
18             # six harmonics of piano wire, struck at 1/7th its length, for various
19             # hammer types. Via http://jjensen.org/DissonanceCurve.html
20             my %AMPLITUDES = (
21             'ones' => [ (1) x 6 ],
22             'pianowire-plucked' => [ 1, 0.8, 0.6, 0.3, 0.1, 0.03 ],
23             'pianowire-soft' => [ 1, 1.9, 1.1, 0.2, 0, 0.05 ],
24             'pianowire-medium' => [ 1, 2.9, 3.6, 2.6, 1.1, 0.2 ],
25             'pianowire-hard' => [ 1, 3.2, 5, 5, 3.2, 1 ],
26             );
27              
28             ########################################################################
29             #
30             # METHODS
31              
32             sub new {
33 6     6 1 3096 my ( $class, %param ) = @_;
34 6         28 my $self = $class->SUPER::new(%param);
35              
36 6         28 $self->{_amplitudes} = {%AMPLITUDES};
37              
38 6 100       17 if ( exists $param{amplitudes} ) {
39 4         6 for my $name ( keys %{ $param{amplitudes} } ) {
  4         14  
40             croak "amplitude profile '$name' must be array reference"
41 4 100 100     40 unless defined $param{amplitudes}->{$name} and ref $param{amplitudes}->{$name} eq 'ARRAY';
42 2         6 $self->{_amplitudes}->{$name} = $param{amplitudes}->{$name};
43             }
44             }
45              
46 4 100       38 if ( exists $param{default_amp_profile} ) {
47             croak "no such profile '$param{default_amp_profile}'"
48 2 100       22 unless exists $self->{_amplitudes}->{ $param{default_amp_profile} };
49 1         4 $self->{_amp_profile} = $param{default_amp_profile};
50             } else {
51 2         5 $self->{_amp_profile} = 'pianowire-medium';
52             }
53              
54             # NOTE will also need normalize if add setter method to update _amplitudes
55 3 100       8 $self->{_normalize_amps} = exists $param{normalize_amps} ? 1 : 0;
56 3 100       9 if ( $self->{_normalize_amps} ) {
57 1         3 for my $amps ( values %{ $self->{_amplitudes} } ) {
  1         5  
58 6         15 my $sum = sum @$amps;
59 6         11 for my $amp (@$amps) {
60 34         62 $amp /= $sum;
61             }
62             }
63             }
64              
65 3         7 bless $self, $class;
66 3         12 return $self;
67             }
68              
69             # not sure if I've followed the papers correctly; they all operate on a
70             # single frequency with overtones above that, while for tension I'm
71             # interested in "given these two frequencies or pitches (with their own
72             # sets of overtones), how dissonant are they to one another" so
73             # hopefully I can just tally up the harmonics between the two different
74             # sets of harmonics?
75             #
76             # also, vertical scaling might take more looking at, perhaps arrange so
77             # with normalize_amps the maximum dissonance has the value of 1? (or
78             # that the most dissonant interval of the scale, e.g. minor 2nd in equal
79             # temperament has the value of one?)
80             sub frequencies {
81 19     19 1 1579 my ( $self, $f1, $f2 ) = @_;
82 19         30 my @harmonics;
83              
84 19 100 100     100 if ( looks_like_number $f1) {
    100 100        
      100        
85 14         22 for my $i ( 0 .. $#{ $self->{_amplitudes}->{ $self->{_amp_profile} } } ) {
  14         50  
86 81         324 push @{ $harmonics[0] },
87 81   100     117 { amp => $self->{_amplitudes}->{ $self->{_amp_profile} }->[$i] || 0,
88             freq => $f1 * ( $i + 1 ),
89             };
90             }
91             } elsif ( defined $f1 and ref $f1 eq 'ARRAY' and @$f1 and ref $f1->[0] eq 'HASH' ) {
92 1         4 $harmonics[0] = $f1;
93             } else {
94 4         36 croak "unknown input for frequency1";
95             }
96              
97 15 100 100     70 if ( looks_like_number $f2) {
    100 100        
      100        
98 10         15 for my $j ( 0 .. $#{ $self->{_amplitudes}->{ $self->{_amp_profile} } } ) {
  10         25  
99 57         224 push @{ $harmonics[1] },
100 57   100     84 { amp => $self->{_amplitudes}->{ $self->{_amp_profile} }->[$j] || 0,
101             freq => $f2 * ( $j + 1 ),
102             };
103             }
104             } elsif ( defined $f2 and ref $f2 eq 'ARRAY' and @$f2 and ref $f2->[0] eq 'HASH' ) {
105 1         3 $harmonics[1] = $f2;
106             } else {
107 4         54 croak "unknown input for frequency2";
108             }
109              
110             # code ported from equation at http://jjensen.org/DissonanceCurve.html
111 11         22 my $tension;
112 11         17 for my $i ( 0 .. $#{ $harmonics[0] } ) {
  11         24  
113 63         90 for my $j ( 0 .. $#{ $harmonics[1] } ) {
  63         112  
114 369         761 my @freqs = sort { $a <=> $b } $harmonics[0]->[$i]{freq},
115 369         715 $harmonics[1]->[$j]{freq};
116 369         614 my $q = ( $freqs[1] - $freqs[0] ) / ( 0.021 * $freqs[0] + 19 );
117             $tension +=
118             $harmonics[0]->[$i]{amp} *
119             $harmonics[1]->[$j]{amp} *
120 369         858 ( exp( -0.84 * $q ) - exp( -1.38 * $q ) );
121             }
122             }
123              
124 11         113 return $tension;
125             }
126              
127             sub pitches {
128 5     5 1 133 my ( $self, $p1, $p2, $freq_harmonics ) = @_;
129 5 100 100     41 croak "two pitches required" if !defined $p1 or !defined $p2;
130 3 100 100     39 croak "pitches must be positive integers"
131             if $p1 !~ m/^[0-9]+$/
132             or $p2 !~ m/^[0-9]+$/;
133              
134 1         10 return $self->frequencies( map( $self->pitch2freq($_), $p1, $p2 ),
135             $freq_harmonics );
136             }
137              
138             sub vertical {
139 5     5 1 2432 my ( $self, $pset ) = @_;
140 5 100 100     41 croak "pitch set must be array ref" unless defined $pset and ref $pset eq 'ARRAY';
141 3 100       16 croak "pitch set must contain multiple elements" if @$pset < 2;
142              
143 2         9 my @freqs = map $self->pitch2freq($_), @$pset;
144              
145 2         4 my $min = ~0;
146 2         4 my $max = 0;
147 2         31 my ( @tensions, $sum );
148 2         8 for my $i ( 1 .. $#freqs ) {
149 4         12 my $t = $self->frequencies( $freqs[0], $freqs[$i] );
150 4         7 $sum += $t;
151 4 100       10 $min = $t
152             if $t < $min;
153 4 100       9 $max = $t
154             if $t > $max;
155 4         9 push @tensions, $t;
156             }
157              
158 2 100       20 return wantarray ? ( $sum, $min, $max, \@tensions ) : $sum;
159             }
160              
161             1;
162             __END__