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   597 use strict;
  1         2  
  1         24  
7 1     1   4 use warnings;
  1         2  
  1         33  
8              
9             our $VERSION = '0.0904';
10              
11 1     1   431 use Algorithm::Combinatorics qw( combinations );
  1         3029  
  1         53  
12 1     1   372 use Math::Factor::XS qw( prime_factors );
  1         24143  
  1         54  
13 1     1   364 use MIDI::Pitch qw( name2freq );
  1         909  
  1         50  
14 1     1   774 use Moo;
  1         7067  
  1         3  
15 1     1   1792 use Music::Intervals::Ratios;
  1         27  
  1         65  
16 1     1   454 use Number::Fraction ();
  1         38803  
  1         27  
17 1     1   401 use strictures 2;
  1         1385  
  1         44  
18 1     1   1067 use namespace::clean;
  1         6984  
  1         6  
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   22 my $self = shift;
33 3         8 my %dyads = $self->dyads($self->notes);
34 3         12 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   8 my $self = shift;
50 1         8 $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   15 my $self = shift;
60 2         12 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   38 my $self = shift;
70 1         2 return { map { $_ => eval "$Music::Intervals::Ratios::ratio->{$_}{ratio}" } @{ $self->notes } };
  2         109  
  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   98 my $self = shift;
80 3         3 return { map { $_ => $Music::Intervals::Ratios::ratio->{$_}{ratio} } @{ $self->notes } };
  6         161  
  3         8  
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   21 my $self = shift;
90             return {
91 3         168 map { $Music::Intervals::Ratios::ratio->{$_}{ratio} => {
92             symbol => $_,
93             name => $Music::Intervals::Ratios::ratio->{$_}{name} }
94 1524         4227 } keys %$Music::Intervals::Ratios::ratio
95             }
96             }
97              
98              
99             sub integer_notation {
100 2     2 1 258 my ($self) = @_;
101              
102             my %integer_notation = map { $_ => sprintf '%.0f',
103             $self->_midikey + $self->_semitones
104 4         71 * log( ($self->_tonic_frequency * (eval $self->_ratio_index->{$_})) / $self->_concert ) / log(2)
105 2         4 } @{ $self->notes };
  2         5  
106              
107 2         10 return \%integer_notation;
108             }
109              
110              
111             sub eq_tempered_cents {
112 2     2 1 263 my ($self) = @_;
113              
114 2         3 my %dyads = %{ $self->_dyads };
  2         39  
115              
116             my %eq_tempered_cents = map {
117 2         17 $_ => log( $dyads{$_}->{eq_tempered} ) * $self->_temper
  3         51  
118             } keys %dyads;
119              
120 2         15 return \%eq_tempered_cents;
121             }
122              
123              
124             sub eq_tempered_frequencies {
125 4     4 1 267 my ($self) = @_;
126              
127             my %eq_tempered_frequencies = map {
128 10   33     138 $_ => name2freq( $_ . $self->_octave ) || $self->_concert * $self->_note_index->{$_}
129 4         6 } @{ $self->notes };
  4         12  
130              
131 4         103 return \%eq_tempered_frequencies;
132             }
133              
134              
135             sub eq_tempered_intervals {
136 4     4 1 301 my ($self) = @_;
137              
138 4         4 my %dyads = %{ $self->_dyads };
  4         89  
139              
140             my %eq_tempered_intervals = map {
141 4         35 $_ => $dyads{$_}->{eq_tempered}
142 9         19 } keys %dyads;
143              
144 4         46 return \%eq_tempered_intervals;
145             }
146              
147              
148             sub natural_cents {
149 4     4 1 272 my ($self) = @_;
150              
151 4         7 my %dyads = %{ $self->_dyads };
  4         78  
152              
153             my %natural_cents = map {
154 4         37 $_ => log( eval $dyads{$_}->{natural} ) * $self->_temper
  9         368  
155             } keys %dyads;
156              
157 4         61 return \%natural_cents;
158             }
159              
160              
161             sub natural_frequencies {
162 2     2 1 916 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         4 } @{ $self->notes };
  4         95  
  2         5  
170              
171 2         55 return \%natural_frequencies;
172             }
173              
174              
175             sub natural_intervals {
176 3     3 1 795 my ($self) = @_;
177              
178 3         6 my %dyads = %{ $self->_dyads };
  3         60  
179              
180             my %natural_intervals = map {
181 3         9 $_ => {
182             $dyads{$_}->{natural} => $self->_ratio_name_index->{ $dyads{$_}->{natural} }{name}
183             }
184 4         77 } keys %dyads;
185              
186 3         43 return \%natural_intervals;
187             }
188              
189              
190             sub natural_prime_factors {
191 2     2 1 267 my ($self) = @_;
192              
193 2         4 my %dyads = %{ $self->_dyads };
  2         37  
194              
195             my %natural_prime_factors = map {
196 2         18 $_ => {
197             $dyads{$_}->{natural} => $self->ratio_factorize( $dyads{$_}->{natural} )
198 3         8 }
199             } keys %dyads;
200              
201 2         13 return \%natural_prime_factors;
202             }
203              
204              
205             sub dyads {
206 5     5 1 473 my $self = shift;
207 5         6 my ($c) = @_;
208              
209 5 100       14 return () if @$c <= 1;
210              
211 4         13 my @pairs = combinations( $c, 2 );
212              
213 4         313 my %dyads;
214 4         6 for my $i (@pairs) {
215             # Construct our "dyadic" fraction.
216 8         303 my $numerator = Number::Fraction->new( $self->_ratio_index->{ $i->[1] } );
217 8         4565 my $denominator = Number::Fraction->new( $self->_ratio_index->{ $i->[0] } );
218 8         1579 my $fraction = $numerator / $denominator;
219              
220 8         1383 my $str = $fraction->to_string;
221             # Handle the octave.
222 8 100       69 $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     81 ( name2freq( $i->[0] . $self->_octave ) || ( $self->_concert * $self->_note_index->{ $i->[0] } ) ),
      33        
232             };
233             }
234              
235 4         163 return %dyads;
236             }
237              
238              
239             sub ratio_factorize {
240 4     4 1 1322 my ($self, $dyad) = @_;
241              
242 4         9 my ( $numerator, $denominator ) = split /\//, $dyad;
243 4         12 $numerator = [ prime_factors($numerator) ];
244 4         10 $denominator = [ prime_factors($denominator) ];
245              
246 4         23 return sprintf( '(%s) / (%s)',
247             join( '*', @$numerator ),
248             join( '*', @$denominator )
249             );
250             }
251              
252              
253             sub by_name {
254 2     2 1 371 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         22 return $self->_ratio_name_index->{$ratio};
262             }
263              
264              
265             sub by_description {
266 1     1 1 10 my ( $self, $string ) = @_;
267 1         4 $string = lc $string;
268 1         2 my %matches;
269 1         35 for my $ratio (keys %$Music::Intervals::Ratios::ratio) {
270 508         559 my $found = $Music::Intervals::Ratios::ratio->{$ratio};
271             $matches{$ratio} = $found
272 508 100       946 if lc($found->{name}) =~ /$string/;
273             }
274 1         24 return \%matches;
275             }
276              
277             1;
278              
279             __END__