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 _):

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

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



789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
# File 'lib/musa-dsl/music/scales.rb', line 789

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:



811
812
813
# File 'lib/musa-dsl/music/scales.rb', line 811

def kind
  @kind
end

#root_pitchInteger (readonly)

Root pitch (MIDI number).

Returns:

  • (Integer)


815
816
817
# File 'lib/musa-dsl/music/scales.rb', line 815

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)


1047
1048
1049
1050
1051
# File 'lib/musa-dsl/music/scales.rb', line 1047

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

#[](grade_or_symbol) ⇒ NoteInScale

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

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



895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
# File 'lib/musa-dsl/music/scales.rb', line 895

def [](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

#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



845
846
847
# File 'lib/musa-dsl/music/scales.rb', line 845

def absolut
  @kind[0]
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



835
836
837
# File 'lib/musa-dsl/music/scales.rb', line 835

def chromatic
  @kind.tuning.chromatic[@root_pitch]
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



925
926
927
928
929
930
931
932
933
934
935
936
# File 'lib/musa-dsl/music/scales.rb', line 925

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:



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

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:



993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
# File 'lib/musa-dsl/music/scales.rb', line 993

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



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

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



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

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



947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
# File 'lib/musa-dsl/music/scales.rb', line 947

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:



825
826
827
# File 'lib/musa-dsl/music/scales.rb', line 825

def root
  self[0]
end