music_set_theory 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,456 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # filename: music_set_theory/scale.rb
4
+ #
5
+
6
+
7
+ #-*- coding: UTF-8 -*-
8
+ # scales.py: Defines the classes for note sequences and scales.
9
+ #
10
+ # Copyright (c) 2008-2020 Peter Murphy <peterkmurphy@gmail.com>
11
+ # All rights reserved.
12
+ #
13
+ # Redistribution and use in source and binary forms, with or without
14
+ # modification, are permitted provided that the following conditions are met:
15
+ # * Redistributions of source code must retain the above copyright
16
+ # notice, this list of conditions and the following disclaimer.
17
+ # * Redistributions in binary form must reproduce the above copyright
18
+ # notice, this list of conditions and the following disclaimer in the
19
+ # documentation and/or other materials provided with the distribution.
20
+ # * The names of its contributors may not be used to endorse or promote
21
+ # products derived from this software without specific prior written
22
+ # permission.
23
+ #
24
+ # THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ''AS IS'' AND ANY
25
+ # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27
+ # DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
28
+ # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
29
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
30
+ # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
31
+ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
32
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
33
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34
+
35
+ # import unittest;
36
+ #
37
+ # from .musutility import rotate, rotate_and_zero, multislice;
38
+ # from .temperament import temperament, WestTemp, seq_dict, NSEQ_SCALE, \
39
+ # NSEQ_CHORD, M_SHARP, M_FLAT, CHROM_NAT_NOTE_POS;
40
+ #
41
+ require_relative "./musutility"
42
+ require_relative "./temperament"
43
+
44
+
45
+ #
46
+ #
47
+ #
48
+ # This represents a pattern of notes - a scale or a chord - each with a
49
+ # specified position from a base key or note. For example, a major scale
50
+ # is specified as the pattern [0, 2, 4, 5, 7, 9, 11], while a major chord
51
+ # is specified as [0, 4, 7].[*] In each case, we don't specify the key.
52
+ # However, this class provides functions that allow users to generate
53
+ # notes with a specified key as a function argument.
54
+ #
55
+ # [* If this doesn't make sense, consider the first note of a major scale
56
+ # is 0 semitones from the first note of a major scale, the second note is
57
+ # 2 semitones (a tone) from the first note, the third note is 4 semitones
58
+ # from the first note... and so on to the final note of the scale, which
59
+ # is 11 tones from the first note. Exercise for the reader: try this
60
+ # concept with a _major_ chord. ]
61
+ #
62
+ module MusicSetTheory
63
+
64
+ class NoteSeq
65
+ include Temperament
66
+ include MusUtility
67
+ end
68
+ class NoteSeqScale < NoteSeq
69
+ #include MusUtility
70
+ end
71
+
72
+ end
73
+
74
+
75
+ #
76
+ #
77
+ #
78
+ module MusicSetTheory
79
+ class NoteSeq
80
+
81
+ #
82
+ # Initialiser. It contains the following arguments.
83
+ # ==== Args
84
+ # nseq_name:: the full name of the note sequence.
85
+ # nseq_type:: the type of note sequence (e.g., NSEQ_SCALE or NSEQ_CHORD).
86
+ # nseq_temp:: the musical temperament from which the sequence of notes is
87
+ # taken. This should be an instance of the class temperament.
88
+ # nseq_posn:: a sequence of integers representing positions of the notes
89
+ # in the sequence relative to its first note. (For best results,
90
+ # the first item should be 0 to represent the first note.)
91
+ # nseq_nat_posns:: a sequence of integers (which should be the same size
92
+ # as nseq_posn). For each item in nseq_posn, the corresponding item
93
+ # in nseq_nat_posns represent the difference in its calculated
94
+ # natural note from the base key's natural note. For example, this
95
+ # parameter is [0, 2, 4] for a major chord; the second note is always
96
+ # two letters higher than the first note, and the third note is two
97
+ # letters higher again.
98
+ # (! this is in degrees.)
99
+ # nseq_abbrv:: the primary abbreviation for the note sequence (if any).
100
+ # nseq_synonyms:: a list of possible synonyms, or extra names, for the
101
+ # note sequence. For example, the "Ionian" mode is another name for
102
+ # the major scale as far as this class is concerned.
103
+ # nseq_other_abbrevs:: a list of possible alternate abbreviations for the
104
+ # note sequence. For example, both "min7" and "m7" are abbreviations
105
+ # for the minor seventh chord.
106
+ #
107
+ # ==== Examples of defining scales:
108
+ # Major scale:: NoteSeq.new("Major", NSEQ_SCALE, WestTemp, [0, 2, 4, 5, 7,
109
+ # 9, 11], range(7), nseq_synonyms: ["Ionian"])
110
+ # Harmonic Minor scale:: NoteSeq.new("Harmonic Minor", NSEQ_SCALE, WestTemp,
111
+ # [0, 2, 3, 5, 7, 8, 11], 0...7);
112
+ #
113
+ # Examples of defining chords:
114
+ # Major chord: noteseq("Major", NSEQ_CHORD, WestTemp, [0, 4, 7],
115
+ # [0, 2, 4], "maj");
116
+ # Minor chord: noteseq("Minor", NSEQ_CHORD, WestTemp,
117
+ # [0, 3, 7], [0, 2, 4], "min", nseq_other_abbrevs = ["m"]);
118
+ # Min 7 chord: noteseq("Minor Seventh", NSEQ_CHORD, WestTemp,
119
+ # [0, 3, 7, 11], [0, 2, 4, 6], "min7", nseq_other_abbrevs = ["m7"]);
120
+ #
121
+ # Note: it is easier to define scales using the noteseq_scale class (this
122
+ # file) and chord using the noteseq_chord class (in chords.py)
123
+ #
124
+ attr_accessor :nseq_name, :nseq_type, :nseq_temp, :nseq_posn
125
+ attr_accessor :nseq_nat_posns, :nseq_abbrev, :nseq_synonyms,
126
+ :nseq_other_abbrevs
127
+ def initialize( nseq_name, nseq_type, nseq_temp, nseq_posn,
128
+ nseq_nat_posns,
129
+ # nseq_abbrev = "", nseq_synonyms = [],
130
+ # nseq_other_abbrevs = [] )
131
+ nseq_abbrev: "", nseq_synonyms: [],
132
+ nseq_other_abbrevs: [] )
133
+ self.nseq_name = nseq_name
134
+ self.nseq_type = nseq_type
135
+ self.nseq_temp = nseq_temp
136
+ self.nseq_posn = nseq_posn
137
+
138
+ self.nseq_nat_posns = nseq_nat_posns
139
+ self.nseq_abbrev = nseq_abbrev
140
+ self.nseq_synonyms = nseq_synonyms
141
+ self.nseq_other_abbrevs = nseq_other_abbrevs
142
+
143
+ self.register_with_temp()
144
+ end
145
+
146
+ #
147
+ # alias name nseq_name
148
+ # alias type nseq_type
149
+ # alias temperament nseq_temp
150
+ # alias tment nseq_temp # *
151
+ # :
152
+ # etc.
153
+ {
154
+ nseq_name: [:name],
155
+ nseq_type: [:type],
156
+ nseq_temp: [:temp, :temperament, :tment],
157
+ nseq_posn: [:posn, :note_pos, :notes],
158
+ nseq_nat_posns: [:nat_posns, :note_nat, :notes_degree,
159
+ :notes_deg],
160
+ nseq_abbrev: [:abbrev, :abbr],
161
+ nseq_synonyms: [:synonyms, :syns],
162
+ nseq_other_abbrevs: [:other_abbrevs, :abbr_others],
163
+ }.each do |k, vary|
164
+
165
+ vary.each do |new_name|
166
+ getter_name = k
167
+ setter_name = k.to_s + "="
168
+ if method_defined? getter_name
169
+ new_getter_name = new_name
170
+ alias_method new_getter_name, getter_name
171
+ end
172
+ if method_defined? setter_name
173
+ new_setter_name = new_name.to_s + "="
174
+ alias_method new_setter_name, setter_name
175
+ end
176
+ end
177
+
178
+ end
179
+
180
+ end
181
+ end
182
+
183
+
184
+ #
185
+ #
186
+ #
187
+ module MusicSetTheory
188
+ class NoteSeq
189
+
190
+ # Registers this note sequence with the underlying temperament, so
191
+ # that it can be looked up by name, by abbreviation or by sequence.
192
+ #
193
+ def register_with_temp
194
+ if self.nseq_synonyms
195
+ #if self.nseq_name not in self.nseq_synonyms
196
+ if !(self.nseq_synonyms.include? self.nseq_name)
197
+ our_names = [self.nseq_name] + self.nseq_synonyms;
198
+ else
199
+ our_names = self.nseq_synonyms;
200
+ end
201
+ else
202
+ our_names = [self.nseq_name];
203
+ end
204
+
205
+ if self.nseq_other_abbrevs
206
+ #if self.nseq_abbrev not in self.nseq_other_abbrevs
207
+ if !(self.nseq_other_abbrevs.include? self.nseq_abbrev)
208
+ our_abbrevs = [self.nseq_abbrev] + self.nseq_other_abbrevs
209
+ else
210
+ our_abbrevs = self.nseq_other_abbrevs
211
+ end
212
+ else
213
+ our_abbrevs = [self.nseq_abbrev]
214
+ end
215
+ self.nseq_temp.seq_maps.add_elem(
216
+ self, self.nseq_type, our_names, our_abbrevs, self.nseq_posn)
217
+ end
218
+
219
+ # Get the notes for the note_seq starting from a given key. Then
220
+ # rotates the sequence (by rotate_by). Then slices it (using the
221
+ # argument in the slice parameter).
222
+ #
223
+ def get_notes_for_key( key, rotate_by=0, slice=nil )
224
+ ret = nil
225
+ default_seq = self.nseq_temp.get_note_sequence(
226
+ key, self.nseq_posn, self.nseq_nat_posns); #C
227
+ modulus = self.nseq_temp.no_nat_keys
228
+ if slice
229
+ ret = multislice(
230
+ default_seq, slice, offset: rotate_by, mod: modulus);
231
+ else
232
+ ret = rotate(default_seq, rotate_by);
233
+ end
234
+ ret
235
+ end
236
+
237
+ # For this noteseq, output its positions relative to its first
238
+ # note, then rotates the sequence (by rotate_by), then slices
239
+ # it (using slice, if not none). If and only if raz is True, the
240
+ # result is rotated-and-zeroed.
241
+ #
242
+ def get_posn_for_offset( rotate_by=0, slice=nil, raz: false )
243
+ if slice
244
+ modulus = self.nseq_temp.no_nat_keys
245
+ multisliced = multislice(
246
+ self.nseq_posn, slice,
247
+ offset: rotate_by, mod: modulus)
248
+ if raz
249
+ return rotate_and_zero(multisliced, 0, self.nseq_temp.no_keys);
250
+ else
251
+ return multisliced;
252
+ end
253
+ else
254
+ if raz
255
+ return rotate_and_zero(
256
+ self.nseq_posn, rotate_by, self.nseq_temp.no_keys)
257
+ else
258
+ return rotate(self.nseq_posn, rotate_by)
259
+ end
260
+ end
261
+ end
262
+
263
+
264
+ end
265
+ end
266
+
267
+
268
+ # FIXME. separate the file.
269
+ #
270
+ #
271
+ # A specialisation of noteseq used exclusively for defining scales -
272
+ # especially heptatonic/diotonic scales.
273
+ #
274
+ module MusicSetTheory
275
+ class NoteSeqScale
276
+
277
+ #
278
+ # ==== Args
279
+ # nseq_name:: name of scale.
280
+ # nseq_temp:: temperament for scale.
281
+ # nseq_posn:: position of notes in scale.
282
+ # nseq_nat_posns:: natural note positions for notes in scales.
283
+ # nseq_modes:: a sequence of modes for the scale, listed in order of
284
+ # position.
285
+ #
286
+ def initialize( nseq_name, nseq_temp, nseq_posn, nseq_nat_posns,
287
+ nseq_modes = [], debug_f: false )
288
+ #
289
+ return super(
290
+ nseq_name, NSEQ_SCALE, nseq_temp, nseq_posn, nseq_nat_posns
291
+ ) if nseq_modes.empty?
292
+
293
+ ###
294
+ super(nseq_name, NSEQ_SCALE, nseq_temp, nseq_posn, nseq_nat_posns,
295
+ nseq_abbrev: "", nseq_synonyms: [nseq_modes[0]])
296
+
297
+ new_nseq_pos = nseq_posn
298
+ new_nseq_nat_pos = nseq_nat_posns
299
+
300
+ # This creates more instances of noteseq_scales - all for the different
301
+ # modes.
302
+ # By creating them, they will be automatically connected to the
303
+ # appropriate temperament's dictionary.
304
+ for i in 1...nseq_posn.size
305
+ $stderr.puts "{#{__method__}} new_nseq_pos: #{new_nseq_pos.inspect}," +
306
+ " new_nseq_nat_pos: #{new_nseq_nat_pos.inspect}" if debug_f
307
+
308
+ new_nseq_pos = rotate_and_zero(new_nseq_pos, 1, nseq_temp.no_keys)
309
+ new_nseq_nat_pos = rotate_and_zero(new_nseq_nat_pos, 1,
310
+ nseq_temp.no_nat_keys)
311
+
312
+ # create an other scale.
313
+ NoteSeqScale.new(
314
+ nseq_modes[i], nseq_temp, new_nseq_pos, new_nseq_nat_pos)
315
+ end
316
+ end
317
+
318
+ def to_s
319
+ self.nseq_name.to_s
320
+ end
321
+
322
+ end
323
+ end
324
+
325
+
326
+ module MusicSetTheory
327
+
328
+ #
329
+ include Temperament
330
+
331
+ # The following scales and names are derived from:
332
+ # http://docs.solfege.org/3.9/C/scales/modes.html
333
+
334
+ # The only exception is the "Discordant Minor", which I invented for
335
+ # shit and giggles.
336
+
337
+ # Note: MelMinScale represents a melodic minor scale _ascending_. See the
338
+ # Aeolian mode of the Major scale for melodic minor scale _descending_.
339
+
340
+ HEPT_NAT_POSNS = (0...7).to_a
341
+ MEL_MIN_NOTE_POS = [0, 2, 3, 5, 7, 9, 11]
342
+ HARM_MIN_NOTE_POS = [0, 2, 3, 5, 7, 8, 11]
343
+ HARM_MAJ_NOTE_POS = [0, 2, 4, 5, 7, 8, 11]
344
+ DISC_MIN_NOTE_POS = [0, 2, 3, 5, 6, 9, 11]
345
+ HUNGARIAN_NOTE_POS = [0, 3, 4, 6, 7, 9, 10]
346
+
347
+ # For ease of comprehension, we have the list of modes as arrays which
348
+ # can be browsed from outside.
349
+ MAJORMODES = [ "Ionian",
350
+ "Dorian",
351
+ "Phrygian",
352
+ "Lydian",
353
+ "Mixolydian",
354
+ "Aeolian",
355
+ "Locrian", ]
356
+
357
+ MELMINORMODES = [ "Jazz Minor",
358
+ "Dorian " + M_FLAT + "9",
359
+ "Lydian Augmented",
360
+ "Lydian Dominant",
361
+ "Mixolydian " + M_FLAT + "13",
362
+ "Semilocrian",
363
+ "Superlocrian", ]
364
+
365
+ HARMINORMODES = [ "Harmonic Minor",
366
+ "Locrian " + M_SHARP + "6",
367
+ "Ionian Augmented",
368
+ "Romanian",
369
+ "Phrygian Dominant",
370
+ "Lydian " + M_SHARP + "2",
371
+ "Ultralocrian", ]
372
+
373
+ HARMMAJORMODES = [ "Harmonic Major",
374
+ "Dorian " + M_FLAT + "6",
375
+ "Phrygian " + M_FLAT + "4",
376
+ "Lydian " + M_FLAT + "3",
377
+ "Mixolydian " + M_FLAT + "9",
378
+ "Lydian " + M_SHARP + "2 " + M_SHARP + "5",
379
+ "Locrian " + M_FLAT + M_FLAT + "7", ]
380
+
381
+ DISCORDMINMODES = [ "Melodic Minor " + M_FLAT + "5",
382
+ "Dorian " + M_FLAT + "9 " + M_FLAT + "4",
383
+ "Minor Lydian Augmented",
384
+ "Lydian Dominant " + M_FLAT + "9",
385
+ "Lydian Augmented " + M_SHARP + "2 " + M_SHARP + "3",
386
+ "Semilocrian " + M_FLAT + M_FLAT + "7",
387
+ "Superlocrian " + M_FLAT + M_FLAT + "6", ]
388
+
389
+ HUNGARIANMODES = [ "Hungarian",
390
+ "Superlocrian " + M_FLAT + M_FLAT + "6 " + M_FLAT + M_FLAT + "7",
391
+ "Harmonic Minor " + M_FLAT + "5",
392
+ "Superlocrian " + M_SHARP + "6",
393
+ "Melodic Minor " + M_SHARP + "5",
394
+ "Dorian " + M_FLAT + "9 " + M_SHARP + "11",
395
+ "Lydian Augmented " + M_SHARP + "3", ]
396
+
397
+ end
398
+
399
+
400
+ # Scales.
401
+ #
402
+ #
403
+ module MusicSetTheory
404
+ MajorScale = NoteSeqScale.new("Major", WestTemp,
405
+ CHROM_NAT_NOTE_POS, HEPT_NAT_POSNS, MAJORMODES)
406
+
407
+ MelMinorScale = NoteSeqScale.new("Melodic Minor", WestTemp,
408
+ MEL_MIN_NOTE_POS, HEPT_NAT_POSNS, MELMINORMODES)
409
+
410
+ HarmMinorScale = NoteSeqScale.new("Harmonic Minor", WestTemp,
411
+ HARM_MIN_NOTE_POS, HEPT_NAT_POSNS, HARMINORMODES)
412
+
413
+ HarmMajorScale = NoteSeqScale.new("Harmonic Major", WestTemp,
414
+ HARM_MAJ_NOTE_POS, HEPT_NAT_POSNS, HARMMAJORMODES)
415
+
416
+ DiscMinorScale = NoteSeqScale.new("Discordant Minor", WestTemp,
417
+ DISC_MIN_NOTE_POS, HEPT_NAT_POSNS, DISCORDMINMODES)
418
+
419
+ HungarianScale = NoteSeqScale.new("Hungarian", WestTemp,
420
+ HUNGARIAN_NOTE_POS, HEPT_NAT_POSNS, HUNGARIANMODES)
421
+ end
422
+
423
+
424
+ module MusicSetTheory
425
+ # This is for decomposing a scale into chords. We use this as a test
426
+ # routine so we know what is required.
427
+ #
428
+ def scale_analysis( scale_name, perm_chord_type )
429
+ # 1. Lookup scale name.
430
+ noteseq_founditem = noteseq.find_by_name(scale_name)
431
+
432
+ # 2. Find scale pattern.
433
+ ournoteseq = noteseq_founditem.noteseq
434
+
435
+ #
436
+ noteseq_founditem.noteseq.getpattern(noteseq_founditem.indices)
437
+
438
+ #for i in range(ournoteseq.nseq_posn.len()):
439
+ for i in 0...ournoteseq.nseq_posn.size
440
+ ournoteslice = noteseq_for_slice(i, perm_chord_type)
441
+ notechord_founditem = noteseq.find_by_chord(ournoteslice)
442
+
443
+ if notechord_founditem == None
444
+ print("None")
445
+ else
446
+ print(notechord_founditem.getName())
447
+ end
448
+ end
449
+
450
+ return
451
+ end
452
+
453
+ end
454
+
455
+
456
+ #### endof filename: music_set_theory/scales.rb