Music - Scales & Chords

Comprehensive framework for working with musical scales, tuning systems, and chord structures. The system resolves two main domains:

When is this the answer

Scales and chords are here so that pitch stops being a number. A MIDI pitch says where a key is; a degree says what a note does -- and everything this subsystem offers follows from working with the second and converting to the first at the edge.

By the shape of what you have:

You have You want This
a tonic and a mode the notes of that scale, indexable tuning.major[60], then scale[0], scale[:dominant]
a degree the note it is, with its context scale[2] -- a NoteInScale, not an Integer
a degree the chord built on it scale.chord_on(2), or scale[2].chord
a chord it in another position or register with_move, with_duplicate, octave
a chord the same function with another quality or size featuring, with_quality, with_size
a chord and a new scale the same notes read as degrees of that scale as_chord_in_scale
a set of pitches to know what chord they are ChordDefinition.find_by_pitches
a scale a piece has moved to to keep writing in degrees build the new scale and go on using degrees

When it is NOT the answer. If what you have really is a MIDI pitch -- it came from a device, it goes to a device, and nothing in between asks what it means -- leave it a number. Wrapping it in a scale to unwrap it two lines later buys nothing.

What it costs to stay in pitches. Transposing becomes arithmetic instead of a change of tonic; modulating becomes rewriting every number; and a chord is a list you have to keep consistent by hand. That is the price the reflex charges later, not at the moment of writing.

And the one that surprises everybody: pitches comes out ordered by pitch, not by chord position. After a drop voicing the root is not the first element -- c_major.tonic.chord(:seventh).with_move(third: -1, seventh: -1).pitches gives [52, 59, 60, 67], and the third is the bass. Ask a position by name (chord.root, chord.third) when the position is what matters.

Scales System

The Scales module provides hierarchical access to musical scales with multiple tuning systems and scale types:

Architecture:

  • ScaleSystem (Musa::Scales::ScaleSystem): Defines tuning systems (e.g., 12-tone equal temperament)
  • ScaleSystemTuning (Musa::Scales::ScaleSystemTuning): A scale system with specific reference frequency (e.g., A=440Hz)
  • ScaleKind (Musa::Scales::ScaleKind): Scale types (major, minor, chromatic, etc.)
  • Scale (Musa::Scales::Scale): A scale kind rooted on a specific pitch (e.g., C major)
  • NoteInScale (Musa::Scales::NoteInScale): A specific note within a scale

Registry:

  • Scales::Scales (Musa::Scales::Scales): Central registry for accessing scale systems by ID or method name

Available scale systems:

  • :et12 (EquallyTempered12ToneScaleSystem): 12-tone equal temperament (default)

Available scale kinds in et12:

Core scales:

  • :major - Major scale (Ionian mode) - 7 notes
  • :minor - Natural minor scale (Aeolian mode) - 7 notes
  • :minor_harmonic - Harmonic minor scale (raised 7th) - 7 notes
  • :major_harmonic - Harmonic major scale (lowered 6th) - 7 notes
  • :chromatic - Chromatic scale (all 12 semitones) - 12 notes

Greek/church modes:

  • :dorian - Dorian mode (minor with major 6th) - 7 notes
  • :phrygian - Phrygian mode (minor with minor 2nd) - 7 notes
  • :lydian - Lydian mode (major with augmented 4th) - 7 notes
  • :mixolydian - Mixolydian mode (major with minor 7th) - 7 notes
  • :locrian - Locrian mode (diminished 5th and minor 2nd) - 7 notes

Pentatonic scales:

  • :pentatonic_major - Major pentatonic (no 4th or 7th) - 5 notes
  • :pentatonic_minor - Minor pentatonic (no 2nd or 6th) - 5 notes

Blues scales:

  • :blues - Blues scale (minor pentatonic + b5 blue note) - 6 notes
  • :blues_major - Major blues scale (major pentatonic + b3 blue note) - 6 notes

Symmetric scales:

  • :whole_tone - Whole tone scale (all whole steps) - 6 notes
  • :diminished_hw - Diminished half-whole (octatonic) - 8 notes
  • :diminished_wh - Diminished whole-half (dominant diminished) - 8 notes

Melodic minor modes:

  • :minor_melodic - Melodic minor (jazz minor) - 7 notes
  • :dorian_b2 - Dorian b2 / Phrygian #6 - 7 notes
  • :lydian_augmented - Lydian augmented (#4, #5) - 7 notes
  • :lydian_dominant - Lydian dominant / Bartók scale (#4, b7) - 7 notes
  • :mixolydian_b6 - Mixolydian b6 / Hindu scale - 7 notes
  • :locrian_sharp2 - Locrian #2 / Half-diminished scale - 7 notes
  • :altered - Altered / Super Locrian (all tensions altered) - 7 notes

Ethnic/exotic scales:

  • :double_harmonic - Double harmonic / Byzantine (two augmented 2nds) - 7 notes
  • :hungarian_minor - Hungarian minor / Gypsy minor (#4, raised 7th) - 7 notes
  • :phrygian_dominant - Phrygian dominant / Spanish Phrygian (b2, major 3rd) - 7 notes
  • :neapolitan_minor - Neapolitan minor (harmonic minor with b2) - 7 notes
  • :neapolitan_major - Neapolitan major (melodic minor with b2) - 7 notes

Bebop scales:

  • :bebop_dominant - Bebop dominant (Mixolydian + major 7th passing) - 8 notes
  • :bebop_major - Bebop major (major + #5 passing) - 8 notes
  • :bebop_minor - Bebop minor (Dorian + major 7th passing) - 8 notes

Chords System

The Chords module provides chord structures with scale context:

Architecture:

  • Chord (Musa::Chords::Chord): Instantiated chord with root note and scale context
  • ChordDefinition (Musa::Chords::ChordDefinition): Abstract chord structure definition (quality, size, intervals)

Features:

  • Access chord tones by name (root, third, fifth, seventh, etc.)
  • Voicing modifications (move, duplicate, octave)
  • Navigate between related chords (change quality, add extensions)
  • Extract pitches and notes

Usage examples:

require 'musa-dsl'

include Musa::Scales
include Musa::Chords

# Access default system and tuning
tuning = Scales.default_system.default_tuning  # A=440Hz

# Create scales using available scale kinds
c_major = tuning.major[60]           # C major (tonic pitch 60)
d_minor = tuning.minor[62]           # D minor (natural)
e_harmonic = tuning.minor_harmonic[64]  # E harmonic minor
chromatic = tuning.chromatic[60]     # C chromatic

# Alternative access methods
c_major = Scales.et12[440.0].major[60]  # Explicit system and frequency

# Access notes by grade (0-based) or function
tonic = c_major[0]      # => C (grade 0)
mediant = c_major[2]    # => E (grade 2)
dominant = c_major[4]   # => G (grade 4)

# Access by function name
tonic = c_major.tonic         # => C
supertonic = c_major.supertonic  # => D
mediant = c_major.mediant     # => E
subdominant = c_major.subdominant  # => F
dominant = c_major.dominant   # => G

# Access by Roman numeral or symbol
c_major[:I]    # => Tonic (C)
c_major[:V]    # => Dominant (G)

# Get pitch values from notes
pitch = c_major.tonic.pitch   # => 60

# Navigate with octaves
note = c_major[2].at_octave(1)  # E transposed up 1 octave
pitch_with_octave = note.pitch      # => 76

# Chromatic operations - sharp and flat
c_sharp = c_major.tonic.sharp   # => C# (chromatic, +1 semitone)
c_flat = c_major.tonic.flat     # => Cb (chromatic, -1 semitone)

# Navigate by semitones
fifth_up = c_major.tonic.sharp(7)  # => G (+7 semitones = perfect fifth)
third_up = c_major.tonic.sharp(4)  # => E (+4 semitones = major third)

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

# Greek modes (church modes)
d_dorian = tuning.dorian[62]         # D Dorian (minor with major 6th)
e_phrygian = tuning.phrygian[64]     # E Phrygian (minor with minor 2nd)
f_lydian = tuning.lydian[65]         # F Lydian (major with augmented 4th)
g_mixolydian = tuning.mixolydian[67] # G Mixolydian (major with minor 7th)
b_locrian = tuning.locrian[71]       # B Locrian (diminished 5th)

# Access notes in Greek modes by function
d_dorian.tonic.pitch      # => 62 (D)
d_dorian[:vi].pitch       # => 71 (B - the major 6th characteristic of Dorian)

e_phrygian[:ii].pitch     # => 65 (F - the minor 2nd characteristic of Phrygian)

f_lydian[:IV].pitch       # => 71 (B - the augmented 4th characteristic of Lydian)

g_mixolydian[:VII].pitch  # => 77 (F - the minor 7th characteristic of Mixolydian)

b_locrian[:v].pitch       # => 77 (F - the diminished 5th characteristic of Locrian)

# Pentatonic and blues scales
c_pent_maj = tuning.pentatonic_major[60]  # C major pentatonic
a_pent_min = tuning.pentatonic_minor[69]  # A minor pentatonic
a_blues = tuning.blues[69]                 # A blues scale
a_blues[:blue].pitch  # => 75 (Eb - the blue note)

# Symmetric scales
c_whole = tuning.whole_tone[60]      # C whole tone
c_dim_hw = tuning.diminished_hw[60]  # C diminished (half-whole)
c_dim_wh = tuning.diminished_wh[60]  # C diminished (whole-half)

# Melodic minor modes
c_mel_min = tuning.minor_melodic[60]     # C melodic minor
g_altered = tuning.altered[67]            # G altered (for G7alt chords)
f_lyd_dom = tuning.lydian_dominant[65]   # F lydian dominant (F7#11)

# Ethnic scales
e_phry_dom = tuning.phrygian_dominant[64]  # E Phrygian dominant (flamenco)
a_hung_min = tuning.hungarian_minor[69]    # A Hungarian minor

# Bebop scales (8 notes for smooth eighth-note lines)
g_bebop = tuning.bebop_dominant[67]  # G bebop dominant
g_bebop[7].pitch  # => 78 (F# - the chromatic passing tone)

# Create chords from scale degrees
i_chord = c_major.tonic.chord        # C major triad [C, E, G]
ii_chord = c_major.supertonic.chord  # D minor triad [D, F, A]
v_chord = c_major.dominant.chord     # G major triad [G, B, D]

# Create extended chords
i_seventh = c_major.tonic.chord :seventh  # C major 7th [C, E, G, B]
v_ninth = c_major.dominant.chord :ninth   # G 9th chord

# Access chord tones by name
root = i_chord.root      # => C (NoteInScale)
third = i_chord.third    # => E (NoteInScale)
fifth = i_chord.fifth    # => G (NoteInScale)

# Get chord pitches
pitches = i_chord.pitches  # => [60, 64, 67] (C, E, G)
notes = i_chord.notes      # Array of ChordGradeNote structs

# Chord features
i_chord.quality  # => :major
i_chord.size     # => :triad

# Navigate between chord qualities
minor_chord = i_chord.with_quality(:minor)      # C minor [C, Eb, G]
diminished = i_chord.with_quality(:diminished)  # C diminished [C, Eb, Gb]

# Change chord extensions
seventh_chord = i_chord.with_size(:seventh)  # C major 7th
ninth_chord = i_chord.with_size(:ninth)      # C major 9th

# Voicing modifications - move specific tones to different octaves
voiced = i_chord.with_move(root: -1, fifth: 1)  # Root down, fifth up
voiced.move   # => { root: -1, fifth: 1 }
i_chord.move  # => {}

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

# `pitches` comes back ordered by pitch, so the root is no longer first -- but
# the positions still answer by name. That is what makes a voicing something you
# can keep working with rather than a final rendering.

# Duplicate tones in other octaves
doubled = i_chord.with_duplicate(root: -2, third: [-1, 1])  # Root 2 down, third 1 down and 1 up
doubled.duplicate  # => { root: -2, third: [-1, 1] }
i_chord.duplicate  # => {}

# `with_move` and `with_duplicate` build a NEW chord; the one they were called
# on is untouched. Reading the settings back off the original is the mistake
# these two lines exist to prevent.

# Transpose entire chord
lower = i_chord.octave(-1)  # Move chord down one octave

Chord-Scale Navigation

MusaDSL provides methods to explore the relationship between chords and scales, enabling harmonic analysis and discovery of functional contexts.

Checking if a Scale Contains a Chord

c_major = Scales.et12[440.0].major[60]
g7 = c_major.dominant.chord :seventh

c_major.contains_chord?(g7)     # => true
c_major.degree_of_chord(g7)     # => 4 (V degree, 0-based)

# Non-diatonic chords return false/nil
cm = c_major.tonic.chord.with_quality(:minor)  # C minor (Eb not in C major)
c_major.contains_chord?(cm)     # => false
c_major.degree_of_chord(cm)     # => nil

Creating a Chord in a Different Scale Context

# Get the same chord but with a different scale as context
g_mixolydian = Scales.et12[440.0].mixolydian[67]
g7_in_mixolydian = g7.as_chord_in_scale(g_mixolydian)
g7_in_mixolydian.scale                        # => G Mixolydian scale
g_mixolydian.degree_of_chord(g7_in_mixolydian)  # => 0 (I degree)

Creating Chords from Scale Degrees

# Create chords directly from scale degrees using Scale#chord_on
c_major.chord_on(0).pitches                       # => [60, 64, 67]
c_major.chord_on(:dominant, :seventh).pitches     # => [67, 71, 74, 77]
c_major.chord_on(:IV, :seventh, :major).pitches   # => [65, 69, 72, 76]

# The degree can be given as a number, as a function name or as a roman
# numeral, and they are the same thing said three ways.

# Equivalent to using the note method then chord:
c_major[0].chord.pitches                    # => [60, 64, 67]
c_major[:dominant].chord(:seventh).pitches  # => [67, 71, 74, 77]

# With voicing parameters, applied at construction
c_major.chord_on(:I, :seventh, move: {root: -1}).pitches
# => [48, 64, 67, 71]
c_major.chord_on(0, :triad, duplicate: {root: 1}).pitches
# => [60, 64, 67, 72]

Finding Scales That Contain a Chord

g_triad = c_major.dominant.chord  # G-B-D

# Search in diatonic scales
g_triad.search_in_scales(family: :diatonic).size  # => 11

# Search using metadata filters
g_triad.search_in_scales(family: :greek_modes, brightness: -1..1)

# Search in all scale types
g_triad.search_in_scales

# Each result has its scale context
g_triad.search_in_scales(family: :diatonic).each do |chord|
  scale = chord.scale
  degree = scale.degree_of_chord(chord)
  puts "#{scale.kind.class.id} rooted on #{scale.root_pitch}: degree #{degree}"
end
# prints:
#    major rooted on 7: degree 0
#    major rooted on 12: degree 4
#    major rooted on 14: degree 3
#    minor rooted on 9: degree 6
#    minor rooted on 11: degree 5
#    minor rooted on 16: degree 2
#    minor_harmonic rooted on 11: degree 5
#    minor_harmonic rooted on 12: degree 4
#    major_harmonic rooted on 7: degree 0
#    major_harmonic rooted on 12: degree 4
#    major_harmonic rooted on 15: degree 2

# The same, as a value:
g_triad.search_in_scales(family: :diatonic)
       .map { |c| [c.scale.kind.class.id, c.scale.root_pitch] }.first(4)
# => [[:major, 7], [:major, 12], [:major, 14], [:minor, 9]]

Three things to read off that list. G major comes first, not C: the search walks roots upward from the chord's own, and G is where this chord is the tonic. The roots are 7, 12 and 14, not 67, 60 and 62 -- a scale that comes back from a search is rooted on a pitch offset within the tuning, not on the octave the chord was taken from. And :diatonic holds four kinds, not three: harmonic major is in there too.

Low-Level Navigation Methods

tuning = Scales.et12[440.0]

# Search at ScaleKind level
tuning.major.find_chord_in_scales(g_triad)

# Search at ScaleSystemTuning level with metadata filters
tuning.search_chord_in_scales(g7, family: :diatonic, roots: 60..71)

Scale Kind Metadata

Scale kinds in MusaDSL have a three-layer metadata system that provides both automatic structural information and extensible custom properties.

Metadata Layers

  1. Intrinsic metadata: Automatically derived from scale structure

    • id: Scale kind identifier
    • grades: Number of scale degrees
    • pitches: Array of pitch offsets from root
    • intervals: Intervals between consecutive degrees
    • has_leading_tone: Whether scale has pitch 11 (semitone below octave)
    • has_tritone: Whether scale contains tritone interval (pitch 6)
    • symmetric: Type of symmetry if any (:equal, :palindrome, :repeating)
  2. Base metadata: Defined by musa-dsl library

    • family: Scale family (:diatonic, :greek_modes, :pentatonic, etc.)
    • brightness: Relative brightness (-3 to +3, major = 0)
    • character: Array of descriptive tags
    • parent: Parent scale and degree for modes (e.g., { scale: :major, degree: 2 })
  3. Custom metadata: Added by users at runtime

Accessing Metadata

tuning = Scales.et12[440.0]
major_class = tuning.major.class

# Get combined metadata (intrinsic < base < custom)
major_class.
# => { id: :major, grades: 7, pitches: [0,2,4,5,7,9,11], family: :diatonic, ... }

# Get specific layer
major_class.  # Structure-derived only
major_class.       # Library-defined only
major_class.     # User-added only

# Query helpers
major_class.has_metadata?(:family)              # => true
major_class.has_metadata?(:family, :diatonic)   # => true
major_class.has_metadata?(:character, :bright)  # => true (array inclusion)
major_class.(:brightness)         # => 0

Extending Metadata

Users can add custom metadata to any scale kind:

# Extend a specific scale kind class
tuning.dorian.class.(
  my_mood: :nostalgic,
  suitable_for: [:jazz, :fusion],
  personal_rating: 5
)

# Or use the convenience method with scale ID
Scales.(:dorian, my_mood: :nostalgic)

# Multiple calls merge metadata
Scales.(:phrygian, mood: :dark)
Scales.(:phrygian, origin: :spanish)  # Merges with previous

# Reset custom metadata if needed
tuning.dorian.class.

Custom Metadata Overrides

Custom metadata takes precedence over base metadata:

# Override library-defined family
Scales.(:dorian, family: :my_custom_category)
tuning.dorian.class.[:family]  # => :my_custom_category

Brightness Scale Reference

Value Meaning Examples
+3 Very bright Lydian augmented
+2 Bright Lydian
+1 Slightly bright Mixolydian, Lydian dominant
0 Neutral (reference) Major (Ionian)
-1 Slightly dark Dorian
-2 Dark Minor harmonic, Phrygian
-3 Very dark Locrian, Natural minor

Scale Families

  • :diatonic - Major, minor natural, minor harmonic, major harmonic
  • :greek_modes - Dorian, Phrygian, Lydian, Mixolydian, Locrian
  • :melodic_minor_modes - Melodic minor and its modes
  • :pentatonic - Pentatonic major/minor
  • :blues - Blues scales
  • :bebop - Bebop scales
  • :symmetric - Whole tone, diminished
  • :ethnic - Hungarian, Spanish, Neapolitan, etc.
  • :chromatic - Chromatic scale

Searching Scale Kinds

The scale_kinds method on ScaleSystemTuning allows searching and filtering scale kinds by their metadata properties.

Basic Usage

tuning = Scales.et12[440.0]

# Get all scale kinds
tuning.scale_kinds
# => [major_kind, minor_kind, dorian_kind, ...]

# Filter by family
tuning.scale_kinds(family: :diatonic)
# => [major_kind, minor_kind, minor_harmonic_kind, harmonic_major_kind]

# Filter by brightness range
tuning.scale_kinds(brightness: -1..1)

# Filter by character
tuning.scale_kinds(character: :jazz)

Custom Filtering with Blocks

Use a block for complex filtering based on any metadata:

# Scales with leading tone
tuning.scale_kinds { |klass| klass.[:has_leading_tone] }

# Combine criteria and block
tuning.scale_kinds(family: :greek_modes) { |klass| klass.[:brightness]&.negative? }
# => [dorian_kind, phrygian_kind, locrian_kind]

The search_chord_in_scales method uses the same metadata filtering:

g7 = tuning.major[60].dominant.chord(:seventh)

# Find G7 in diatonic scales
tuning.search_chord_in_scales(g7, family: :diatonic)

# Find G7 in scales with specific brightness
tuning.search_chord_in_scales(g7, brightness: -1..1)

Defining Custom Scale Systems, Scale Kinds, and Chord Definitions

The framework is extensible, allowing users to define custom tuning systems, scale types, and chord structures:

Custom Scale Systems:

Users can create custom tuning systems by subclassing Musa::Scales::ScaleSystem and implementing:

  • .id - Unique symbol identifier
  • .notes_in_octave - Number of notes per octave
  • .part_of_tone_size - Size of smallest pitch unit
  • .intervals - Hash mapping interval names to semitone offsets
  • .frequency_of_pitch(pitch, root_pitch, a_frequency) - Pitch to frequency conversion

After defining, register with Musa::Scales::Scales.register(CustomScaleSystem, default: false)

Custom Scale Kinds:

Users can define new scale types by subclassing Musa::Scales::ScaleKind and implementing:

  • .id - Unique symbol identifier (e.g., :dorian, :pentatonic)
  • .pitches - Array defining scale structure with functions and pitch offsets
  • .chromatic? - Whether this is the chromatic scale (optional, default: false)
  • .grades - Number of grades per octave (optional, default: pitches.length)

After defining, register with YourScaleSystem.register(CustomScaleKind)

Custom Chord Definitions:

Users can register new chord types using Musa::Chords::ChordDefinition.register:

  • name - Symbol identifier (e.g., :sus4, :add9)
  • offsets: - Hash defining semitone intervals from root (e.g., { root: 0, fourth: 5, fifth: 7 })
  • **features - Chord characteristics like quality: and size:

Examples of custom definitions:

require 'musa-dsl'

include Musa::Scales
include Musa::Chords

# Example 1: Define a custom Hirajoshi scale (Japanese pentatonic)
class HirajoshiScaleKind < ScaleKind
  class << self
    def id
      :hirajoshi
    end

    def pitches
      [{ functions: [:I, :_1, :tonic], pitch: 0 },
       { functions: [:II, :_2], pitch: 2 },
       { functions: [:III, :_3], pitch: 3 },
       { functions: [:V, :_4], pitch: 7 },
       { functions: [:VI, :_5], pitch: 8 }]
    end

    def grades
      5  # 5 notes per octave
    end
  end
end

# Register the new scale kind with the 12-tone system
Scales.et12.register(HirajoshiScaleKind)

# Use the new scale kind
tuning = Scales.default_system.default_tuning
c_hirajoshi = tuning[:hirajoshi][60]  # C Hirajoshi
puts c_hirajoshi[0].pitch  # => 60 (C)
puts c_hirajoshi[1].pitch  # => 62 (D)
puts c_hirajoshi[2].pitch  # => 63 (Eb)

# Example 2: Register a custom chord definition (sus4)
Musa::Chords::ChordDefinition.register :sus4,
  quality: :suspended,
  size: :triad,
  offsets: { root: 0, fourth: 5, fifth: 7 }

# Use it. The name goes as a KEYWORD -- `chord(:sus4)` reads :sus4 as a feature
# value and raises "Unable to find chord definition".
c_major = tuning.major[60]

c_major.tonic.chord(name: :sus4).pitches     # => [60, 65, 67]
c_major.dominant.chord(name: :sus4).pitches  # => [67, 72, 74]

# Example 3: Register a custom chord definition (add9)
Musa::Chords::ChordDefinition.register :add9,
  quality: :major,
  size: :extended,
  offsets: { root: 0, third: 4, fifth: 7, ninth: 14 }

Musa::Chords::Chord.with_root(60, scale: c_major, name: :add9).pitches
# => [60, 64, 67, 74]

API Reference

Complete API documentation:

Source code: lib/musa-dsl/music/