HeadMusic

CI Security Gem Version Documentation

The head_music Ruby gem provides a toolkit for working with Western music theory. Model and manipulate the fundamental elements of music including pitches, scales, key signatures, intervals, and chords.

Features

  • Western Music Theory Fundamentals: Work with pitches, scales, intervals, chords, and key signatures
  • Musical Analysis: Analyze harmonic progressions, voice leading, and counterpoint
  • Style Analysis: Rules for species counterpoint and voice leading
  • Internationalization: Support for multiple languages (English, French, German, Italian, Russian, Spanish)
  • Instrument Modeling: Extensive database of musical instruments with ranges and properties
  • Notation Formats: Read ABC and LilyPond into compositions; write compositions as ABC, LilyPond, and MusicXML

Installation

Add this line to your application's Gemfile:

gem 'head_music'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install head_music

Quick Start

require 'head_music'

# Work with pitches and intervals
pitch = HeadMusic::Rudiment::Pitch.get('C4')
higher_pitch = HeadMusic::Rudiment::Pitch.get('E4')
interval = HeadMusic::Analysis::DiatonicInterval.new(pitch, higher_pitch)
puts interval.name  # => "major third"

# Create scales
scale = HeadMusic::Rudiment::Scale.get('C', :major)
puts scale.pitches.map(&:to_s)  # => ["C4", "D4", "E4", "F4", "G4", "A4", "B4"]

# Analyze chords
pitches = %w[C4 E4 G4].map { |p| HeadMusic::Rudiment::Pitch.get(p) }
chord = HeadMusic::Analysis::PitchSet.new(pitches)
puts chord.major_triad?  # => true

# Import a LilyPond excerpt as a composition
composition = HeadMusic::Notation::LilyPond.parse(<<~'LILY')
  \relative c' {
    \key g \major
    \time 4/4
    g8 a b c d c b g |
  }
LILY
puts composition.voices.first.pitches.map(&:to_s)  # => ["G3", "A3", "B3", "C4", "D4", "C4", "B3", "G3"]
puts composition.to_lilypond                        # => a complete LilyPond document

The LilyPond reader covers absolute and \relative pitches, durations and dots, rests and whole-bar rests, chords, intra-bar ties, \key, \time, \clef, bar checks, \header title and composer, and \new Staff / \new Voice contexts. Constructs outside that subset (tuplets, lyrics, variables, articulations, and so on) raise an UnsupportedFeatureError rather than being skipped.

Style Analysis

Look up a style guide by key and analyze a voice against it. Style::Guide.get returns nil for an unknown key, so a stored key can be validated before use.

guide = HeadMusic::Style::Guide.get('first_species_harmony')
guide.category      # => :harmony
guide.display_name  # => "First Species Harmony"

assessment = guide.assess(voice)
assessment.fitness   # => 0.0 to 1.0
assessment.messages  # => ["Prefer contrary motion. Move voices in different melodic directions."]

A guide declares its guidelines in three tiers, and the tier decides how much each one counts:

guide.gate_items       # preconditions -- can this voice be assessed at all?
guide.primary_items    # what the guide is about
guide.secondary_items  # background craft it inherits rather than teaches
guide.guide_items      # all three, in that order

A gate asks whether the voice can be assessed at all. Failing one stops the assessment — the rubric is not computed, and the grade is the gates alone:

assessment.assessable?   # => false for a voice too short to judge, or with no companion voice
assessment.fitness       # => the gates' product; the rubric was never reached

Among the rules that are assessed, primaries share φ⁻¹ of the rubric and secondaries share φ⁻², which is why a species guide weighs its own rules as heavily as all the craft it inherits put together. The budgets are fixed rather than divided by item count, so what a guide teaches does not thin out as it inherits more. A rubric that declares only one tier is renormalized to the full range.

Within a tier, a second axis: strength. A prohibition (:strong, the default) weighs twice a preference (:weak), normalized by that tier's own total. Strength never crosses a tier boundary, and it is inert on gates, which multiply the whole rubric:

HeadMusic::Style::Guidelines::NoParallelPerfectOnDownbeats.strength  # => :strong
HeadMusic::Style::Guidelines::PreferContraryMotion.strength          # => :weak

Unlike tier, strength is a property of the guideline rather than of the list it was declared in — a preference is a preference in every guide that names it. An item may override it for the tradition-dependent case, with Guideline.with(strength: :weak).

Each entry is a Style::GuideItem — a guideline plus the configuration this guide gives it — and assessing one yields a frozen Style::GuideItemAssessment:

item = guide.primary_items.first
item.guideline               # => HeadMusic::Style::Guidelines::NoUnisonsInMiddle
item.config                  # => {}
item.strength                # => :strong

assessment.guide_item_assessments.first.tier      # => :gate
assessment.guide_item_assessments.first.strength  # => :strong
assessment.guide_item_assessments.first.fitness   # => 0.0 to 1.0

Guides whose tiers vary by configuration are built with .with. The six contour melodies are registered under their own keys, and each key is exactly one such configuration:

HeadMusic::Style::Guide.get('arch_contour_melody')
# the same guide, spelled out
HeadMusic::Style::Guides::ContourMelody.with(contour: :arch, minimum_melodic_intervals: 2)

Configure it differently and you get a different guide — one the registry does not hold, whose key is nil and whose display_name falls back to the class. Prefer the key when you mean a registered guide, and .with when you deliberately want a configuration of your own.

Documentation

Requirements

  • Ruby 3.3.0 or higher
  • ActiveSupport 7.0+

Development

After checking out the repo, run bin/setup to install dependencies.

Running Tests

# Run all tests
bundle exec rspec

# Run tests with coverage
bundle exec rake

# Run quality checks (tests + linting + security)
bundle exec rake quality

Code Quality

# Run linting
bundle exec rubocop

# Run security audit
bundle exec rake bundle:audit:check

# Generate documentation
bundle exec rake doc

Available Rake Tasks

  • rake spec - Run tests
  • rake quality - Run tests, linting, and security audit
  • rake doc - Generate YARD documentation
  • rake doc_stats - Show documentation coverage statistics
  • rake coverage - Open coverage report in browser

Releasing a New Version

The release checklist lives in .claude/skills/release/SKILL.md. Run /release in Claude Code, or follow it by hand. In short: move the Unreleased changelog entries under a dated heading, bump lib/head_music/version.rb, refresh Gemfile.lock, commit as Release X.Y.Z, and then bundle exec rake release:source_control_push tags the release. The tag push runs the release workflow, which publishes the gem to RubyGems and creates the GitHub Release.

Contributing

We welcome contributions! Please see our Contributing Guide for details.

Project Structure

lib/head_music/
├── analysis/          # Musical analysis tools (intervals, chords, etc.)
├── content/           # Musical content (compositions, voices, notes)
├── instruments/       # Instrument definitions and properties
├── rudiment/          # Basic music theory elements (pitches, scales, etc.)
└── style/             # Style analysis and composition rules

Code of Conduct

This project is intended to be a safe, welcoming space for collaboration. Contributors are expected to adhere to our Code of Conduct.

License

The gem is available as open source under the terms of the MIT License.

Support