musa-dsl 0.40.0 → 0.41.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 +2 -0
- data/Gemfile +0 -1
- data/docs/subsystems/music.md +326 -15
- data/lib/musa-dsl/generative/darwin.rb +36 -1
- data/lib/musa-dsl/generative/generative-grammar.rb +28 -0
- data/lib/musa-dsl/generative/markov.rb +2 -0
- 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 +10 -0
- data/lib/musa-dsl/music/chords.rb +54 -9
- 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 +427 -0
- data/lib/musa-dsl/series/buffer-serie.rb +6 -0
- data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +23 -0
- data/lib/musa-dsl/series/quantizer-serie.rb +12 -0
- data/lib/musa-dsl/series/queue-serie.rb +13 -0
- data/lib/musa-dsl/version.rb +2 -1
- data/musa-dsl.gemspec +20 -15
- metadata +85 -22
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Musa
|
|
4
|
+
module Scales
|
|
5
|
+
# Base class for 12-semitone scale systems.
|
|
6
|
+
#
|
|
7
|
+
# TwelveSemitonesScaleSystem provides the foundation for any scale system
|
|
8
|
+
# using 12 semitones per octave. It defines intervals and structure but
|
|
9
|
+
# doesn't specify tuning (frequency calculation).
|
|
10
|
+
#
|
|
11
|
+
# Concrete subclasses must implement frequency calculation:
|
|
12
|
+
#
|
|
13
|
+
# - {EquallyTempered12ToneScaleSystem}: Equal temperament (12-TET)
|
|
14
|
+
# - Other temperaments could be added (e.g., meantone, just intonation)
|
|
15
|
+
#
|
|
16
|
+
# ## Intervals
|
|
17
|
+
#
|
|
18
|
+
# Defines standard interval names using semitone distances:
|
|
19
|
+
#
|
|
20
|
+
# { P0: 0, m2: 1, M2: 2, m3: 3, M3: 4, P4: 5, TT: 6,
|
|
21
|
+
# P5: 7, m6: 8, M6: 9, m7: 10, M7: 11, P8: 12 }
|
|
22
|
+
#
|
|
23
|
+
# @abstract Subclasses must implement {frequency_of_pitch}
|
|
24
|
+
# @see EquallyTempered12ToneScaleSystem Concrete equal temperament implementation
|
|
25
|
+
class TwelveSemitonesScaleSystem < ScaleSystem
|
|
26
|
+
class << self
|
|
27
|
+
@@intervals = { P0: 0, m2: 1, M2: 2, m3: 3, M3: 4, P4: 5, TT: 6, P5: 7, m6: 8, M6: 9, m7: 10, M7: 11, P8: 12 }
|
|
28
|
+
|
|
29
|
+
# System identifier.
|
|
30
|
+
# @return [Symbol] :et12
|
|
31
|
+
def id
|
|
32
|
+
:et12
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Number of distinct notes per octave.
|
|
36
|
+
# @return [Integer] 12
|
|
37
|
+
def notes_in_octave
|
|
38
|
+
12
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Size of smallest pitch division.
|
|
42
|
+
# @return [Integer] 1 (semitone)
|
|
43
|
+
def part_of_tone_size
|
|
44
|
+
1
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Interval definitions.
|
|
48
|
+
#
|
|
49
|
+
# @return [Hash{Symbol => Integer}] interval name to semitones mapping
|
|
50
|
+
#
|
|
51
|
+
# @example
|
|
52
|
+
# intervals[:P5] # => 7 (perfect fifth)
|
|
53
|
+
# intervals[:M3] # => 4 (major third)
|
|
54
|
+
def intervals
|
|
55
|
+
@@intervals
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -146,6 +146,31 @@ module Musa
|
|
|
146
146
|
def self.default_system
|
|
147
147
|
@default_scale_system
|
|
148
148
|
end
|
|
149
|
+
|
|
150
|
+
# Convenience method to extend metadata for a scale kind by ID.
|
|
151
|
+
#
|
|
152
|
+
# Finds the ScaleKind class by its ID symbol and adds custom metadata to it.
|
|
153
|
+
# This is a shortcut for accessing the class directly and calling extend_metadata.
|
|
154
|
+
#
|
|
155
|
+
# @param scale_kind_id [Symbol] the scale kind identifier (e.g., :major, :dorian)
|
|
156
|
+
# @param metadata [Hash] key-value pairs to add as custom metadata
|
|
157
|
+
# @return [Hash] the updated custom_metadata hash
|
|
158
|
+
# @raise [KeyError] if scale kind not found
|
|
159
|
+
#
|
|
160
|
+
# @example
|
|
161
|
+
# Scales.extend_metadata(:major, my_tag: :favorite)
|
|
162
|
+
# Scales.extend_metadata(:dorian, mood: :nostalgic, suitable_for: [:jazz])
|
|
163
|
+
#
|
|
164
|
+
# @see ScaleKind.extend_metadata
|
|
165
|
+
def self.extend_metadata(scale_kind_id, **metadata)
|
|
166
|
+
system = default_system
|
|
167
|
+
raise KeyError, "No default scale system registered" unless system
|
|
168
|
+
|
|
169
|
+
klass = system.scale_kind_class(scale_kind_id)
|
|
170
|
+
raise KeyError, "Scale kind :#{scale_kind_id} not found" unless klass
|
|
171
|
+
|
|
172
|
+
klass.extend_metadata(**metadata)
|
|
173
|
+
end
|
|
149
174
|
end
|
|
150
175
|
|
|
151
176
|
# Abstract base class for musical scale systems.
|
|
@@ -459,6 +484,113 @@ module Musa
|
|
|
459
484
|
@scale_system.frequency_of_pitch(pitch, root, @a_frequency)
|
|
460
485
|
end
|
|
461
486
|
|
|
487
|
+
# Returns scale kinds matching the given metadata criteria.
|
|
488
|
+
#
|
|
489
|
+
# Without arguments, returns all registered scale kinds.
|
|
490
|
+
# With keyword arguments, filters by metadata values.
|
|
491
|
+
# With a block, filters using custom predicate on ScaleKind class.
|
|
492
|
+
#
|
|
493
|
+
# @param metadata_criteria [Hash] metadata key-value pairs to match
|
|
494
|
+
# @yield [kind_class] optional block for custom filtering
|
|
495
|
+
# @yieldparam kind_class [Class] the ScaleKind subclass
|
|
496
|
+
# @yieldreturn [Boolean] true to include this scale kind
|
|
497
|
+
# @return [Array<ScaleKind>] matching scale kind instances
|
|
498
|
+
#
|
|
499
|
+
# @example All scale kinds
|
|
500
|
+
# tuning.scale_kinds
|
|
501
|
+
# # => [major_kind, minor_kind, dorian_kind, ...]
|
|
502
|
+
#
|
|
503
|
+
# @example Filter by metadata
|
|
504
|
+
# tuning.scale_kinds(family: :diatonic)
|
|
505
|
+
# tuning.scale_kinds(brightness: -1..1)
|
|
506
|
+
# tuning.scale_kinds(character: :jazz)
|
|
507
|
+
#
|
|
508
|
+
# @example Filter with block
|
|
509
|
+
# tuning.scale_kinds { |klass| klass.intrinsic_metadata[:has_leading_tone] }
|
|
510
|
+
#
|
|
511
|
+
# @example Combined
|
|
512
|
+
# tuning.scale_kinds(family: :greek_modes) { |klass| klass.metadata[:brightness] < 0 }
|
|
513
|
+
#
|
|
514
|
+
# @see ScaleKind.metadata
|
|
515
|
+
# @see ScaleKind.has_metadata?
|
|
516
|
+
def scale_kinds(**metadata_criteria, &block)
|
|
517
|
+
result = @scale_system.scale_kind_classes.keys.map { |id| self[id] }
|
|
518
|
+
|
|
519
|
+
unless metadata_criteria.empty?
|
|
520
|
+
result = result.select do |kind|
|
|
521
|
+
matches_metadata?(kind.class, metadata_criteria)
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
if block
|
|
526
|
+
result = result.select { |kind| block.call(kind.class) }
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
result
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# Searches for a chord across multiple scale types.
|
|
533
|
+
#
|
|
534
|
+
# Iterates through the specified scale kinds and pitch roots to find
|
|
535
|
+
# all scales that contain the given chord. Returns chords with their
|
|
536
|
+
# containing scale as context.
|
|
537
|
+
#
|
|
538
|
+
# @param chord [Musa::Chords::Chord] the chord to search for
|
|
539
|
+
# @param roots [Range, Array, nil] pitch offsets to search (default: 0...notes_in_octave)
|
|
540
|
+
# @param metadata_criteria [Hash] metadata filters for scale kinds
|
|
541
|
+
# @return [Array<Musa::Chords::Chord>] chords with their containing scales
|
|
542
|
+
#
|
|
543
|
+
# @example Search G7 in greek mode scales
|
|
544
|
+
# tuning = Scales.et12[440.0]
|
|
545
|
+
# g7 = tuning.major[60].dominant.chord :seventh
|
|
546
|
+
# tuning.chords_of(g7, family: :greek_modes)
|
|
547
|
+
#
|
|
548
|
+
# @example Search with brightness filter
|
|
549
|
+
# tuning.chords_of(g7, brightness: -1..1)
|
|
550
|
+
#
|
|
551
|
+
# @example Search in all scale types
|
|
552
|
+
# tuning.chords_of(g7)
|
|
553
|
+
#
|
|
554
|
+
# @see ScaleKind#scales_containing
|
|
555
|
+
# @see Musa::Chords::Chord#in_scales
|
|
556
|
+
def chords_of(chord, roots: nil, **metadata_criteria)
|
|
557
|
+
roots ||= 0...notes_in_octave
|
|
558
|
+
kinds = filtered_scale_kind_ids(**metadata_criteria)
|
|
559
|
+
|
|
560
|
+
kinds.flat_map do |kind_id|
|
|
561
|
+
self[kind_id].scales_containing(chord, roots: roots)
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
private
|
|
566
|
+
|
|
567
|
+
def filtered_scale_kind_ids(**metadata_criteria)
|
|
568
|
+
kinds = @scale_system.scale_kind_classes.keys
|
|
569
|
+
|
|
570
|
+
return kinds if metadata_criteria.empty?
|
|
571
|
+
|
|
572
|
+
kinds.select do |kind_id|
|
|
573
|
+
kind_class = @scale_system.scale_kind_class(kind_id)
|
|
574
|
+
matches_metadata?(kind_class, metadata_criteria)
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def matches_metadata?(kind_class, criteria)
|
|
579
|
+
criteria.all? do |key, value|
|
|
580
|
+
actual = kind_class.metadata[key]
|
|
581
|
+
case value
|
|
582
|
+
when Range
|
|
583
|
+
actual.is_a?(Numeric) && value.include?(actual)
|
|
584
|
+
when Array
|
|
585
|
+
value.any? { |v| actual == v || (actual.is_a?(Array) && actual.include?(v)) }
|
|
586
|
+
else
|
|
587
|
+
actual == value || (actual.is_a?(Array) && actual.include?(value))
|
|
588
|
+
end
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
public
|
|
593
|
+
|
|
462
594
|
def ==(other)
|
|
463
595
|
self.class == other.class &&
|
|
464
596
|
@scale_system == other.scale_system &&
|
|
@@ -577,6 +709,35 @@ module Musa
|
|
|
577
709
|
self[0]
|
|
578
710
|
end
|
|
579
711
|
|
|
712
|
+
# Finds all scales of this kind that contain the given chord.
|
|
713
|
+
#
|
|
714
|
+
# Searches through scales rooted on different pitches to find which ones
|
|
715
|
+
# contain all the notes of the given chord. Returns chords with their
|
|
716
|
+
# containing scale as context.
|
|
717
|
+
#
|
|
718
|
+
# @param chord [Musa::Chords::Chord] the chord to search for
|
|
719
|
+
# @param roots [Range, Array, nil] pitch offsets to search (default: 0...notes_in_octave)
|
|
720
|
+
# @return [Array<Musa::Chords::Chord>] chords with their containing scales
|
|
721
|
+
#
|
|
722
|
+
# @example Find G major triad in all major scales
|
|
723
|
+
# tuning = Scales.et12[440.0]
|
|
724
|
+
# g_triad = tuning.major[60].dominant.chord
|
|
725
|
+
# tuning.major.scales_containing(g_triad)
|
|
726
|
+
# # => [Chord in C major (V), Chord in G major (I), Chord in D major (IV)]
|
|
727
|
+
#
|
|
728
|
+
# @see Scale#chord_on
|
|
729
|
+
# @see ScaleSystemTuning#chords_in_scales
|
|
730
|
+
def scales_containing(chord, roots: nil)
|
|
731
|
+
roots ||= 0...tuning.notes_in_octave
|
|
732
|
+
base_pitch = chord.root.pitch % tuning.notes_in_octave
|
|
733
|
+
|
|
734
|
+
roots.filter_map do |root_offset|
|
|
735
|
+
root_pitch = base_pitch + root_offset
|
|
736
|
+
scale = self[root_pitch]
|
|
737
|
+
scale.chord_on(chord)
|
|
738
|
+
end
|
|
739
|
+
end
|
|
740
|
+
|
|
580
741
|
# Checks scale kind equality.
|
|
581
742
|
#
|
|
582
743
|
# @param other [ScaleKind]
|
|
@@ -685,8 +846,200 @@ module Musa
|
|
|
685
846
|
@grade_names_index.keys
|
|
686
847
|
end
|
|
687
848
|
|
|
849
|
+
# Returns intrinsic metadata derived from scale structure.
|
|
850
|
+
#
|
|
851
|
+
# This metadata is automatically calculated from the scale's pitch
|
|
852
|
+
# structure and cannot be modified. It includes:
|
|
853
|
+
#
|
|
854
|
+
# - **:id**: Scale kind identifier
|
|
855
|
+
# - **:grades**: Number of diatonic degrees
|
|
856
|
+
# - **:pitches**: Array of pitch offsets from root
|
|
857
|
+
# - **:intervals**: Intervals between consecutive degrees
|
|
858
|
+
# - **:has_leading_tone**: Whether scale has pitch 11 (semitone below octave)
|
|
859
|
+
# - **:has_tritone**: Whether scale contains tritone (pitch 6)
|
|
860
|
+
# - **:symmetric**: Type of symmetry if any (:equal, :palindrome, :repeating)
|
|
861
|
+
#
|
|
862
|
+
# @return [Hash] intrinsic metadata derived from structure
|
|
863
|
+
#
|
|
864
|
+
# @example
|
|
865
|
+
# MajorScaleKind.intrinsic_metadata
|
|
866
|
+
# # => { id: :major, grades: 7, pitches: [0, 2, 4, 5, 7, 9, 11],
|
|
867
|
+
# # intervals: [2, 2, 1, 2, 2, 2], has_leading_tone: true,
|
|
868
|
+
# # has_tritone: true, symmetric: nil }
|
|
869
|
+
def self.intrinsic_metadata
|
|
870
|
+
result = {}
|
|
871
|
+
result[:id] = id if respond_to?(:id)
|
|
872
|
+
result[:grades] = grades if respond_to?(:grades)
|
|
873
|
+
if respond_to?(:pitches)
|
|
874
|
+
result[:pitches] = pitches.map { |p| p[:pitch] }
|
|
875
|
+
result[:intervals] = compute_intervals
|
|
876
|
+
result[:has_leading_tone] = pitches.any? { |p| p[:pitch] == 11 }
|
|
877
|
+
result[:has_tritone] = pitches.any? { |p| p[:pitch] == 6 }
|
|
878
|
+
result[:symmetric] = compute_symmetry
|
|
879
|
+
end
|
|
880
|
+
result.compact
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
# Returns base metadata defined by the musa-dsl library.
|
|
884
|
+
#
|
|
885
|
+
# This metadata is defined in each ScaleKind subclass using the
|
|
886
|
+
# `@base_metadata` class instance variable. It typically includes:
|
|
887
|
+
#
|
|
888
|
+
# - **:family**: Scale family (:diatonic, :greek_modes, :pentatonic, etc.)
|
|
889
|
+
# - **:brightness**: Relative brightness (-3 to +3, major = 0)
|
|
890
|
+
# - **:character**: Array of descriptive tags
|
|
891
|
+
# - **:parent**: Parent scale and degree for modes
|
|
892
|
+
#
|
|
893
|
+
# @return [Hash] library-defined metadata
|
|
894
|
+
#
|
|
895
|
+
# @example
|
|
896
|
+
# MajorScaleKind.base_metadata
|
|
897
|
+
# # => { family: :diatonic, brightness: 0, character: [:bright, :stable] }
|
|
898
|
+
def self.base_metadata
|
|
899
|
+
@base_metadata || {}
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
# Returns custom metadata added by users at runtime.
|
|
903
|
+
#
|
|
904
|
+
# This metadata is added via {.extend_metadata} and can be cleared
|
|
905
|
+
# with {.reset_custom_metadata}. Takes precedence over base_metadata.
|
|
906
|
+
#
|
|
907
|
+
# @return [Hash] user-defined metadata
|
|
908
|
+
#
|
|
909
|
+
# @example
|
|
910
|
+
# MajorScaleKind.extend_metadata(my_tag: :favorite)
|
|
911
|
+
# MajorScaleKind.custom_metadata # => { my_tag: :favorite }
|
|
912
|
+
def self.custom_metadata
|
|
913
|
+
@custom_metadata || {}
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
# Adds custom metadata to this scale kind.
|
|
917
|
+
#
|
|
918
|
+
# Custom metadata takes precedence over base_metadata when queried
|
|
919
|
+
# via {.metadata}. Multiple calls merge metadata together.
|
|
920
|
+
#
|
|
921
|
+
# @param metadata [Hash] key-value pairs to add
|
|
922
|
+
# @return [Hash] the updated custom_metadata hash (frozen)
|
|
923
|
+
#
|
|
924
|
+
# @example
|
|
925
|
+
# MajorScaleKind.extend_metadata(my_mood: :happy, rating: 5)
|
|
926
|
+
# MajorScaleKind.extend_metadata(suitable_for: [:pop, :classical])
|
|
927
|
+
# MajorScaleKind.custom_metadata
|
|
928
|
+
# # => { my_mood: :happy, rating: 5, suitable_for: [:pop, :classical] }
|
|
929
|
+
def self.extend_metadata(**metadata)
|
|
930
|
+
@custom_metadata ||= {}
|
|
931
|
+
@custom_metadata = @custom_metadata.merge(metadata).freeze
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
# Clears all custom metadata from this scale kind.
|
|
935
|
+
#
|
|
936
|
+
# @return [nil]
|
|
937
|
+
#
|
|
938
|
+
# @example
|
|
939
|
+
# MajorScaleKind.extend_metadata(temp: :data)
|
|
940
|
+
# MajorScaleKind.reset_custom_metadata
|
|
941
|
+
# MajorScaleKind.custom_metadata # => {}
|
|
942
|
+
def self.reset_custom_metadata
|
|
943
|
+
@custom_metadata = nil
|
|
944
|
+
end
|
|
945
|
+
|
|
946
|
+
# Returns combined metadata from all three layers.
|
|
947
|
+
#
|
|
948
|
+
# Layers are merged with later layers taking precedence:
|
|
949
|
+
# intrinsic_metadata < base_metadata < custom_metadata
|
|
950
|
+
#
|
|
951
|
+
# @return [Hash] combined metadata from all layers
|
|
952
|
+
#
|
|
953
|
+
# @example
|
|
954
|
+
# MajorScaleKind.metadata
|
|
955
|
+
# # => { id: :major, grades: 7, pitches: [...], family: :diatonic, ... }
|
|
956
|
+
def self.metadata
|
|
957
|
+
intrinsic_metadata
|
|
958
|
+
.merge(base_metadata)
|
|
959
|
+
.merge(custom_metadata)
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
# Returns a specific metadata value.
|
|
963
|
+
#
|
|
964
|
+
# @param key [Symbol] the metadata key
|
|
965
|
+
# @return [Object, nil] the value or nil if not found
|
|
966
|
+
#
|
|
967
|
+
# @example
|
|
968
|
+
# MajorScaleKind.metadata_value(:family) # => :diatonic
|
|
969
|
+
def self.metadata_value(key)
|
|
970
|
+
metadata[key]
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
# Checks whether metadata contains a key or key-value match.
|
|
974
|
+
#
|
|
975
|
+
# When called with just a key, checks for key existence.
|
|
976
|
+
# When called with key and value, checks for exact match or
|
|
977
|
+
# array inclusion (if metadata value is an array).
|
|
978
|
+
#
|
|
979
|
+
# @param key [Symbol] the metadata key
|
|
980
|
+
# @param value [Object, nil] optional value to match
|
|
981
|
+
# @return [Boolean] whether the condition is satisfied
|
|
982
|
+
#
|
|
983
|
+
# @example Key existence
|
|
984
|
+
# MajorScaleKind.has_metadata?(:family) # => true
|
|
985
|
+
# MajorScaleKind.has_metadata?(:nonexistent) # => false
|
|
986
|
+
#
|
|
987
|
+
# @example Value matching
|
|
988
|
+
# MajorScaleKind.has_metadata?(:family, :diatonic) # => true
|
|
989
|
+
# MajorScaleKind.has_metadata?(:family, :pentatonic) # => false
|
|
990
|
+
#
|
|
991
|
+
# @example Array inclusion
|
|
992
|
+
# MajorScaleKind.has_metadata?(:character, :bright) # => true
|
|
993
|
+
def self.has_metadata?(key, value = nil)
|
|
994
|
+
if value.nil?
|
|
995
|
+
metadata.key?(key)
|
|
996
|
+
else
|
|
997
|
+
metadata[key] == value ||
|
|
998
|
+
(metadata[key].is_a?(Array) && metadata[key].include?(value))
|
|
999
|
+
end
|
|
1000
|
+
end
|
|
1001
|
+
|
|
688
1002
|
private
|
|
689
1003
|
|
|
1004
|
+
# Computes intervals between consecutive scale degrees.
|
|
1005
|
+
# @return [Array<Integer>, nil] intervals or nil if not calculable
|
|
1006
|
+
# @api private
|
|
1007
|
+
def self.compute_intervals
|
|
1008
|
+
return nil unless respond_to?(:pitches) && pitches.size > 1
|
|
1009
|
+
pitch_values = pitches.map { |p| p[:pitch] }
|
|
1010
|
+
# Only compute within first octave
|
|
1011
|
+
first_octave = pitch_values.take_while { |p| p < 12 }
|
|
1012
|
+
first_octave.push(12) if first_octave.last != 12
|
|
1013
|
+
first_octave.each_cons(2).map { |a, b| b - a }
|
|
1014
|
+
end
|
|
1015
|
+
|
|
1016
|
+
# Computes symmetry type of the scale.
|
|
1017
|
+
# @return [Symbol, nil] :equal, :palindrome, :repeating, or nil
|
|
1018
|
+
# @api private
|
|
1019
|
+
def self.compute_symmetry
|
|
1020
|
+
return nil unless respond_to?(:pitches)
|
|
1021
|
+
intervals = compute_intervals
|
|
1022
|
+
return nil unless intervals && intervals.any?
|
|
1023
|
+
|
|
1024
|
+
# Check if intervals are all equal (e.g., whole tone: [2,2,2,2,2,2])
|
|
1025
|
+
return :equal if intervals.uniq.size == 1
|
|
1026
|
+
|
|
1027
|
+
# Check for palindrome pattern
|
|
1028
|
+
return :palindrome if intervals == intervals.reverse
|
|
1029
|
+
|
|
1030
|
+
# Check for repeating pattern
|
|
1031
|
+
(1..intervals.size / 2).each do |len|
|
|
1032
|
+
pattern = intervals.take(len)
|
|
1033
|
+
if intervals.each_slice(len).all? { |slice| slice == pattern || slice.size < len }
|
|
1034
|
+
return :repeating
|
|
1035
|
+
end
|
|
1036
|
+
end
|
|
1037
|
+
|
|
1038
|
+
nil
|
|
1039
|
+
end
|
|
1040
|
+
|
|
1041
|
+
public
|
|
1042
|
+
|
|
690
1043
|
# Creates internal index mapping function names to grade indices.
|
|
691
1044
|
#
|
|
692
1045
|
# @return [self]
|
|
@@ -1038,6 +1391,80 @@ module Musa
|
|
|
1038
1391
|
@kind.tuning.offset_of_interval(interval_name)
|
|
1039
1392
|
end
|
|
1040
1393
|
|
|
1394
|
+
# Checks if all chord pitches exist in this scale.
|
|
1395
|
+
#
|
|
1396
|
+
# Uses the chord's definition to verify that every pitch in the chord
|
|
1397
|
+
# can be found as a diatonic note in this scale.
|
|
1398
|
+
#
|
|
1399
|
+
# @param chord [Musa::Chords::Chord] the chord to check
|
|
1400
|
+
# @return [Boolean] true if all chord notes are in scale
|
|
1401
|
+
#
|
|
1402
|
+
# @example
|
|
1403
|
+
# c_major = Scales.et12[440.0].major[60]
|
|
1404
|
+
# g7 = c_major.dominant.chord :seventh
|
|
1405
|
+
# c_major.contains_chord?(g7) # => true
|
|
1406
|
+
#
|
|
1407
|
+
# cm = c_major.tonic.chord.with_quality(:minor)
|
|
1408
|
+
# c_major.contains_chord?(cm) # => false (Eb not in C major)
|
|
1409
|
+
#
|
|
1410
|
+
# @see #degree_of_chord
|
|
1411
|
+
# @see #chord_on
|
|
1412
|
+
def contains_chord?(chord)
|
|
1413
|
+
chord.chord_definition.in_scale?(self, chord_root_pitch: chord.root.pitch)
|
|
1414
|
+
end
|
|
1415
|
+
|
|
1416
|
+
# Returns the grade (0-based) where the chord root falls in this scale.
|
|
1417
|
+
#
|
|
1418
|
+
# @param chord [Musa::Chords::Chord] the chord to check
|
|
1419
|
+
# @return [Integer, nil] grade (0-based) or nil if chord not in scale
|
|
1420
|
+
#
|
|
1421
|
+
# @example
|
|
1422
|
+
# c_major = Scales.et12[440.0].major[60]
|
|
1423
|
+
# g_chord = c_major.dominant.chord
|
|
1424
|
+
# c_major.degree_of_chord(g_chord) # => 4 (V degree, 0-based)
|
|
1425
|
+
#
|
|
1426
|
+
# @see #contains_chord?
|
|
1427
|
+
def degree_of_chord(chord)
|
|
1428
|
+
return nil unless contains_chord?(chord)
|
|
1429
|
+
|
|
1430
|
+
note = note_of_pitch(chord.root.pitch, allow_chromatic: false)
|
|
1431
|
+
note&.grade
|
|
1432
|
+
end
|
|
1433
|
+
|
|
1434
|
+
# Creates an equivalent chord with this scale as its context.
|
|
1435
|
+
#
|
|
1436
|
+
# Returns a new Chord object that represents the same chord but with
|
|
1437
|
+
# this scale as its harmonic context. The chord's voicing (move and
|
|
1438
|
+
# duplicate settings) is preserved.
|
|
1439
|
+
#
|
|
1440
|
+
# @param chord [Musa::Chords::Chord] the source chord
|
|
1441
|
+
# @return [Musa::Chords::Chord, nil] new chord with this scale, or nil if not contained
|
|
1442
|
+
#
|
|
1443
|
+
# @example
|
|
1444
|
+
# c_major = Scales.et12[440.0].major[60]
|
|
1445
|
+
# g7 = c_major.dominant.chord :seventh
|
|
1446
|
+
#
|
|
1447
|
+
# g_mixolydian = Scales.et12[440.0].mixolydian[67]
|
|
1448
|
+
# g7_in_mixolydian = g_mixolydian.chord_on(g7)
|
|
1449
|
+
# g7_in_mixolydian.scale # => G Mixolydian scale
|
|
1450
|
+
#
|
|
1451
|
+
# @see #contains_chord?
|
|
1452
|
+
# @see #degree_of_chord
|
|
1453
|
+
def chord_on(chord)
|
|
1454
|
+
return nil unless contains_chord?(chord)
|
|
1455
|
+
|
|
1456
|
+
root_note = note_of_pitch(chord.root.pitch, allow_chromatic: false)
|
|
1457
|
+
return nil unless root_note
|
|
1458
|
+
|
|
1459
|
+
Musa::Chords::Chord.with_root(
|
|
1460
|
+
root_note,
|
|
1461
|
+
scale: self,
|
|
1462
|
+
name: chord.chord_definition.name,
|
|
1463
|
+
move: chord.move.empty? ? nil : chord.move,
|
|
1464
|
+
duplicate: chord.duplicate.empty? ? nil : chord.duplicate
|
|
1465
|
+
)
|
|
1466
|
+
end
|
|
1467
|
+
|
|
1041
1468
|
# Checks scale equality.
|
|
1042
1469
|
#
|
|
1043
1470
|
# Scales are equal if they have same kind and root pitch.
|
|
@@ -76,6 +76,9 @@ module Musa
|
|
|
76
76
|
@source.next_value.tap { |value| @history << value unless value.nil? && !@history.empty? && @history.last.nil? }
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
+
# Creates buffer reader for this buffered serie.
|
|
80
|
+
#
|
|
81
|
+
# @return [Buffer] buffer serie reader
|
|
79
82
|
def buffer
|
|
80
83
|
@buffer ||= Buffer.new(@history)
|
|
81
84
|
@buffer.send(state).tap { |_| @buffers << _ }
|
|
@@ -156,6 +159,9 @@ module Musa
|
|
|
156
159
|
init
|
|
157
160
|
end
|
|
158
161
|
|
|
162
|
+
# Creates buffer reader for this buffered serie.
|
|
163
|
+
#
|
|
164
|
+
# @return [Buffer] buffer serie reader
|
|
159
165
|
def buffer
|
|
160
166
|
Buffer.new(self)
|
|
161
167
|
end
|
|
@@ -40,6 +40,7 @@ module Musa
|
|
|
40
40
|
# first = pairs[0]
|
|
41
41
|
# second = pairs[1]
|
|
42
42
|
#
|
|
43
|
+
# @return [Splitter] hash/array splitter
|
|
43
44
|
# @api public
|
|
44
45
|
def split
|
|
45
46
|
Splitter.new(self)
|
|
@@ -68,6 +69,11 @@ module Musa
|
|
|
68
69
|
@proxy = SplitterProxy.new(@source)
|
|
69
70
|
end
|
|
70
71
|
|
|
72
|
+
# Accesses component serie by key or index.
|
|
73
|
+
#
|
|
74
|
+
# @param key_or_index [Symbol, Integer] hash key or array index
|
|
75
|
+
#
|
|
76
|
+
# @return [Split] component serie
|
|
71
77
|
def [](key_or_index)
|
|
72
78
|
raise "Can't get a component because Splitter is a prototype. To get a component you need a Splitter instance." unless instance?
|
|
73
79
|
|
|
@@ -78,6 +84,13 @@ module Musa
|
|
|
78
84
|
end
|
|
79
85
|
end
|
|
80
86
|
|
|
87
|
+
# Iterates over component series.
|
|
88
|
+
#
|
|
89
|
+
# @yield [key, split] for hash mode, [split] for array mode
|
|
90
|
+
# @yieldparam key [Symbol] hash key (hash mode only)
|
|
91
|
+
# @yieldparam split [Split] component serie
|
|
92
|
+
#
|
|
93
|
+
# @return [Enumerator, void] enumerator if no block given
|
|
81
94
|
def each
|
|
82
95
|
raise "Can't iterate because Splitter is in state '#{state}'. To iterate you need a Splitter in state 'instance'." unless instance?
|
|
83
96
|
|
|
@@ -104,6 +117,11 @@ module Musa
|
|
|
104
117
|
end
|
|
105
118
|
end
|
|
106
119
|
|
|
120
|
+
# Converts to hash of component series.
|
|
121
|
+
#
|
|
122
|
+
# @return [Hash{Symbol => Split}] hash of component series
|
|
123
|
+
#
|
|
124
|
+
# @raise [RuntimeError] if not in hash mode
|
|
107
125
|
def to_hash
|
|
108
126
|
if @proxy.hash_mode?
|
|
109
127
|
@proxy.components.collect { |key| [key, self[key]] }.to_h
|
|
@@ -112,6 +130,11 @@ module Musa
|
|
|
112
130
|
end
|
|
113
131
|
end
|
|
114
132
|
|
|
133
|
+
# Converts to array of component series.
|
|
134
|
+
#
|
|
135
|
+
# @return [Array<Split>] array of component series
|
|
136
|
+
#
|
|
137
|
+
# @raise [RuntimeError] if not in array mode
|
|
115
138
|
def to_ary
|
|
116
139
|
if @proxy.array_mode?
|
|
117
140
|
[].tap { |_| @proxy.components.each { |i| _[i] = self[i] } }
|
|
@@ -63,6 +63,18 @@ module Musa
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
module Series::Constructors
|
|
66
|
+
# Quantizes time-value serie to discrete steps.
|
|
67
|
+
#
|
|
68
|
+
# @param time_value_serie [Serie] source timed serie
|
|
69
|
+
# @param reference [Numeric, nil] quantization reference
|
|
70
|
+
# @param step [Numeric, nil] step size
|
|
71
|
+
# @param value_attribute [Symbol, nil] attribute to quantize
|
|
72
|
+
# @param stops [Boolean, nil] include stop points
|
|
73
|
+
# @param predictive [Boolean, nil] use predictive mode
|
|
74
|
+
# @param left_open [Boolean, nil] left boundary open
|
|
75
|
+
# @param right_open [Boolean, nil] right boundary open
|
|
76
|
+
#
|
|
77
|
+
# @return [RawQuantizer, PredictiveQuantizer] quantized serie
|
|
66
78
|
def QUANTIZE(time_value_serie,
|
|
67
79
|
reference: nil, step: nil,
|
|
68
80
|
value_attribute: nil,
|
|
@@ -79,6 +79,13 @@ module Musa
|
|
|
79
79
|
init
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
# Adds serie to queue.
|
|
83
|
+
#
|
|
84
|
+
# @param serie [Serie] instance serie to add
|
|
85
|
+
#
|
|
86
|
+
# @return [self] for chaining
|
|
87
|
+
#
|
|
88
|
+
# @raise [ArgumentError] if serie is not an instance
|
|
82
89
|
def <<(serie)
|
|
83
90
|
# when queue is a prototype it is also frozen so no serie can be added (it would raise an Exception if tried).
|
|
84
91
|
# when queue is an instance the added serie should also be an instance (raise an Exception otherwise)
|
|
@@ -91,6 +98,9 @@ module Musa
|
|
|
91
98
|
self
|
|
92
99
|
end
|
|
93
100
|
|
|
101
|
+
# Clears all series from queue.
|
|
102
|
+
#
|
|
103
|
+
# @return [self] for chaining
|
|
94
104
|
def clear
|
|
95
105
|
@sources.clear
|
|
96
106
|
init
|
|
@@ -148,6 +158,9 @@ module Musa
|
|
|
148
158
|
end
|
|
149
159
|
|
|
150
160
|
module Series::Operations
|
|
161
|
+
# Wraps this serie in a queue.
|
|
162
|
+
#
|
|
163
|
+
# @return [QueueSerie] queue containing this serie
|
|
151
164
|
def queued
|
|
152
165
|
Series::Constructors.QUEUE(self)
|
|
153
166
|
end
|
data/lib/musa-dsl/version.rb
CHANGED