File Coverage

blib/lib/Music/ToRoman.pm
Criterion Covered Total %
statement 148 153 96.7
branch 85 100 85.0
condition 24 27 88.8
subroutine 23 24 95.8
pod 4 4 100.0
total 284 308 92.2


line stmt bran cond sub pod time code
1             package Music::ToRoman;
2             our $AUTHORITY = 'cpan:GENE';
3              
4             # ABSTRACT: Convert notes and chords to Roman numeral notation
5              
6             our $VERSION = '0.2002';
7              
8 18     18   7475030 use strictures 2;
  18         35616  
  18         891  
9 18     18   19323 use List::SomeUtils qw(any first_index);
  18         274321  
  18         2126  
10 18     18   11000 use Moo;
  18         146719  
  18         112  
11 18     18   44647 use Music::Note ();
  18         40314  
  18         678  
12 18     18   9342 use Music::Scales qw(get_scale_notes);
  18         110958  
  18         1792  
13 18     18   9958 use namespace::clean;
  18         223380  
  18         146  
14              
15              
16             has scale_note => (
17             is => 'ro',
18             isa => sub { die 'Invalid note' unless _valid_note( $_[0] ) },
19             default => sub { 'C' },
20             );
21              
22              
23             has scale_name => (
24             is => 'ro',
25             isa => sub { die 'Invalid scale' unless _valid_scale( $_[0] ) },
26             default => sub { 'major' },
27             );
28              
29              
30             has major_tonic => (
31             is => 'ro',
32             isa => sub { die 'Invalid note' unless _valid_note( $_[0] ) },
33             default => sub { 'C' },
34             );
35              
36              
37             has chords => (
38             is => 'ro',
39             isa => sub { die 'Invalid boolean' unless $_[0] == 0 || $_[0] == 1 },
40             default => sub { 1 },
41             );
42              
43              
44             has verbose => (
45             is => 'ro',
46             default => sub { 0 },
47             );
48              
49              
50             sub parse {
51 1947     1947 1 468015 my ( $self, $chord ) = @_;
52              
53 1947 100       5948 die 'No chord to parse'
54             unless $chord;
55              
56 1946         7569 my $note_re = qr/[A-G][#b]?[#b]?/;
57              
58             # Get the roman representation of the scale
59 1946         5671 my @scale = $self->get_scale_mode;
60 1946 50       5612 print "SCALE: @scale\n" if $self->verbose;
61              
62 1946         2829 my @notes;
63              
64             # If the note has a double sharp and is not in major, manually rotate the scale notes, since Music::Scales does not.
65 1946 100 66     15102 if ( $self->scale_note =~ /##/ && $self->scale_name ne 'major' && $self->scale_name ne 'ionian' ) {
      66        
66 42         197 my %modes = (
67             dorian => 2,
68             phrygian => 3,
69             lydian => 4,
70             mixolydian => 5,
71             aeolian => 6,
72             minor => 6,
73             locrian => 7,
74             );
75              
76 42         165 @notes = get_scale_notes( $self->major_tonic, 'major' );
77              
78             # Rotate the major scale to the correct mode
79 42         8393 push @notes, shift @notes for 1 .. $modes{ $self->scale_name } - 1;
80             }
81             else {
82 1904         8017 @notes = get_scale_notes( $self->scale_note, $self->scale_name );
83             }
84 1946 50       424181 print "NOTES: @notes\n" if $self->verbose;
85              
86             # XXX Not working?
87             # my %ss_enharmonics = (
88             # 'C##' => 'D',
89             # 'D##' => 'E',
90             # 'F##' => 'G',
91             # 'G##' => 'A',
92             # 'A##' => 'B',
93             # );
94             # for ( @notes ) {
95             # $_ = $ss_enharmonics{$_}
96             # if $ss_enharmonics{$_};
97             # }
98             #use Data::Dumper;warn(__PACKAGE__,' ',__LINE__," MARK: ",Dumper\@notes);
99              
100             # Convert a diminished chord
101 1946         4330 $chord =~ s/dim/o/;
102              
103             # Get just the note part of the chord name
104 1946         25648 ( my $note = $chord ) =~ s/^($note_re).*$/$1/;
105              
106 1946         11119 my %bb_enharmonics = (
107             Cbb => 'Bb',
108             Dbb => 'C',
109             Ebb => 'D',
110             Fbb => 'Eb',
111             Gbb => 'F',
112             Abb => 'G',
113             Bbb => 'A',
114             );
115              
116 1946 100       6822 $note = $bb_enharmonics{$note}
117             if $note =~ /bb$/;
118              
119             # Get the roman representation based on the scale position
120 1946     8155   12637 my $position = first_index { $_ eq $note } @notes;
  8155         13285  
121              
122 1946 100 100     12496 if ( $position < 0 && ( $note eq 'Cb' || $note eq 'Fb' ) ) {
    100 100        
123 56 100       258 $note = 'B'
124             if $note eq 'Cb';
125 56 100       172 $note = 'E'
126             if $note eq 'Fb';
127 56     329   261 $position = first_index { $_ eq $note } @notes;
  329         501  
128             }
129             elsif ( $note eq 'E#' ) { # XXX Why does this work?
130 62         151 $note = 'F';
131             }
132              
133 1946         3528 my $accidental = '';
134 1946 100 100     5575 if ( $position < 0 && $note =~ /[#b]+$/ ) {
135 208         1007 my $n = Music::Note->new( $note, 'isobase' );
136 208         12679 my $name = $n->format('isobase');
137 208         6905 ( $accidental = $name ) =~ s/^[A-G]([#b]+)$/$1/;
138 208 100       1062 $n->en_eq( $accidental =~ /^#/ ? 'b' : '#' );
139 208         3384 $note = $n->format('isobase');
140 208     1244   4967 $position = first_index { $_ eq $note } @notes;
  1244         1847  
141 208         977 $accidental = '';
142             }
143              
144             # If the note is not in the scale find the new position and accidental
145 1946 100       3840 if ( $position < 0 ) {
146 352         1083 ( $position, $accidental ) = _pos_acc( $note, $position, \@notes );
147             }
148              
149 1946         3835 my $roman = $scale[$position];
150 1946 50       9202 print "ROMAN 1: $roman\n" if $self->verbose;
151              
152             # Get everything but the note part
153 1946         17469 ( my $decorator = $chord ) =~ s/^(?:$note_re)(.*)$/$1/;
154              
155             # Are we minor or diminished?
156 1946 100       5233 my $minor = $decorator =~ /[-moø]/ ? 1 : 0;
157 1946 50       4638 print "CHORD: $chord, NOTE: $note, NEW ACCI: $accidental, DECO: $decorator, MINOR: $minor, POSN: $position\n" if $self->verbose;
158              
159             # Convert the case of the roman representation based on minor or major
160 1946 100       5626 if ( $self->chords ) {
161 719 100 100     2675 $roman = $minor && $decorator !~ /maj/i ? lc($roman) : uc($roman);
162             }
163              
164             # Add any accidental found in a non-scale note
165 1946 100       4059 $roman = $accidental . $roman if $accidental;
166 1946 50       4054 print "ROMAN 2: $roman\n" if $self->verbose;
167              
168             # Handle these unfortunate edge cases:
169 1946         5015 $roman = _up_to_flat( $roman, \@scale );
170 1946 50       4856 print "ROMAN 3: $roman\n" if $self->verbose;
171              
172             # Handle the decorator variations
173 1946 100 100     9791 if ( $decorator =~ /maj/i || $decorator =~ /min/i ) {
    100          
    100          
174 55         155 $decorator = lc $decorator;
175             }
176             elsif ( $decorator =~ /△/ ) {
177 1         5 $decorator =~ s/△/maj/;
178             }
179             elsif ( $decorator =~ /ø/ ) {
180 1         4 $decorator =~ s/ø/7b5/;
181             }
182             else {
183             # Drop the minor and major part of the chord name
184 1889         3706 $decorator =~ s/[-Mm]//i;
185             }
186 1946 50       15388 print "DECO: $decorator\n" if $self->verbose;
187              
188             # A remaining note name is a bass decorator
189 1946 100       9330 if ( $decorator =~ /($note_re)/ ) {
190 207         532 my $name = $1;
191              
192 207     1194   1044 $position = first_index { $_ eq $name } @notes;
  1194         1864  
193 207 50       886 print "BASS NOTE: $name, POSN: $position\n" if $self->verbose;
194              
195 207 100       498 if ( $position >= 0 ) {
196 119         869 $decorator =~ s/$note_re/$scale[$position]/;
197             }
198             else {
199 88         239 ( $position, $accidental ) = _pos_acc( $name, $position, \@notes );
200 88 50       339 print "NEW POSN: $position, ACCI: $accidental\n" if $self->verbose;
201              
202 88         208 my $bass = $accidental . $scale[$position];
203 88         716 $decorator =~ s/$note_re/$bass/;
204              
205             # Handle these unfortunate edge cases
206 88         285 $decorator = _up_to_flat( $decorator, \@scale );
207             }
208 207 50       625 print "NEW DECO: $decorator\n" if $self->verbose;
209             }
210              
211             # Append the remaining decorator to the roman representation
212 1946         3310 $roman .= $decorator;
213              
214 1946         3508 $roman =~ s/bI\b/vii/g;
215 1946         3069 $roman =~ s/bIV\b/iii/g;
216              
217 1946 50       4106 print "ROMAN 4: $roman\n" if $self->verbose;
218              
219 1946         20430 return $roman;
220             }
221              
222              
223             sub get_scale_mode {
224 1946     1946 1 3487 my ($self) = @_;
225              
226 1946         6529 my @scale = qw( I ii iii IV V vi vii ); # Default to major/ionian
227              
228 1946 100 66     23020 if ( $self->scale_name eq 'dorian' ) {
    100          
    100          
    100          
    100          
    100          
229 126         440 @scale = qw( i ii III IV v vi VII );
230             }
231             elsif ( $self->scale_name eq 'phrygian' ) {
232 126         513 @scale = qw( i II III iv v VI vii );
233             }
234             elsif ( $self->scale_name eq 'lydian' ) {
235 126         432 @scale = qw( I II iii iv V vi vii );
236             }
237             elsif ( $self->scale_name eq 'mixolydian' ) {
238 126         433 @scale = qw( I ii iii IV v vi VII );
239             }
240             elsif ( $self->scale_name eq 'minor' || $self->scale_name eq 'aeolian' ) {
241 128         463 @scale = qw( i ii III iv v VI VII );
242             }
243             elsif ( $self->scale_name eq 'locrian' ) {
244 126         450 @scale = qw( i II iii iv V VI vii );
245             }
246              
247 1946         7334 return @scale;
248             }
249              
250              
251             sub get_scale_degree {
252 21     21 1 42099 my ($self, $roman) = @_;
253 21         42 my $degree = 1; # Default to major/ionian
254 21         44 my $type = 'major';
255 21 100       173 if ( $roman =~ /^vii/i ) {
    100          
    100          
    100          
    100          
    100          
256 3         7 $degree = 7;
257             }
258             elsif ( $roman =~ /^vi/i ) {
259 3         8 $degree = 6;
260             }
261             elsif ( $roman =~ /^v/i ) {
262 3         7 $degree = 5;
263             }
264             elsif ( $roman =~ /^iv/i ) {
265 3         6 $degree = 4;
266             }
267             elsif ( $roman =~ /^iii/i ) {
268 3         6 $degree = 3;
269             }
270             elsif ( $roman =~ /^ii/i ) {
271 3         7 $degree = 2;
272             }
273 21 100       113 if ( $roman =~ /o$/ ) {
    100          
274 7         16 $type = 'diminished';
275             }
276             elsif ( $roman =~ /^[iv]+$/ ) {
277 7         12 $type = 'minor';
278             }
279 21         81 return $degree, $type;
280             }
281              
282              
283             sub get_scale_chords {
284 0     0 1 0 my ($self) = @_;
285              
286 0         0 my %diminished = (
287             major => 'vii',
288             ionian => 'vii',
289             dorian => 'vi',
290             phrygian => 'v',
291             lydian => 'iv',
292             mixolydian => 'iii',
293             minor => 'ii',
294             aeolian => 'ii',
295             locrian => 'i',
296             );
297 0 0       0 my @chords = map { m/^$diminished{ $self->scale_name }$/ ? 'dim' : m/^[A-Z]+$/ ? '' : 'm' } $self->get_scale_mode;
  0 0       0  
298              
299 0         0 return @chords;
300             }
301              
302             sub _up_to_flat {
303 2034     2034   4503 my ($numeral, $roman) = @_;
304              
305             # Change a roman sharp to a flat of the succeeding scale position
306 2034     664   4613 $numeral =~ s/#([IV]+)/b$roman->[ ( ( first_index { lc($1) eq lc($_) } @$roman ) + 1 ) % @$roman ]/i;
  664         1771  
307              
308 2034         8822 return $numeral;
309             };
310              
311             sub _pos_acc {
312 440     440   1171 my ( $note, $position, $notes ) = @_;
313              
314 440         713 my $accidental;
315              
316             # If the note has no accidental...
317 440 100       1060 if ( length($note) == 1 ) {
318             # Find the scale position of the closest similar note
319 265     1091   1196 $position = first_index { $_ =~ /^$note/ } @$notes;
  1091         7139  
320              
321             # Get the accidental of the scale note
322 265         2011 ( $accidental = $notes->[$position] ) =~ s/^[A-G](.)$/$1/;
323              
324             # TODO: Explain why.
325 265 100       770 $accidental = $accidental eq '#' ? 'b' : '#';
326             }
327             else {
328             # Enharmonic double sharp equivalents
329 175         1044 my %previous_enharmonics = (
330             'C#' => 'C##',
331             'Db' => 'C##',
332             'F#' => 'F##',
333             'Gb' => 'F##',
334             'G#' => 'G##',
335             'Ab' => 'G##',
336             );
337             $note = $previous_enharmonics{$note}
338 175 100 100 611   980 if exists $previous_enharmonics{$note} && any { $_ =~ /[CFG]##/ } @$notes;
  611         1278  
339              
340             # Get the accidental of the given note
341 175         1283 ( my $letter, $accidental ) = $note =~ /^([A-G])(.+)$/;
342              
343             # Get the scale position of the closest similar note
344 175     767   747 $position = first_index { $_ =~ /^$letter/ } @$notes;
  767         4571  
345              
346 175 100       884 $accidental = $accidental eq '##' ? 'b' : $accidental;
347             }
348              
349 440         1394 return $position, $accidental;
350             }
351              
352             sub _valid_note {
353 302     302   656 my ($note) = @_;
354              
355 302         583 my @valid = ();
356              
357 302         1089 my @notes = 'A' .. 'G';
358              
359 302         1066 push @valid, @notes;
360 302         583 push @valid, map { $_ . '#' } @notes;
  2114         4078  
361 302         666 push @valid, map { $_ . '##' } @notes;
  2114         3941  
362 302         611 push @valid, map { $_ . 'b' } @notes;
  2114         3803  
363              
364 302     2167   1978 return any { $_ eq $note } @valid;
  2167         10767  
365             }
366              
367             sub _valid_scale {
368 151     151   367 my ($name) = @_;
369              
370 151         662 my @valid = qw(
371             ionian
372             major
373             dorian
374             phrygian
375             lydian
376             mixolydian
377             aeolian
378             minor
379             locrian
380             );
381              
382 151     683   712 return any { $_ eq $name } @valid;
  683         4137  
383             }
384              
385             1;
386              
387             __END__