File Coverage

blib/lib/Music/MelodicDevice/Inversion.pm
Criterion Covered Total %
statement 56 56 100.0
branch 12 16 75.0
condition n/a
subroutine 12 12 100.0
pod 2 2 100.0
total 82 86 95.3


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.0502';
7              
8 1     1   974 use Data::Dumper::Compact qw(ddc);
  1         9503  
  1         4  
9 1     1   537 use List::SomeUtils qw(first_index);
  1         9425  
  1         71  
10 1     1   432 use Music::Scales qw(get_scale_MIDI is_scale);
  1         3772  
  1         75  
11 1     1   447 use Moo;
  1         6017  
  1         5  
12 1     1   1368 use strictures 2;
  1         1160  
  1         32  
13 1     1   493 use namespace::clean;
  1         5917  
  1         6  
14              
15             with('Music::PitchNum');
16              
17 1     1   243 use constant OCTAVES => 10;
  1         2  
  1         639  
18              
19              
20             has scale_note => (
21             is => 'ro',
22             isa => sub { die "$_[0] is not a valid note" unless $_[0] =~ /^[A-G][#b]?$/ },
23             default => sub { 'C' },
24             );
25              
26              
27             has scale_name => (
28             is => 'ro',
29             isa => sub { die "$_[0] is not a valid scale name" unless is_scale($_[0]) },
30             default => sub { 'chromatic' },
31             );
32              
33             has _scale => (
34             is => 'lazy',
35             init_args => undef,
36             );
37              
38             sub _build__scale {
39 2     2   17 my ($self) = @_;
40              
41 2         4 my @scale = map { get_scale_MIDI($self->scale_note, $_, $self->scale_name) } -1 .. OCTAVES - 1;
  22         718  
42 2 50       72 print 'Scale: ', ddc(\@scale) if $self->verbose;
43              
44 2         10 return \@scale;
45             }
46              
47              
48             has verbose => (
49             is => 'ro',
50             isa => sub { die "$_[0] is not a valid boolean" unless $_[0] =~ /^[01]$/ },
51             default => sub { 0 },
52             );
53              
54              
55             sub intervals {
56 12     12 1 2512 my ($self, $notes) = @_;
57              
58 12         31 my @pitches;
59              
60 12         22 for my $note (@$notes) {
61 84         126 my ($i, $pitch) = $self->_find_pitch($note);
62 84         146 push @pitches, $i;
63             }
64 12 50       34 print 'Pitches: ', ddc(\@pitches) if $self->verbose;
65              
66 12         13 my @intervals;
67             my $last;
68              
69 12         21 for my $pitch (@pitches) {
70 84 100       137 if (defined $last) {
71 72         83 push @intervals, $pitch - $last;
72             }
73 84         91 $last = $pitch;
74             }
75 12 50       26 print 'Intervals: ', ddc(\@intervals) if $self->verbose;
76              
77 12         31 return \@intervals;
78             }
79              
80              
81             sub invert {
82 6     6 1 2653 my ($self, $note, $notes) = @_;
83              
84 6 100       28 my $named = $note =~ /[A-G]/ ? 1 : 0;
85              
86 6         13 my @inverted = ($note);
87              
88 6         13 my $intervals = $self->intervals($notes);
89              
90 6         12 for my $interval (@$intervals) {
91             # Find the note that is the opposite interval away from the original note
92 36         50 (my $i, $note) = $self->_find_pitch($note);
93 36         426 my $pitch = $self->_scale->[ $i - $interval ];
94              
95 36 100       216 $note = $named ? $self->pitchname($pitch) : $pitch;
96              
97 36         348 push @inverted, $note;
98             }
99              
100 6 50       18 print 'Inverted: ', ddc(\@inverted) if $self->verbose;
101              
102 6         20 return \@inverted;
103             }
104              
105             sub _find_pitch {
106 120     120   158 my ($self, $pitch) = @_;
107              
108 120 100       329 $pitch = $self->pitchnum($pitch)
109             if $pitch =~ /[A-G]/;
110              
111 120     7015   3892 my $i = first_index { $_ eq $pitch } @{ $self->_scale };
  7015         7438  
  120         1496  
112              
113 120         297 return $i, $pitch;
114             }
115              
116             1;
117              
118             __END__