Class: Musa::Scales::ScaleKind Abstract

Inherits:
Object
  • Object
show all
Defined in:
lib/musa-dsl/music/scales.rb

Overview

This class is abstract.

Subclass and implement abstract methods

Abstract base class for scale types (major, minor, chromatic, etc.).

ScaleKind defines a type of scale (major, minor, chromatic, etc.) independent of root pitch or tuning. It specifies:

  • Scale degrees and their pitch offsets
  • Function names for each degree (tonic, dominant, etc.)
  • Number of grades per octave
  • Whether the scale is chromatic (contains all pitches)

Subclass Requirements

Subclasses must implement:

Pitch Structure

The ScaleKind.pitches array defines the scale structure:

[{ functions: [:I, :tonic, :_1], pitch: 0 },
 { functions: [:II, :supertonic, :_2], pitch: 2 },
 ...]
  • functions: Array of symbols that can access this degree
  • pitch: Semitone offset from root

Dynamic Method Creation

Each scale instance gets methods for all registered scale kinds:

note.major     # Get major scale rooted on this note
note.minor     # Get minor scale rooted on this note

Usage

ScaleKind instances are accessed via tuning:

tuning = Scales[:et12][440.0]
major_kind = tuning[:major]        # ScaleKind instance
c_major = major_kind[60]           # Scale instance

Or directly via convenience methods:

c_major = tuning.major[60]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(tuning) ⇒ ScaleKind

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Creates a scale kind instance.

Parameters:



760
761
762
763
# File 'lib/musa-dsl/music/scales.rb', line 760

def initialize(tuning)
  @tuning = tuning
  @scales = {}
end

Instance Attribute Details

#tuningScaleSystemTuning (readonly)

The tuning context.

Returns:



767
768
769
# File 'lib/musa-dsl/music/scales.rb', line 767

def tuning
  @tuning
end

Class Method Details

.base_metadataHash

Returns base metadata defined by the musa-dsl library.

This metadata is defined in each ScaleKind subclass using the @base_metadata class instance variable. It typically includes:

  • :family: Scale family (:diatonic, :greek_modes, :pentatonic, etc.)
  • :brightness: Relative brightness (-3 to +3, major = 0)
  • :character: Array of descriptive tags
  • :parent: Parent scale and degree for modes

Examples:

MajorScaleKind.
# => { family: :diatonic, brightness: 0, character: [:bright, :stable] }

Returns:

  • (Hash)

    library-defined metadata



993
994
995
# File 'lib/musa-dsl/music/scales.rb', line 993

def self.
  @base_metadata || {}
end

.chromatic?Boolean

Indicates whether this is the chromatic scale.

Only one scale kind per system should return true. The chromatic scale contains all notes in the scale system and is used as a fallback for non-diatonic notes.

Examples:

ChromaticScaleKind.chromatic?  # => true
MajorScaleKind.chromatic?      # => false

Returns:

  • (Boolean)

    true if chromatic scale (default: false)



896
897
898
# File 'lib/musa-dsl/music/scales.rb', line 896

def self.chromatic?
  false
end

.compute_intervalsArray<Integer>?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Computes intervals between consecutive scale degrees.

Returns:

  • (Array<Integer>, nil)

    intervals or nil if not calculable



1102
1103
1104
1105
1106
1107
1108
1109
# File 'lib/musa-dsl/music/scales.rb', line 1102

def self.compute_intervals
  return nil unless respond_to?(:pitches) && pitches.size > 1
  pitch_values = pitches.map { |p| p[:pitch] }
  # Only compute within first octave
  first_octave = pitch_values.take_while { |p| p < 12 }
  first_octave.push(12) if first_octave.last != 12
  first_octave.each_cons(2).map { |a, b| b - a }
end

.compute_symmetrySymbol?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Computes symmetry type of the scale.

Returns:

  • (Symbol, nil)

    :equal, :palindrome, :repeating, or nil



1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
# File 'lib/musa-dsl/music/scales.rb', line 1114

def self.compute_symmetry
  return nil unless respond_to?(:pitches)
  intervals = compute_intervals
  return nil unless intervals && intervals.any?

  # Check if intervals are all equal (e.g., whole tone: [2,2,2,2,2,2])
  return :equal if intervals.uniq.size == 1

  # Check for palindrome pattern
  return :palindrome if intervals == intervals.reverse

  # Check for repeating pattern
  (1..intervals.size / 2).each do |len|
    pattern = intervals.take(len)
    if intervals.each_slice(len).all? { |slice| slice == pattern || slice.size < len }
      return :repeating
    end
  end

  nil
end

.create_grade_functions_indexself

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Creates internal index mapping function names to grade indices.

Returns:

  • (self)


1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
# File 'lib/musa-dsl/music/scales.rb', line 1143

def self.create_grade_functions_index
  @grade_names_index = {}
  pitches.each_index do |i|
    pitches[i][:functions].each do |function|
      @grade_names_index[function] = i
    end
  end

  self
end

.custom_metadataHash

Returns custom metadata added by users at runtime.

This metadata is added via extend_metadata and can be cleared with reset_custom_metadata. Takes precedence over base_metadata.

Examples:

MajorScaleKind.(my_tag: :favorite)
MajorScaleKind.  # => { my_tag: :favorite }

Returns:

  • (Hash)

    user-defined metadata



1007
1008
1009
# File 'lib/musa-dsl/music/scales.rb', line 1007

def self.
  @custom_metadata || {}
end

.extend_metadata(**metadata) ⇒ Hash

Adds custom metadata to this scale kind.

Custom metadata takes precedence over base_metadata when queried via metadata. Multiple calls merge metadata together.

Examples:

MajorScaleKind.(my_mood: :happy, rating: 5)
MajorScaleKind.(suitable_for: [:pop, :classical])
MajorScaleKind.
# => { my_mood: :happy, rating: 5, suitable_for: [:pop, :classical] }

Parameters:

  • metadata (Hash)

    key-value pairs to add

Returns:

  • (Hash)

    the updated custom_metadata hash (frozen)



1024
1025
1026
1027
# File 'lib/musa-dsl/music/scales.rb', line 1024

def self.(**)
  @custom_metadata ||= {}
  @custom_metadata = @custom_metadata.merge().freeze
end

.grade_of_function(symbol) ⇒ Integer?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns grade index for a function symbol.

Examples:

MajorScaleKind.grade_of_function(:tonic)     # => 0
MajorScaleKind.grade_of_function(:dominant)  # => 4
MajorScaleKind.grade_of_function(:V)         # => 4

Parameters:

  • symbol (Symbol)

    function name (e.g., :tonic, :dominant, :V)

Returns:

  • (Integer, nil)

    grade index or nil if not found



925
926
927
928
# File 'lib/musa-dsl/music/scales.rb', line 925

def self.grade_of_function(symbol)
  create_grade_functions_index unless @grade_names_index
  @grade_names_index[symbol]
end

.gradesInteger

Returns the number of grades per octave.

For scales defining extended harmony (8th, 9th, etc.), this returns the number of diatonic degrees within one octave. Defaults to the number of pitch definitions.

Examples:

MajorScaleKind.grades  # => 7 (not 13, even with extended degrees)

Returns:

  • (Integer)

    number of grades per octave



910
911
912
# File 'lib/musa-dsl/music/scales.rb', line 910

def self.grades
  pitches.length
end

.grades_functionsArray<Symbol>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns all function symbols for accessing scale degrees.

Examples:

MajorScaleKind.grades_functions
# => [:I, :_1, :tonic, :first, :II, :_2, :supertonic, :second, ...]

Returns:

  • (Array<Symbol>)

    all function names



939
940
941
942
# File 'lib/musa-dsl/music/scales.rb', line 939

def self.grades_functions
  create_grade_functions_index unless @grade_names_index
  @grade_names_index.keys
end

.has_metadata?(key, value = nil) ⇒ Boolean

Checks whether metadata contains a key or key-value match.

When called with just a key, checks for key existence. When called with key and value, checks for exact match or array inclusion (if metadata value is an array).

Examples:

Key existence

MajorScaleKind.has_metadata?(:family)  # => true
MajorScaleKind.has_metadata?(:nonexistent)  # => false

Value matching

MajorScaleKind.has_metadata?(:family, :diatonic)  # => true
MajorScaleKind.has_metadata?(:family, :pentatonic)  # => false

Array inclusion

MajorScaleKind.has_metadata?(:character, :bright)  # => true

Parameters:

  • key (Symbol)

    the metadata key

  • value (Object, nil) (defaults to: nil)

    optional value to match

Returns:

  • (Boolean)

    whether the condition is satisfied



1088
1089
1090
1091
1092
1093
1094
1095
# File 'lib/musa-dsl/music/scales.rb', line 1088

def self.has_metadata?(key, value = nil)
  if value.nil?
    .key?(key)
  else
    [key] == value ||
      ([key].is_a?(Array) && [key].include?(value))
  end
end

.idSymbol

This method is abstract.

Subclass must implement

Returns the unique identifier for this scale kind.

Examples:

MajorScaleKind.id  # => :major

Returns:

  • (Symbol)

    scale kind ID (e.g., :major, :minor, :chromatic)

Raises:

  • (RuntimeError)

    if not implemented in subclass



861
862
863
# File 'lib/musa-dsl/music/scales.rb', line 861

def self.id
  raise 'Method not implemented. Should be implemented in subclass.'
end

.intrinsic_metadataHash

Returns intrinsic metadata derived from scale structure.

This metadata is automatically calculated from the scale's pitch structure and cannot be modified. It includes:

  • :id: Scale kind identifier
  • :grades: Number of diatonic degrees
  • :pitches: Array of pitch offsets from root
  • :intervals: Intervals between consecutive degrees
  • :has_leading_tone: Whether scale has pitch 11 (semitone below octave)
  • :has_tritone: Whether scale contains tritone (pitch 6)
  • :symmetric: Type of symmetry if any (:equal, :palindrome, :repeating)

Examples:

MajorScaleKind.
# => { id: :major, grades: 7, pitches: [0, 2, 4, 5, 7, 9, 11],
#      intervals: [2, 2, 1, 2, 2, 2], has_leading_tone: true,
#      has_tritone: true, symmetric: nil }

Returns:

  • (Hash)

    intrinsic metadata derived from structure



964
965
966
967
968
969
970
971
972
973
974
975
976
# File 'lib/musa-dsl/music/scales.rb', line 964

def self.
  result = {}
  result[:id] = id if respond_to?(:id)
  result[:grades] = grades if respond_to?(:grades)
  if respond_to?(:pitches)
    result[:pitches] = pitches.map { |p| p[:pitch] }
    result[:intervals] = compute_intervals
    result[:has_leading_tone] = pitches.any? { |p| p[:pitch] == 11 }
    result[:has_tritone] = pitches.any? { |p| p[:pitch] == 6 }
    result[:symmetric] = compute_symmetry
  end
  result.compact
end

.metadataHash

Returns combined metadata from all three layers.

Layers are merged with later layers taking precedence: intrinsic_metadata < base_metadata < custom_metadata

Examples:

MajorScaleKind.
# => { id: :major, grades: 7, pitches: [...], family: :diatonic, ... }

Returns:

  • (Hash)

    combined metadata from all layers



1051
1052
1053
1054
1055
# File 'lib/musa-dsl/music/scales.rb', line 1051

def self.
  
    .merge()
    .merge()
end

.metadata_value(key) ⇒ Object?

Returns a specific metadata value.

Examples:

MajorScaleKind.(:family)  # => :diatonic

Parameters:

  • key (Symbol)

    the metadata key

Returns:

  • (Object, nil)

    the value or nil if not found



1064
1065
1066
# File 'lib/musa-dsl/music/scales.rb', line 1064

def self.(key)
  [key]
end

.pitchesArray<Hash>

This method is abstract.

Subclass must implement

Returns the pitch structure definition.

Defines the scale degrees and their pitch offsets from the root. Each entry specifies function names and semitone offset.

Examples:

Major scale structure (partial)

[{ functions: [:I, :tonic, :_1], pitch: 0 },
 { functions: [:II, :supertonic, :_2], pitch: 2 },
 { functions: [:III, :mediant, :_3], pitch: 4 },
 ...]

Returns:

  • (Array<Hash>)

    array of pitch definitions with:

    • :functions [Array]: function names for this degree
    • :pitch [Integer]: semitone offset from root

Raises:

  • (RuntimeError)

    if not implemented in subclass



881
882
883
# File 'lib/musa-dsl/music/scales.rb', line 881

def self.pitches
  raise 'Method not implemented. Should be implemented in subclass.'
end

.reset_custom_metadatanil

Clears all custom metadata from this scale kind.

Examples:

MajorScaleKind.(temp: :data)
MajorScaleKind.
MajorScaleKind.  # => {}

Returns:

  • (nil)


1037
1038
1039
# File 'lib/musa-dsl/music/scales.rb', line 1037

def self.
  @custom_metadata = nil
end

Instance Method Details

#==(other) ⇒ Boolean

Checks scale kind equality.

Parameters:

Returns:

  • (Boolean)


840
841
842
# File 'lib/musa-dsl/music/scales.rb', line 840

def ==(other)
  self.class == other.class && @tuning == other.tuning
end

#absolutScale

Returns scale with absolute root (MIDI 0).

Examples:

tuning.major.absolut  # Scale rooted at MIDI 0

Returns:

  • (Scale)

    scale rooted on MIDI 0



803
804
805
# File 'lib/musa-dsl/music/scales.rb', line 803

def absolut
  self[0]
end

#default_rootScale

Returns scale with default root (middle C, MIDI 60).

Examples:

tuning.major.default_root  # C major

Returns:

  • (Scale)

    scale rooted on middle C



793
794
795
# File 'lib/musa-dsl/music/scales.rb', line 793

def default_root
  self[60]
end

#find_chord_in_scales(chord, roots: nil) ⇒ Array<Musa::Chords::Chord>

Finds all scales of this kind that contain the given chord.

Searches through scales rooted on different pitches to find which ones contain all the notes of the given chord. Returns chords with their containing scale as context.

Examples:

Find G major triad in all major scales

tuning = Scales.et12[440.0]
g_triad = tuning.major[60].dominant.chord
tuning.major.find_chord_in_scales(g_triad)
# => [Chord in C major (V), Chord in G major (I), Chord in D major (IV)]

Parameters:

  • chord (Musa::Chords::Chord)

    the chord to search for

  • roots (Range, Array, nil) (defaults to: nil)

    pitch offsets to search (default: 0...notes_in_octave)

Returns:

See Also:



825
826
827
828
829
830
831
832
833
834
# File 'lib/musa-dsl/music/scales.rb', line 825

def find_chord_in_scales(chord, roots: nil)
  roots ||= 0...tuning.notes_in_octave
  base_pitch = chord.root.pitch % tuning.notes_in_octave

  roots.filter_map do |root_offset|
    root_pitch = base_pitch + root_offset
    scale = self[root_pitch]
    chord.as_chord_in_scale(scale)
  end
end

#get(root_pitch) ⇒ Scale Also known as: []

Creates or retrieves a scale rooted on specific pitch.

Scales are cached—repeated calls with same pitch return same instance.

Examples:

major_kind = tuning[:major]
c_major = major_kind[60]     # C major
g_major = major_kind[67]     # G major

Parameters:

  • root_pitch (Integer)

    MIDI root pitch (60 = middle C)

Returns:

  • (Scale)

    scale instance



780
781
782
783
# File 'lib/musa-dsl/music/scales.rb', line 780

def get(root_pitch)
  @scales[root_pitch] = Scale.new(self, root_pitch: root_pitch) unless @scales.key?(root_pitch)
  @scales[root_pitch]
end

#inspectString Also known as: to_s

Returns string representation.

Returns:



847
848
849
# File 'lib/musa-dsl/music/scales.rb', line 847

def inspect
  "<#{self.class.name}: tuning = #{@tuning}>"
end