File Coverage

blib/lib/Music/Intervals.pm
Criterion Covered Total %
statement 176 176 100.0
branch 16 16 100.0
condition 6 12 50.0
subroutine 39 39 100.0
pod 16 16 100.0
total 253 259 97.6


line stmt bran cond sub pod time code
1             package Music::Intervals;
2             our $AUTHORITY = 'cpan:GENE';
3              
4             # ABSTRACT: Breakdown of musical intervals
5              
6 2     2   752326 use strict;
  2         4  
  2         87  
7 2     2   9 use warnings;
  2         4  
  2         179  
8              
9             our $VERSION = '0.1101';
10              
11 2     2   1209 use strictures 2;
  2         3885  
  2         107  
12 2     2   2335 use Algorithm::Combinatorics qw( combinations );
  2         9135  
  2         177  
13 2     2   1421 use Data::Dumper::Compact qw(ddc);
  2         33506  
  2         8  
14 2     2   283 use List::Util qw(first);
  2         5  
  2         166  
15 2     2   3771 use Math::Prime::Util qw(factor lcm);
  2         19213  
  2         11  
16 2     2   2450 use Math::Factor::XS qw( prime_factors );
  2         67063  
  2         152  
17 2     2   965 use MIDI::Pitch qw( name2freq );
  2         2074  
  2         140  
18 2     2   1117 use Moo;
  2         16665  
  2         14  
19 2     2   5276 use Music::Intervals::Ratios;
  2         11  
  2         206  
20 2     2   1409 use Music::Tension::Cope ();
  2         5881  
  2         93  
21 2     2   1403 use Number::Fraction ();
  2         369578  
  2         86  
22 2     2   570 use POSIX qw(log2);
  2         6943  
  2         19  
23 2     2   2990 use namespace::clean;
  2         26516  
  2         17  
24              
25              
26             has notes => (
27             is => 'ro',
28             default => sub { [qw( C E G )] },
29             );
30              
31             has _dyads => (
32             is => 'ro',
33             lazy => 1,
34             builder => 1,
35             );
36             sub _build__dyads {
37 8     8   97 my $self = shift;
38 8         49 my %dyads = $self->dyads($self->notes);
39 8         58 return \%dyads;
40             }
41              
42             has _octave => ( is => 'ro', default => sub { 4 } );
43             has _concert => ( is => 'ro', default => sub { 440 } );
44             has _tonic => ( is => 'ro', default => sub { 'C' } );
45             has _semitones => ( is => 'ro', default => sub { 12 } );
46             has _midikey => ( is => 'ro', default => sub { 69 } );
47              
48             has _temper => (
49             is => 'ro',
50             lazy => 1,
51             builder => 1,
52             );
53             sub _build__temper {
54 2     2   25 my $self = shift;
55 2         22 $self->_semitones * 100 / log(2);
56             }
57              
58             has _tonic_frequency => (
59             is => 'ro',
60             lazy => 1,
61             builder => 1,
62             );
63             sub _build__tonic_frequency {
64 7     7   75 my $self = shift;
65 7         49 return name2freq($self->_tonic . $self->_octave);
66             }
67              
68             has _note_index => (
69             is => 'ro',
70             lazy => 1,
71             builder => 1,
72             );
73             sub _build__note_index {
74 1     1   67 my $self = shift;
75 1         3 return { map { $_ => eval "$Music::Intervals::Ratios::ratio->{$_}{ratio}" } @{ $self->notes } };
  2         130  
  1         5  
76             }
77              
78             has _ratio_index => (
79             is => 'ro',
80             lazy => 1,
81             builder => 1,
82             );
83             sub _build__ratio_index {
84 10     10   340 my $self = shift;
85 10         23 return { map { $_ => $Music::Intervals::Ratios::ratio->{$_}{ratio} } @{ $self->notes } };
  23         621  
  10         44  
86             }
87              
88             has _ratio_name_index => (
89             is => 'ro',
90             lazy => 1,
91             builder => 1,
92             );
93             sub _build__ratio_name_index {
94 3     3   38 my $self = shift;
95             return {
96 3         434 map { $Music::Intervals::Ratios::ratio->{$_}{ratio} => {
97             symbol => $_,
98             name => $Music::Intervals::Ratios::ratio->{$_}{name} }
99 1524         14547 } keys %$Music::Intervals::Ratios::ratio
100             }
101             }
102              
103              
104             sub integer_notation {
105 6     6 1 2993 my ($self) = @_;
106              
107             my %integer_notation = map { $_ => sprintf '%.0f',
108             $self->_midikey + $self->_semitones
109 12         422 * log( ($self->_tonic_frequency * (eval $self->_ratio_index->{$_})) / $self->_concert ) / log(2)
110 6         13 } @{ $self->notes };
  6         25  
111              
112 6         36 return \%integer_notation;
113             }
114              
115              
116             sub eq_tempered_cents {
117 2     2 1 553 my ($self) = @_;
118              
119 2         6 my %dyads = %{ $self->_dyads };
  2         71  
120              
121             my %eq_tempered_cents = map {
122 2         26 $_ => log( $dyads{$_}->{eq_tempered} ) * $self->_temper
  3         88  
123             } keys %dyads;
124              
125 2         27 return \%eq_tempered_cents;
126             }
127              
128              
129             sub eq_tempered_frequencies {
130 4     4 1 1274 my ($self) = @_;
131              
132             my %eq_tempered_frequencies = map {
133 10   33     225 $_ => name2freq( $_ . $self->_octave ) || $self->_concert * $self->_note_index->{$_}
134 4         11 } @{ $self->notes };
  4         17  
135              
136 4         160 return \%eq_tempered_frequencies;
137             }
138              
139              
140             sub eq_tempered_intervals {
141 4     4 1 533 my ($self) = @_;
142              
143 4         8 my %dyads = %{ $self->_dyads };
  4         326  
144              
145             my %eq_tempered_intervals = map {
146 4         61 $_ => $dyads{$_}->{eq_tempered}
147 9         62 } keys %dyads;
148              
149 4         50 return \%eq_tempered_intervals;
150             }
151              
152              
153             sub natural_cents {
154 4     4 1 560 my ($self) = @_;
155              
156 4         9 my %dyads = %{ $self->_dyads };
  4         182  
157              
158             my %natural_cents = map {
159 4         77 $_ => log( eval $dyads{$_}->{natural} ) * $self->_temper
  9         764  
160             } keys %dyads;
161              
162 4         124 return \%natural_cents;
163             }
164              
165              
166             sub natural_frequencies {
167 2     2 1 20501 my ($self) = @_;
168              
169             my %natural_frequencies = map {
170             $_ => {
171             $self->_tonic_frequency * eval $self->_ratio_index->{$_} . ''
172             => { $self->_ratio_index->{$_} => $Music::Intervals::Ratios::ratio->{$_}{name} }
173             }
174 2         6 } @{ $self->notes };
  4         210  
  2         13  
175              
176 2         65 return \%natural_frequencies;
177             }
178              
179              
180             sub natural_intervals {
181 3     3 1 1667 my ($self) = @_;
182              
183 3         6 my %dyads = %{ $self->_dyads };
  3         111  
184              
185             my %natural_intervals = map {
186 3         12 $_ => {
187             $dyads{$_}->{natural} => $self->_ratio_name_index->{ $dyads{$_}->{natural} }{name}
188             }
189 4         148 } keys %dyads;
190              
191 3         58 return \%natural_intervals;
192             }
193              
194              
195             sub natural_prime_factors {
196 2     2 1 537 my ($self) = @_;
197              
198 2         4 my %dyads = %{ $self->_dyads };
  2         71  
199              
200             my %natural_prime_factors = map {
201 2         26 $_ => {
202             $dyads{$_}->{natural} => $self->ratio_factorize( $dyads{$_}->{natural} )
203 3         14 }
204             } keys %dyads;
205              
206 2         19 return \%natural_prime_factors;
207             }
208              
209              
210             sub dyads {
211 10     10 1 1757 my $self = shift;
212 10         27 my ($c) = @_;
213              
214 10 100       41 return () if @$c <= 1;
215              
216 9         69 my @pairs = combinations( $c, 2 );
217              
218 9         1060 my %dyads;
219 9         28 for my $i (@pairs) {
220             # Construct our "dyadic" fraction.
221 15         1002 my $numerator = Number::Fraction->new( $self->_ratio_index->{ $i->[1] } );
222 15         22695 my $denominator = Number::Fraction->new( $self->_ratio_index->{ $i->[0] } );
223 15         4641 my $fraction = $numerator / $denominator;
224              
225 15         4370 my $str = $fraction->to_string;
226             # Handle the octave.
227 15 100       200 $str .= '/1' if $fraction->to_string eq 2;
228              
229             # Calculate both natural and equal temperament values for our ratio.
230             $dyads{"@$i"} = {
231             natural => $str,
232             # The value is either the known pitch ratio or a calculation
233             eq_tempered =>
234             ( name2freq( $i->[1] . $self->_octave ) || ( $self->_concert * $self->_note_index->{ $i->[1] } ) ) /
235 15   66     282 ( name2freq( $i->[0] . $self->_octave ) || ( $self->_concert * $self->_note_index->{ $i->[0] } ) ),
      33        
236             };
237             }
238              
239 9         659 return %dyads;
240             }
241              
242              
243             sub ratio_factorize {
244 4     4 1 12 my ($self, $dyad) = @_;
245              
246 4         15 my ( $numerator, $denominator ) = split /\//, $dyad;
247 4         22 $numerator = [ prime_factors($numerator) ];
248 4         14 $denominator = [ prime_factors($denominator) ];
249              
250 4         30 return sprintf( '(%s) / (%s)',
251             join( '*', @$numerator ),
252             join( '*', @$denominator )
253             );
254             }
255              
256              
257             sub by_name {
258 2     2 1 901 my ( $self, $name ) = @_;
259 2         19 return $Music::Intervals::Ratios::ratio->{$name};
260             }
261              
262              
263             sub by_ratio {
264 1     1 1 5 my ( $self, $ratio ) = @_;
265 1         39 return $self->_ratio_name_index->{$ratio};
266             }
267              
268              
269             sub by_description {
270 1     1 1 3 my ( $self, $string ) = @_;
271 1         4 $string = lc $string;
272 1         2 my %matches;
273 1         69 for my $ratio (keys %$Music::Intervals::Ratios::ratio) {
274 508         1163 my $found = $Music::Intervals::Ratios::ratio->{$ratio};
275             $matches{$ratio} = $found
276 508 100       1999 if lc($found->{name}) =~ /$string/;
277             }
278 1         50 return \%matches;
279             }
280              
281              
282             sub cope {
283 4     4 1 2735 my ($self) = @_;
284 4         160 my $dyads = $self->_dyads;
285 4         17 my $midi = $self->integer_notation;
286 4         9 my %cope;
287 4         30 my $tension = Music::Tension::Cope->new;
288 4         256 for my $d (keys %$dyads) {
289 4         19 my ($i, $j) = split / /, $d;
290 4         27 $cope{$d} = $tension->vertical([ $midi->{$i}, $midi->{$j} ]);
291             }
292 4         335 return \%cope;
293             }
294              
295              
296             sub tenney {
297 4     4 1 2544 my ($self) = @_;
298 4         146 my $dyads = $self->_dyads;
299 4         39 my %interval;
300 4         14 for my $d (keys %$dyads) {
301 4     1387   418 my $first = first { $Music::Intervals::Ratios::ratio->{$_}{ratio} eq $dyads->{$d}{natural} } keys %$Music::Intervals::Ratios::ratio;
  1387         3294  
302 4 100       185 $first = 0 unless $first;
303             $interval{$first} = {
304 4 100       43 ratio => $first ? $Music::Intervals::Ratios::ratio->{$first}{ratio} : '0/0',
305             dyad => $d,
306             };
307             }
308 4         9 my %tenney;
309 4         14 for my $int (keys %interval) {
310 4         23 my ($i, $j) = split /\//, $interval{$int}{ratio};
311 4 100 66     70 $tenney{ $interval{$int}{dyad} } = !($i && $j) ? 0 : log2($i * $j);
312             }
313 4         30 return \%tenney;
314             }
315              
316              
317             sub suavitatis {
318 4     4 1 2532 my ($self) = @_;
319 4         161 my $dyads = $self->_dyads;
320 4         41 my %interval;
321 4         34 for my $d (keys %$dyads) {
322 4     1387   335 my $first = first { $Music::Intervals::Ratios::ratio->{$_}{ratio} eq $dyads->{$d}{natural} } keys %$Music::Intervals::Ratios::ratio;
  1387         3235  
323 4 100       177 $first = 0 unless $first;
324             $interval{$first} = {
325 4 100       36 ratio => $first ? $Music::Intervals::Ratios::ratio->{$first}{ratio} : '0/0',
326             dyad => $d,
327             };
328             }
329 4         8 my %suavitatis;
330 4         13 for my $int (keys %interval) {
331 4         24 my ($i, $j) = split /\//, $interval{$int}{ratio};
332 4         79 my $lcm = lcm($i, $j);
333 4         77 my @factors = factor($lcm);
334 4         10 my $sum = 0;
335 4         17 $sum += $_ - 1 for @factors;
336 4         19 $suavitatis{ $interval{$int}{dyad} } = 1 + $sum;
337             }
338 4         28 return \%suavitatis;
339             }
340              
341             1;
342              
343             __END__