Class: Musa::Scales::Scale
- 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
Instance Attribute Summary collapse
-
#kind ⇒ ScaleKind
readonly
Scale kind (major, minor, etc.).
-
#root_pitch ⇒ Integer
readonly
Root pitch (MIDI number).
Instance Method Summary collapse
-
#==(other) ⇒ Boolean
Checks scale equality.
-
#absolut ⇒ Scale
Returns the scale rooted at absolute pitch 0.
-
#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.
-
#chromatic ⇒ Scale
Returns the chromatic scale at the same root.
-
#contains_chord?(chord) ⇒ Boolean
Checks if all chord pitches exist in this scale.
-
#degree_of_chord(chord) ⇒ Integer?
Returns the grade (0-based) where the chord root falls in this scale.
-
#get(grade_or_symbol) ⇒ NoteInScale
(also: #[])
Accesses scale degree by grade, symbol, or function name.
-
#grade_of(grade_or_string_or_symbol) ⇒ Array(Integer, Integer)
Resolves any way of naming a grade into the number this scale uses for it, plus its accidentals.
-
#initialize(kind, root_pitch:) ⇒ Scale
constructor
private
Creates a scale instance.
-
#inspect ⇒ String
(also: #to_s)
Returns string representation.
-
#note_of_pitch(pitch, allow_chromatic: nil, allow_nearest: nil) ⇒ NoteInScale?
Finds note for a specific MIDI pitch.
-
#octave(octave) ⇒ Scale
Transposes scale by octaves.
-
#offset_of_interval(interval_name) ⇒ Integer
Returns semitone offset for a named interval.
-
#parse_grade(neuma_grade) ⇒ Array(Symbol, Integer, Integer)
Reads the notation of a grade, without deciding what it means.
-
#root ⇒ NoteInScale
Returns the root note (first degree).
-
#tuning ⇒ ScaleSystemTuning
Returns the tuning system associated with this scale.
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.
1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 |
# File 'lib/musa-dsl/music/scales.rb', line 1235 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
#kind ⇒ ScaleKind (readonly)
Scale kind (major, minor, etc.).
1266 1267 1268 |
# File 'lib/musa-dsl/music/scales.rb', line 1266 def kind @kind end |
#root_pitch ⇒ Integer (readonly)
Root pitch (MIDI number).
1270 1271 1272 |
# File 'lib/musa-dsl/music/scales.rb', line 1270 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.
1640 1641 1642 1643 1644 |
# File 'lib/musa-dsl/music/scales.rb', line 1640 def ==(other) self.class == other.class && @kind == other.kind && @root_pitch == other.root_pitch end |
#absolut ⇒ Scale
Returns the scale rooted at absolute pitch 0.
1300 1301 1302 |
# File 'lib/musa-dsl/music/scales.rb', line 1300 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(...).
1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 |
# File 'lib/musa-dsl/music/scales.rb', line 1622 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 |
#chromatic ⇒ Scale
Returns the chromatic scale at the same root.
1290 1291 1292 |
# File 'lib/musa-dsl/music/scales.rb', line 1290 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.
1571 1572 1573 |
# File 'lib/musa-dsl/music/scales.rb', line 1571 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.
1586 1587 1588 1589 1590 1591 |
# File 'lib/musa-dsl/music/scales.rb', line 1586 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.
1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 |
# File 'lib/musa-dsl/music/scales.rb', line 1350 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)
Resolves any way of naming a grade into the number this scale uses for it, plus its accidentals.
This is the step #get takes before looking a note up, and it is public
because a piece often has to take a grade from somewhere that is not
Ruby -- a configuration file, a text score, a message from an editor --
and needs the same reading the scale itself would make. Where
#parse_grade stops at the syntax, this resolves the function name
through the scale kind: :dominant becomes 4 in a diatonic scale.
1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 |
# File 'lib/musa-dsl/music/scales.rb', line 1402 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 |
#inspect ⇒ String Also known as: to_s
Returns string representation.
1649 1650 1651 |
# File 'lib/musa-dsl/music/scales.rb', line 1649 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.
1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 |
# File 'lib/musa-dsl/music/scales.rb', line 1505 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.
1313 1314 1315 1316 1317 |
# File 'lib/musa-dsl/music/scales.rb', line 1313 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.
1549 1550 1551 |
# File 'lib/musa-dsl/music/scales.rb', line 1549 def offset_of_interval(interval_name) @kind.tuning.offset_of_interval(interval_name) end |
#parse_grade(neuma_grade) ⇒ Array(Symbol, Integer, Integer)
Reads the notation of a grade, without deciding what it means.
Splits a written grade into its three parts: a function name, a numeric
grade, and a count of accidentals. Exactly one of name and grade comes
back, according to how it was written -- 'II' is a name, '2' is a
number -- and resolving a name into a number is #grade_of's job, since
that depends on the scale kind and this does not.
Accidentals are a single signed count: # adds one, _ subtracts one,
and they may repeat.
Public because it is the only way to read the grade notation without a scale deciding for you, which is what a custom decoder needs.
1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 |
# File 'lib/musa-dsl/music/scales.rb', line 1447 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 |
#root ⇒ NoteInScale
Returns the root note (first degree).
Equivalent to scale or scale.tonic.
1280 1281 1282 |
# File 'lib/musa-dsl/music/scales.rb', line 1280 def root self[0] end |
#tuning ⇒ ScaleSystemTuning
Returns the tuning system associated with this scale.
Delegated from ScaleKind#tuning.
1262 |
# File 'lib/musa-dsl/music/scales.rb', line 1262 def_delegators :@kind, :tuning |