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)
private
Converts grade specifier to numeric grade and 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)
private
Parses grade string/symbol into components.
-
#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.
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
#kind ⇒ ScaleKind (readonly)
Scale kind (major, minor, etc.).
1268 1269 1270 |
# File 'lib/musa-dsl/music/scales.rb', line 1268 def kind @kind end |
#root_pitch ⇒ Integer (readonly)
Root pitch (MIDI number).
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.
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 |
#absolut ⇒ Scale
Returns the scale rooted at absolute pitch 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(...).
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 |
#chromatic ⇒ Scale
Returns the chromatic scale at the same root.
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.
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.
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.
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.
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 |
#inspect ⇒ String Also known as: to_s
Returns string representation.
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.
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.
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.
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.
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 |
#root ⇒ NoteInScale
Returns the root note (first degree).
Equivalent to scale[0] or scale.tonic.
1282 1283 1284 |
# File 'lib/musa-dsl/music/scales.rb', line 1282 def root self[0] end |
#tuning ⇒ ScaleSystemTuning
Returns the tuning system associated with this scale.
Delegated from ScaleKind#tuning.
1264 |
# File 'lib/musa-dsl/music/scales.rb', line 1264 def_delegators :@kind, :tuning |