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.
1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 |
# File 'lib/musa-dsl/music/scales.rb', line 1301 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.).
1334 1335 1336 |
# File 'lib/musa-dsl/music/scales.rb', line 1334 def kind @kind end |
#root_pitch ⇒ Integer (readonly)
Root pitch (MIDI number).
1338 1339 1340 |
# File 'lib/musa-dsl/music/scales.rb', line 1338 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.
1725 1726 1727 1728 1729 |
# File 'lib/musa-dsl/music/scales.rb', line 1725 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.
1368 1369 1370 |
# File 'lib/musa-dsl/music/scales.rb', line 1368 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(...).
1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 |
# File 'lib/musa-dsl/music/scales.rb', line 1707 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.
1358 1359 1360 |
# File 'lib/musa-dsl/music/scales.rb', line 1358 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.
1656 1657 1658 |
# File 'lib/musa-dsl/music/scales.rb', line 1656 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.
1671 1672 1673 1674 1675 1676 |
# File 'lib/musa-dsl/music/scales.rb', line 1671 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.
1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 |
# File 'lib/musa-dsl/music/scales.rb', line 1431 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.
1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 |
# File 'lib/musa-dsl/music/scales.rb', line 1483 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.
1734 1735 1736 |
# File 'lib/musa-dsl/music/scales.rb', line 1734 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.
1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 |
# File 'lib/musa-dsl/music/scales.rb', line 1590 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.
1388 1389 1390 1391 1392 |
# File 'lib/musa-dsl/music/scales.rb', line 1388 def octave(octave) raise ArgumentError, "#{octave} is not integer" unless octave == octave.to_i @kind[@root_pitch + octave * @kind.tuning.notes_in_octave] end |
#offset_of_interval(interval_name) ⇒ Integer
Returns semitone offset for a named interval.
1634 1635 1636 |
# File 'lib/musa-dsl/music/scales.rb', line 1634 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.
1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 |
# File 'lib/musa-dsl/music/scales.rb', line 1528 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.
1348 1349 1350 |
# File 'lib/musa-dsl/music/scales.rb', line 1348 def root self[0] end |
#tuning ⇒ ScaleSystemTuning
Returns the tuning system associated with this scale.
Delegated from ScaleKind#tuning.
1330 |
# File 'lib/musa-dsl/music/scales.rb', line 1330 def_delegators :@kind, :tuning |