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.
- checksums.yaml +7 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +104 -0
- data/Rakefile +14 -0
- data/examples/mst_chords.rb +25 -0
- data/examples/mst_temperament.rb +46 -0
- data/lib/music_set_theory/chord_generator.rb +331 -0
- data/lib/music_set_theory/chords.rb +406 -0
- data/lib/music_set_theory/musutility.rb +284 -0
- data/lib/music_set_theory/scales.rb +456 -0
- data/lib/music_set_theory/temperament.rb +783 -0
- data/lib/music_set_theory/version.rb +11 -0
- data/lib/music_set_theory.rb +24 -0
- data/sig/music_theory.rbs +4 -0
- metadata +73 -0
@@ -0,0 +1,783 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# filename: music_set_theory/temperament.rb
|
4
|
+
#
|
5
|
+
|
6
|
+
|
7
|
+
#-*- coding: UTF-8 -*-
|
8
|
+
# temperament.py: Representing musical temperaments (and storing sequences
|
9
|
+
# associated with them).
|
10
|
+
#
|
11
|
+
# Copyright (c) 2008-2020 Peter Murphy <peterkmurphy@gmail.com>
|
12
|
+
# All rights reserved.
|
13
|
+
#
|
14
|
+
# Redistribution and use in source and binary forms, with or without
|
15
|
+
# modification, are permitted provided that the following conditions are met:
|
16
|
+
# * Redistributions of source code must retain the above copyright
|
17
|
+
# notice, this list of conditions and the following disclaimer.
|
18
|
+
# * Redistributions in binary form must reproduce the above copyright
|
19
|
+
# notice, this list of conditions and the following disclaimer in the
|
20
|
+
# documentation and/or other materials provided with the distribution.
|
21
|
+
# * The names of its contributors may not be used to endorse or promote
|
22
|
+
# products derived from this software without specific prior written
|
23
|
+
# permission.
|
24
|
+
#
|
25
|
+
# THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ''AS IS'' AND ANY
|
26
|
+
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
27
|
+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
28
|
+
# DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
|
29
|
+
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
30
|
+
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
31
|
+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
32
|
+
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
33
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
34
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
35
|
+
|
36
|
+
# Too many ideas, so let's work on the fundamentals.
|
37
|
+
# A musical temperament consists of the following things - keys, and their
|
38
|
+
# positions within it. For example, we will start with the 12 chromatic
|
39
|
+
# scale. We only need to specify the seven natural keys, and their position
|
40
|
+
# within it. Doing anymore is overkill. For example, this code does not concern
|
41
|
+
# itself with the frequencies of notes in Hz.
|
42
|
+
|
43
|
+
|
44
|
+
# import re;
|
45
|
+
# import unittest;
|
46
|
+
# from .musutility import rotate, multislice, repseq;
|
47
|
+
require_relative "./musutility"
|
48
|
+
|
49
|
+
|
50
|
+
# Structure.
|
51
|
+
#
|
52
|
+
#
|
53
|
+
module MusicSetTheory
|
54
|
+
|
55
|
+
#
|
56
|
+
#
|
57
|
+
#
|
58
|
+
module Temperament
|
59
|
+
#
|
60
|
+
extend MusUtility
|
61
|
+
include MusUtility
|
62
|
+
|
63
|
+
# The seq_dict class acts as a dictionary for noteseq instances. It
|
64
|
+
# allows users and programmers to look up noteseqs by name, by
|
65
|
+
# abbreviation, and by the sequence of integers representing the
|
66
|
+
# positions in the sequence. Each seq_dict should be assigned to one
|
67
|
+
# dictionary.
|
68
|
+
#
|
69
|
+
#! seq_dict is the SeqDict class.
|
70
|
+
class SeqDict
|
71
|
+
|
72
|
+
# The nseqtype_ins_dict class represents a subdictionary of noteseq
|
73
|
+
# instances sharing the same nseq_type. It is useful to split up
|
74
|
+
# noteseqs by nseq_type, lest property lookup return the "wrong"
|
75
|
+
# object. For example, a major 13 chord has the same pattern of
|
76
|
+
# integral positions as the major scale; i.e., [0, 2, 4, 5, 7, 9,
|
77
|
+
# 11]. To look up the correct object of the two by pattern, the
|
78
|
+
# nseq_type must be specified.
|
79
|
+
#
|
80
|
+
#! nseqtype_ins_dict is the NseqtypeInsDict class.
|
81
|
+
class NseqtypeInsDict; end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
# A musical temperament is used to define the possible keys in music,
|
86
|
+
# and also the positions of keys relevant to each other. Our primary
|
87
|
+
# example is the western or chromatic temperament, with 12 possible keys.
|
88
|
+
# A temperament also defines which of the keys can be represented as
|
89
|
+
# naturals (like "C", "D" and "E"), and which need to be represented with
|
90
|
+
# accidentals (like "C#" and "Db".
|
91
|
+
#
|
92
|
+
# Doing anything more at the moment is overkill. For example, we are not
|
93
|
+
# intereted in the possible frequencies for notes of a given key.
|
94
|
+
class Temperament; end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
#
|
103
|
+
#
|
104
|
+
#
|
105
|
+
module MusicSetTheory
|
106
|
+
module Temperament
|
107
|
+
|
108
|
+
# Unicode string constants for music notation.
|
109
|
+
# NOTE. DO NOT USE SINGLE QUOTES.
|
110
|
+
M_SHARP = "\u266f"
|
111
|
+
M_NATURAL = "\u266e"
|
112
|
+
M_FLAT = "\u266d"
|
113
|
+
|
114
|
+
# Since this may not display on browsers, there are non-Unicode equivalents.
|
115
|
+
#MNU_SHARP = "#"
|
116
|
+
MNU_SHARP = "#"
|
117
|
+
RE_SHARP = "\\#{MNU_SHARP}"
|
118
|
+
MNU_FLAT = "b"
|
119
|
+
MNU_NATURAL = "n"
|
120
|
+
|
121
|
+
# This constant is used for parsing note names into keys.
|
122
|
+
#
|
123
|
+
# https://docs.ruby-lang.org/ja/latest/doc/spec=2fregexp.html#capture
|
124
|
+
# https://docs.ruby-lang.org/ja/latest/doc/spec=2fregexp.html#option
|
125
|
+
#
|
126
|
+
# ok.
|
127
|
+
#RE_NOTEPARSE = /(?<basenotename> # named capture.
|
128
|
+
# [A-Z]
|
129
|
+
# (b|n|\#| # WARN: hashtag MUST BE ESCAPED
|
130
|
+
# # with backslash in Free format mode.
|
131
|
+
# #{M_FLAT}|#{M_NATURAL}|#{M_SHARP})*
|
132
|
+
# )/x # free format mode.
|
133
|
+
#
|
134
|
+
# (#{MNU_FLAT}|#{MNU_NATURAL}|\#|
|
135
|
+
#ng. (#{MNU_FLAT}|#{MNU_NATURAL}|#{MNU_SHARP}| #ng.
|
136
|
+
RE_NOTEPARSE = /(?<basenotename> # named capture.
|
137
|
+
[A-Z]
|
138
|
+
(#{MNU_FLAT}|#{MNU_NATURAL}|#{RE_SHARP}|
|
139
|
+
#{M_FLAT} |#{M_NATURAL} |#{M_SHARP})*
|
140
|
+
)/x # free format mode.
|
141
|
+
|
142
|
+
# Constants used for the nseq_type arguments in noteseqs. See scales.py for
|
143
|
+
# more information.
|
144
|
+
NSEQ_SCALE = 0 # Used for specifying scales.
|
145
|
+
NSEQ_CHORD = 1 # Used for specifying chords.
|
146
|
+
NSEQ_TYPE_HASH = {
|
147
|
+
NSEQ_SCALE => 'SCALE',
|
148
|
+
NSEQ_CHORD => 'CHORD',
|
149
|
+
}
|
150
|
+
deep_freeze(NSEQ_TYPE_HASH)
|
151
|
+
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
|
156
|
+
#
|
157
|
+
#
|
158
|
+
#
|
159
|
+
module MusicSetTheory
|
160
|
+
module Temperament
|
161
|
+
|
162
|
+
|
163
|
+
# Turns all Unicode musical characters in str_note to their ASCII
|
164
|
+
# equivalent, and returns the result.
|
165
|
+
#
|
166
|
+
# Note: the function is intended for viewing musical scales through
|
167
|
+
# deficient browsers like IE 6 and 7. Characters may appear as blocks
|
168
|
+
# in these environments.
|
169
|
+
def un_unicode_accdtls(str_note)
|
170
|
+
ret = str_note.gsub(/#{M_FLAT}/, MNU_FLAT).
|
171
|
+
gsub(/#{M_SHARP}/, MNU_SHARP).
|
172
|
+
gsub(/#{M_NATURAL}/, MNU_NATURAL)
|
173
|
+
ret
|
174
|
+
end
|
175
|
+
|
176
|
+
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
|
181
|
+
#
|
182
|
+
#
|
183
|
+
#
|
184
|
+
# The following class seq_dict is used as a multiple key map to noteseq
|
185
|
+
# instances, which are defined in scales.poy. However, seq_dict instances are
|
186
|
+
# stored with temperament objects, which is why they are stored here.
|
187
|
+
module MusicSetTheory
|
188
|
+
module Temperament
|
189
|
+
|
190
|
+
# The nseqtype_ins_dict class represents a subdictionary of noteseq
|
191
|
+
# instances sharing the same nseq_type. It is useful to split up
|
192
|
+
# noteseqs by nseq_type, lest property lookup return the "wrong"
|
193
|
+
# object. For example, a major 13 chord has the same pattern of
|
194
|
+
# integral positions as the major scale; i.e., [0, 2, 4, 5, 7, 9,
|
195
|
+
# 11]. To look up the correct object of the two by pattern, the
|
196
|
+
# nseq_type must be specified.
|
197
|
+
class SeqDict::NseqtypeInsDict
|
198
|
+
attr_accessor :nseq_type
|
199
|
+
attr_accessor :name_dict, :abbrv_dict, :seqpos_dict
|
200
|
+
def initialize( nseq_type )
|
201
|
+
# The constructed for nseqtype_ins_dict. This sets up seperate
|
202
|
+
# dictionaries for names, abbreviations and integral position
|
203
|
+
# dictionaries.
|
204
|
+
self.nseq_type = nseq_type
|
205
|
+
self.name_dict = {}
|
206
|
+
self.abbrv_dict = {}
|
207
|
+
self.seqpos_dict = {}
|
208
|
+
end
|
209
|
+
|
210
|
+
{
|
211
|
+
nseq_type: [:type],
|
212
|
+
abbrv_dict: [:abbr_dict],
|
213
|
+
}.each do |k, vary|
|
214
|
+
|
215
|
+
vary.each do |new_name|
|
216
|
+
getter_name = k
|
217
|
+
setter_name = k.to_s + "="
|
218
|
+
if method_defined? getter_name
|
219
|
+
new_getter_name = new_name
|
220
|
+
alias_method new_getter_name, getter_name
|
221
|
+
end
|
222
|
+
if method_defined? setter_name
|
223
|
+
new_setter_name = new_name.to_s + "="
|
224
|
+
alias_method new_setter_name, setter_name
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
end
|
229
|
+
|
230
|
+
end
|
231
|
+
|
232
|
+
# The seq_dict class acts as a dictionary for noteseq instances. It
|
233
|
+
# allows users and programmers to look up noteseqs by name, by
|
234
|
+
# abbreviation, and by the sequence of integers representing the
|
235
|
+
# positions in the sequence. Each seq_dict should be assigned to one
|
236
|
+
# dictionary.
|
237
|
+
class SeqDict
|
238
|
+
attr_accessor :nseq_temp
|
239
|
+
attr_accessor :nseqtype_maps
|
240
|
+
|
241
|
+
# The constructor for seq_dict.
|
242
|
+
# ==== Args
|
243
|
+
# nseq_types:: a list of possible nseq_type values for lookups.
|
244
|
+
# nseq_temp:: the associated temperament object.
|
245
|
+
#
|
246
|
+
def initialize( nseq_types, nseq_temp )
|
247
|
+
self.nseq_temp = nseq_temp;
|
248
|
+
self.nseqtype_maps = {};
|
249
|
+
for i in nseq_types
|
250
|
+
self.nseqtype_maps[i] = NseqtypeInsDict.new(i);
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
|
260
|
+
#
|
261
|
+
#
|
262
|
+
#
|
263
|
+
module MusicSetTheory
|
264
|
+
module Temperament
|
265
|
+
|
266
|
+
class SeqDict
|
267
|
+
|
268
|
+
# This adds an element to seq_dict.
|
269
|
+
# ==== Args
|
270
|
+
# elem:: the element to add to seq_dict instance.
|
271
|
+
# nseq_type:: the type of the elemenet (such as scale or chord).
|
272
|
+
# name_s:: a string, or a sequence of strings. This provides names
|
273
|
+
# as keys that map onto elem.
|
274
|
+
# abbrv_s:: a string, or a sequence of strings. This provides
|
275
|
+
# abbreviations as keys that map onto elem.
|
276
|
+
# seqpos:: a sequence. A tuple form will be used as a key that
|
277
|
+
# maps onto elem.
|
278
|
+
# ==== Desc
|
279
|
+
# If nseq_type is not associated with any of the sub-dictionaries in
|
280
|
+
# seq_dict, then this function exits.
|
281
|
+
def add_elem( elem, nseq_type, name_s, abbrv_s, seqpos )
|
282
|
+
|
283
|
+
#
|
284
|
+
if !(self.nseqtype_maps.include? nseq_type)
|
285
|
+
return false
|
286
|
+
end
|
287
|
+
|
288
|
+
#
|
289
|
+
sub_dictionary = self.nseqtype_maps[nseq_type]
|
290
|
+
|
291
|
+
#
|
292
|
+
ourmod = self.nseq_temp.no_keys
|
293
|
+
ourtuple = seqpos.map{|i| i % ourmod }.sort.freeze
|
294
|
+
sub_dictionary.seqpos_dict[ourtuple] = elem
|
295
|
+
|
296
|
+
#
|
297
|
+
if name_s.is_a? String
|
298
|
+
sub_dictionary.name_dict[name_s] = elem
|
299
|
+
else
|
300
|
+
for s in name_s
|
301
|
+
sub_dictionary.name_dict[s] = elem
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
# if isinstance(abbrv_s, str)
|
306
|
+
if abbrv_s.is_a? String
|
307
|
+
sub_dictionary.abbrv_dict[abbrv_s] = elem
|
308
|
+
else
|
309
|
+
for s in abbrv_s
|
310
|
+
sub_dictionary.abbrv_dict[s] = elem
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
true
|
315
|
+
end
|
316
|
+
|
317
|
+
# Checks if there is a subdictionary associated with nseq_type.
|
318
|
+
def check_nseqby_subdict( nseq_type )
|
319
|
+
#return nseq_type in self.nseqtype_maps
|
320
|
+
self.nseqtype_maps.key?(nseq_type)
|
321
|
+
end
|
322
|
+
|
323
|
+
# Checks if there is a noteseq with a given name.
|
324
|
+
def check_nseqby_name( nseq_type, name )
|
325
|
+
# return name in self.nseqtype_maps[nseq_type].name_dict;
|
326
|
+
self.nseqtype_maps[nseq_type].name_dict.key?(name)
|
327
|
+
end
|
328
|
+
|
329
|
+
# Checks if there is a noteseq with a given abbreviation.
|
330
|
+
def check_nseqby_abbrv( nseq_type, abbrv )
|
331
|
+
# return abbrv in self.nseqtype_maps[nseq_type].abbrv_dict
|
332
|
+
self.nseqtype_maps[nseq_type].abbrv_dict.key?(abbrv)
|
333
|
+
end
|
334
|
+
|
335
|
+
# Checks if there is a noteseq with a given sequence position.
|
336
|
+
def check_nseqby_seqpos( nseq_type, seqpos )
|
337
|
+
# sortedseqpos = tuple(sorted(seqpos))
|
338
|
+
# return sortedseqpos in self.nseqtype_maps[nseq_type].seqpos_dict
|
339
|
+
sortedseqpos = seqpos.sort.freeze
|
340
|
+
self.nseqtype_maps[nseq_type].seqpos_dict.key?(sortedseqpos)
|
341
|
+
end
|
342
|
+
|
343
|
+
|
344
|
+
### getters.
|
345
|
+
|
346
|
+
# Looks up a noteseq by name.
|
347
|
+
def get_nseqby_name( name, nseq_type )
|
348
|
+
# return self.nseqtype_maps[nseq_type].name_dict[name]
|
349
|
+
self.nseqtype_maps[nseq_type].name_dict[name]
|
350
|
+
end
|
351
|
+
|
352
|
+
# Looks up a noteseq by abbreviation.
|
353
|
+
def get_nseqby_abbrv( abbrv, nseq_type )
|
354
|
+
# return self.nseqtype_maps[nseq_type].abbrv_dict[abbrv]
|
355
|
+
self.nseqtype_maps[nseq_type].abbrv_dict[abbrv]
|
356
|
+
end
|
357
|
+
|
358
|
+
# Looks up a noteseq by sequence position.
|
359
|
+
def get_nseqby_seqpos( seqpos, nseq_type )
|
360
|
+
# sortedseqpos = tuple(sorted(seqpos))
|
361
|
+
# return self.nseqtype_maps[nseq_type].seqpos_dict[sortedseqpos]
|
362
|
+
sortedseqpos = seqpos.sort.freeze
|
363
|
+
self.nseqtype_maps[nseq_type].seqpos_dict[sortedseqpos]
|
364
|
+
end
|
365
|
+
|
366
|
+
end
|
367
|
+
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
|
372
|
+
#
|
373
|
+
#
|
374
|
+
#
|
375
|
+
# A musical temperament is used to define the possible keys in music,
|
376
|
+
# and also the positions of keys relevant to each other. Our primary
|
377
|
+
# example is the western or chromatic temperament, with 12 possible keys.
|
378
|
+
# A temperament also defines which of the keys can be represented as
|
379
|
+
# naturals (like "C", "D" and "E"), and which need to be represented with
|
380
|
+
# accidentals (like "C#" and "Db".
|
381
|
+
#
|
382
|
+
# Doing anything more at the moment is overkill. For example, we are not
|
383
|
+
# intereted in the possible frequencies for notes of a given key.
|
384
|
+
module MusicSetTheory
|
385
|
+
module Temperament
|
386
|
+
|
387
|
+
class Temperament
|
388
|
+
|
389
|
+
#
|
390
|
+
attr_accessor :no_keys, :nat_keys, :nat_key_posn
|
391
|
+
attr_accessor :no_nat_keys, :nat_key_pos_lookup
|
392
|
+
attr_accessor :pos_lookup_nat_key, :nat_key_lookup_order
|
393
|
+
attr_accessor :parsenote
|
394
|
+
attr_accessor :seq_maps
|
395
|
+
|
396
|
+
# Initialiser.
|
397
|
+
# ==== Args
|
398
|
+
# no_keys:: the number of keys in the temperament.
|
399
|
+
# nat_keys:: an array consisting of the names of the natural
|
400
|
+
# (unsharped or unflattened) keys in the temperament.
|
401
|
+
# nat_key_posn:: the position of the natural keys in the temperament.
|
402
|
+
# These should correspond to the elements in nat_keys.
|
403
|
+
# Positions are calculated base zero.
|
404
|
+
#
|
405
|
+
def initialize( no_keys, nat_keys, nat_key_posn )
|
406
|
+
self.no_keys = no_keys
|
407
|
+
self.nat_keys = nat_keys
|
408
|
+
self.nat_key_posn = nat_key_posn
|
409
|
+
|
410
|
+
# Extra variable: no_nat_keys: number of natural keys.
|
411
|
+
#self.no_nat_keys = min(len(nat_keys), len(nat_key_posn))
|
412
|
+
self.no_nat_keys = [nat_keys.size, nat_key_posn.size].min
|
413
|
+
|
414
|
+
# Extra variable: nat_key_pos_lookup:
|
415
|
+
# key -> position (e.g. A -> 9, C->0).
|
416
|
+
self.nat_key_pos_lookup = {}
|
417
|
+
|
418
|
+
# Extra variable: post_lookup_nat_key: position
|
419
|
+
# -> key/None (e.g., 9->A, 0->C).
|
420
|
+
self.pos_lookup_nat_key = {}
|
421
|
+
|
422
|
+
# Extra variable: nat_key_lookup_order: looks up order of
|
423
|
+
# nat_keys (e.g, C->0, D->1... B->6). Reverse translation
|
424
|
+
# available by self.nat_keys[order].
|
425
|
+
self.nat_key_lookup_order = {}
|
426
|
+
|
427
|
+
#
|
428
|
+
for i in 0...self.no_nat_keys
|
429
|
+
self.nat_key_pos_lookup[nat_keys[i]] = nat_key_posn[i]
|
430
|
+
self.pos_lookup_nat_key[nat_key_posn[i]] = nat_keys[i]
|
431
|
+
self.nat_key_lookup_order[nat_keys[i]] = i
|
432
|
+
end
|
433
|
+
|
434
|
+
# self.parsenote = re.compile(RE_NOTEPARSE);
|
435
|
+
self.parsenote = Regexp.compile(RE_NOTEPARSE)
|
436
|
+
|
437
|
+
# Useful for dictionary lookup later.
|
438
|
+
#self.seq_maps = seq_dict([NSEQ_SCALE, NSEQ_CHORD], self);
|
439
|
+
#self.seq_maps = SeqDict.new(NSEQ_SCALE, NSEQ_CHORD);
|
440
|
+
self.seq_maps = SeqDict.new([NSEQ_SCALE, NSEQ_CHORD], self)
|
441
|
+
|
442
|
+
end
|
443
|
+
|
444
|
+
{
|
445
|
+
seq_maps: [:seq_dict],
|
446
|
+
}.each do |k, vary|
|
447
|
+
|
448
|
+
vary.each do |new_name|
|
449
|
+
getter_name = k
|
450
|
+
setter_name = k.to_s + "="
|
451
|
+
if method_defined? getter_name
|
452
|
+
new_getter_name = new_name
|
453
|
+
alias_method new_getter_name, getter_name
|
454
|
+
end
|
455
|
+
if method_defined? setter_name
|
456
|
+
new_setter_name = new_name.to_s + "="
|
457
|
+
alias_method new_setter_name, setter_name
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
end
|
462
|
+
|
463
|
+
end
|
464
|
+
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
|
469
|
+
#
|
470
|
+
#
|
471
|
+
#
|
472
|
+
module MusicSetTheory
|
473
|
+
module Temperament
|
474
|
+
|
475
|
+
class Temperament
|
476
|
+
|
477
|
+
# Parses the name of a key into the tuple (natural key name,
|
478
|
+
# sharp_or_flat count).
|
479
|
+
# ==== Args
|
480
|
+
# key:: note.
|
481
|
+
#
|
482
|
+
# ==== Returns
|
483
|
+
# an array: [ <root note>, <adj number> ].
|
484
|
+
# For example "C#" is parsed into ["C", 1], "Db" is parsed
|
485
|
+
# into ["D", -1], and "E" is parsed into ["E", 0].
|
486
|
+
# As the reader may gather, negative numbers are used for
|
487
|
+
# flattened notes.
|
488
|
+
#
|
489
|
+
def note_parse( key, debug_f: false )
|
490
|
+
noteMatch = self.parsenote.match(key)
|
491
|
+
$stderr.puts "{#{__method__}} key (#{key.length}): #{key}" if debug_f
|
492
|
+
$stderr.puts " =>#{noteMatch[:basenotename].chars}" if debug_f
|
493
|
+
return nil if !(noteMatch)
|
494
|
+
|
495
|
+
#
|
496
|
+
# $stderr.puts "note_match: #{noteMatch}:#{noteMatch.class}"
|
497
|
+
# noteGroup = noteMatch.group('basenotename')
|
498
|
+
noteGroup = noteMatch[:basenotename].chars
|
499
|
+
baseNote = noteGroup[0];
|
500
|
+
$stderr.puts "key: #{key}::" +
|
501
|
+
" note_group: #{noteGroup.inspect}:#{noteGroup.class}," +
|
502
|
+
" chars(#{noteGroup.size}): #{noteGroup};" +
|
503
|
+
" base_note: #{baseNote}" if debug_f
|
504
|
+
|
505
|
+
#
|
506
|
+
flatSharpAdj = 0
|
507
|
+
for chr in noteGroup[1..]
|
508
|
+
$stderr.puts "{#{__method__}} adj: #{chr}" if debug_f
|
509
|
+
if /#{chr}/ =~ (M_FLAT + MNU_FLAT)
|
510
|
+
flatSharpAdj += -1
|
511
|
+
end
|
512
|
+
if /#{chr}/ =~ (M_SHARP + MNU_SHARP)
|
513
|
+
flatSharpAdj += +1
|
514
|
+
end
|
515
|
+
end
|
516
|
+
|
517
|
+
[ baseNote, flatSharpAdj,]
|
518
|
+
end
|
519
|
+
|
520
|
+
# Returns the position of the key in the temperament.
|
521
|
+
# ==== Args
|
522
|
+
# key:: key note. ex. "C", "D#", "Cb"
|
523
|
+
#
|
524
|
+
# ==== Returns
|
525
|
+
# position of the key in the unit of halftone. ex. key: "C" =>0,
|
526
|
+
# key: "D#" =>3, key: "Cb" =>-1
|
527
|
+
#
|
528
|
+
def get_pos_of_key( key, debug_f: false )
|
529
|
+
key_parsed = self.note_parse(key)
|
530
|
+
$stderr.puts "key_parsed: #{key_parsed}" if debug_f
|
531
|
+
$stderr.puts "nat_key_pos_lookup: #{self.nat_key_pos_lookup}" if \
|
532
|
+
debug_f
|
533
|
+
|
534
|
+
ret = self.nat_key_pos_lookup[key_parsed[0]] + key_parsed[1]
|
535
|
+
ret
|
536
|
+
end
|
537
|
+
|
538
|
+
# Given a position in the temperament, this function attempts to
|
539
|
+
# return the "best" key name corresponding to it. This is not a
|
540
|
+
# straight-forward reverse of get_pos_of_key, as there may be two
|
541
|
+
# different keynames for the same position, with one preferred. For
|
542
|
+
# example, "C# and "Db" are the same key, but "C# is preferred in an
|
543
|
+
# A major scale. Fortunately, arguments are provided to indicate the
|
544
|
+
# programmer's preference.
|
545
|
+
# ==== Args
|
546
|
+
# pos:: the position inside the temperament.
|
547
|
+
# desired_nat_note:: the preferred natural key to start the key name.
|
548
|
+
# For example "C" makes the function return "C#" instead of "Db".
|
549
|
+
# If None, then the preference depends on...
|
550
|
+
# sharp_not_flat:: if True, returns the sharpened accidental form
|
551
|
+
# (e.g., "C#"); if False, returns the flattened accidental form
|
552
|
+
# (i.e., "Db").
|
553
|
+
# ==== Returns
|
554
|
+
#
|
555
|
+
def get_key_of_pos( pos, desired_nat_note = nil, sharp_not_flat = nil )
|
556
|
+
|
557
|
+
# accdtls: number of sharps or flats to add to the output string.
|
558
|
+
|
559
|
+
if desired_nat_note
|
560
|
+
accdtls = pos - self.nat_key_pos_lookup[desired_nat_note]
|
561
|
+
accdtls = (accdtls % self.no_keys)
|
562
|
+
|
563
|
+
if accdtls > (self.no_keys / 2)
|
564
|
+
accdtls = accdtls - self.no_keys
|
565
|
+
end
|
566
|
+
|
567
|
+
if accdtls > 0
|
568
|
+
return desired_nat_note + (M_SHARP * accdtls)
|
569
|
+
elsif accdtls < 0
|
570
|
+
return desired_nat_note + (M_FLAT * (-1 * accdtls))
|
571
|
+
else
|
572
|
+
return desired_nat_note
|
573
|
+
end
|
574
|
+
else
|
575
|
+
accdtls = 0
|
576
|
+
# if (pos % self.no_keys) in self.pos_lookup_nat_key
|
577
|
+
if self.pos_lookup_nat_key.key?( pos % self.no_keys )
|
578
|
+
return self.pos_lookup_nat_key[pos % self.no_keys]
|
579
|
+
|
580
|
+
elsif sharp_not_flat
|
581
|
+
while accdtls < self.no_keys
|
582
|
+
accdtls = accdtls - 1
|
583
|
+
#if ((pos + accdtls) % self.no_keys) in self.pos_lookup_nat_key
|
584
|
+
if self.pos_lookup_nat_key.key?((pos + accdtls) % self.no_keys)
|
585
|
+
return self.pos_lookup_nat_key[
|
586
|
+
(pos + accdtls) % self.no_keys] + (M_SHARP * (-1 * accdtls))
|
587
|
+
end
|
588
|
+
end
|
589
|
+
|
590
|
+
else
|
591
|
+
while accdtls < self.no_keys
|
592
|
+
accdtls = accdtls + 1
|
593
|
+
#if ((pos + accdtls) % self.no_keys) in self.pos_lookup_nat_key:
|
594
|
+
if self.pos_lookup_nat_key.key?((pos + accdtls) % self.no_keys)
|
595
|
+
return self.pos_lookup_nat_key[
|
596
|
+
(pos + accdtls) % self.no_keys] + (M_FLAT * accdtls)
|
597
|
+
end
|
598
|
+
end
|
599
|
+
|
600
|
+
end
|
601
|
+
end
|
602
|
+
|
603
|
+
return nil
|
604
|
+
end
|
605
|
+
|
606
|
+
|
607
|
+
# This function takes a key and a sequence of positions relative to
|
608
|
+
# it. It returns a sequence of notes.
|
609
|
+
# ==== Args
|
610
|
+
# key:: the starting key; examples are "C", "C#" and "Db".
|
611
|
+
# pos_seq:: a list of positions relative to it in the temperament
|
612
|
+
# base 0). For example, in the Western/Chromatic temperament, a
|
613
|
+
# key of "C" and a sequence of [0, 1] returns ["C", "C#].
|
614
|
+
# nat_pos_seq:: a list of numbers. These are used to calculate the
|
615
|
+
# desired natural notes produced from the corresponding positions
|
616
|
+
# in pos_seq. A number of 0 means to use the same natural note as
|
617
|
+
# in key, a number of 1 means using the next natural note, and so
|
618
|
+
# on. Examples for the Western/Chromatic scale:
|
619
|
+
# get_note_sequence("C", [0, 1], [0, 0]) =>["C", "C#"]
|
620
|
+
# get_note_sequence("C", [0, 1], [0, 1]) =>["C", "Dbb"]
|
621
|
+
# get_note_sequence("C", [0, 1], [0, 6]) =>["C", "B##"]
|
622
|
+
# sharp_not_flat:: if nat_pos_seq is None, indicates whether notes
|
623
|
+
# with accidentals are preferred to have sharps (True) or flats
|
624
|
+
# (False)
|
625
|
+
# ==== Returns
|
626
|
+
#
|
627
|
+
# ==== See Also
|
628
|
+
# - `#get_keyseq_notes()`
|
629
|
+
#
|
630
|
+
def get_note_sequence( key, pos_seq,
|
631
|
+
nat_pos_seq = nil, sharp_not_flat = nil )
|
632
|
+
ret = nil
|
633
|
+
base_key_pos = self.get_pos_of_key(key)
|
634
|
+
#result_pos_seq = [((base_key_pos + i) % self.no_keys) for i in pos_seq]
|
635
|
+
result_pos_seq = pos_seq.map{|i| (base_key_pos + i) % self.no_keys }
|
636
|
+
|
637
|
+
if nat_pos_seq
|
638
|
+
result_seq = []
|
639
|
+
desired_nat_key = self.note_parse(key)[0]
|
640
|
+
desired_nat_key_posn = self.nat_keys.index(desired_nat_key)
|
641
|
+
|
642
|
+
#for i in range(min(len(nat_pos_seq), len(result_pos_seq))):
|
643
|
+
for i in 0...([nat_pos_seq.size, result_pos_seq.size].min)
|
644
|
+
des_nat_note = self.nat_keys[
|
645
|
+
(desired_nat_key_posn + nat_pos_seq[i]) % self.no_nat_keys]
|
646
|
+
des_key = self.get_key_of_pos(result_pos_seq[i], des_nat_note)
|
647
|
+
result_seq.append(des_key)
|
648
|
+
end
|
649
|
+
ret = result_seq
|
650
|
+
|
651
|
+
else
|
652
|
+
# return [self.get_key_of_pos(i, None, sharp_not_flat) for i in
|
653
|
+
# result_pos_seq];
|
654
|
+
ret = result_pos_seq.map{|i|
|
655
|
+
self.get_key_of_pos(i, nil, sharp_not_flat) }
|
656
|
+
end
|
657
|
+
|
658
|
+
ret
|
659
|
+
end
|
660
|
+
|
661
|
+
|
662
|
+
# The reverse of get_note_sequence().
|
663
|
+
# ==== Args
|
664
|
+
# note_seq:: a sequence of notes (note_seq)
|
665
|
+
# ==== Returns
|
666
|
+
# [base key, position sequence]
|
667
|
+
# ==== Examples
|
668
|
+
# ["C", "D"] =>["C", [0, 2]]
|
669
|
+
# ["D", "C#", "E"] =>["D", [0, 11, 2]]
|
670
|
+
#
|
671
|
+
# this is returned by the function.
|
672
|
+
#
|
673
|
+
def get_keyseq_notes( note_seq )
|
674
|
+
base_key = note_seq[0]
|
675
|
+
base_pos = self.get_pos_of_key(base_key)
|
676
|
+
pos_seq = note_seq.map{|i|
|
677
|
+
(self.get_pos_of_key(i) - base_pos) % self.no_keys }
|
678
|
+
|
679
|
+
[base_key, pos_seq]
|
680
|
+
end
|
681
|
+
|
682
|
+
|
683
|
+
### The next few functions are rip-offs of SeqDict functions.
|
684
|
+
|
685
|
+
# This adds an element to the dictionary inside the temperament. The
|
686
|
+
# arguments:
|
687
|
+
# elem: the element to add to the dictionary .
|
688
|
+
# nseq_type: the type of the elemenet (such as scale or chord).
|
689
|
+
# name_s: a string, or a sequence of strings. This provides names
|
690
|
+
# as keys that map onto elem.
|
691
|
+
# abbrv_s: a string, or a sequence of strings. This provides
|
692
|
+
# abbreviations as keys that map onto elem.
|
693
|
+
# seqpos: a sequence. A tuple form will be used as a key that
|
694
|
+
# maps onto elem.
|
695
|
+
#
|
696
|
+
# If nseq_type is not associated with any of the sub-dictionaries in
|
697
|
+
# the dictionary in the temperament, then this function exits.
|
698
|
+
#
|
699
|
+
def add_elem( elem, nseq_type, name_s, abbrv_s, seqpos )
|
700
|
+
self.seq_maps.add_elem(elem, nseq_type, name_s, abbrv_s, seqpos)
|
701
|
+
end
|
702
|
+
|
703
|
+
|
704
|
+
# Checks if there is a subdictionary associated with nseq_type.
|
705
|
+
def check_nseqby_subdict( nseq_type )
|
706
|
+
self.seq_maps.check_nseqby_subdict(nseq_type)
|
707
|
+
end
|
708
|
+
|
709
|
+
# Checks if there is a noteseq with a given name.
|
710
|
+
def check_nseqby_name( nseq_type, name )
|
711
|
+
self.seq_maps.check_nseqby_name(nseq_type, name)
|
712
|
+
end
|
713
|
+
|
714
|
+
# Checks if there is a noteseq with a given abbreviation.
|
715
|
+
def check_nseqby_abbrv( nseq_type, abbrv )
|
716
|
+
self.seq_maps.check_nseqby_abbrv(nseq_type, abbrv)
|
717
|
+
end
|
718
|
+
|
719
|
+
# Checks if there is a noteseq with a given sequence position.
|
720
|
+
def check_nseqby_seqpos( nseq_type, seqpos )
|
721
|
+
self.seq_maps.check_nseqby_seqpos(nseq_type, seqpos)
|
722
|
+
end
|
723
|
+
|
724
|
+
# Looks up a noteseq (or anything else) by name.
|
725
|
+
def get_nseqby_name( name, nseq_type )
|
726
|
+
if self.seq_maps.check_nseqby_name(nseq_type, name)
|
727
|
+
self.seq_maps.get_nseqby_name(name, nseq_type)
|
728
|
+
else
|
729
|
+
nil
|
730
|
+
end
|
731
|
+
end
|
732
|
+
|
733
|
+
# Looks up a noteseq (or anything else) by abbreviation.
|
734
|
+
def get_nseqby_abbrv( abbrv, nseq_type )
|
735
|
+
if self.seq_maps.check_nseqby_abbrv(nseq_type, abbrv)
|
736
|
+
self.seq_maps.get_nseqby_abbrv(abbrv, nseq_type)
|
737
|
+
else
|
738
|
+
nil
|
739
|
+
end
|
740
|
+
end
|
741
|
+
|
742
|
+
# Looks up a noteseq (or anything else) by sequence position.
|
743
|
+
def get_nseqby_seqpos( seqpos, nseq_type )
|
744
|
+
if self.seq_maps.check_nseqby_seqpos(nseq_type, seqpos)
|
745
|
+
self.seq_maps.get_nseqby_seqpos(seqpos, nseq_type)
|
746
|
+
else
|
747
|
+
nil
|
748
|
+
end
|
749
|
+
end
|
750
|
+
|
751
|
+
|
752
|
+
end
|
753
|
+
end
|
754
|
+
end
|
755
|
+
|
756
|
+
|
757
|
+
# We immediately use this class to define a Western or Chromatic temperament
|
758
|
+
# object. Apart from useful for understanding the class, the object is used in
|
759
|
+
# testing.
|
760
|
+
module MusicSetTheory
|
761
|
+
module Temperament
|
762
|
+
|
763
|
+
CHROM_NAT_NOTES = ["C", "D", "E", "F", "G", "A", "B"]
|
764
|
+
CHROM_NAT_NOTE_POS = [0, 2, 4, 5, 7, 9, 11]
|
765
|
+
CHROM_SIZE = 12
|
766
|
+
def self.WestTempNew
|
767
|
+
Temperament.new(CHROM_SIZE, CHROM_NAT_NOTES, CHROM_NAT_NOTE_POS)
|
768
|
+
end
|
769
|
+
def WestTempNew
|
770
|
+
MusicSetTheory::Temperament.WestTempNew()
|
771
|
+
end
|
772
|
+
|
773
|
+
#WestTemp = deep_freeze(WestTempNew()) # the last failed in chords_test(1).
|
774
|
+
#WestTemp = WestTempNew() # the last failed in chords_test(1).
|
775
|
+
WestTemp = MusicSetTheory.deep_freeze(WestTempNew())
|
776
|
+
#ng. WestTemp.deep_freeze
|
777
|
+
|
778
|
+
end
|
779
|
+
end
|
780
|
+
|
781
|
+
|
782
|
+
|
783
|
+
#### endof filename: music_set_theory/temperament.rb
|