Class: Musa::Scales::Scale

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

Overview

Instantiated scale with specific root pitch.

Scale represents a concrete scale (major, minor, etc.) rooted on a specific pitch. It provides access to scale degrees, interval calculations, frequency generation, and chord construction.

Creation

Scales are created via ScaleKind:

tuning = Scales[:et12][440.0]
c_major = tuning.major[60]        # Via convenience method
a_minor = tuning[:minor][69]      # Via bracket notation

Accessing Notes

By numeric grade (0-based):

scale[0]    # First degree (tonic)
scale[1]    # Second degree
scale[4]    # Fifth degree

By function name (dynamic methods):

scale.tonic       # First degree
scale.dominant    # Fifth degree
scale.mediant     # Third degree

By Roman numeral:

scale[:I]     # First degree
scale[:V]     # Fifth degree
scale[:IV]    # Fourth degree

With accidentals (sharp # or flat _). Use strings for #:

scale['I#']   # Raised tonic
scale[:V_]    # Flatted dominant
scale['II##'] # Double-raised second

Note Operations

Each note is a NoteInScale instance with full capabilities:

note = scale.tonic
note.pitch              # MIDI pitch number
note.frequency          # Frequency in Hz
note.chord              # Build chord from note
note.up(:P5)            # Navigate by interval
note.sharp              # Raise by semitone

Special Methods

  • chromatic: Access chromatic scale at same root
  • octave: Transpose scale to different octave
  • note_of_pitch: Find note for specific MIDI pitch

Examples:

Basic scale access

c_major = tuning.major[60]
c_major.tonic.pitch      # => 60 (C)
c_major.dominant.pitch   # => 67 (G)
c_major[:III].pitch      # => 64 (E)

Chromatic alterations (use strings for #)

c_major['I#'].pitch      # => 61 (C#)
c_major[:V_].pitch       # => 66 (F#/Gb)

Building chords

c_major.tonic.chord              # C major triad
c_major.dominant.chord :seventh  # G dominant 7th

See Also:

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(kind, root_pitch:) ⇒ Scale

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 instance.

Parameters:

  • kind (ScaleKind)

    the scale kind

  • root_pitch (Integer)

    MIDI root pitch



1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
# File 'lib/musa-dsl/music/scales.rb', line 1237

def initialize(kind, root_pitch:)
  @notes_by_grade = {}
  @notes_by_pitch = {}

  @kind = kind

  @root_pitch = root_pitch

  @kind.class.grades_functions.each do |name|
    define_singleton_method name do
      self[name]
    end
  end

  freeze
end

Instance Attribute Details

#kindScaleKind (readonly)

Scale kind (major, minor, etc.).

Returns:



1268
1269
1270
# File 'lib/musa-dsl/music/scales.rb', line 1268

def kind
  @kind
end

#root_pitchInteger (readonly)

Root pitch (MIDI number).

Returns:

  • (Integer)


1272
1273
1274
# File 'lib/musa-dsl/music/scales.rb', line 1272

def root_pitch
  @root_pitch
end

Instance Method Details

#==(other) ⇒ Boolean

Checks scale equality.

Scales are equal if they have same kind and root pitch.

Parameters:

Returns:

  • (Boolean)


1587
1588
1589
1590
1591
# File 'lib/musa-dsl/music/scales.rb', line 1587

def ==(other)
  self.class == other.class &&
      @kind == other.kind &&
      @root_pitch == other.root_pitch
end

#absolutScale

Returns the scale rooted at absolute pitch 0.

Examples:

c_major.absolut  # Major scale at MIDI 0

Returns:

  • (Scale)

    scale of same kind at MIDI 0



1302
1303
1304
# File 'lib/musa-dsl/music/scales.rb', line 1302

def absolut
  @kind[0]
end

#chord_on(grade, *feature_values, allow_chromatic: nil, move: nil, duplicate: nil, **features_hash) ⇒ Chords::Chord

Creates a chord rooted on the specified scale degree.

This is a convenience method that combines scale note access with chord creation. It's equivalent to scale[grade].chord(...).

Examples:

Create triads

scale.chord_on(0)           # Tonic triad (I)
scale.chord_on(:dominant)   # Dominant triad (V)
scale.chord_on(:IV)         # Subdominant triad

Create extended chords

scale.chord_on(4, :seventh)              # V7
scale.chord_on(:dominant, :ninth)        # V9
scale.chord_on(0, :seventh, :major)      # Imaj7

With voicing

scale.chord_on(:I, :seventh, move: {root: -1})
scale.chord_on(0, :triad, duplicate: {root: 1})

Parameters:

  • grade (Integer, Symbol, String)

    scale degree (0-based numeric, function name like :tonic, or Roman numeral like :V)

  • feature_values (Array<Symbol>)

    chord feature values (:seventh, :major, etc.)

  • allow_chromatic (Boolean) (defaults to: nil)

    allow non-diatonic chord notes

  • move (Hash{Symbol => Integer}) (defaults to: nil)

    initial octave moves for chord tones

  • duplicate (Hash{Symbol => Integer, Array}) (defaults to: nil)

    initial duplications

  • features_hash (Hash)

    additional feature key-value pairs

Returns:

See Also:



1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
# File 'lib/musa-dsl/music/scales.rb', line 1569

def chord_on(grade, *feature_values,
             allow_chromatic: nil,
             move: nil,
             duplicate: nil,
             **features_hash)
  self[grade].chord(*feature_values,
                    allow_chromatic: allow_chromatic,
                    move: move,
                    duplicate: duplicate,
                    **features_hash)
end

#chromaticScale

Returns the chromatic scale at the same root.

Examples:

c_major.chromatic  # Chromatic scale starting at C

Returns:

  • (Scale)

    chromatic scale rooted at same pitch



1292
1293
1294
# File 'lib/musa-dsl/music/scales.rb', line 1292

def chromatic
  @kind.tuning.chromatic[@root_pitch]
end

#contains_chord?(chord) ⇒ Boolean

Checks if all chord pitches exist in this scale.

Uses the chord's definition to verify that every pitch in the chord can be found as a diatonic note in this scale.

Examples:

c_major = Scales.et12[440.0].major[60]
g7 = c_major.dominant.chord :seventh
c_major.contains_chord?(g7)  # => true

cm = c_major.tonic.chord.with_quality(:minor)
c_major.contains_chord?(cm)  # => false (Eb not in C major)

Parameters:

Returns:

  • (Boolean)

    true if all chord notes are in scale

See Also:



1518
1519
1520
# File 'lib/musa-dsl/music/scales.rb', line 1518

def contains_chord?(chord)
  chord.chord_definition.in_scale?(self, chord_root_pitch: chord.root.pitch)
end

#degree_of_chord(chord) ⇒ Integer?

Returns the grade (0-based) where the chord root falls in this scale.

Examples:

c_major = Scales.et12[440.0].major[60]
g_chord = c_major.dominant.chord
c_major.degree_of_chord(g_chord)  # => 4 (V degree, 0-based)

Parameters:

Returns:

  • (Integer, nil)

    grade (0-based) or nil if chord not in scale

See Also:



1533
1534
1535
1536
1537
1538
# File 'lib/musa-dsl/music/scales.rb', line 1533

def degree_of_chord(chord)
  return nil unless contains_chord?(chord)

  note = note_of_pitch(chord.root.pitch, allow_chromatic: false)
  note&.grade
end

#get(grade_or_symbol) ⇒ NoteInScale Also known as: []

Accesses scale degree by grade, symbol, or function name.

Supports multiple access patterns:

  • Integer: Numeric grade (0-based)
  • Symbol/String: Function name or Roman numeral
  • With accidentals: Add '#' for sharp, '_' for flat

Notes are cached—repeated access returns same instance.

Examples:

Numeric access

scale[0]    # Tonic
scale[4]    # Dominant (in major/minor)

Function name access

scale[:tonic]
scale[:dominant]
scale[:mediant]

Roman numeral access

scale[:I]     # Tonic
scale[:V]     # Dominant
scale[:IV]    # Subdominant

With accidentals (use strings for #)

scale['I#']    # Raised tonic
scale[:V_]     # Flatted dominant
scale['II##']  # Double-raised second

Parameters:

  • grade_or_symbol (Integer, Symbol, String)

    degree specifier

Returns:

Raises:

  • (ArgumentError)

    if grade_or_symbol is invalid type



1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
# File 'lib/musa-dsl/music/scales.rb', line 1352

def get(grade_or_symbol)

  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)

  wide_grade, sharps = grade_of(grade_or_symbol)

  unless @notes_by_grade.key?(wide_grade)

    octave = wide_grade / @kind.class.grades
    grade = wide_grade % @kind.class.grades

    pitch = @root_pitch +
        octave * @kind.tuning.notes_in_octave +
        @kind.class.pitches[grade][:pitch]

    note = NoteInScale.new self, grade, octave, pitch

    @notes_by_grade[wide_grade] = @notes_by_pitch[pitch] = note
  end


  @notes_by_grade[wide_grade].sharp(sharps)
end

#grade_of(grade_or_string_or_symbol) ⇒ Array(Integer, 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.

Converts grade specifier to numeric grade and accidentals.

Parameters:

  • grade_or_string_or_symbol (Integer, Symbol, String)

    grade specifier

Returns:

  • (Array(Integer, Integer))

    wide grade and accidentals count



1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
# File 'lib/musa-dsl/music/scales.rb', line 1384

def grade_of(grade_or_string_or_symbol)
  name, wide_grade, accidentals = parse_grade(grade_or_string_or_symbol)

  grade = @kind.class.grade_of_function name if name

  octave = wide_grade / @kind.class.grades if wide_grade
  grade = wide_grade % @kind.class.grades if wide_grade

  octave ||= 0

  return octave * @kind.class.grades + grade, accidentals
end

#inspectString Also known as: to_s

Returns string representation.

Returns:



1596
1597
1598
# File 'lib/musa-dsl/music/scales.rb', line 1596

def inspect
  "<Scale: kind = #{@kind} root_pitch = #{@root_pitch}>"
end

#note_of_pitch(pitch, allow_chromatic: nil, allow_nearest: nil) ⇒ NoteInScale?

Finds note for a specific MIDI pitch.

Searches for a note in the scale matching the given pitch. Options control behavior when pitch is not in scale.

Examples:

Diatonic note

c_major.note_of_pitch(64)  # => E (in scale)

Chromatic note

c_major.note_of_pitch(63, allow_chromatic: true)  # => Eb (chromatic)

Nearest note

c_major.note_of_pitch(63, allow_nearest: true)  # => E or D (nearest)

Parameters:

  • pitch (Integer)

    MIDI pitch number

  • allow_chromatic (Boolean) (defaults to: nil)

    if true, return chromatic note when not in scale

  • allow_nearest (Boolean) (defaults to: nil)

    if true, return nearest scale note

Returns:



1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
# File 'lib/musa-dsl/music/scales.rb', line 1452

def note_of_pitch(pitch, allow_chromatic: nil, allow_nearest: nil)
  allow_chromatic ||= false
  allow_nearest ||= false

  note = @notes_by_pitch[pitch]

  unless note
    pitch_offset = pitch - @root_pitch

    pitch_offset_in_octave = pitch_offset % @kind.tuning.scale_system.notes_in_octave
    pitch_offset_octave = pitch_offset / @kind.tuning.scale_system.notes_in_octave

    grade = @kind.class.pitches.find_index { |pitch_definition| pitch_definition[:pitch] == pitch_offset_in_octave }

    if grade
      wide_grade = pitch_offset_octave * @kind.class.grades + grade
      note = self[wide_grade]

    elsif allow_nearest
      sharps = 0

      until note
        note = note_of_pitch(pitch - (sharps += 1) * @kind.tuning.scale_system.part_of_tone_size)
        note ||= note_of_pitch(pitch + sharps * @kind.tuning.scale_system.part_of_tone_size)
      end

    elsif allow_chromatic
      nearest = note_of_pitch(pitch, allow_nearest: true)

      note = chromatic.note_of_pitch(pitch).with_background(scale: self, grade: nearest.grade, octave: nearest.octave, sharps: (pitch - nearest.pitch) / @kind.tuning.scale_system.part_of_tone_size)
    end
  end

  note
end

#octave(octave) ⇒ Scale

Transposes scale by octaves.

Examples:

c_major.octave(1)   # C major one octave higher
c_major.octave(-1)  # C major one octave lower

Parameters:

  • octave (Integer)

    octave offset (positive = up, negative = down)

Returns:

  • (Scale)

    transposed scale

Raises:

  • (ArgumentError)

    if octave is not integer



1315
1316
1317
1318
1319
# File 'lib/musa-dsl/music/scales.rb', line 1315

def octave(octave)
  raise ArgumentError, "#{octave} is not integer" unless octave == octave.to_i

  @kind[@root_pitch + octave * @kind.class.grades]
end

#offset_of_interval(interval_name) ⇒ Integer

Returns semitone offset for a named interval.

Examples:

scale.offset_of_interval(:P5)  # => 7
scale.offset_of_interval(:M3)  # => 4

Parameters:

  • interval_name (Symbol)

    interval name (e.g., :M3, :P5)

Returns:

  • (Integer)

    semitone offset



1496
1497
1498
# File 'lib/musa-dsl/music/scales.rb', line 1496

def offset_of_interval(interval_name)
  @kind.tuning.offset_of_interval(interval_name)
end

#parse_grade(neuma_grade) ⇒ Array(Symbol, Integer, 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.

Parses grade string/symbol into components.

Handles formats like "I#", ":V_", "7##", extracting function name, numeric grade, and accidentals.

Parameters:

  • neuma_grade (Integer, Symbol, String)

    grade to parse

Returns:

  • (Array(Symbol, Integer, Integer))

    name, wide_grade, accidentals



1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
# File 'lib/musa-dsl/music/scales.rb', line 1406

def parse_grade(neuma_grade)
  name = wide_grade = nil
  accidentals = 0

  case neuma_grade
  when Symbol, String
    match = /\A(?<name>[^[#|_]]*)(?<accidental_sharps>#*)(?<accidental_flats>_*)\Z/.match neuma_grade.to_s

    if match
      if match[:name] == match[:name].to_i.to_s
        wide_grade = match[:name].to_i
      else
        name = match[:name].to_sym unless match[:name].empty?
      end
      accidentals = match[:accidental_sharps].length - match[:accidental_flats].length
    else
      name = neuma_grade.to_sym unless (neuma_grade.nil? || neuma_grade.empty?)
    end
  when Numeric
    wide_grade = neuma_grade.to_i

  else
    raise ArgumentError, "Cannot eval #{neuma_grade} as name or grade position."
  end

  return name, wide_grade, accidentals
end

#rootNoteInScale

Returns the root note (first degree).

Equivalent to scale[0] or scale.tonic.

Examples:

c_major.root.pitch  # => 60

Returns:



1282
1283
1284
# File 'lib/musa-dsl/music/scales.rb', line 1282

def root
  self[0]
end

#tuningScaleSystemTuning

Returns the tuning system associated with this scale.

Delegated from ScaleKind#tuning.

Examples:

scale = Scales.et12[440.0].major[60]
scale.tuning  # => ScaleSystemTuning for 12-TET at A=440Hz

Returns:



1264
# File 'lib/musa-dsl/music/scales.rb', line 1264

def_delegators :@kind, :tuning