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:



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

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

Instance Attribute Details

#tuningScaleSystemTuning (readonly)

The tuning context.

Returns:



808
809
810
# File 'lib/musa-dsl/music/scales.rb', line 808

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, :resolved], parent: nil }

Returns:

  • (Hash)

    library-defined metadata



1057
1058
1059
# File 'lib/musa-dsl/music/scales.rb', line 1057

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)



947
948
949
# File 'lib/musa-dsl/music/scales.rb', line 947

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



1166
1167
1168
1169
1170
1171
1172
1173
# File 'lib/musa-dsl/music/scales.rb', line 1166

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



1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
# File 'lib/musa-dsl/music/scales.rb', line 1178

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)


1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
# File 'lib/musa-dsl/music/scales.rb', line 1207

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



1071
1072
1073
# File 'lib/musa-dsl/music/scales.rb', line 1071

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)



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

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



976
977
978
979
# File 'lib/musa-dsl/music/scales.rb', line 976

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



961
962
963
# File 'lib/musa-dsl/music/scales.rb', line 961

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



990
991
992
993
# File 'lib/musa-dsl/music/scales.rb', line 990

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



1152
1153
1154
1155
1156
1157
1158
1159
# File 'lib/musa-dsl/music/scales.rb', line 1152

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



914
915
916
# File 'lib/musa-dsl/music/scales.rb', line 914

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, 12, 14, 16, 17, 19, 21],
#      intervals: [2, 2, 1, 2, 2, 2, 1],
#      has_leading_tone: true, has_tritone: false }

# Three things a reader should not have to discover by running it:
#
# `pitches` carries the scale TWICE -- the second octave included --
# because a scale kind is asked for grades beyond the seventh.
#
# `has_tritone` is false for the major scale, which contains F-B. The
# key is read from the root: it asks whether pitch 6 is in the scale,
# not whether any two of its pitches are a tritone apart.
#
# `symmetric` is absent, not nil: the key is only set when there is a
# symmetry to report.

Returns:

  • (Hash)

    intrinsic metadata derived from structure



1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
# File 'lib/musa-dsl/music/scales.rb', line 1028

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



1115
1116
1117
1118
1119
# File 'lib/musa-dsl/music/scales.rb', line 1115

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



1128
1129
1130
# File 'lib/musa-dsl/music/scales.rb', line 1128

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

MajorScaleKind.pitches.first(2)
# => [{ functions: [:I, :_1, :tonic, :first], pitch: 0 }, { functions: [:II, :_2, :supertonic, :second], pitch: 2 }]

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



932
933
934
# File 'lib/musa-dsl/music/scales.rb', line 932

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)


1101
1102
1103
# File 'lib/musa-dsl/music/scales.rb', line 1101

def self.
  @custom_metadata = nil
end

Instance Method Details

#==(other) ⇒ Boolean

Checks scale kind equality.

Parameters:

Returns:

  • (Boolean)


893
894
895
# File 'lib/musa-dsl/music/scales.rb', line 893

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

#absolutScale

Returns scale with absolute root (MIDI 0).

Examples:

tuning.major.absolut.root.pitch  # => 0

# Rooted at MIDI 0: the scale as a shape, before it is placed anywhere.

Returns:

  • (Scale)

    scale rooted on MIDI 0



849
850
851
# File 'lib/musa-dsl/music/scales.rb', line 849

def absolut
  self[0]
end

#default_rootScale

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

Examples:

tuning.major.default_root.root.pitch  # => 60

# Middle C, because that is where a scale sits when nobody says.

Returns:

  • (Scale)

    scale rooted on middle C



837
838
839
# File 'lib/musa-dsl/music/scales.rb', line 837

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
found = tuning.major.find_chord_in_scales(g_triad)

found.map { |c| [c.scale.root_pitch, c.scale.degree_of_chord(c)] }
# => [[7, 0], [12, 4], [14, 3]]

# G first, and the roots are 7, 12 and 14 rather than 67, 60 and 62:
# the scales come back rooted on pitch offsets from the tuning, not on
# the octave the chord was taken from. G major (root 7, degree 0), then
# C major (12, its dominant) and D major (14, its subdominant).

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:



878
879
880
881
882
883
884
885
886
887
# File 'lib/musa-dsl/music/scales.rb', line 878

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]

major_kind[60].root.pitch  # => 60   (C major)
major_kind[67].root.pitch  # => 67   (G major)

Parameters:

  • root_pitch (Integer)

    MIDI root pitch (60 = middle C)

Returns:

  • (Scale)

    scale instance



822
823
824
825
# File 'lib/musa-dsl/music/scales.rb', line 822

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:



900
901
902
# File 'lib/musa-dsl/music/scales.rb', line 900

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