File Coverage

blib/lib/Music/MelodicDevice/Inversion.pm
Criterion Covered Total %
statement 59 59 100.0
branch 12 16 75.0
condition n/a
subroutine 13 13 100.0
pod 2 2 100.0
total 86 90 95.5


line stmt bran cond sub pod time code
1             package Music::MelodicDevice::Inversion;
2             our $AUTHORITY = 'cpan:GENE';
3              
4             # ABSTRACT: Apply melodic inversion to a series of notes
5              
6             our $VERSION = '0.0501';
7              
8 1     1   1631 use Data::Dumper::Compact qw(ddc);
  1         11261  
  1         3  
9 1     1   1174 use List::SomeUtils qw(first_index);
  1         10973  
  1         60  
10 1     1   378 use Music::Note;
  1         1387  
  1         31  
11 1     1   380 use Music::Scales qw(get_scale_MIDI is_scale);
  1         4408  
  1         64  
12 1     1   1442 use Moo;
  1         7077  
  1         6  
13 1     1   2121 use strictures 2;
  1         1359  
  1         39  
14 1     1   1074 use namespace::clean;
  1         7303  
  1         9  
15              
16 1     1   287 use constant OCTAVES => 10;
  1         3  
  1         760  
17              
18              
19             has scale_note => (
20             is => 'ro',
21             isa => sub { die "$_[0] is not a valid note" unless $_[0] =~ /^[A-G][#b]?$/ },
22             default => sub { 'C' },
23             );
24              
25              
26             has scale_name => (
27             is => 'ro',
28             isa => sub { die "$_[0] is not a valid scale name" unless is_scale($_[0]) },
29             default => sub { 'chromatic' },
30             );
31              
32             has _scale => (
33             is => 'lazy',
34             init_args => undef,
35             );
36              
37             sub _build__scale {
38 2     2   17 my ($self) = @_;
39              
40 2         4 my @scale = map { get_scale_MIDI($self->scale_note, $_, $self->scale_name) } -1 .. OCTAVES - 1;
  22         829  
41 2 50       79 print 'Scale: ', ddc(\@scale) if $self->verbose;
42              
43 2         13 return \@scale;
44             }
45              
46              
47             has verbose => (
48             is => 'ro',
49             isa => sub { die "$_[0] is not a valid boolean" unless $_[0] =~ /^[01]$/ },
50             default => sub { 0 },
51             );
52              
53              
54             sub intervals {
55 12     12 1 2686 my ($self, $notes) = @_;
56              
57 12         18 my @pitches;
58              
59 12         20 for my $note (@$notes) {
60 84         138 my ($i, $pitch) = $self->_find_pitch($note);
61 84         162 push @pitches, $i;
62             }
63 12 50       36 print 'Pitches: ', ddc(\@pitches) if $self->verbose;
64              
65 12         15 my @intervals;
66             my $last;
67              
68 12         20 for my $pitch (@pitches) {
69 84 100       126 if (defined $last) {
70 72         124 push @intervals, $pitch - $last;
71             }
72 84         109 $last = $pitch;
73             }
74 12 50       24 print 'Intervals: ', ddc(\@intervals) if $self->verbose;
75              
76 12         33 return \@intervals;
77             }
78              
79              
80             sub invert {
81 6     6 1 2888 my ($self, $note, $notes) = @_;
82              
83 6 100       25 my $named = $note =~ /[A-G]/ ? 1 : 0;
84              
85 6         15 my @inverted = ($note);
86              
87 6         9 my $intervals = $self->intervals($notes);
88              
89 6         10 for my $interval (@$intervals) {
90             # Find the note that is the opposite interval away from the original note
91 36         67 (my $i, $note) = $self->_find_pitch($note);
92 36         502 my $pitch = $self->_scale->[ $i - $interval ];
93              
94 36 100       241 $note = $named
95             ? Music::Note->new($pitch, 'midinum')->format('ISO')
96             : $pitch;
97              
98 36         1198 push @inverted, $note;
99             }
100              
101 6 50       15 print 'Inverted: ', ddc(\@inverted) if $self->verbose;
102              
103 6         20 return \@inverted;
104             }
105              
106             sub _find_pitch {
107 120     120   179 my ($self, $pitch) = @_;
108              
109 120 100       400 $pitch = Music::Note->new($pitch, 'ISO')->format('midinum')
110             if $pitch =~ /[A-G]/;
111              
112 120     7015   4699 my $i = first_index { $_ eq $pitch } @{ $self->_scale };
  7015         8320  
  120         1858  
113              
114 120         351 return $i, $pitch;
115             }
116              
117             1;
118              
119             __END__