File Coverage

blib/lib/Music/Intervals.pm
Criterion Covered Total %
statement 122 122 100.0
branch 6 6 100.0
condition 4 9 44.4
subroutine 29 29 100.0
pod 13 13 100.0
total 174 179 97.2


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 1     1   662 use strict;
  1         2  
  1         24  
7 1     1   5 use warnings;
  1         1  
  1         34  
8              
9             our $VERSION = '0.0905';
10              
11 1     1   417 use Algorithm::Combinatorics qw( combinations );
  1         2952  
  1         52  
12 1     1   415 use Math::Factor::XS qw( prime_factors );
  1         23005  
  1         56  
13 1     1   368 use MIDI::Pitch qw( name2freq );
  1         934  
  1         49  
14 1     1   442 use Moo;
  1         7061  
  1         3  
15 1     1   1789 use Music::Intervals::Ratios;
  1         4  
  1         58  
16 1     1   478 use Number::Fraction ();
  1         38012  
  1         25  
17 1     1   396 use strictures 2;
  1         1370  
  1         41  
18 1     1   579 use namespace::clean;
  1         6650  
  1         5  
19              
20              
21             has notes => (
22             is => 'ro',
23             default => sub { [qw( C E G )] },
24             );
25              
26             has _dyads => (
27             is => 'ro',
28             lazy => 1,
29             builder => 1,
30             );
31             sub _build__dyads {
32 3     3   33 my $self = shift;
33 3         7 my %dyads = $self->dyads($self->notes);
34 3         13 return \%dyads;
35             }
36              
37             has _octave => ( is => 'ro', default => sub { 4 } );
38             has _concert => ( is => 'ro', default => sub { 440 } );
39             has _tonic => ( is => 'ro', default => sub { 'C' } );
40             has _semitones => ( is => 'ro', default => sub { 12 } );
41             has _midikey => ( is => 'ro', default => sub { 69 } );
42              
43             has _temper => (
44             is => 'ro',
45             lazy => 1,
46             builder => 1,
47             );
48             sub _build__temper {
49 1     1   7 my $self = shift;
50 1         10 $self->_semitones * 100 / log(2);
51             }
52              
53             has _tonic_frequency => (
54             is => 'ro',
55             lazy => 1,
56             builder => 1,
57             );
58             sub _build__tonic_frequency {
59 2     2   14 my $self = shift;
60 2         14 return name2freq($self->_tonic . $self->_octave);
61             }
62              
63             has _note_index => (
64             is => 'ro',
65             lazy => 1,
66             builder => 1,
67             );
68             sub _build__note_index {
69 1     1   34 my $self = shift;
70 1         2 return { map { $_ => eval "$Music::Intervals::Ratios::ratio->{$_}{ratio}" } @{ $self->notes } };
  2         74  
  1         3  
71             }
72              
73             has _ratio_index => (
74             is => 'ro',
75             lazy => 1,
76             builder => 1,
77             );
78             sub _build__ratio_index {
79 3     3   93 my $self = shift;
80 3         4 return { map { $_ => $Music::Intervals::Ratios::ratio->{$_}{ratio} } @{ $self->notes } };
  6         169  
  3         7  
81             }
82              
83             has _ratio_name_index => (
84             is => 'ro',
85             lazy => 1,
86             builder => 1,
87             );
88             sub _build__ratio_name_index {
89 3     3   19 my $self = shift;
90             return {
91 3         141 map { $Music::Intervals::Ratios::ratio->{$_}{ratio} => {
92             symbol => $_,
93             name => $Music::Intervals::Ratios::ratio->{$_}{name} }
94 1524         3987 } keys %$Music::Intervals::Ratios::ratio
95             }
96             }
97              
98              
99             sub integer_notation {
100 2     2 1 261 my ($self) = @_;
101              
102             my %integer_notation = map { $_ => sprintf '%.0f',
103             $self->_midikey + $self->_semitones
104 4         67 * log( ($self->_tonic_frequency * (eval $self->_ratio_index->{$_})) / $self->_concert ) / log(2)
105 2         2 } @{ $self->notes };
  2         7  
106              
107 2         17 return \%integer_notation;
108             }
109              
110              
111             sub eq_tempered_cents {
112 2     2 1 269 my ($self) = @_;
113              
114 2         3 my %dyads = %{ $self->_dyads };
  2         38  
115              
116             my %eq_tempered_cents = map {
117 2         16 $_ => log( $dyads{$_}->{eq_tempered} ) * $self->_temper
  3         50  
118             } keys %dyads;
119              
120 2         19 return \%eq_tempered_cents;
121             }
122              
123              
124             sub eq_tempered_frequencies {
125 4     4 1 271 my ($self) = @_;
126              
127             my %eq_tempered_frequencies = map {
128 10   33     137 $_ => name2freq( $_ . $self->_octave ) || $self->_concert * $self->_note_index->{$_}
129 4         6 } @{ $self->notes };
  4         12  
130              
131 4         108 return \%eq_tempered_frequencies;
132             }
133              
134              
135             sub eq_tempered_intervals {
136 4     4 1 266 my ($self) = @_;
137              
138 4         5 my %dyads = %{ $self->_dyads };
  4         79  
139              
140             my %eq_tempered_intervals = map {
141 4         36 $_ => $dyads{$_}->{eq_tempered}
142 9         18 } keys %dyads;
143              
144 4         46 return \%eq_tempered_intervals;
145             }
146              
147              
148             sub natural_cents {
149 4     4 1 277 my ($self) = @_;
150              
151 4         7 my %dyads = %{ $self->_dyads };
  4         78  
152              
153             my %natural_cents = map {
154 4         48 $_ => log( eval $dyads{$_}->{natural} ) * $self->_temper
  9         366  
155             } keys %dyads;
156              
157 4         68 return \%natural_cents;
158             }
159              
160              
161             sub natural_frequencies {
162 2     2 1 915 my ($self) = @_;
163              
164             my %natural_frequencies = map {
165             $_ => {
166             $self->_tonic_frequency * eval $self->_ratio_index->{$_} . ''
167             => { $self->_ratio_index->{$_} => $Music::Intervals::Ratios::ratio->{$_}{name} }
168             }
169 2         2 } @{ $self->notes };
  4         105  
  2         7  
170              
171 2         42 return \%natural_frequencies;
172             }
173              
174              
175             sub natural_intervals {
176 3     3 1 756 my ($self) = @_;
177              
178 3         4 my %dyads = %{ $self->_dyads };
  3         59  
179              
180             my %natural_intervals = map {
181 3         8 $_ => {
182             $dyads{$_}->{natural} => $self->_ratio_name_index->{ $dyads{$_}->{natural} }{name}
183             }
184 4         77 } keys %dyads;
185              
186 3         44 return \%natural_intervals;
187             }
188              
189              
190             sub natural_prime_factors {
191 2     2 1 271 my ($self) = @_;
192              
193 2         3 my %dyads = %{ $self->_dyads };
  2         38  
194              
195             my %natural_prime_factors = map {
196 2         17 $_ => {
197             $dyads{$_}->{natural} => $self->ratio_factorize( $dyads{$_}->{natural} )
198 3         7 }
199             } keys %dyads;
200              
201 2         15 return \%natural_prime_factors;
202             }
203              
204              
205             sub dyads {
206 5     5 1 478 my $self = shift;
207 5         8 my ($c) = @_;
208              
209 5 100       13 return () if @$c <= 1;
210              
211 4         11 my @pairs = combinations( $c, 2 );
212              
213 4         277 my %dyads;
214 4         4 for my $i (@pairs) {
215             # Construct our "dyadic" fraction.
216 8         297 my $numerator = Number::Fraction->new( $self->_ratio_index->{ $i->[1] } );
217 8         3777 my $denominator = Number::Fraction->new( $self->_ratio_index->{ $i->[0] } );
218 8         1544 my $fraction = $numerator / $denominator;
219              
220 8         1341 my $str = $fraction->to_string;
221             # Handle the octave.
222 8 100       66 $str .= '/1' if $fraction->to_string eq 2;
223              
224             # Calculate both natural and equal temperament values for our ratio.
225             $dyads{"@$i"} = {
226             natural => $str,
227             # The value is either the known pitch ratio or ...
228             eq_tempered =>
229             ( name2freq( $i->[1] . $self->_octave ) || ( $self->_concert * $self->_note_index->{ $i->[1] } ) )
230             /
231 8   66     77 ( name2freq( $i->[0] . $self->_octave ) || ( $self->_concert * $self->_note_index->{ $i->[0] } ) ),
      33        
232             };
233             }
234              
235 4         164 return %dyads;
236             }
237              
238              
239             sub ratio_factorize {
240 4     4 1 1337 my ($self, $dyad) = @_;
241              
242 4         10 my ( $numerator, $denominator ) = split /\//, $dyad;
243 4         13 $numerator = [ prime_factors($numerator) ];
244 4         8 $denominator = [ prime_factors($denominator) ];
245              
246 4         25 return sprintf( '(%s) / (%s)',
247             join( '*', @$numerator ),
248             join( '*', @$denominator )
249             );
250             }
251              
252              
253             sub by_name {
254 2     2 1 376 my ( $self, $name ) = @_;
255 2         11 return $Music::Intervals::Ratios::ratio->{$name};
256             }
257              
258              
259             sub by_ratio {
260 1     1 1 2 my ( $self, $ratio ) = @_;
261 1         21 return $self->_ratio_name_index->{$ratio};
262             }
263              
264              
265             sub by_description {
266 1     1 1 4 my ( $self, $string ) = @_;
267 1         5 $string = lc $string;
268 1         2 my %matches;
269 1         34 for my $ratio (keys %$Music::Intervals::Ratios::ratio) {
270 508         532 my $found = $Music::Intervals::Ratios::ratio->{$ratio};
271             $matches{$ratio} = $found
272 508 100       920 if lc($found->{name}) =~ /$string/;
273             }
274 1         26 return \%matches;
275             }
276              
277             1;
278              
279             __END__