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,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