musa-dsl 0.40.0 → 0.42.0
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 +4 -4
- data/.gitignore +3 -0
- data/Gemfile +0 -1
- data/README.md +15 -1
- data/docs/README.md +1 -0
- data/docs/subsystems/datasets.md +75 -0
- data/docs/subsystems/generative.md +92 -6
- data/docs/subsystems/music.md +349 -19
- data/docs/subsystems/transport.md +26 -0
- data/lib/musa-dsl/datasets/dataset.rb +2 -0
- data/lib/musa-dsl/datasets/gdv.rb +3 -3
- data/lib/musa-dsl/datasets/p.rb +1 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +4 -2
- data/lib/musa-dsl/datasets/score.rb +3 -1
- data/lib/musa-dsl/generative/darwin.rb +36 -1
- data/lib/musa-dsl/generative/generative-grammar.rb +31 -1
- data/lib/musa-dsl/generative/markov.rb +3 -1
- data/lib/musa-dsl/generative/rules.rb +54 -0
- data/lib/musa-dsl/generative/variatio.rb +69 -0
- data/lib/musa-dsl/midi/midi-recorder.rb +4 -0
- data/lib/musa-dsl/midi/midi-voices.rb +13 -1
- data/lib/musa-dsl/music/chord-definition.rb +7 -5
- data/lib/musa-dsl/music/chord-definitions.rb +37 -0
- data/lib/musa-dsl/music/chords.rb +88 -21
- data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +70 -521
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_dominant_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_major_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_minor_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/blues/blues_major_scale_kind.rb +100 -0
- data/lib/musa-dsl/music/scale_kinds/blues/blues_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_kinds/chromatic_scale_kind.rb +79 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/double_harmonic_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/hungarian_minor_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_major_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_minor_scale_kind.rb +101 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/phrygian_dominant_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/harmonic_major/harmonic_major_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/major_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/altered_scale_kind.rb +106 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/dorian_b2_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/locrian_sharp2_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_augmented_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_dominant_scale_kind.rb +106 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/melodic_minor_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/mixolydian_b6_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/minor_harmonic_scale_kind.rb +125 -0
- data/lib/musa-dsl/music/scale_kinds/minor_natural_scale_kind.rb +123 -0
- data/lib/musa-dsl/music/scale_kinds/modes/dorian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/locrian_scale_kind.rb +114 -0
- data/lib/musa-dsl/music/scale_kinds/modes/lydian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/mixolydian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/phrygian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_major_scale_kind.rb +93 -0
- data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_minor_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_hw_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_wh_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/whole_tone_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_systems/equally_tempered_12_tone_scale_system.rb +80 -0
- data/lib/musa-dsl/music/scale_systems/twelve_semitones_scale_system.rb +60 -0
- data/lib/musa-dsl/music/scales.rb +606 -67
- data/lib/musa-dsl/musicxml/builder/note.rb +31 -92
- data/lib/musa-dsl/musicxml/builder/pitched-note.rb +33 -94
- data/lib/musa-dsl/musicxml/builder/rest.rb +30 -91
- data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +31 -91
- data/lib/musa-dsl/neumas/array-to-neumas.rb +1 -1
- data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +2 -2
- data/lib/musa-dsl/sequencer/sequencer-dsl.rb +367 -3
- data/lib/musa-dsl/series/base-series.rb +250 -240
- data/lib/musa-dsl/series/buffer-serie.rb +16 -5
- data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +29 -3
- data/lib/musa-dsl/series/main-serie-constructors.rb +19 -15
- data/lib/musa-dsl/series/main-serie-operations.rb +74 -29
- data/lib/musa-dsl/series/proxy-serie.rb +5 -1
- data/lib/musa-dsl/series/quantizer-serie.rb +16 -2
- data/lib/musa-dsl/series/queue-serie.rb +15 -1
- data/lib/musa-dsl/series/series-composer.rb +5 -2
- data/lib/musa-dsl/series/timed-serie.rb +8 -4
- data/lib/musa-dsl/transport/timer-clock.rb +4 -2
- data/lib/musa-dsl/transport/timer.rb +27 -4
- data/lib/musa-dsl/version.rb +1 -1
- data/musa-dsl.gemspec +18 -15
- metadata +85 -22
|
@@ -104,7 +104,7 @@ module Musa
|
|
|
104
104
|
# Makes the scale system available via symbol lookup and dynamic method.
|
|
105
105
|
# Optionally marks it as the default system.
|
|
106
106
|
#
|
|
107
|
-
# @param scale_system [Class] the ScaleSystem subclass to register
|
|
107
|
+
# @param scale_system [Class<ScaleSystem>] the ScaleSystem subclass to register
|
|
108
108
|
# @param default [Boolean] whether to set as default system
|
|
109
109
|
# @return [self]
|
|
110
110
|
#
|
|
@@ -126,26 +126,55 @@ module Musa
|
|
|
126
126
|
# Retrieves a registered scale system by ID.
|
|
127
127
|
#
|
|
128
128
|
# @param id [Symbol] the scale system identifier
|
|
129
|
-
# @return [Class] the ScaleSystem subclass
|
|
129
|
+
# @return [Class<ScaleSystem>] the ScaleSystem subclass
|
|
130
130
|
# @raise [KeyError] if scale system not found
|
|
131
131
|
#
|
|
132
132
|
# @example
|
|
133
133
|
# Scales[:et12] # => EquallyTempered12ToneScaleSystem
|
|
134
|
-
def self.
|
|
134
|
+
def self.get(id)
|
|
135
135
|
raise KeyError, "Scale system :#{id} not found" unless @scale_systems.key?(id)
|
|
136
136
|
|
|
137
137
|
@scale_systems[id]
|
|
138
138
|
end
|
|
139
139
|
|
|
140
|
+
class << self
|
|
141
|
+
alias_method :[], :get
|
|
142
|
+
end
|
|
143
|
+
|
|
140
144
|
# Returns the default scale system.
|
|
141
145
|
#
|
|
142
|
-
# @return [Class] the default ScaleSystem subclass
|
|
146
|
+
# @return [Class<ScaleSystem>] the default ScaleSystem subclass
|
|
143
147
|
#
|
|
144
148
|
# @example
|
|
145
149
|
# Scales.default_system # => EquallyTempered12ToneScaleSystem
|
|
146
150
|
def self.default_system
|
|
147
151
|
@default_scale_system
|
|
148
152
|
end
|
|
153
|
+
|
|
154
|
+
# Convenience method to extend metadata for a scale kind by ID.
|
|
155
|
+
#
|
|
156
|
+
# Finds the ScaleKind class by its ID symbol and adds custom metadata to it.
|
|
157
|
+
# This is a shortcut for accessing the class directly and calling extend_metadata.
|
|
158
|
+
#
|
|
159
|
+
# @param scale_kind_id [Symbol] the scale kind identifier (e.g., :major, :dorian)
|
|
160
|
+
# @param metadata [Hash] key-value pairs to add as custom metadata
|
|
161
|
+
# @return [Hash] the updated custom_metadata hash
|
|
162
|
+
# @raise [KeyError] if scale kind not found
|
|
163
|
+
#
|
|
164
|
+
# @example
|
|
165
|
+
# Scales.extend_metadata(:major, my_tag: :favorite)
|
|
166
|
+
# Scales.extend_metadata(:dorian, mood: :nostalgic, suitable_for: [:jazz])
|
|
167
|
+
#
|
|
168
|
+
# @see ScaleKind.extend_metadata
|
|
169
|
+
def self.extend_metadata(scale_kind_id, **metadata)
|
|
170
|
+
system = default_system
|
|
171
|
+
raise KeyError, "No default scale system registered" unless system
|
|
172
|
+
|
|
173
|
+
klass = system.scale_kind_class(scale_kind_id)
|
|
174
|
+
raise KeyError, "Scale kind :#{scale_kind_id} not found" unless klass
|
|
175
|
+
|
|
176
|
+
klass.extend_metadata(**metadata)
|
|
177
|
+
end
|
|
149
178
|
end
|
|
150
179
|
|
|
151
180
|
# Abstract base class for musical scale systems.
|
|
@@ -294,7 +323,7 @@ module Musa
|
|
|
294
323
|
#
|
|
295
324
|
# @example Modern high pitch
|
|
296
325
|
# modern = ScaleSystem[442.0]
|
|
297
|
-
def self.
|
|
326
|
+
def self.get(a_frequency)
|
|
298
327
|
a_frequency = a_frequency.to_f
|
|
299
328
|
|
|
300
329
|
@a_tunings ||= {}
|
|
@@ -303,6 +332,10 @@ module Musa
|
|
|
303
332
|
@a_tunings[a_frequency]
|
|
304
333
|
end
|
|
305
334
|
|
|
335
|
+
class << self
|
|
336
|
+
alias_method :[], :get
|
|
337
|
+
end
|
|
338
|
+
|
|
306
339
|
# Returns semitone offset for a named interval.
|
|
307
340
|
#
|
|
308
341
|
# @param name [Symbol] interval name (e.g., :M3, :P5)
|
|
@@ -326,7 +359,7 @@ module Musa
|
|
|
326
359
|
|
|
327
360
|
# Registers a scale kind (major, minor, etc.) with this system.
|
|
328
361
|
#
|
|
329
|
-
# @param scale_kind_class [Class] ScaleKind subclass to register
|
|
362
|
+
# @param scale_kind_class [Class<ScaleKind>] ScaleKind subclass to register
|
|
330
363
|
# @return [self]
|
|
331
364
|
#
|
|
332
365
|
# @example
|
|
@@ -343,7 +376,7 @@ module Musa
|
|
|
343
376
|
# Retrieves a registered scale kind by ID.
|
|
344
377
|
#
|
|
345
378
|
# @param id [Symbol] scale kind identifier
|
|
346
|
-
# @return [Class] ScaleKind subclass
|
|
379
|
+
# @return [Class<ScaleKind>] ScaleKind subclass
|
|
347
380
|
# @raise [KeyError] if not found
|
|
348
381
|
def self.scale_kind_class(id)
|
|
349
382
|
raise KeyError, "Scale kind class [#{id}] not found in scale system [#{self.id}]" unless @scale_kind_classes.key? id
|
|
@@ -368,7 +401,7 @@ module Musa
|
|
|
368
401
|
|
|
369
402
|
# Returns the chromatic scale kind class.
|
|
370
403
|
#
|
|
371
|
-
# @return [Class] chromatic ScaleKind subclass
|
|
404
|
+
# @return [Class<ScaleKind>] chromatic ScaleKind subclass
|
|
372
405
|
# @raise [RuntimeError] if chromatic scale not defined
|
|
373
406
|
def self.chromatic_class
|
|
374
407
|
raise "Chromatic scale kind class for [#{self.id}] scale system undefined" if @chromatic_scale_kind_class.nil?
|
|
@@ -427,6 +460,12 @@ module Musa
|
|
|
427
460
|
class ScaleSystemTuning
|
|
428
461
|
extend Forwardable
|
|
429
462
|
|
|
463
|
+
# Creates a tuning instance for a scale system.
|
|
464
|
+
#
|
|
465
|
+
# @param scale_system [Class<ScaleSystem>] the ScaleSystem subclass
|
|
466
|
+
# @param a_frequency [Numeric] reference A frequency in Hz
|
|
467
|
+
#
|
|
468
|
+
# @api private
|
|
430
469
|
def initialize(scale_system, a_frequency)
|
|
431
470
|
@scale_system = scale_system
|
|
432
471
|
@a_frequency = a_frequency
|
|
@@ -443,28 +482,214 @@ module Musa
|
|
|
443
482
|
|
|
444
483
|
# TODO: allow scales not based in octaves but in other intervals (like fifths or other ratios). Possibly based on intervals definition of ScaleSystem plus a "generator interval" attribute
|
|
445
484
|
|
|
485
|
+
# @!method notes_in_octave
|
|
486
|
+
# Returns the number of notes in one octave.
|
|
487
|
+
# Delegated from {ScaleSystem.notes_in_octave}.
|
|
488
|
+
# @return [Integer] notes per octave (e.g., 12 for chromatic)
|
|
489
|
+
|
|
490
|
+
# @!method offset_of_interval(name)
|
|
491
|
+
# Returns semitone offset for a named interval.
|
|
492
|
+
# Delegated from {ScaleSystem.offset_of_interval}.
|
|
493
|
+
# @param name [Symbol] interval name (e.g., :M3, :P5)
|
|
494
|
+
# @return [Integer] semitone offset
|
|
495
|
+
|
|
446
496
|
def_delegators :@scale_system, :notes_in_octave, :offset_of_interval
|
|
447
497
|
|
|
448
|
-
|
|
498
|
+
# Reference A frequency in Hz.
|
|
499
|
+
# @return [Float]
|
|
500
|
+
attr_reader :a_frequency
|
|
501
|
+
|
|
502
|
+
# The parent scale system.
|
|
503
|
+
# @return [Class<ScaleSystem>] ScaleSystem subclass
|
|
504
|
+
attr_reader :scale_system
|
|
449
505
|
|
|
450
|
-
|
|
506
|
+
# Retrieves a scale kind by ID.
|
|
507
|
+
#
|
|
508
|
+
# Creates and caches {ScaleKind} instances for efficient reuse.
|
|
509
|
+
#
|
|
510
|
+
# @param scale_kind_class_id [Symbol] scale kind identifier (e.g., :major, :minor)
|
|
511
|
+
# @return [ScaleKind] scale kind instance
|
|
512
|
+
# @raise [KeyError] if scale kind not found
|
|
513
|
+
#
|
|
514
|
+
# @example
|
|
515
|
+
# tuning[:major][60] # C major scale
|
|
516
|
+
# tuning[:minor][69] # A minor scale
|
|
517
|
+
#
|
|
518
|
+
# @see ScaleKind
|
|
519
|
+
def get(scale_kind_class_id)
|
|
451
520
|
@scale_kinds[scale_kind_class_id] ||= @scale_system.scale_kind_class(scale_kind_class_id).new self
|
|
452
521
|
end
|
|
453
522
|
|
|
523
|
+
alias_method :[], :get
|
|
524
|
+
|
|
525
|
+
# Returns the chromatic scale kind.
|
|
526
|
+
#
|
|
527
|
+
# Provides access to the chromatic scale, which contains all pitches
|
|
528
|
+
# in the scale system. Used as fallback for non-diatonic notes.
|
|
529
|
+
#
|
|
530
|
+
# @return [ScaleKind] chromatic scale kind instance
|
|
531
|
+
#
|
|
532
|
+
# @example
|
|
533
|
+
# tuning.chromatic[60] # Chromatic scale at C
|
|
534
|
+
#
|
|
535
|
+
# @see ScaleSystem.chromatic_class
|
|
454
536
|
def chromatic
|
|
455
537
|
@chromatic_scale_kind
|
|
456
538
|
end
|
|
457
539
|
|
|
540
|
+
# Calculates frequency for a MIDI pitch.
|
|
541
|
+
#
|
|
542
|
+
# Delegates to the scale system's frequency calculation using
|
|
543
|
+
# this tuning's A frequency as reference.
|
|
544
|
+
#
|
|
545
|
+
# @param pitch [Numeric] MIDI pitch number (60 = middle C)
|
|
546
|
+
# @param root [Numeric] root pitch of the scale (for non-equal temperaments)
|
|
547
|
+
# @return [Float] frequency in Hz
|
|
548
|
+
#
|
|
549
|
+
# @example
|
|
550
|
+
# tuning.frequency_of_pitch(69, 60) # => 440.0 (A4)
|
|
551
|
+
# tuning.frequency_of_pitch(60, 60) # => ~261.63 (C4)
|
|
552
|
+
#
|
|
553
|
+
# @see ScaleSystem.frequency_of_pitch
|
|
458
554
|
def frequency_of_pitch(pitch, root)
|
|
459
555
|
@scale_system.frequency_of_pitch(pitch, root, @a_frequency)
|
|
460
556
|
end
|
|
461
557
|
|
|
558
|
+
# Returns scale kinds matching the given metadata criteria.
|
|
559
|
+
#
|
|
560
|
+
# Without arguments, returns all registered scale kinds.
|
|
561
|
+
# With keyword arguments, filters by metadata values.
|
|
562
|
+
# With a block, filters using custom predicate on ScaleKind class.
|
|
563
|
+
#
|
|
564
|
+
# @param metadata_criteria [Hash] metadata key-value pairs to match
|
|
565
|
+
# @yield [kind_class] optional block for custom filtering
|
|
566
|
+
# @yieldparam kind_class [Class<ScaleKind>] the ScaleKind subclass
|
|
567
|
+
# @yieldreturn [Boolean] true to include this scale kind
|
|
568
|
+
# @return [Array<ScaleKind>] matching scale kind instances
|
|
569
|
+
#
|
|
570
|
+
# @example All scale kinds
|
|
571
|
+
# tuning.scale_kinds
|
|
572
|
+
# # => [major_kind, minor_kind, dorian_kind, ...]
|
|
573
|
+
#
|
|
574
|
+
# @example Filter by metadata
|
|
575
|
+
# tuning.scale_kinds(family: :diatonic)
|
|
576
|
+
# tuning.scale_kinds(brightness: -1..1)
|
|
577
|
+
# tuning.scale_kinds(character: :jazz)
|
|
578
|
+
#
|
|
579
|
+
# @example Filter with block
|
|
580
|
+
# tuning.scale_kinds { |klass| klass.intrinsic_metadata[:has_leading_tone] }
|
|
581
|
+
#
|
|
582
|
+
# @example Combined
|
|
583
|
+
# tuning.scale_kinds(family: :greek_modes) { |klass| klass.metadata[:brightness] < 0 }
|
|
584
|
+
#
|
|
585
|
+
# @see ScaleKind.metadata
|
|
586
|
+
# @see ScaleKind.has_metadata?
|
|
587
|
+
def scale_kinds(**metadata_criteria, &block)
|
|
588
|
+
result = @scale_system.scale_kind_classes.keys.map { |id| self[id] }
|
|
589
|
+
|
|
590
|
+
unless metadata_criteria.empty?
|
|
591
|
+
result = result.select do |kind|
|
|
592
|
+
matches_metadata?(kind.class, metadata_criteria)
|
|
593
|
+
end
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
if block
|
|
597
|
+
result = result.select { |kind| block.call(kind.class) }
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
result
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
# Searches for a chord across multiple scale types.
|
|
604
|
+
#
|
|
605
|
+
# Iterates through the specified scale kinds and pitch roots to find
|
|
606
|
+
# all scales that contain the given chord. Returns chords with their
|
|
607
|
+
# containing scale as context.
|
|
608
|
+
#
|
|
609
|
+
# @param chord [Musa::Chords::Chord] the chord to search for
|
|
610
|
+
# @param roots [Range, Array, nil] pitch offsets to search (default: 0...notes_in_octave)
|
|
611
|
+
# @param metadata_criteria [Hash] metadata filters for scale kinds
|
|
612
|
+
# @return [Array<Musa::Chords::Chord>] chords with their containing scales
|
|
613
|
+
#
|
|
614
|
+
# @example Search G7 in greek mode scales
|
|
615
|
+
# tuning = Scales.et12[440.0]
|
|
616
|
+
# g7 = tuning.major[60].dominant.chord :seventh
|
|
617
|
+
# tuning.search_chord_in_scales(g7, family: :greek_modes)
|
|
618
|
+
#
|
|
619
|
+
# @example Search with brightness filter
|
|
620
|
+
# tuning.search_chord_in_scales(g7, brightness: -1..1)
|
|
621
|
+
#
|
|
622
|
+
# @example Search in all scale types
|
|
623
|
+
# tuning.search_chord_in_scales(g7)
|
|
624
|
+
#
|
|
625
|
+
# @see ScaleKind#find_chord_in_scales
|
|
626
|
+
# @see Musa::Chords::Chord#search_in_scales
|
|
627
|
+
def search_chord_in_scales(chord, roots: nil, **metadata_criteria)
|
|
628
|
+
roots ||= 0...notes_in_octave
|
|
629
|
+
kinds = filtered_scale_kind_ids(**metadata_criteria)
|
|
630
|
+
|
|
631
|
+
kinds.flat_map do |kind_id|
|
|
632
|
+
self[kind_id].find_chord_in_scales(chord, roots: roots)
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
private
|
|
637
|
+
|
|
638
|
+
# Returns scale kind IDs filtered by metadata criteria.
|
|
639
|
+
#
|
|
640
|
+
# @param metadata_criteria [Hash] key-value pairs to match
|
|
641
|
+
# @return [Array<Symbol>] matching scale kind IDs
|
|
642
|
+
#
|
|
643
|
+
# @api private
|
|
644
|
+
def filtered_scale_kind_ids(**metadata_criteria)
|
|
645
|
+
kinds = @scale_system.scale_kind_classes.keys
|
|
646
|
+
|
|
647
|
+
return kinds if metadata_criteria.empty?
|
|
648
|
+
|
|
649
|
+
kinds.select do |kind_id|
|
|
650
|
+
kind_class = @scale_system.scale_kind_class(kind_id)
|
|
651
|
+
matches_metadata?(kind_class, metadata_criteria)
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
# Checks if a scale kind class matches the given criteria.
|
|
656
|
+
#
|
|
657
|
+
# @param kind_class [Class<ScaleKind>] ScaleKind subclass to check
|
|
658
|
+
# @param criteria [Hash] metadata key-value pairs to match
|
|
659
|
+
# @return [Boolean] true if all criteria match
|
|
660
|
+
#
|
|
661
|
+
# @api private
|
|
662
|
+
def matches_metadata?(kind_class, criteria)
|
|
663
|
+
criteria.all? do |key, value|
|
|
664
|
+
actual = kind_class.metadata[key]
|
|
665
|
+
case value
|
|
666
|
+
when Range
|
|
667
|
+
actual.is_a?(Numeric) && value.include?(actual)
|
|
668
|
+
when Array
|
|
669
|
+
value.any? { |v| actual == v || (actual.is_a?(Array) && actual.include?(v)) }
|
|
670
|
+
else
|
|
671
|
+
actual == value || (actual.is_a?(Array) && actual.include?(value))
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
public
|
|
677
|
+
|
|
678
|
+
# Checks tuning equality.
|
|
679
|
+
#
|
|
680
|
+
# Tunings are equal if they have the same scale system and A frequency.
|
|
681
|
+
#
|
|
682
|
+
# @param other [ScaleSystemTuning] the tuning to compare
|
|
683
|
+
# @return [Boolean] true if equal
|
|
462
684
|
def ==(other)
|
|
463
685
|
self.class == other.class &&
|
|
464
686
|
@scale_system == other.scale_system &&
|
|
465
687
|
@a_frequency == other.a_frequency
|
|
466
688
|
end
|
|
467
689
|
|
|
690
|
+
# Returns string representation.
|
|
691
|
+
#
|
|
692
|
+
# @return [String] human-readable description
|
|
468
693
|
def inspect
|
|
469
694
|
"<ScaleSystemTuning: scale_system = #{@scale_system} a_frequency = #{@a_frequency}>"
|
|
470
695
|
end
|
|
@@ -552,11 +777,13 @@ module Musa
|
|
|
552
777
|
# major_kind = tuning[:major]
|
|
553
778
|
# c_major = major_kind[60] # C major
|
|
554
779
|
# g_major = major_kind[67] # G major
|
|
555
|
-
def
|
|
780
|
+
def get(root_pitch)
|
|
556
781
|
@scales[root_pitch] = Scale.new(self, root_pitch: root_pitch) unless @scales.key?(root_pitch)
|
|
557
782
|
@scales[root_pitch]
|
|
558
783
|
end
|
|
559
784
|
|
|
785
|
+
alias_method :[], :get
|
|
786
|
+
|
|
560
787
|
# Returns scale with default root (middle C, MIDI 60).
|
|
561
788
|
#
|
|
562
789
|
# @return [Scale] scale rooted on middle C
|
|
@@ -577,6 +804,35 @@ module Musa
|
|
|
577
804
|
self[0]
|
|
578
805
|
end
|
|
579
806
|
|
|
807
|
+
# Finds all scales of this kind that contain the given chord.
|
|
808
|
+
#
|
|
809
|
+
# Searches through scales rooted on different pitches to find which ones
|
|
810
|
+
# contain all the notes of the given chord. Returns chords with their
|
|
811
|
+
# containing scale as context.
|
|
812
|
+
#
|
|
813
|
+
# @param chord [Musa::Chords::Chord] the chord to search for
|
|
814
|
+
# @param roots [Range, Array, nil] pitch offsets to search (default: 0...notes_in_octave)
|
|
815
|
+
# @return [Array<Musa::Chords::Chord>] chords with their containing scales
|
|
816
|
+
#
|
|
817
|
+
# @example Find G major triad in all major scales
|
|
818
|
+
# tuning = Scales.et12[440.0]
|
|
819
|
+
# g_triad = tuning.major[60].dominant.chord
|
|
820
|
+
# tuning.major.find_chord_in_scales(g_triad)
|
|
821
|
+
# # => [Chord in C major (V), Chord in G major (I), Chord in D major (IV)]
|
|
822
|
+
#
|
|
823
|
+
# @see Musa::Chords::Chord#as_chord_in_scale
|
|
824
|
+
# @see ScaleSystemTuning#search_chord_in_scales
|
|
825
|
+
def find_chord_in_scales(chord, roots: nil)
|
|
826
|
+
roots ||= 0...tuning.notes_in_octave
|
|
827
|
+
base_pitch = chord.root.pitch % tuning.notes_in_octave
|
|
828
|
+
|
|
829
|
+
roots.filter_map do |root_offset|
|
|
830
|
+
root_pitch = base_pitch + root_offset
|
|
831
|
+
scale = self[root_pitch]
|
|
832
|
+
chord.as_chord_in_scale(scale)
|
|
833
|
+
end
|
|
834
|
+
end
|
|
835
|
+
|
|
580
836
|
# Checks scale kind equality.
|
|
581
837
|
#
|
|
582
838
|
# @param other [ScaleKind]
|
|
@@ -685,8 +941,200 @@ module Musa
|
|
|
685
941
|
@grade_names_index.keys
|
|
686
942
|
end
|
|
687
943
|
|
|
944
|
+
# Returns intrinsic metadata derived from scale structure.
|
|
945
|
+
#
|
|
946
|
+
# This metadata is automatically calculated from the scale's pitch
|
|
947
|
+
# structure and cannot be modified. It includes:
|
|
948
|
+
#
|
|
949
|
+
# - **:id**: Scale kind identifier
|
|
950
|
+
# - **:grades**: Number of diatonic degrees
|
|
951
|
+
# - **:pitches**: Array of pitch offsets from root
|
|
952
|
+
# - **:intervals**: Intervals between consecutive degrees
|
|
953
|
+
# - **:has_leading_tone**: Whether scale has pitch 11 (semitone below octave)
|
|
954
|
+
# - **:has_tritone**: Whether scale contains tritone (pitch 6)
|
|
955
|
+
# - **:symmetric**: Type of symmetry if any (:equal, :palindrome, :repeating)
|
|
956
|
+
#
|
|
957
|
+
# @return [Hash] intrinsic metadata derived from structure
|
|
958
|
+
#
|
|
959
|
+
# @example
|
|
960
|
+
# MajorScaleKind.intrinsic_metadata
|
|
961
|
+
# # => { id: :major, grades: 7, pitches: [0, 2, 4, 5, 7, 9, 11],
|
|
962
|
+
# # intervals: [2, 2, 1, 2, 2, 2], has_leading_tone: true,
|
|
963
|
+
# # has_tritone: true, symmetric: nil }
|
|
964
|
+
def self.intrinsic_metadata
|
|
965
|
+
result = {}
|
|
966
|
+
result[:id] = id if respond_to?(:id)
|
|
967
|
+
result[:grades] = grades if respond_to?(:grades)
|
|
968
|
+
if respond_to?(:pitches)
|
|
969
|
+
result[:pitches] = pitches.map { |p| p[:pitch] }
|
|
970
|
+
result[:intervals] = compute_intervals
|
|
971
|
+
result[:has_leading_tone] = pitches.any? { |p| p[:pitch] == 11 }
|
|
972
|
+
result[:has_tritone] = pitches.any? { |p| p[:pitch] == 6 }
|
|
973
|
+
result[:symmetric] = compute_symmetry
|
|
974
|
+
end
|
|
975
|
+
result.compact
|
|
976
|
+
end
|
|
977
|
+
|
|
978
|
+
# Returns base metadata defined by the musa-dsl library.
|
|
979
|
+
#
|
|
980
|
+
# This metadata is defined in each ScaleKind subclass using the
|
|
981
|
+
# `@base_metadata` class instance variable. It typically includes:
|
|
982
|
+
#
|
|
983
|
+
# - **:family**: Scale family (:diatonic, :greek_modes, :pentatonic, etc.)
|
|
984
|
+
# - **:brightness**: Relative brightness (-3 to +3, major = 0)
|
|
985
|
+
# - **:character**: Array of descriptive tags
|
|
986
|
+
# - **:parent**: Parent scale and degree for modes
|
|
987
|
+
#
|
|
988
|
+
# @return [Hash] library-defined metadata
|
|
989
|
+
#
|
|
990
|
+
# @example
|
|
991
|
+
# MajorScaleKind.base_metadata
|
|
992
|
+
# # => { family: :diatonic, brightness: 0, character: [:bright, :stable] }
|
|
993
|
+
def self.base_metadata
|
|
994
|
+
@base_metadata || {}
|
|
995
|
+
end
|
|
996
|
+
|
|
997
|
+
# Returns custom metadata added by users at runtime.
|
|
998
|
+
#
|
|
999
|
+
# This metadata is added via {.extend_metadata} and can be cleared
|
|
1000
|
+
# with {.reset_custom_metadata}. Takes precedence over base_metadata.
|
|
1001
|
+
#
|
|
1002
|
+
# @return [Hash] user-defined metadata
|
|
1003
|
+
#
|
|
1004
|
+
# @example
|
|
1005
|
+
# MajorScaleKind.extend_metadata(my_tag: :favorite)
|
|
1006
|
+
# MajorScaleKind.custom_metadata # => { my_tag: :favorite }
|
|
1007
|
+
def self.custom_metadata
|
|
1008
|
+
@custom_metadata || {}
|
|
1009
|
+
end
|
|
1010
|
+
|
|
1011
|
+
# Adds custom metadata to this scale kind.
|
|
1012
|
+
#
|
|
1013
|
+
# Custom metadata takes precedence over base_metadata when queried
|
|
1014
|
+
# via {.metadata}. Multiple calls merge metadata together.
|
|
1015
|
+
#
|
|
1016
|
+
# @param metadata [Hash] key-value pairs to add
|
|
1017
|
+
# @return [Hash] the updated custom_metadata hash (frozen)
|
|
1018
|
+
#
|
|
1019
|
+
# @example
|
|
1020
|
+
# MajorScaleKind.extend_metadata(my_mood: :happy, rating: 5)
|
|
1021
|
+
# MajorScaleKind.extend_metadata(suitable_for: [:pop, :classical])
|
|
1022
|
+
# MajorScaleKind.custom_metadata
|
|
1023
|
+
# # => { my_mood: :happy, rating: 5, suitable_for: [:pop, :classical] }
|
|
1024
|
+
def self.extend_metadata(**metadata)
|
|
1025
|
+
@custom_metadata ||= {}
|
|
1026
|
+
@custom_metadata = @custom_metadata.merge(metadata).freeze
|
|
1027
|
+
end
|
|
1028
|
+
|
|
1029
|
+
# Clears all custom metadata from this scale kind.
|
|
1030
|
+
#
|
|
1031
|
+
# @return [nil]
|
|
1032
|
+
#
|
|
1033
|
+
# @example
|
|
1034
|
+
# MajorScaleKind.extend_metadata(temp: :data)
|
|
1035
|
+
# MajorScaleKind.reset_custom_metadata
|
|
1036
|
+
# MajorScaleKind.custom_metadata # => {}
|
|
1037
|
+
def self.reset_custom_metadata
|
|
1038
|
+
@custom_metadata = nil
|
|
1039
|
+
end
|
|
1040
|
+
|
|
1041
|
+
# Returns combined metadata from all three layers.
|
|
1042
|
+
#
|
|
1043
|
+
# Layers are merged with later layers taking precedence:
|
|
1044
|
+
# intrinsic_metadata < base_metadata < custom_metadata
|
|
1045
|
+
#
|
|
1046
|
+
# @return [Hash] combined metadata from all layers
|
|
1047
|
+
#
|
|
1048
|
+
# @example
|
|
1049
|
+
# MajorScaleKind.metadata
|
|
1050
|
+
# # => { id: :major, grades: 7, pitches: [...], family: :diatonic, ... }
|
|
1051
|
+
def self.metadata
|
|
1052
|
+
intrinsic_metadata
|
|
1053
|
+
.merge(base_metadata)
|
|
1054
|
+
.merge(custom_metadata)
|
|
1055
|
+
end
|
|
1056
|
+
|
|
1057
|
+
# Returns a specific metadata value.
|
|
1058
|
+
#
|
|
1059
|
+
# @param key [Symbol] the metadata key
|
|
1060
|
+
# @return [Object, nil] the value or nil if not found
|
|
1061
|
+
#
|
|
1062
|
+
# @example
|
|
1063
|
+
# MajorScaleKind.metadata_value(:family) # => :diatonic
|
|
1064
|
+
def self.metadata_value(key)
|
|
1065
|
+
metadata[key]
|
|
1066
|
+
end
|
|
1067
|
+
|
|
1068
|
+
# Checks whether metadata contains a key or key-value match.
|
|
1069
|
+
#
|
|
1070
|
+
# When called with just a key, checks for key existence.
|
|
1071
|
+
# When called with key and value, checks for exact match or
|
|
1072
|
+
# array inclusion (if metadata value is an array).
|
|
1073
|
+
#
|
|
1074
|
+
# @param key [Symbol] the metadata key
|
|
1075
|
+
# @param value [Object, nil] optional value to match
|
|
1076
|
+
# @return [Boolean] whether the condition is satisfied
|
|
1077
|
+
#
|
|
1078
|
+
# @example Key existence
|
|
1079
|
+
# MajorScaleKind.has_metadata?(:family) # => true
|
|
1080
|
+
# MajorScaleKind.has_metadata?(:nonexistent) # => false
|
|
1081
|
+
#
|
|
1082
|
+
# @example Value matching
|
|
1083
|
+
# MajorScaleKind.has_metadata?(:family, :diatonic) # => true
|
|
1084
|
+
# MajorScaleKind.has_metadata?(:family, :pentatonic) # => false
|
|
1085
|
+
#
|
|
1086
|
+
# @example Array inclusion
|
|
1087
|
+
# MajorScaleKind.has_metadata?(:character, :bright) # => true
|
|
1088
|
+
def self.has_metadata?(key, value = nil)
|
|
1089
|
+
if value.nil?
|
|
1090
|
+
metadata.key?(key)
|
|
1091
|
+
else
|
|
1092
|
+
metadata[key] == value ||
|
|
1093
|
+
(metadata[key].is_a?(Array) && metadata[key].include?(value))
|
|
1094
|
+
end
|
|
1095
|
+
end
|
|
1096
|
+
|
|
688
1097
|
private
|
|
689
1098
|
|
|
1099
|
+
# Computes intervals between consecutive scale degrees.
|
|
1100
|
+
# @return [Array<Integer>, nil] intervals or nil if not calculable
|
|
1101
|
+
# @api private
|
|
1102
|
+
def self.compute_intervals
|
|
1103
|
+
return nil unless respond_to?(:pitches) && pitches.size > 1
|
|
1104
|
+
pitch_values = pitches.map { |p| p[:pitch] }
|
|
1105
|
+
# Only compute within first octave
|
|
1106
|
+
first_octave = pitch_values.take_while { |p| p < 12 }
|
|
1107
|
+
first_octave.push(12) if first_octave.last != 12
|
|
1108
|
+
first_octave.each_cons(2).map { |a, b| b - a }
|
|
1109
|
+
end
|
|
1110
|
+
|
|
1111
|
+
# Computes symmetry type of the scale.
|
|
1112
|
+
# @return [Symbol, nil] :equal, :palindrome, :repeating, or nil
|
|
1113
|
+
# @api private
|
|
1114
|
+
def self.compute_symmetry
|
|
1115
|
+
return nil unless respond_to?(:pitches)
|
|
1116
|
+
intervals = compute_intervals
|
|
1117
|
+
return nil unless intervals && intervals.any?
|
|
1118
|
+
|
|
1119
|
+
# Check if intervals are all equal (e.g., whole tone: [2,2,2,2,2,2])
|
|
1120
|
+
return :equal if intervals.uniq.size == 1
|
|
1121
|
+
|
|
1122
|
+
# Check for palindrome pattern
|
|
1123
|
+
return :palindrome if intervals == intervals.reverse
|
|
1124
|
+
|
|
1125
|
+
# Check for repeating pattern
|
|
1126
|
+
(1..intervals.size / 2).each do |len|
|
|
1127
|
+
pattern = intervals.take(len)
|
|
1128
|
+
if intervals.each_slice(len).all? { |slice| slice == pattern || slice.size < len }
|
|
1129
|
+
return :repeating
|
|
1130
|
+
end
|
|
1131
|
+
end
|
|
1132
|
+
|
|
1133
|
+
nil
|
|
1134
|
+
end
|
|
1135
|
+
|
|
1136
|
+
public
|
|
1137
|
+
|
|
690
1138
|
# Creates internal index mapping function names to grade indices.
|
|
691
1139
|
#
|
|
692
1140
|
# @return [self]
|
|
@@ -738,9 +1186,9 @@ module Musa
|
|
|
738
1186
|
# scale[:V] # Fifth degree
|
|
739
1187
|
# scale[:IV] # Fourth degree
|
|
740
1188
|
#
|
|
741
|
-
# **With accidentals** (sharp # or flat _)
|
|
1189
|
+
# **With accidentals** (sharp # or flat _). Use strings for #:
|
|
742
1190
|
#
|
|
743
|
-
# scale[
|
|
1191
|
+
# scale['I#'] # Raised tonic
|
|
744
1192
|
# scale[:V_] # Flatted dominant
|
|
745
1193
|
# scale['II##'] # Double-raised second
|
|
746
1194
|
#
|
|
@@ -767,8 +1215,8 @@ module Musa
|
|
|
767
1215
|
# c_major.dominant.pitch # => 67 (G)
|
|
768
1216
|
# c_major[:III].pitch # => 64 (E)
|
|
769
1217
|
#
|
|
770
|
-
# @example Chromatic alterations
|
|
771
|
-
# c_major[
|
|
1218
|
+
# @example Chromatic alterations (use strings for #)
|
|
1219
|
+
# c_major['I#'].pitch # => 61 (C#)
|
|
772
1220
|
# c_major[:V_].pitch # => 66 (F#/Gb)
|
|
773
1221
|
#
|
|
774
1222
|
# @example Building chords
|
|
@@ -803,7 +1251,16 @@ module Musa
|
|
|
803
1251
|
freeze
|
|
804
1252
|
end
|
|
805
1253
|
|
|
806
|
-
#
|
|
1254
|
+
# @!method tuning
|
|
1255
|
+
# Returns the tuning system associated with this scale.
|
|
1256
|
+
#
|
|
1257
|
+
# Delegated from ScaleKind#tuning.
|
|
1258
|
+
#
|
|
1259
|
+
# @return [ScaleSystemTuning] the tuning system used by this scale
|
|
1260
|
+
#
|
|
1261
|
+
# @example
|
|
1262
|
+
# scale = Scales.et12[440.0].major[60]
|
|
1263
|
+
# scale.tuning # => ScaleSystemTuning for 12-TET at A=440Hz
|
|
807
1264
|
def_delegators :@kind, :tuning
|
|
808
1265
|
|
|
809
1266
|
# Scale kind (major, minor, etc.).
|
|
@@ -888,11 +1345,11 @@ module Musa
|
|
|
888
1345
|
# scale[:V] # Dominant
|
|
889
1346
|
# scale[:IV] # Subdominant
|
|
890
1347
|
#
|
|
891
|
-
# @example With accidentals
|
|
892
|
-
# scale[
|
|
1348
|
+
# @example With accidentals (use strings for #)
|
|
1349
|
+
# scale['I#'] # Raised tonic
|
|
893
1350
|
# scale[:V_] # Flatted dominant
|
|
894
1351
|
# scale['II##'] # Double-raised second
|
|
895
|
-
def
|
|
1352
|
+
def get(grade_or_symbol)
|
|
896
1353
|
|
|
897
1354
|
raise ArgumentError, "grade_or_symbol '#{grade_or_symbol}' should be a Integer, String or Symbol" unless grade_or_symbol.is_a?(Symbol) || grade_or_symbol.is_a?(String) || grade_or_symbol.is_a?(Integer)
|
|
898
1355
|
|
|
@@ -916,6 +1373,8 @@ module Musa
|
|
|
916
1373
|
@notes_by_grade[wide_grade].sharp(sharps)
|
|
917
1374
|
end
|
|
918
1375
|
|
|
1376
|
+
alias_method :[], :get
|
|
1377
|
+
|
|
919
1378
|
# Converts grade specifier to numeric grade and accidentals.
|
|
920
1379
|
#
|
|
921
1380
|
# @param grade_or_string_or_symbol [Integer, Symbol, String] grade specifier
|
|
@@ -1038,6 +1497,87 @@ module Musa
|
|
|
1038
1497
|
@kind.tuning.offset_of_interval(interval_name)
|
|
1039
1498
|
end
|
|
1040
1499
|
|
|
1500
|
+
# Checks if all chord pitches exist in this scale.
|
|
1501
|
+
#
|
|
1502
|
+
# Uses the chord's definition to verify that every pitch in the chord
|
|
1503
|
+
# can be found as a diatonic note in this scale.
|
|
1504
|
+
#
|
|
1505
|
+
# @param chord [Musa::Chords::Chord] the chord to check
|
|
1506
|
+
# @return [Boolean] true if all chord notes are in scale
|
|
1507
|
+
#
|
|
1508
|
+
# @example
|
|
1509
|
+
# c_major = Scales.et12[440.0].major[60]
|
|
1510
|
+
# g7 = c_major.dominant.chord :seventh
|
|
1511
|
+
# c_major.contains_chord?(g7) # => true
|
|
1512
|
+
#
|
|
1513
|
+
# cm = c_major.tonic.chord.with_quality(:minor)
|
|
1514
|
+
# c_major.contains_chord?(cm) # => false (Eb not in C major)
|
|
1515
|
+
#
|
|
1516
|
+
# @see #degree_of_chord
|
|
1517
|
+
# @see #chord_on
|
|
1518
|
+
def contains_chord?(chord)
|
|
1519
|
+
chord.chord_definition.in_scale?(self, chord_root_pitch: chord.root.pitch)
|
|
1520
|
+
end
|
|
1521
|
+
|
|
1522
|
+
# Returns the grade (0-based) where the chord root falls in this scale.
|
|
1523
|
+
#
|
|
1524
|
+
# @param chord [Musa::Chords::Chord] the chord to check
|
|
1525
|
+
# @return [Integer, nil] grade (0-based) or nil if chord not in scale
|
|
1526
|
+
#
|
|
1527
|
+
# @example
|
|
1528
|
+
# c_major = Scales.et12[440.0].major[60]
|
|
1529
|
+
# g_chord = c_major.dominant.chord
|
|
1530
|
+
# c_major.degree_of_chord(g_chord) # => 4 (V degree, 0-based)
|
|
1531
|
+
#
|
|
1532
|
+
# @see #contains_chord?
|
|
1533
|
+
def degree_of_chord(chord)
|
|
1534
|
+
return nil unless contains_chord?(chord)
|
|
1535
|
+
|
|
1536
|
+
note = note_of_pitch(chord.root.pitch, allow_chromatic: false)
|
|
1537
|
+
note&.grade
|
|
1538
|
+
end
|
|
1539
|
+
|
|
1540
|
+
# Creates a chord rooted on the specified scale degree.
|
|
1541
|
+
#
|
|
1542
|
+
# This is a convenience method that combines scale note access with
|
|
1543
|
+
# chord creation. It's equivalent to `scale[grade].chord(...)`.
|
|
1544
|
+
#
|
|
1545
|
+
# @param grade [Integer, Symbol, String] scale degree (0-based numeric, function name like :tonic, or Roman numeral like :V)
|
|
1546
|
+
# @param feature_values [Array<Symbol>] chord feature values (:seventh, :major, etc.)
|
|
1547
|
+
# @param allow_chromatic [Boolean] allow non-diatonic chord notes
|
|
1548
|
+
# @param move [Hash{Symbol => Integer}] initial octave moves for chord tones
|
|
1549
|
+
# @param duplicate [Hash{Symbol => Integer, Array}] initial duplications
|
|
1550
|
+
# @param features_hash [Hash] additional feature key-value pairs
|
|
1551
|
+
# @return [Chords::Chord] chord rooted on the specified degree
|
|
1552
|
+
#
|
|
1553
|
+
# @example Create triads
|
|
1554
|
+
# scale.chord_on(0) # Tonic triad (I)
|
|
1555
|
+
# scale.chord_on(:dominant) # Dominant triad (V)
|
|
1556
|
+
# scale.chord_on(:IV) # Subdominant triad
|
|
1557
|
+
#
|
|
1558
|
+
# @example Create extended chords
|
|
1559
|
+
# scale.chord_on(4, :seventh) # V7
|
|
1560
|
+
# scale.chord_on(:dominant, :ninth) # V9
|
|
1561
|
+
# scale.chord_on(0, :seventh, :major) # Imaj7
|
|
1562
|
+
#
|
|
1563
|
+
# @example With voicing
|
|
1564
|
+
# scale.chord_on(:I, :seventh, move: {root: -1})
|
|
1565
|
+
# scale.chord_on(0, :triad, duplicate: {root: 1})
|
|
1566
|
+
#
|
|
1567
|
+
# @see NoteInScale#chord
|
|
1568
|
+
# @see #get
|
|
1569
|
+
def chord_on(grade, *feature_values,
|
|
1570
|
+
allow_chromatic: nil,
|
|
1571
|
+
move: nil,
|
|
1572
|
+
duplicate: nil,
|
|
1573
|
+
**features_hash)
|
|
1574
|
+
self[grade].chord(*feature_values,
|
|
1575
|
+
allow_chromatic: allow_chromatic,
|
|
1576
|
+
move: move,
|
|
1577
|
+
duplicate: duplicate,
|
|
1578
|
+
**features_hash)
|
|
1579
|
+
end
|
|
1580
|
+
|
|
1041
1581
|
# Checks scale equality.
|
|
1042
1582
|
#
|
|
1043
1583
|
# Scales are equal if they have same kind and root pitch.
|
|
@@ -1109,9 +1649,10 @@ module Musa
|
|
|
1109
1649
|
#
|
|
1110
1650
|
# ## Scale Navigation
|
|
1111
1651
|
#
|
|
1112
|
-
# note.scale
|
|
1113
|
-
# note.
|
|
1114
|
-
# note.
|
|
1652
|
+
# note.scale # Parent scale this note belongs to
|
|
1653
|
+
# note.as_root_of(:minor) # New minor scale with this pitch as root
|
|
1654
|
+
# note.minor # Same as note.as_root_of(:minor)
|
|
1655
|
+
# note.chromatic # Same as note.as_root_of(:chromatic)
|
|
1115
1656
|
#
|
|
1116
1657
|
# ## Chord Construction
|
|
1117
1658
|
#
|
|
@@ -1175,7 +1716,7 @@ module Musa
|
|
|
1175
1716
|
|
|
1176
1717
|
@scale.kind.tuning.scale_system.scale_kind_classes.each_key do |name|
|
|
1177
1718
|
define_singleton_method name do
|
|
1178
|
-
|
|
1719
|
+
as_root_of(name)
|
|
1179
1720
|
end
|
|
1180
1721
|
end
|
|
1181
1722
|
end
|
|
@@ -1198,34 +1739,27 @@ module Musa
|
|
|
1198
1739
|
@scale.kind.class.pitches[grade][:functions]
|
|
1199
1740
|
end
|
|
1200
1741
|
|
|
1201
|
-
#
|
|
1202
|
-
#
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
#
|
|
1742
|
+
# Current octave relative to scale root.
|
|
1743
|
+
# @return [Integer]
|
|
1744
|
+
attr_reader :octave
|
|
1745
|
+
|
|
1746
|
+
# Returns note transposed by octave offset.
|
|
1206
1747
|
#
|
|
1207
|
-
# @param
|
|
1748
|
+
# @param offset [Integer] octave offset (positive = up, negative = down)
|
|
1208
1749
|
# @param absolute [Boolean] if true, ignore current octave
|
|
1209
|
-
# @return [
|
|
1210
|
-
# @raise [ArgumentError] if
|
|
1211
|
-
#
|
|
1212
|
-
# @example Query octave
|
|
1213
|
-
# note.octave # => 0 (at scale root octave)
|
|
1750
|
+
# @return [NoteInScale] transposed note
|
|
1751
|
+
# @raise [ArgumentError] if offset is not integer
|
|
1214
1752
|
#
|
|
1215
1753
|
# @example Transpose relative
|
|
1216
|
-
# note.
|
|
1217
|
-
# note.
|
|
1754
|
+
# note.at_octave(1).pitch # Up one octave from current
|
|
1755
|
+
# note.at_octave(-1).pitch # Down one octave from current
|
|
1218
1756
|
#
|
|
1219
1757
|
# @example Transpose absolute
|
|
1220
|
-
# note.
|
|
1221
|
-
def
|
|
1222
|
-
|
|
1223
|
-
@octave
|
|
1224
|
-
else
|
|
1225
|
-
raise ArgumentError, "#{octave} is not integer" unless octave == octave.to_i
|
|
1758
|
+
# note.at_octave(2, absolute: true).pitch # At octave 2, regardless of current
|
|
1759
|
+
def at_octave(offset, absolute: false)
|
|
1760
|
+
raise ArgumentError, "#{offset} is not integer" unless offset == offset.to_i
|
|
1226
1761
|
|
|
1227
|
-
|
|
1228
|
-
end
|
|
1762
|
+
@scale[@grade + ((absolute ? 0 : @octave) + offset) * @scale.kind.class.grades]
|
|
1229
1763
|
end
|
|
1230
1764
|
|
|
1231
1765
|
# Creates a copy with background scale context.
|
|
@@ -1320,6 +1854,13 @@ module Musa
|
|
|
1320
1854
|
end
|
|
1321
1855
|
end
|
|
1322
1856
|
|
|
1857
|
+
# Calculates note at given pitch offset from current note.
|
|
1858
|
+
#
|
|
1859
|
+
# @param in_scale_pitch [Numeric] base pitch
|
|
1860
|
+
# @param sharps [Integer] number of semitones to add (negative for flats)
|
|
1861
|
+
# @return [NoteInScale] resulting note
|
|
1862
|
+
#
|
|
1863
|
+
# @api private
|
|
1323
1864
|
def calculate_note_of_pitch(in_scale_pitch, sharps)
|
|
1324
1865
|
pitch = in_scale_pitch + sharps * @scale.kind.tuning.scale_system.part_of_tone_size
|
|
1325
1866
|
|
|
@@ -1391,33 +1932,31 @@ module Musa
|
|
|
1391
1932
|
@scale.kind.tuning.frequency_of_pitch(@pitch, @scale.root_pitch)
|
|
1392
1933
|
end
|
|
1393
1934
|
|
|
1394
|
-
#
|
|
1395
|
-
#
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
#
|
|
1935
|
+
# Parent scale this note belongs to.
|
|
1936
|
+
# @return [Scale]
|
|
1937
|
+
attr_reader :scale
|
|
1938
|
+
|
|
1939
|
+
# Creates a new scale with this note's pitch as the root.
|
|
1399
1940
|
#
|
|
1400
|
-
# @param kind_id_or_kind [Symbol, ScaleKind
|
|
1401
|
-
# @return [Scale
|
|
1941
|
+
# @param kind_id_or_kind [Symbol, ScaleKind] scale kind or ID
|
|
1942
|
+
# @return [Scale] new scale rooted at this pitch
|
|
1402
1943
|
#
|
|
1403
|
-
# @example
|
|
1404
|
-
#
|
|
1944
|
+
# @example Create minor scale from a note
|
|
1945
|
+
# e = c_major[64] # E in C major
|
|
1946
|
+
# e_minor = e.as_root_of(:minor) # E minor scale
|
|
1405
1947
|
#
|
|
1406
|
-
# @example
|
|
1407
|
-
#
|
|
1948
|
+
# @example With ScaleKind object
|
|
1949
|
+
# minor_kind = tuning[:minor]
|
|
1950
|
+
# e_minor = e.as_root_of(minor_kind)
|
|
1408
1951
|
#
|
|
1409
|
-
# @example Dynamic method
|
|
1410
|
-
# note.minor # Same as note.
|
|
1411
|
-
# note.major # Same as note.
|
|
1412
|
-
def
|
|
1413
|
-
if kind_id_or_kind.
|
|
1414
|
-
@
|
|
1952
|
+
# @example Dynamic method (equivalent)
|
|
1953
|
+
# note.minor # Same as note.as_root_of(:minor)
|
|
1954
|
+
# note.major # Same as note.as_root_of(:major)
|
|
1955
|
+
def as_root_of(kind_id_or_kind)
|
|
1956
|
+
if kind_id_or_kind.is_a? ScaleKind
|
|
1957
|
+
kind_id_or_kind[@pitch]
|
|
1415
1958
|
else
|
|
1416
|
-
|
|
1417
|
-
kind_id_or_kind[@pitch]
|
|
1418
|
-
else
|
|
1419
|
-
@scale.kind.tuning[kind_id_or_kind][@pitch]
|
|
1420
|
-
end
|
|
1959
|
+
@scale.kind.tuning[kind_id_or_kind][@pitch]
|
|
1421
1960
|
end
|
|
1422
1961
|
end
|
|
1423
1962
|
|
|
@@ -1450,7 +1989,7 @@ module Musa
|
|
|
1450
1989
|
# @param move [Hash{Symbol => Integer}] initial octave moves
|
|
1451
1990
|
# @param duplicate [Hash{Symbol => Integer, Array<Integer>}] initial duplications
|
|
1452
1991
|
# @param features_hash [Hash] feature key-value pairs
|
|
1453
|
-
# @return [Chord] chord rooted on this note
|
|
1992
|
+
# @return [Chords::Chord] chord rooted on this note
|
|
1454
1993
|
#
|
|
1455
1994
|
# @example Default triad
|
|
1456
1995
|
# note.chord # Major triad
|