File Coverage

blib/lib/Music/Scales.pm
Criterion Covered Total %
statement 109 119 91.6
branch 37 50 74.0
condition 20 26 76.9
subroutine 14 15 93.3
pod 6 10 60.0
total 186 220 84.5


line stmt bran cond sub pod time code
1             package Music::Scales;
2 5     5   164909 use strict;
  5         15  
  5         247  
3 5     5   6609 use Text::Abbrev;
  5         335  
  5         539  
4              
5             BEGIN {
6 5     5   32 use Exporter ();
  5         15  
  5         108  
7 5     5   106 use vars qw ($VERSION @ISA @EXPORT);
  5         9  
  5         588  
8 5     5   12 $VERSION = 0.07;
9 5         73 @ISA = qw (Exporter);
10 5         13618 @EXPORT = qw (get_scale_notes get_scale_nums get_scale_offsets is_scale get_scale_PDL get_scale_MIDI);
11             }
12              
13              
14             =head1 NAME
15              
16             Scales - supply necessary notes / offsets for musical scales
17              
18             =head1 SYNOPSIS
19              
20             use Music::Scales;
21              
22             my @maj = get_scale_notes('Eb'); # defaults to major
23             print join(" ",@maj); # "Eb F G Ab Bb C D"
24             my @blues = get_scale_nums('bl'); # 'bl','blu','blue','blues'
25             print join(" ",@blues); # "0 3 5 6 7 10"
26             my %min = get_scale_offsets ('G','mm',1); # descending melodic minor
27             print map {"$_=$min{$_} "} sort keys %min; # "A=0 B=-1 C=0 D=0 E=-1 F=0 G=0"
28              
29              
30             =head1 DESCRIPTION
31              
32             Given a keynote A-G(#/b) and a scale-name, will return the scale,
33             either as an array of notenames or as a hash of semitone-offsets for each note.
34              
35             =head1 METHODS
36              
37             =head2 get_scale_nums($scale[,$descending])
38              
39             returns an array of semitone offsets for the requested scale, ascending/descending the given scale for one octave.
40             The descending flag determines the direction of the scale, and also affects those scales (such as melodic minor) where the notes vary depending upon the direction.
41             Scaletypes and valid values for $scale are listed below.
42              
43             =head2 get_scale_notes($notename[,$scale,$descending,$keypref])
44              
45             returns an array of notenames, starting from the given keynote.
46             Enharmonic equivalencies (whether to use F# or Gb, for instance) are calculated based on the keynote and the scale. Basically, it attempts to do the Right Thing if the scale is an 8-note one,
47             (the 7th in G harmonic minor being F# rather than Gb, although G minor is a 'flat' key), but for any other scales, (Chromatic, blues etc.) it picks equivalencies based upon the keynote.
48             This can be overidden with $keypref, setting to be either '#' or 'b' for sharps and flats respectively. Cruftiness abounds here :)
49              
50             =head2 get_scale_offsets($notename[,$scale,$descending,$keypref])
51              
52             as get_scale_notes(), except it returns a hash of notenames with the values being a semitone offset (-1, 0 or 1) as shown in the synopsis.
53              
54             =head2 get_scale_MIDI($notename,$octave[,$scale,$descending])
55              
56             as get_scale_notes(), but returns an array of MIDI note-numbers, given an octave number (-1..9).
57              
58             =head2 get_scale_PDL($notename,$octave[,$scale,$descending])
59              
60             as get_scale_MIDI(), but returns an array of PDL-format notes.
61              
62             =head2 is_scale($scalename)
63              
64             returns true if $scalename is a valid scale name used in this module.
65              
66             =head1 SCALES
67              
68             Scales can be passed either by name or number.
69             The default scale is 'major' if none / invalid is given.
70             Text::Abbrev is used on scalenames, so they can be as abbreviated as unambiguously possible ('dor','io' etc.).
71             Other abbreviations are shown in brackets.
72              
73             1 ionian / major / hypolydian
74             2 dorian / hypmixolydian
75             3 phrygian / hypoaeolian
76             4 lydian / hypolocrian
77             5 mixolydian / hypoionian
78             6 aeolian / hypodorian / minor / m
79             7 locrian / hypophrygian
80             8 harmonic minor / hm
81             9 melodic minor / mm
82             10 blues
83             11 pentatonic (pmajor)
84             12 chromatic
85             13 diminished
86             14 wholetone
87             15 augmented
88             16 hungarian minor
89             17 3 semitone
90             18 4 semitone
91             19 neapolitan minor (nmin)
92             20 neapolitan major (nmaj)
93             21 todi
94             22 marva
95             23 persian
96             24 oriental
97             25 romanian
98             26 pelog
99             27 iwato
100             28 hirajoshi
101             29 egyptian
102             30 pentatonic minor (pminor)
103              
104             =head1 EXAMPLE
105              
106             This will print every scale in every key, adjusting the enharmonic equivalents accordingly.
107              
108             foreach my $note qw (C C# D D# E F F# G G# A A# B) {
109             foreach my $mode (1..30) {
110             my @notes = get_scale_notes($note,$mode);
111             push @notes, get_scale_notes($note,$mode,1); # descending
112             print join(" ",@notes),"\n";
113             }
114             }
115              
116              
117             =head1 TODO
118            
119             Add further range of scales from http://www.cs.ruu.nl/pub/MIDI/DOC/scales.zip
120             Improve enharmonic eqivalents.
121             Microtones
122             Generate ragas,gamelan etc. - maybe needs an 'ethnic' subset of modules
123              
124             =head1 AUTHOR
125              
126             Ben Daglish (bdaglish@surfnet-ds.co.uk)
127              
128             Thanks to Steve Hay for pointing out my 'minor' mix-up and many suggestions.
129             Thanks also to Gene Boggs for the 'is_scale' suggestion / code.
130              
131             =head1 BUGS
132            
133             A few enharmonic problems still...
134              
135             All feedback most welcome.
136              
137             =head1 COPYRIGHT
138              
139             Copyright (c) 2003, Ben Daglish. All Rights Reserved.
140             This program is free software; you can redistribute
141             it and/or modify it under the same terms as Perl itself.
142              
143             The full text of the license can be found in the
144             LICENSE file included with this module.
145              
146              
147             =head1 SEE ALSO
148              
149             PDL::Audio::Scale, perl(1).
150              
151             =cut
152              
153             my %modes = qw(ionian 1 major 1 hypolydian 1 dorian 2 hypomyxolydian 2
154             phrygian 3 hypoaeolian 3 lydian 4 hypolocrian 4 mixolydian 5 hypoionian 5
155             aeolian 6 minor 6 m 6 hypodorian 6 locrian 7 hypophrygian 7
156             harmonicminor 8 hm 8 melodicminor 9 mm 9
157             blues 10 pentatonic 11 pmaj 11 chromatic 12 diminished 13 wholetone 14
158             augmented 15 hungarianminor 16 3semitone 17 4semitone 18
159             neapolitanminor 19 nmin 19 neapolitanmajor 20 nmaj 20
160             todi 21 marva 22 persian 23 oriental 24 romanian 25 pelog 26
161             iwato 27 hirajoshi 28 egyptian 29 pminor 30 pentatonicminor 30
162             );
163              
164             my %abbrevs = abbrev(keys %modes);
165             while (my ($k,$v) = each %abbrevs) {
166             $modes{$k} = $modes{$v};
167             }
168              
169             my @scales=([0,2,4,5,7,9,11], # Ionian(1)
170             [0,2,3,5,7,9,10], # Dorian (2)
171             [0,1,3,5,7,8,10], # Phrygian (3)
172             [0,2,4,6,7,9,11], # Lydian (4)
173             [0,2,4,5,7,9,10], # Mixolydian (5)
174             [0,2,3,5,7,8,10], # Aeolian (6)
175             [0,1,3,5,6,8,10], # Locrian (7)
176             [0,2,3,5,7,8,11], # Harmonic Minor (8)
177             [0,2,3,5,7,9,11], # Melodic Minor (9)
178             [0,3,5,6,7,10], # Blues (10)
179             [0,2,4,7,9], # Pentatonic (11)
180             [0,1,2,3,4,5,6,7,8,9,10,11],# Chromatic (12)
181             [0,2,3,5,6,8,9,11], # Diminished (13)
182             [0,2,4,6,8,10], # Whole tone(14)
183             [0,3,4,7,8,11], # Augmented (15)
184             [0,2,3,6,7,8,11], # Hungarian minor (16)
185             [0,3,6,9], # 3 semitone (dimished arpeggio) (17)
186             [0,4,8], # 4 semitone (augmented arpeggio) (18)
187             [0,1,3,5,7,8,11], # Neapolitan minor (19)
188             [0,1,3,5,7,9,11], # Neapolitan major (20)
189             [0,1,3,6,7,8,11], # Todi (Indian) (21)
190             [0,1,4,6,7,9,11], # Marva (Indian) (22)
191             [0,1,4,5,6,8,11], # Persian (23)
192             [0,1,4,5,6,9,10], # Oriental (24)
193             [0,2,3,6,7,9,10], # Romanian (25)
194             [0,1,3,7,10], # Pelog (Balinese) (26)
195             [0,1,5,6,10], # Iwato (Japanese) (27)
196             [0,2,3,7,8], # Hirajoshi (Japanese) (28)
197             [0,2,5,7,10], # Egyptian (29)
198             [0,3,5,7,10], # Pentatonic Minor (30)
199             );
200              
201             sub get_scale_nums {
202 14     14 1 19 my ($mode,$descending) = @_;
203 14         33 $mode = get_mode($mode);
204 14         19 my @dists = @{$scales[$mode-1]};
  14         54  
205 14 50 66     50 if ($descending && $mode == 9) {
206 0         0 $dists[5]-- ;$dists[6]--;
  0         0  
207             }
208 14 100       68 ($descending) ? reverse @dists : @dists;
209             }
210              
211             sub get_scale_offsets {
212 0     0 1 0 my @scale = get_scale_notes(@_);
213 0         0 my %key_alts = qw(C 0 D 0 E 0 F 0 G 0 A 0 B 0);
214 0         0 foreach (@scale) {
215 0 0       0 $key_alts{$_}++ if s/#//;
216 0 0       0 $key_alts{$_}-- if s/b//;
217             }
218 0         0 %key_alts;
219             }
220              
221             sub get_mode {
222 23   100 23 0 56 my $mode = shift() || 1;
223 23         167 $mode =~ s/[^a-zA-Z0-9]//g;
224 23 100       95 $mode = $modes{lc($mode)} unless $mode =~/^[0-9]+$/;
225 23 50 33     125 ($mode && ($mode <= @scales)) ? $mode : 1;
226             }
227              
228             sub note_to_num {
229 46     46 0 54 my $note = shift();
230 46         492 my %note2num = ('A','0','A#','1','BB','1','B','2','C','3','C#','4','DB','4','D','5','D#','6','EB','6','E','7','F','8','F#','9','GB','9','G','10','G#','11','AB','11');
231 46 50       117 return $note if ($note =~/^[0-9]+$/);
232 46 50       268 (defined $note2num{uc($note)}) ? $note2num{uc($note)} : 0;
233             }
234              
235             sub note_to_MIDI {
236 3     3 0 4 my ($note,$octave) = @_;
237 3         9 ((note_to_num($note)+9) % 12) + (12 * ++$octave );
238             }
239              
240             sub get_scale_MIDI {
241 3     3 1 14 my ($note,$octave,$mode,$descending) = @_;
242 3         8 my $basenum = note_to_MIDI($note,$octave);
243 3         11 return map {$basenum + $_} get_scale_nums($mode,$descending);
  21         51  
244             }
245              
246             sub get_scale_PDL {
247 4     4 1 16 my ($note,$octave,$mode,$descending,$keypref) = @_;
248 4         11 scale_to_PDL($octave,get_scale_notes($note,$mode,$descending,$keypref));
249             }
250              
251             sub get_scale_notes {
252 9     9 1 26 my ($keynote,$mode,$descending,$keypref) = @_;
253 9         35 my @notes = ('A'..'G');
254 9         20 my @nums = (2,1,2,2,1,2,2);
255              
256 9         23 $keynote =~ s/^[a-z]/\u$&/;
257 9 50       27 $keypref='' unless defined $keypref;
258 9         29 my $keynum = note_to_num(uc($keynote));
259 9         25 $mode = get_mode($mode);
260 9         21 my @dists = get_scale_nums($mode,$descending);
261 9 100       24 @dists = reverse @dists if $descending;
262 9         15 my @scale = map {($_+$keynum-$dists[0])%12} @dists;
  67         121  
263 9 100 66     57 $keypref='b' if (!$keypref && $descending && $mode == 12); #prefer flat descending chromatic
      100        
264              
265 9         97 my %num2note = (0,'A',1,'A#',2,'B',3,'C',4,'C#',5,'D',6,'D#',7,'E',8,'F',9,'F#',10,'G',11,'G#');
266 9 100 100     65 %num2note = (0,'A',1,'Bb',2,'B',3,'C',4,'Db',5,'D',6,'Eb',7,'E',8,'F',9,'Gb',10,'G',11,'Ab') if (($keypref eq 'b') || ($keynote =~ /.b/i));
267 9         16 my @mscale = $keynote;
268 9 100       23 if (@scale > 7) { # we're not bothered by niceties, so just convert
269 2         5 @mscale = map {$num2note{$_}} @scale;
  24         81  
270             }
271             else {
272 7 50       19 $keynote = $num2note{$keynote} if $keynote =~/^[0-9]+$/;
273 7         9 my $kk = $keynote; $kk =~ s/b|\#//; $kk = ord($kk) - ord('A');
  7         31  
  7         10  
274 7         18 foreach(0..$kk-1) {# rotate to keynote
275 27         60 push @notes,shift(@notes);
276 27         41 push @nums,shift(@nums);
277             }
278 7         34 push @notes,shift(@notes);
279 7         10 shift(@dists);
280 7         11 my $cu = shift(@nums);
281 7 100       20 $cu++ if ($keynote =~ /b/);
282 7 100       19 $cu-- if ($keynote =~ /#/);
283 7         12 foreach (@dists) {
284 36         43 my $m = $_ - $cu;
285 36         42 my $ns = shift(@nums);
286 36         42 push @nums,$ns;
287 36         47 my $n = shift(@notes);
288 36         45 push @notes,$n;
289 36   100     198 while (abs($m) > 2 || (@scale < 7 && abs($m) >= $ns)) { # step up/down notes, 'reducing' flats/sharps
      33        
290 6         8 $n = shift(@notes); push @notes,$n;
  6         7  
291 6 50       12 if ($m > 0) {$m -= $ns;$cu += $ns }
  6 0       5  
  6         8  
  0         0  
292 0         0 elsif ($m < 0){$m += $ns;$cu -= $ns}
293 6         6 $ns = shift(@nums); push @nums,$ns;
  6         31  
294             }
295 36 100       78 $n .= '#' x $m if ($m > 0);
296 36 100       217 $n .= 'b' x abs($m) if ($m < 0);
297 36         51 push @mscale,$n;
298 36         62 $cu += $ns;
299             }
300             }
301 9 100       32 if ($descending) {
302 2         3 @mscale = reverse @mscale;
303 2         7 unshift @mscale,pop(@mscale);
304             }
305 9         101 @mscale;
306             }
307              
308              
309             sub is_scale {
310 6     6 1 18 my $name = shift();
311 6         14 $name =~ s/[^a-zA-Z0-9]//g;
312 6 100       41 return exists $modes{lc $name} ? 1 : 0;
313             }
314              
315             sub scale_to_PDL {
316 4     4 0 154 my ($octave,@scale)=@_;
317 4         4 my @result;
318             my $descending;
319 4         9 my $n1 = note_to_num($scale[0]);
320 4         7 my $n2 = note_to_num($scale[1]);
321 4 100 100     21 if ($n2 < $n1 && ($n1-$n2 < 5)) {
322 1         2 $descending = 1;
323 1         2 @scale = reverse @scale;
324             }
325 4         8 my $last = (note_to_num($scale[0]) + 9) % 12;
326 4         6 foreach (@scale) {
327 22         40 my $n = (note_to_num($_) + 9) % 12;
328 22 100       43 $octave++ if ($last > $n); #switched over octave at 'c'
329 22         34 s/\#/s/g;
330 22         28 s/b/f/g;
331 22         38 push @result,lc($_).$octave;
332 22         35 $last = $n;
333             }
334 4 100       10 @result = reverse @result if $descending;
335 4         38 @result;
336             }
337              
338             1;
339             __END__