Class: Musa::Scales::NoteInScale

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

Overview

Note within a scale context.

NoteInScale represents a specific note within a scale, providing rich musical functionality including:

  • Pitch and frequency information
  • Interval navigation (up, down, by named intervals)
  • Chromatic alterations (sharp, flat)
  • Scale navigation (change scales while keeping pitch)
  • Chord construction
  • Octave transposition

Creation

Notes are created via scale access, not directly:

scale = tuning.major[60]
note = scale.tonic           # NoteInScale instance
note = scale[:V]             # Another NoteInScale

Basic Properties

note.pitch       # MIDI pitch number
note.grade       # Scale degree (0-based)
note.octave      # Octave relative to scale root
note.frequency   # Frequency in Hz
note.functions   # Function names for this degree

Interval Navigation

Natural intervals (diatonic, within scale):

note.up(2)        # Up 2 scale degrees
note.down(1)      # Down 1 scale degree

Chromatic intervals (by semitones or named intervals):

note.up(:P5)      # Up perfect fifth
note.up(7)        # Up 7 semitones (if chromatic specified)
note.down(:M3)    # Down major third

Chromatic Alterations

note.sharp        # Raise by 1 semitone
note.sharp(2)     # Raise by 2 semitones
note.flat         # Lower by 1 semitone
note.flat(2)      # Lower by 2 semitones

Scale Navigation

note.scale                  # Parent scale this note belongs to
note.as_root_of(:minor)     # New minor scale with this pitch as root
note.minor                  # Same as note.as_root_of(:minor)
note.chromatic              # Same as note.as_root_of(:chromatic)

Chord Construction

note.chord                      # Build triad
note.chord :seventh             # Build seventh chord
note.chord quality: :minor      # Build with features

Background Scale Context

Chromatic notes remember their diatonic context:

c_sharp = c_major.tonic.sharp        # C# in C major context
c_sharp.background_scale             # => c_major
c_sharp.background_note              # => C (natural)
c_sharp.background_sharps            # => 1

Examples:

Basic usage

c_major = tuning.major[60]
tonic = c_major.tonic
tonic.pitch       # => 60
tonic.frequency   # => ~261.63 Hz

Interval navigation

tonic.up(:P5).pitch          # => 67  (G, by interval)
tonic.up(4, :natural).pitch  # => 67  (G, by scale degree)

# The two agree here by coincidence and not by rule: a perfect fifth and
# four degrees of a major scale both land on the dominant. In another
# scale kind, or from another degree, they part company.

Chromatic alterations

tonic.sharp.pitch  # => 61 (C#)
tonic.flat.pitch   # => 59 (B)

Chord building

tonic.chord              # C major triad
tonic.chord :seventh     # C major 7th

See Also:

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(scale, grade, octave, pitch, background_scale: nil, background_grade: nil, background_octave: nil, background_sharps: nil) ⇒ NoteInScale

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 note within a scale.

Parameters:

  • scale (Scale)

    parent scale

  • grade (Integer)

    scale degree (0-based)

  • octave (Integer)

    octave relative to scale root

  • pitch (Numeric)

    MIDI pitch (Integer, Rational, or Float for microtones)

  • background_scale (Scale, nil) (defaults to: nil)

    diatonic context for chromatic notes

  • background_grade (Integer, nil) (defaults to: nil)

    diatonic grade for chromatic notes

  • background_octave (Integer, nil) (defaults to: nil)

    diatonic octave for chromatic notes

  • background_sharps (Integer, nil) (defaults to: nil)

    sharps/flats from diatonic note



1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
# File 'lib/musa-dsl/music/scales.rb', line 1848

def initialize(scale, grade, octave, pitch, background_scale: nil, background_grade: nil, background_octave: nil, background_sharps: nil)
  @scale = scale
  @grade = grade
  @octave = octave
  @pitch = pitch

  @background_scale = background_scale
  @background_grade = background_grade
  @background_octave = background_octave
  @background_sharps = background_sharps

  @scale.kind.tuning.scale_system.scale_kind_classes.each_key do |name|
    define_singleton_method name do
      as_root_of(name)
    end
  end
end

Instance Attribute Details

#background_scaleScale? (readonly)

Background diatonic scale (for chromatic notes).

Returns:



1934
1935
1936
# File 'lib/musa-dsl/music/scales.rb', line 1934

def background_scale
  @background_scale
end

#background_sharpsInteger? (readonly)

Sharps/flats from background note.

Returns:

  • (Integer, nil)


1949
1950
1951
# File 'lib/musa-dsl/music/scales.rb', line 1949

def background_sharps
  @background_sharps
end

#gradeInteger (readonly)

Scale degree (0-based).

Returns:

  • (Integer)


1868
1869
1870
# File 'lib/musa-dsl/music/scales.rb', line 1868

def grade
  @grade
end

#octaveInteger (readonly)

Current octave relative to scale root.

Returns:

  • (Integer)


1886
1887
1888
# File 'lib/musa-dsl/music/scales.rb', line 1886

def octave
  @octave
end

#pitchNumeric (readonly)

MIDI pitch number.

Returns:

  • (Numeric)


1872
1873
1874
# File 'lib/musa-dsl/music/scales.rb', line 1872

def pitch
  @pitch
end

#scaleScale (readonly)

Parent scale this note belongs to.

Returns:



2098
2099
2100
# File 'lib/musa-dsl/music/scales.rb', line 2098

def scale
  @scale
end

Instance Method Details

#==(other) ⇒ Boolean

Checks note equality.

Notes are equal if they have same scale, grade, octave, and pitch.

Parameters:

Returns:

  • (Boolean)


2208
2209
2210
2211
2212
2213
2214
# File 'lib/musa-dsl/music/scales.rb', line 2208

def ==(other)
  self.class == other.class &&
      @scale == other.scale &&
      @grade == other.grade &&
      @octave == other.octave &&
      @pitch == other.pitch
end

#as_root_of(kind_id_or_kind) ⇒ Scale

Creates a new scale with this note's pitch as the root.

Examples:

Create minor scale from a note

e = c_major[2]                  # E in C major
e_minor = e.as_root_of(:minor)  # E minor scale

e_minor              # => a Scale
e_minor.root.pitch   # => 64

With ScaleKind object

minor_kind = tuning[:minor]
e_minor = e.as_root_of(minor_kind)

Dynamic method (equivalent)

note.minor   # Same as note.as_root_of(:minor)
note.major   # Same as note.as_root_of(:major)

Parameters:

  • kind_id_or_kind (Symbol, ScaleKind)

    scale kind or ID

Returns:

  • (Scale)

    new scale rooted at this pitch



2119
2120
2121
2122
2123
2124
2125
# File 'lib/musa-dsl/music/scales.rb', line 2119

def as_root_of(kind_id_or_kind)
  if kind_id_or_kind.is_a? ScaleKind
    kind_id_or_kind[@pitch]
  else
    @scale.kind.tuning[kind_id_or_kind][@pitch]
  end
end

#at_octave(offset, absolute: false) ⇒ NoteInScale

Returns note transposed by octave offset.

Examples:

Transpose relative

note = c_major[0]

note.at_octave(1).pitch   # => 72   (up one octave from current)
note.at_octave(-1).pitch  # => 48   (down one)

Transpose absolute

c_major[0].at_octave(1).at_octave(2, absolute: true).pitch  # => 84

# The first move is forgotten, not added to: `absolute:` says WHICH
# octave, not how many to travel.

Parameters:

  • offset (Integer)

    octave offset (positive = up, negative = down)

  • absolute (Boolean) (defaults to: false)

    if true, ignore current octave

Returns:

Raises:

  • (ArgumentError)

    if offset is not integer



1906
1907
1908
1909
1910
# File 'lib/musa-dsl/music/scales.rb', line 1906

def at_octave(offset, absolute: false)
  raise ArgumentError, "#{offset} is not integer" unless offset == offset.to_i

  @scale[@grade + ((absolute ? 0 : @octave) + offset) * @scale.kind.class.grades]
end

#background_noteNoteInScale?

Returns the diatonic note this chromatic note is based on.

Examples:

c_sharp = c_major.tonic.sharp
c_sharp.background_note.pitch  # => 60 (C natural)

Returns:



1943
1944
1945
# File 'lib/musa-dsl/music/scales.rb', line 1943

def background_note
  @background_scale[@background_grade + (@background_octave || 0) * @background_scale.kind.class.grades] if @background_grade
end

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

Builds a chord rooted on this note.

Creates a chord using this note as the root. Chord can be specified by:

  • Feature values (:triad, :seventh, :major, :minor, etc.)
  • Feature hash (quality:, size:)
  • Chord definition name (not shown here, see Chord.with_root)

If no features specified, defaults to major triad.

Examples:

Default triad

note.chord  # Major triad

Specified size

c_major[0].chord(:seventh).pitches  # => [60, 64, 67, 71]
c_major[0].chord(:ninth).pitches    # => [60, 64, 67, 71, 74]

With features

# The features must be reachable in the scale: a minor seventh on the
# supertonic is diatonic in C major, one on the dominant is not.
c_major.supertonic.chord(quality: :minor, size: :seventh).features
# => { quality: :minor, size: :seventh }
c_major.supertonic.chord(:minor, :seventh).features  # Same as above
# => { quality: :minor, size: :seventh }

With voicing

voiced = c_major[0].chord(:seventh, move: {root: -1}, duplicate: {fifth: 1})

voiced.pitches     # => [48, 64, 67, 71, 79]
voiced.root.pitch  # => 48
voiced.fifth.pitch # => 67

# The positions still answer by name after the voicing: the root is
# where it was moved to, and the fifth reports the original of the two
# copies. Reading a voiced chord by position is what makes voicing
# composable rather than a final rendering step.

Parameters:

  • feature_values (Array<Symbol>)

    feature values (size, quality, etc.)

  • allow_chromatic (Boolean) (defaults to: nil)

    allow non-diatonic chord notes

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

    initial octave moves

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

    initial duplications

  • features_hash (Hash)

    feature key-value pairs

Returns:

See Also:

  • Chord class


2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
# File 'lib/musa-dsl/music/scales.rb', line 2186

def chord(*feature_values,
          allow_chromatic: nil,
          move: nil,
          duplicate: nil,
          **features_hash)

  features = { size: :triad } if feature_values.empty? && features_hash.empty?
  features ||= Musa::Chords::ChordDefinition.features_from(feature_values, features_hash)

  Musa::Chords::Chord.with_root(self,
                                allow_chromatic: allow_chromatic,
                                move: move,
                                duplicate: duplicate,
                                **features)
end

#down(interval_name_or_interval, natural_or_chromatic = nil) ⇒ NoteInScale

Navigates downward by interval.

Same as #up but in reverse direction.

Examples:

c_major.dominant.down(2, :natural).pitch  # => 64   (down 2 scale degrees)
c_major.dominant.down(:P5).pitch          # => 60   (down a perfect fifth)

Parameters:

  • interval_name_or_interval (Symbol, Integer)

    interval

  • natural_or_chromatic (Symbol, nil) (defaults to: nil)

    :natural or :chromatic

Returns:



2053
2054
2055
# File 'lib/musa-dsl/music/scales.rb', line 2053

def down(interval_name_or_interval, natural_or_chromatic = nil)
  up(interval_name_or_interval, natural_or_chromatic, sign: -1)
end

#flat(count = nil) ⇒ NoteInScale

Lowers note by semitones (adds flats).

Examples:

note.flat.pitch     # Down 1 semitone
note.flat(2).pitch  # Down 2 semitones

Parameters:

  • count (Integer, nil) (defaults to: nil)

    number of semitones (default 1)

Returns:



2078
2079
2080
2081
# File 'lib/musa-dsl/music/scales.rb', line 2078

def flat(count = nil)
  count ||= 1
  sharp(-count)
end

#frequencyFloat

Calculates frequency in Hz.

Uses the scale system's frequency calculation (equal temperament, just intonation, etc.) and the tuning's A frequency.

Examples:

c_major.tonic.frequency  # => ~261.63 Hz (middle C at A=440)

Returns:

  • (Float)

    frequency in Hz



2092
2093
2094
# File 'lib/musa-dsl/music/scales.rb', line 2092

def frequency
  @scale.kind.tuning.frequency_of_pitch(@pitch, @scale.root_pitch)
end

#functionsArray<Symbol>

Returns function names for this scale degree.

Examples:

c_major.tonic.functions  # => [:I, :_1, :tonic, :first]

Returns:

  • (Array<Symbol>)

    function symbols



1880
1881
1882
# File 'lib/musa-dsl/music/scales.rb', line 1880

def functions
  @scale.kind.class.pitches[grade][:functions]
end

#inspectString Also known as: to_s

Returns string representation.

Returns:



2219
2220
2221
# File 'lib/musa-dsl/music/scales.rb', line 2219

def inspect
  "<NoteInScale: grade = #{@grade} octave = #{@octave} pitch = #{@pitch} scale = (#{@scale.kind.class.name} on #{scale.root_pitch})>"
end

#on(scale) ⇒ NoteInScale?

Finds this note in another scale.

Searches for a note with the same pitch in the target scale.

Examples:

c_major_tonic = c_major.tonic
c_minor = tuning.minor[60]
c_major_tonic.on(c_minor)  # C in C minor scale

Parameters:

  • scale (Scale)

    target scale to search

Returns:



2138
2139
2140
# File 'lib/musa-dsl/music/scales.rb', line 2138

def on(scale)
  scale.note_of_pitch @pitch
end

#sharp(count = nil) ⇒ NoteInScale

Raises note by semitones (adds sharps).

Examples:

note.sharp.pitch     # Up 1 semitone
note.sharp(2).pitch  # Up 2 semitones

Parameters:

  • count (Integer, nil) (defaults to: nil)

    number of semitones (default 1)

Returns:



2065
2066
2067
2068
# File 'lib/musa-dsl/music/scales.rb', line 2065

def sharp(count = nil)
  count ||= 1
  calculate_note_of_pitch(@pitch, count)
end

#up(interval_name_or_interval, natural_or_chromatic = nil, sign: nil) ⇒ NoteInScale

Navigates upward by interval.

Supports both natural (diatonic) and chromatic (semitone) intervals.

  • Numeric interval + :natural: Move by scale degrees
  • Symbol or numeric interval + :chromatic: Move by semitones or named interval

Examples:

Natural interval (scale degrees)

c_major.tonic.up(2, :natural).pitch  # => 64  (E, two degrees up)

# Two DEGREES, so four semitones here and three from the second degree:
# the distance depends on where in the scale you start.

Chromatic interval (semitones)

note.up(:P5)  # Up perfect fifth (7 semitones)
note.up(7)    # Up 7 semitones (if chromatic)

Parameters:

  • interval_name_or_interval (Symbol, Integer)

    interval

  • natural_or_chromatic (Symbol, nil) (defaults to: nil)

    :natural or :chromatic

  • sign (Integer) (defaults to: nil)

    direction multiplier (internal use)

Returns:



1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
# File 'lib/musa-dsl/music/scales.rb', line 1995

def up(interval_name_or_interval, natural_or_chromatic = nil, sign: nil)

  sign ||= 1

  if interval_name_or_interval.is_a?(Numeric)
    natural_or_chromatic ||= :natural
  else
    natural_or_chromatic = :chromatic
  end

  if natural_or_chromatic == :chromatic
    interval = if interval_name_or_interval.is_a?(Symbol)
                 @scale.kind.tuning.offset_of_interval(interval_name_or_interval)
               else
                 interval_name_or_interval
               end

    calculate_note_of_pitch(@pitch, sign * interval)
  else
    @scale[@grade + sign * interval_name_or_interval]
  end
end

#wide_gradeInteger

The grade counted straight through the octaves, rather than restarting at each one.

grade + octave * grades_per_octave. It is what makes the distance between two notes a subtraction: within one octave grade would do, but across octaves it wraps, and this does not. That is why the delta encoding of Datasets::GDV and Datasets::GDVd is written in terms of this and not of grade.

Examples:

Seven grades to the octave in a diatonic scale

c_major.tonic.wide_grade                # => 0
c_major.tonic.at_octave(1).wide_grade   # => 7
c_major[7].wide_grade                   # => 7
# Asking for grade 7 of a seven-grade scale is the tonic an octave up.

Which is what makes an interval a subtraction

c_major.dominant.at_octave(1).wide_grade - c_major.tonic.wide_grade  # => 11

Returns:

  • (Integer)


1970
1971
1972
# File 'lib/musa-dsl/music/scales.rb', line 1970

def wide_grade
  @grade + @octave * @scale.kind.class.grades
end

#with_background(scale:, grade: nil, octave: nil, sharps: nil) ⇒ NoteInScale

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 copy with background scale context.

Used internally when creating chromatic notes to remember their diatonic context.

Parameters:

  • scale (Scale)

    background diatonic scale

  • grade (Integer, nil) (defaults to: nil)

    background grade

  • octave (Integer, nil) (defaults to: nil)

    background octave

  • sharps (Integer, nil) (defaults to: nil)

    accidentals from background note

Returns:



1924
1925
1926
1927
1928
1929
1930
# File 'lib/musa-dsl/music/scales.rb', line 1924

def with_background(scale:, grade: nil, octave: nil, sharps: nil)
  NoteInScale.new(@scale, @grade, @octave, @pitch,
                  background_scale: scale,
                  background_grade: grade,
                  background_octave: octave,
                  background_sharps: sharps)
end