HeadMusic
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, LilyPond, and Humdrum
**kerninto compositions; write compositions as ABC, LilyPond, MusicXML, and**kern
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 documentThe LilyPond reader covers absolute and \relative pitches, durations and dots, rests and whole-bar rests, chords, ties (including across bar checks), \key, \time, \clef, bar checks, \header title and composer, \new Staff / \new Voice contexts, and grand staves and staff groups with \change Staff. Constructs outside that subset (tuplets, lyrics, variables, articulations, and so on) raise an UnsupportedFeatureError rather than being skipped.
# Read a Humdrum **kern file, such as a Bach chorale from KernScores
chorale = HeadMusic::Notation::Kern.parse(File.read("chor001.krn"))
chorale.parts.map { |part| part.player.name } # => ["Soprano", "Alto", "Tenor", "Bass"]
chorale.work.catalog_number # => "BWV 269"
puts chorale.to_kern # => the same spines backThe kern reader covers pitches, durations and dots, rests, chords, ties across barlines, pickups and short final bars, repeat barlines, *k, key and mode designations, *M, *MM, *clef, *I names and vocal and piano codes, *part / *staff grouping, spine splits, joins, and exchanges, **text lyrics, and the reference records of a work. Tuplets, *+, and transposed spines raise an UnsupportedFeatureError; grace notes, ornaments, articulations, beams, and fermatas are dropped.
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 orderA 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 reachedAmong 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 # => :weakUnlike 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.0Guides 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
- API Documentation: rubydoc.info/gems/head_music
- Contributing Guide: CONTRIBUTING.md
- Changelog: CHANGELOG.md
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 qualityCode Quality
# Run linting
bundle exec rubocop
# Run security audit
bundle exec rake bundle:audit:check
# Generate documentation
bundle exec rake docAvailable 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
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Security: For security issues, please email robert.head@gmail.com