File Coverage

blib/lib/Music/MelodicDevice/Ornamentation.pm
Criterion Covered Total %
statement 142 142 100.0
branch 38 50 76.0
condition 7 13 53.8
subroutine 19 19 100.0
pod 5 5 100.0
total 211 229 92.1


line stmt bran cond sub pod time code
1             package Music::MelodicDevice::Ornamentation;
2             our $AUTHORITY = 'cpan:GENE';
3              
4             # ABSTRACT: Chromatic and diatonic melodic ornamentation
5              
6             our $VERSION = '0.0701';
7              
8 1     1   637 use Carp qw(croak);
  1         2  
  1         46  
9 1     1   392 use Data::Dumper::Compact qw(ddc);
  1         9783  
  1         3  
10 1     1   440 use List::SomeUtils qw(first_index);
  1         9377  
  1         57  
11 1     1   472 use MIDI::Simple ();
  1         15321  
  1         21  
12 1     1   320 use Music::Duration;
  1         273  
  1         26  
13 1     1   312 use Music::Scales qw(get_scale_MIDI is_scale);
  1         3704  
  1         50  
14 1     1   372 use Moo;
  1         6598  
  1         4  
15 1     1   1383 use strictures 2;
  1         1196  
  1         32  
16 1     1   475 use namespace::clean;
  1         5805  
  1         7  
17              
18             with('Music::PitchNum');
19              
20 1     1   236 use constant TICKS => 96;
  1         3  
  1         36  
21 1     1   4 use constant OCTAVES => 10;
  1         1  
  1         1378  
22              
23              
24             has scale_note => (
25             is => 'ro',
26             isa => sub { die "$_[0] is not a valid note" unless $_[0] =~ /^[A-G][#b]?$/ },
27             default => sub { 'C' },
28             );
29              
30              
31             has scale_name => (
32             is => 'ro',
33             isa => sub { die "$_[0] is not a valid scale name" unless is_scale($_[0]) },
34             default => sub { 'chromatic' },
35             );
36              
37             has _scale => (
38             is => 'lazy',
39             init_args => undef,
40             );
41              
42             sub _build__scale {
43 3     3   21 my ($self) = @_;
44              
45 3         7 my @scale = map { get_scale_MIDI($self->scale_note, $_, $self->scale_name) } -1 .. OCTAVES - 1;
  33         1002  
46 3 50       99 print 'Scale: ', ddc(\@scale) if $self->verbose;
47              
48 3         13 return \@scale;
49             }
50              
51              
52             has verbose => (
53             is => 'ro',
54             isa => sub { die "$_[0] is not a valid boolean" unless $_[0] =~ /^[01]$/ },
55             default => sub { 0 },
56             );
57              
58              
59              
60             sub grace_note {
61 18     18 1 9528 my ($self, $duration, $pitch, $offset) = @_;
62              
63 18   50     36 $offset //= 1; # Default one note above
64              
65 18 100       56 my $named = $pitch =~ /[A-G]/ ? 1 : 0;
66              
67 18         32 (my $i, $pitch) = $self->_find_pitch($pitch);
68 18         245 my $grace_note = $self->_scale->[ $i + $offset ];
69              
70 18 100       126 if ($named) {
71 9         24 $pitch = $self->pitchname($pitch);
72 9         111 $grace_note = $self->pitchname($grace_note);
73             }
74              
75             # Compute the ornament durations
76 18         85 my $x = $MIDI::Simple::Length{$duration} * TICKS;
77 18         24 my $y = $MIDI::Simple::Length{xn} * TICKS; # Thirty-second note
78 18         96 my $z = sprintf '%0.f', $x - $y;
79 18 50       49 print "Durations: $x, $y, $z\n" if $self->verbose;
80 18         29 $y = 'd' . $y;
81 18         28 $z = 'd' . $z;
82              
83 18         45 my @grace_note = ([$y, $grace_note], [$z, $pitch]);
84 18 50       41 print 'Grace note: ', ddc(\@grace_note) if $self->verbose;
85              
86 18         55 return \@grace_note;
87             }
88              
89              
90             sub turn {
91 12     12 1 9020 my ($self, $duration, $pitch, $offset) = @_;
92              
93 12         16 my $number = 4; # Number of notes in the ornament
94 12   50     24 $offset //= 1; # Default one note above
95              
96 12 100       39 my $named = $pitch =~ /[A-G]/ ? 1 : 0;
97              
98 12         22 (my $i, $pitch) = $self->_find_pitch($pitch);
99 12         156 my $above = $self->_scale->[ $i + $offset ];
100 12         180 my $below = $self->_scale->[ $i - $offset ];
101              
102 12 100       65 if ($named) {
103 6         14 $pitch = $self->pitchname($pitch);
104 6         62 $above = $self->pitchname($above);
105 6         43 $below = $self->pitchname($below);
106             }
107              
108             # Compute the ornament durations
109 12         53 my $x = $MIDI::Simple::Length{$duration} * TICKS;
110 12         62 my $z = sprintf '%0.f', $x / $number;
111 12 50       32 print "Durations: $x, $z\n" if $self->verbose;
112 12         18 $z = 'd' . $z;
113              
114 12         42 my @turn = ([$z, $above], [$z, $pitch], [$z, $below], [$z, $pitch]);
115 12 50       20 print 'Turn: ', ddc(\@turn) if $self->verbose;
116              
117 12         37 return \@turn;
118             }
119              
120              
121             sub trill {
122 12     12 1 9710 my ($self, $duration, $pitch, $number, $offset) = @_;
123              
124 12   50     24 $number ||= 2; # Number of notes in the ornament
125 12   50     18 $offset //= 1; # Default one note above
126              
127 12 100       39 my $named = $pitch =~ /[A-G]/ ? 1 : 0;
128              
129 12         22 (my $i, $pitch) = $self->_find_pitch($pitch);
130 12         156 my $alt = $self->_scale->[ $i + $offset ];
131              
132 12 100       72 if ($named) {
133 6         14 $pitch = $self->pitchname($pitch);
134 6         64 $alt = $self->pitchname($alt);
135             }
136              
137             # Compute the ornament durations
138 12         54 my $x = $MIDI::Simple::Length{$duration} * TICKS;
139 12         63 my $z = sprintf '%0.f', ($x / $number / 2);
140 12 50       33 print "Durations: $x, $z\n" if $self->verbose;
141 12         19 $z = 'd' . $z;
142              
143 12         19 my @trill;
144              
145 12         59 push @trill, [$z, $pitch], [$z, $alt] for 1 .. $number;
146 12 50       26 print 'Trill: ', ddc(\@trill) if $self->verbose;
147              
148 12         35 return \@trill;
149             }
150              
151              
152             sub mordent {
153 12     12 1 8590 my ($self, $duration, $pitch, $offset) = @_;
154              
155 12         15 my $number = 4; # Finest division needed
156 12   50     23 $offset //= 1; # Default one note above
157              
158 12 100       40 my $named = $pitch =~ /[A-G]/ ? 1 : 0;
159              
160 12         20 (my $i, $pitch) = $self->_find_pitch($pitch);
161 12         154 my $alt = $self->_scale->[ $i + $offset ];
162              
163 12 100       71 if ($named) {
164 6         13 $pitch = $self->pitchname($pitch);
165 6         70 $alt = $self->pitchname($alt);
166             }
167              
168             # Compute the ornament durations
169 12         59 my $x = $MIDI::Simple::Length{$duration} * TICKS;
170 12         58 my $y = sprintf '%0.f', $x / $number;
171 12         38 my $z = sprintf '%0.f', $x - (2 * $y);
172 12 50       35 print "Durations: $x, $y, $z\n" if $self->verbose;
173 12         19 $y = 'd' . $y;
174 12         15 $z = 'd' . $z;
175              
176 12         15 my @mordent;
177              
178 12         34 push @mordent, [$y, $pitch], [$y, $alt], [$z, $pitch];
179 12 50       23 print 'Mordent: ', ddc(\@mordent) if $self->verbose;
180              
181 12         34 return \@mordent;
182             }
183              
184              
185             sub slide {
186 4     4 1 3127 my ($self, $duration, $from, $to) = @_;
187              
188 4         9 my @scale = map { get_scale_MIDI($self->scale_note, $_, 'chromatic') } -1 .. OCTAVES - 1;
  44         1412  
189              
190 4 100       144 my $named = $from =~ /[A-G]/ ? 1 : 0;
191              
192 4         11 (my $i, $from) = $self->_find_pitch($from, \@scale);
193 4         11 (my $j, $to) = $self->_find_pitch($to, \@scale);
194              
195 4         8 my ($start, $end);
196 4 100       7 if ($i <= $j) {
197 2         4 $start = $i;
198 2         3 $end = $j;
199             }
200             else {
201 2         3 $start = $j;
202 2         4 $end = $i;
203             }
204              
205             # Compute the ornament durations
206 4         6 my $x = $MIDI::Simple::Length{$duration} * TICKS;
207 4         5 my $y = $end - $start + 1; # Number of notes in the slide
208 4         22 my $z = sprintf '%0.f', $x / $y;
209 4 50       13 print "Durations: $x, $y, $z\n" if $self->verbose;
210 4         6 $z = 'd' . $z;
211              
212 4         8 my @slide;
213 4 100       6 if ($named) {
214 2         4 @slide = map { [ $z, $self->pitchname($scale[$_]) ] } $start .. $end;
  8         70  
215             }
216             else {
217 2         5 @slide = map { [ $z, $scale[$_] ] } $start .. $end;
  8         15  
218             }
219 4 100       24 @slide = reverse @slide if $j < $i;
220 4 50       8 print 'Slide: ', ddc(\@slide) if $self->verbose;
221              
222 4         31 return \@slide;
223             }
224              
225             sub _find_pitch {
226 62     62   95 my ($self, $pitch, $scale) = @_;
227              
228 62   66     985 $scale //= $self->_scale;
229              
230 62 100       447 $pitch = $self->pitchnum($pitch)
231             if $pitch =~ /[A-G]/;
232              
233 62     3516   1433 my $i = first_index { $_ eq $pitch } @$scale;
  3516         3616  
234 62 50       167 croak "Unknown pitch: $pitch" if $i < 0;
235              
236 62         134 return $i, $pitch;
237             }
238              
239             1;
240              
241             __END__