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.0706';
7              
8 2     2   665874 use Moo;
  2         14784  
  2         10  
9 2     2   3693 use strictures 2;
  2         3664  
  2         69  
10 2     2   631 use Carp qw(croak);
  2         3  
  2         89  
11 2     2   1068 use Data::Dumper::Compact qw(ddc);
  2         42010  
  2         27  
12 2     2   1354 use List::SomeUtils qw(first_index);
  2         22481  
  2         198  
13 2     2   1477 use MIDI::Simple ();
  2         40348  
  2         72  
14 2     2   969 use Music::Duration ();
  2         818  
  2         66  
15 2     2   1109 use Music::Scales qw(get_scale_MIDI is_scale);
  2         10326  
  2         248  
16 2     2   1113 use namespace::clean;
  2         21821  
  2         14  
17              
18             with('Music::PitchNum');
19              
20 2     2   578 use constant TICKS => 96;
  2         4  
  2         144  
21 2     2   51 use constant OCTAVES => 10;
  2         5  
  2         3937  
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   36 my ($self) = @_;
44              
45 3         10 my @scale = map { get_scale_MIDI($self->scale_note, $_, $self->scale_name) } -1 .. OCTAVES - 1;
  33         1672  
46 3 50       212 print 'Scale: ', ddc(\@scale) if $self->verbose;
47              
48 3         25 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 25929 my ($self, $duration, $pitch, $offset) = @_;
62              
63 18   50     98 $offset //= 1; # Default one note above
64              
65 18 100       107 my $named = $pitch =~ /[A-G]/ ? 1 : 0;
66              
67 18         72 (my $i, $pitch) = $self->_find_pitch($pitch);
68 18         1053 my $grace_note = $self->_scale->[ $i + $offset ];
69              
70 18 100       184 if ($named) {
71 9         52 $pitch = $self->pitchname($pitch);
72 9         181 $grace_note = $self->pitchname($grace_note);
73             }
74              
75             # Compute the ornament durations
76 18         135 my $x = $MIDI::Simple::Length{$duration} * TICKS;
77 18         52 my $y = $MIDI::Simple::Length{yn} * TICKS; # 64th note
78 18         195 my $z = sprintf '%0.f', $x - $y;
79 18 50       72 print "Durations: $x, $y, $z\n" if $self->verbose;
80 18         43 $y = 'd' . $y;
81 18         33 $z = 'd' . $z;
82              
83 18         103 my @grace_note = ([$y, $grace_note], [$z, $pitch]);
84 18 50       55 print 'Grace note: ', ddc(\@grace_note) if $self->verbose;
85              
86 18         95 return \@grace_note;
87             }
88              
89              
90             sub turn {
91 12     12 1 24189 my ($self, $duration, $pitch, $offset) = @_;
92              
93 12         50 my $number = 4; # Number of notes in the ornament
94 12   50     68 $offset //= 1; # Default one note above
95              
96 12 100       102 my $named = $pitch =~ /[A-G]/ ? 1 : 0;
97              
98 12         51 (my $i, $pitch) = $self->_find_pitch($pitch);
99 12         378 my $above = $self->_scale->[ $i + $offset ];
100 12         352 my $below = $self->_scale->[ $i - $offset ];
101              
102 12 100       100 if ($named) {
103 6         26 $pitch = $self->pitchname($pitch);
104 6         146 $above = $self->pitchname($above);
105 6         73 $below = $self->pitchname($below);
106             }
107              
108             # Compute the ornament durations
109 12         93 my $x = $MIDI::Simple::Length{$duration} * TICKS;
110 12         106 my $z = sprintf '%0.f', $x / $number;
111 12 50       60 print "Durations: $x, $z\n" if $self->verbose;
112 12         28 $z = 'd' . $z;
113              
114 12         138 my @turn = ([$z, $above], [$z, $pitch], [$z, $below], [$z, $pitch]);
115 12 50       39 print 'Turn: ', ddc(\@turn) if $self->verbose;
116              
117 12         73 return \@turn;
118             }
119              
120              
121             sub trill {
122 12     12 1 23328 my ($self, $duration, $pitch, $number, $offset) = @_;
123              
124 12   50     43 $number ||= 2; # Number of notes in the ornament
125 12   50     34 $offset //= 1; # Default one note above
126              
127 12 100       100 my $named = $pitch =~ /[A-G]/ ? 1 : 0;
128              
129 12         40 (my $i, $pitch) = $self->_find_pitch($pitch);
130 12         332 my $alt = $self->_scale->[ $i + $offset ];
131              
132 12 100       114 if ($named) {
133 6         26 $pitch = $self->pitchname($pitch);
134 6         113 $alt = $self->pitchname($alt);
135             }
136              
137             # Compute the ornament durations
138 12         110 my $x = $MIDI::Simple::Length{$duration} * TICKS;
139 12         152 my $z = sprintf '%0.f', ($x / $number / 2);
140 12 50       50 print "Durations: $x, $z\n" if $self->verbose;
141 12         25 $z = 'd' . $z;
142              
143 12         21 my @trill;
144              
145 12         110 push @trill, [$z, $pitch], [$z, $alt] for 1 .. $number;
146 12 50       42 print 'Trill: ', ddc(\@trill) if $self->verbose;
147              
148 12         91 return \@trill;
149             }
150              
151              
152             sub mordent {
153 12     12 1 20797 my ($self, $duration, $pitch, $offset) = @_;
154              
155 12         26 my $number = 4; # Finest division needed
156 12   50     64 $offset //= 1; # Default one note above
157              
158 12 100       74 my $named = $pitch =~ /[A-G]/ ? 1 : 0;
159              
160 12         44 (my $i, $pitch) = $self->_find_pitch($pitch);
161 12         892 my $alt = $self->_scale->[ $i + $offset ];
162              
163 12 100       131 if ($named) {
164 6         27 $pitch = $self->pitchname($pitch);
165 6         197 $alt = $self->pitchname($alt);
166             }
167              
168             # Compute the ornament durations
169 12         99 my $x = $MIDI::Simple::Length{$duration} * TICKS;
170 12         127 my $y = sprintf '%0.f', $x / $number;
171 12         56 my $z = sprintf '%0.f', $x - (2 * $y);
172 12 50       47 print "Durations: $x, $y, $z\n" if $self->verbose;
173 12         24 $y = 'd' . $y;
174 12         43 $z = 'd' . $z;
175              
176 12         22 my @mordent;
177              
178 12         95 push @mordent, [$y, $pitch], [$y, $alt], [$z, $pitch];
179 12 50       37 print 'Mordent: ', ddc(\@mordent) if $self->verbose;
180              
181 12         78 return \@mordent;
182             }
183              
184              
185             sub slide {
186 4     4 1 7928 my ($self, $duration, $from, $to) = @_;
187              
188 4         15 my @scale = map { get_scale_MIDI($self->scale_note, $_, 'chromatic') } -1 .. OCTAVES - 1;
  44         2494  
189              
190 4 100       230 my $named = $from =~ /[A-G]/ ? 1 : 0;
191              
192 4         16 (my $i, $from) = $self->_find_pitch($from, \@scale);
193 4         16 (my $j, $to) = $self->_find_pitch($to, \@scale);
194              
195 4         10 my ($start, $end);
196 4 100       14 if ($i <= $j) {
197 2         5 $start = $i;
198 2         4 $end = $j;
199             }
200             else {
201 2         170 $start = $j;
202 2         6 $end = $i;
203             }
204              
205             # Compute the ornament durations
206 4         16 my $x = $MIDI::Simple::Length{$duration} * TICKS;
207 4         9 my $y = $end - $start + 1; # Number of notes in the slide
208 4         40 my $z = sprintf '%0.f', $x / $y;
209 4 50       19 print "Durations: $x, $y, $z\n" if $self->verbose;
210 4         10 $z = 'd' . $z;
211              
212 4         8 my @slide;
213 4 100       10 if ($named) {
214 2         9 @slide = map { [ $z, $self->pitchname($scale[$_]) ] } $start .. $end;
  8         151  
215             }
216             else {
217 2         7 @slide = map { [ $z, $scale[$_] ] } $start .. $end;
  8         26  
218             }
219 4 100       36 @slide = reverse @slide if $j < $i;
220 4 50       13 print 'Slide: ', ddc(\@slide) if $self->verbose;
221              
222 4         52 return \@slide;
223             }
224              
225             sub _find_pitch {
226 62     62   175 my ($self, $pitch, $scale) = @_;
227              
228 62   66     2175 $scale //= $self->_scale;
229              
230 62 100       834 $pitch = $self->pitchnum($pitch)
231             if $pitch =~ /[A-G]/;
232              
233 62     3516   3519 my $i = first_index { $_ eq $pitch } @$scale;
  3516         6220  
234 62 50       311 croak "Unknown pitch: $pitch" if $i < 0;
235              
236 62         277 return $i, $pitch;
237             }
238              
239             1;
240              
241             __END__