File Coverage

blib/lib/Music/Tension/PlompLevelt.pm
Criterion Covered Total %
statement 76 85 89.4
branch 18 34 52.9
condition 6 22 27.2
subroutine 11 11 100.0
pod 4 4 100.0
total 115 156 73.7


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