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   659 use strict;
  1         2  
  1         25  
7 1     1   5 use warnings;
  1         1  
  1         65  
8              
9             our $VERSION = '0.0903';
10              
11 1     1   463 use Algorithm::Combinatorics qw( combinations );
  1         3227  
  1         56  
12 1     1   389 use Math::Factor::XS qw( prime_factors );
  1         25366  
  1         60  
13 1     1   386 use MIDI::Pitch qw( name2freq );
  1         1266  
  1         57  
14 1     1   836 use Moo;
  1         7350  
  1         4  
15 1     1   1845 use Music::Intervals::Ratios;
  1         12  
  1         31  
16 1     1   463 use Number::Fraction ();
  1         42270  
  1         32  
17 1     1   470 use strictures 2;
  1         1518  
  1         111  
18 1     1   1079 use namespace::clean;
  1         7946  
  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   37 my $self = shift;
33 3         12 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   11 my $self = shift;
50 1         18 $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   19 my $self = shift;
60 2         17 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   44 my $self = shift;
70 1         2 return { map { $_ => eval "$Music::Intervals::Ratios::ratio->{$_}{ratio}" } @{ $self->notes } };
  2         79  
  1         4  
71             }
72              
73             has _ratio_index => (
74             is => 'ro',
75             lazy => 1,
76             builder => 1,
77             );
78             sub _build__ratio_index {
79 3     3   127 my $self = shift;
80 3         13 return { map { $_ => $Music::Intervals::Ratios::ratio->{$_}{ratio} } @{ $self->notes } };
  6         172  
  3         9  
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   27 my $self = shift;
90             return {
91 3         212 map { $Music::Intervals::Ratios::ratio->{$_}{ratio} => {
92             symbol => $_,
93             name => $Music::Intervals::Ratios::ratio->{$_}{name} }
94 1341         4943 } keys %$Music::Intervals::Ratios::ratio
95             }
96             }
97              
98              
99             sub integer_notation {
100 2     2 1 274 my ($self) = @_;
101              
102             my %integer_notation = map { $_ => sprintf '%.0f',
103             $self->_midikey + $self->_semitones
104 4         72 * log( ($self->_tonic_frequency * (eval $self->_ratio_index->{$_})) / $self->_concert ) / log(2)
105 2         4 } @{ $self->notes };
  2         7  
106              
107 2         13 return \%integer_notation;
108             }
109              
110              
111             sub eq_tempered_cents {
112 2     2 1 270 my ($self) = @_;
113              
114 2         5 my %dyads = %{ $self->_dyads };
  2         40  
115              
116             my %eq_tempered_cents = map {
117 2         27 $_ => log( $dyads{$_}->{eq_tempered} ) * $self->_temper
  3         54  
118             } keys %dyads;
119              
120 2         19 return \%eq_tempered_cents;
121             }
122              
123              
124             sub eq_tempered_frequencies {
125 4     4 1 281 my ($self) = @_;
126              
127             my %eq_tempered_frequencies = map {
128 10   33     147 $_ => name2freq( $_ . $self->_octave ) || $self->_concert * $self->_note_index->{$_}
129 4         8 } @{ $self->notes };
  4         12  
130              
131 4         106 return \%eq_tempered_frequencies;
132             }
133              
134              
135             sub eq_tempered_intervals {
136 4     4 1 316 my ($self) = @_;
137              
138 4         6 my %dyads = %{ $self->_dyads };
  4         81  
139              
140             my %eq_tempered_intervals = map {
141 4         37 $_ => $dyads{$_}->{eq_tempered}
142 9         20 } keys %dyads;
143              
144 4         62 return \%eq_tempered_intervals;
145             }
146              
147              
148             sub natural_cents {
149 4     4 1 292 my ($self) = @_;
150              
151 4         5 my %dyads = %{ $self->_dyads };
  4         79  
152              
153             my %natural_cents = map {
154 4         47 $_ => log( eval $dyads{$_}->{natural} ) * $self->_temper
  9         383  
155             } keys %dyads;
156              
157 4         82 return \%natural_cents;
158             }
159              
160              
161             sub natural_frequencies {
162 2     2 1 945 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         3 } @{ $self->notes };
  4         109  
  2         10  
170              
171 2         58 return \%natural_frequencies;
172             }
173              
174              
175             sub natural_intervals {
176 3     3 1 861 my ($self) = @_;
177              
178 3         6 my %dyads = %{ $self->_dyads };
  3         61  
179              
180             my %natural_intervals = map {
181 3         6 $_ => {
182             $dyads{$_}->{natural} => $self->_ratio_name_index->{ $dyads{$_}->{natural} }{name}
183             }
184 4         78 } keys %dyads;
185              
186 3         58 return \%natural_intervals;
187             }
188              
189              
190             sub natural_prime_factors {
191 2     2 1 270 my ($self) = @_;
192              
193 2         5 my %dyads = %{ $self->_dyads };
  2         40  
194              
195             my %natural_prime_factors = map {
196 2         18 $_ => {
197             $dyads{$_}->{natural} => $self->ratio_factorize( $dyads{$_}->{natural} )
198 3         11 }
199             } keys %dyads;
200              
201 2         14 return \%natural_prime_factors;
202             }
203              
204              
205             sub dyads {
206 5     5 1 510 my $self = shift;
207 5         19 my ($c) = @_;
208              
209 5 100       14 return () if @$c <= 1;
210              
211 4         15 my @pairs = combinations( $c, 2 );
212              
213 4         611 my %dyads;
214 4         8 for my $i (@pairs) {
215             # Construct our "dyadic" fraction.
216 8         314 my $numerator = Number::Fraction->new( $self->_ratio_index->{ $i->[1] } );
217 8         5426 my $denominator = Number::Fraction->new( $self->_ratio_index->{ $i->[0] } );
218 8         1559 my $fraction = $numerator / $denominator;
219              
220 8         1411 my $str = $fraction->to_string;
221             # Handle the octave.
222 8 100       73 $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     85 ( name2freq( $i->[0] . $self->_octave ) || ( $self->_concert * $self->_note_index->{ $i->[0] } ) ),
      33        
232             };
233             }
234              
235 4         180 return %dyads;
236             }
237              
238              
239             sub ratio_factorize {
240 4     4 1 1358 my ($self, $dyad) = @_;
241              
242 4         11 my ( $numerator, $denominator ) = split /\//, $dyad;
243 4         16 $numerator = [ prime_factors($numerator) ];
244 4         10 $denominator = [ prime_factors($denominator) ];
245              
246 4         63 return sprintf( '(%s) / (%s)',
247             join( '*', @$numerator ),
248             join( '*', @$denominator )
249             );
250             }
251              
252              
253             sub by_name {
254 2     2 1 382 my ( $self, $name ) = @_;
255 2         15 return $Music::Intervals::Ratios::ratio->{$name};
256             }
257              
258              
259             sub by_ratio {
260 1     1 1 3 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         4 $string = lc $string;
268 1         2 my %matches;
269 1         33 for my $ratio (keys %$Music::Intervals::Ratios::ratio) {
270 447         473 my $found = $Music::Intervals::Ratios::ratio->{$ratio};
271             $matches{$ratio} = $found
272 447 100       831 if lc($found->{name}) =~ /$string/;
273             }
274 1         23 return \%matches;
275             }
276              
277             1;
278              
279             __END__