From 51ca98779dae4f6ceb8de62756c2cfaaf37ad017 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 22 Mar 2026 13:05:33 -0400 Subject: [PATCH] Add CLI tool and Jupyter notebook tutorial MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLI (pytheory command): pytheory tone C4 — frequency, MIDI, overtones pytheory scale C major — notes and intervals pytheory chord C E G — identify, harmony, tension pytheory key C major — full key analysis with diatonic chords pytheory fingering Am — ASCII guitar tab pytheory progression C major I V vi IV — build from Roman numerals pytheory detect C D E G — detect the key Jupyter notebook (examples/tutorial.ipynb): 46-cell interactive tutorial covering tones, scales, modes, keys, chord analysis, progressions, world music systems, guitar fingerings, and building a song from scratch. Co-Authored-By: Claude Opus 4.6 (1M context) --- examples/tutorial.ipynb | 677 ++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 3 + pytheory/cli.py | 157 ++++++++++ 3 files changed, 837 insertions(+) create mode 100644 examples/tutorial.ipynb create mode 100644 pytheory/cli.py diff --git a/examples/tutorial.ipynb b/examples/tutorial.ipynb new file mode 100644 index 0000000..e722126 --- /dev/null +++ b/examples/tutorial.ipynb @@ -0,0 +1,677 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# PyTheory: Music Theory for Humans\n", + "\n", + "A hands-on tutorial exploring music theory with Python.\n", + "\n", + "PyTheory lets you reason about tones, scales, chords, and progressions\n", + "using an intuitive, Pythonic API. Whether you're a musician who codes\n", + "or a coder who plays music, this library gives you the building blocks\n", + "to explore harmony, composition, and world music systems." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Getting Started\n", + "\n", + "Everything begins with a **Tone** -- the fundamental unit of music.\n", + "A tone has a name (like `C`, `F#`, or `Bb`), an optional octave number,\n", + "and a frequency in Hz computed from equal temperament tuning (A4 = 440 Hz)." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from pytheory import Tone, TonedScale, Key, Chord, Fretboard, CHARTS, Interval\n", + "from pytheory import analyze_progression\n", + "from pytheory.scales import PROGRESSIONS" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Create tones with octave numbers (scientific pitch notation)\n", + "middle_c = Tone.from_string(\"C4\")\n", + "concert_a = Tone.from_string(\"A4\")\n", + "\n", + "print(f\"Middle C: {middle_c} -> {middle_c.frequency:.2f} Hz\")\n", + "print(f\"Concert A: {concert_a} -> {concert_a.frequency:.2f} Hz\")\n", + "print(f\"MIDI note: {middle_c.midi}\")\n", + "print(f\"Is natural? {middle_c.is_natural}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Create tones from frequencies or MIDI numbers\n", + "from_hz = Tone.from_frequency(440.0)\n", + "from_midi = Tone.from_midi(60)\n", + "\n", + "print(f\"440 Hz -> {from_hz}\")\n", + "print(f\"MIDI 60 -> {from_midi}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Explore the harmonic series -- the physics behind consonance\n", + "c3 = Tone.from_string(\"C3\")\n", + "harmonics = c3.overtones(8)\n", + "print(f\"Harmonic series of {c3} ({c3.frequency:.1f} Hz):\")\n", + "for i, hz in enumerate(harmonics, 1):\n", + " print(f\" Harmonic {i}: {hz:.1f} Hz\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Tone Arithmetic\n", + "\n", + "Tones support arithmetic operations. Adding an integer to a tone raises it\n", + "by that many **semitones** (half steps). Subtracting two tones gives the\n", + "semitone distance between them. You can also compare tones by pitch." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "c4 = Tone.from_string(\"C4\")\n", + "\n", + "# Add semitones: C + 4 semitones = E (a major third)\n", + "e4 = c4 + 4\n", + "g4 = c4 + Interval.PERFECT_FIFTH\n", + "print(f\"{c4} + 4 semitones = {e4}\")\n", + "print(f\"{c4} + perfect 5th = {g4}\")\n", + "\n", + "# Subtract to find interval distance\n", + "distance = g4 - c4\n", + "print(f\"\\nDistance from {c4} to {g4}: {distance} semitones\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Name the interval between two tones\n", + "print(f\"{c4} -> {e4}: {c4.interval_to(e4)}\")\n", + "print(f\"{c4} -> {g4}: {c4.interval_to(g4)}\")\n", + "\n", + "c5 = Tone.from_string(\"C5\")\n", + "print(f\"{c4} -> {c5}: {c4.interval_to(c5)}\")\n", + "\n", + "# Compare tones by pitch\n", + "print(f\"\\n{c4} < {g4}? {c4 < g4}\")\n", + "print(f\"{c4} == {c4}? {c4 == c4}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# The circle of fifths -- the backbone of Western harmony\n", + "c = Tone.from_string(\"C4\")\n", + "fifths = c.circle_of_fifths()\n", + "print(\"Circle of fifths from C:\")\n", + "print(\" -> \".join(str(t) for t in fifths))" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Scales and Modes\n", + "\n", + "A **scale** is a collection of tones arranged in ascending order.\n", + "The `TonedScale` class provides access to dozens of scales from a given tonic.\n", + "\n", + "**Modes** are rotations of the same set of intervals. The seven modes of the\n", + "major scale each have a distinct character:\n", + "\n", + "| Mode | Character |\n", + "|------------|--------------------|\n", + "| Ionian | Bright, happy |\n", + "| Dorian | Jazzy, soulful |\n", + "| Phrygian | Spanish, dark |\n", + "| Lydian | Dreamy, floating |\n", + "| Mixolydian | Bluesy, rock |\n", + "| Aeolian | Sad, natural minor |\n", + "| Locrian | Tense, unstable |" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Build a scale from a tonic\n", + "ts = TonedScale(tonic=\"C4\")\n", + "\n", + "# See all available scale names\n", + "print(\"Available scales:\")\n", + "for name in ts.scales:\n", + " print(f\" {name}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Get a specific scale and iterate its tones\n", + "c_major = ts[\"major\"]\n", + "print(f\"C major: {c_major.note_names}\")\n", + "\n", + "c_minor = ts[\"minor\"]\n", + "print(f\"C minor: {c_minor.note_names}\")\n", + "\n", + "# Check if a note belongs to the scale\n", + "print(f\"\\nIs F# in C major? {'F#' in c_major}\")\n", + "print(f\"Is G in C major? {'G' in c_major}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": "from pytheory.scales import Scale\n\n# Transpose a scale\nd_major = c_major.transpose(2)\nprint(f\"D major (C major transposed up 2): {d_major.note_names}\")\n\n# Detect a scale from a set of notes\nresult = Scale.detect(\"A\", \"B\", \"C#\", \"D\", \"E\", \"F#\", \"G#\")\nprint(f\"\\nDetected scale: {result}\")", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. The Key Class\n", + "\n", + "A **Key** is the most convenient entry point for working with harmony.\n", + "It wraps a tonic and mode, giving you instant access to scales, diatonic\n", + "chords, key signatures, and related keys." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "key = Key(\"C\", \"major\")\n", + "\n", + "print(f\"Key: {key}\")\n", + "print(f\"Notes: {key.note_names}\")\n", + "print(f\"Signature: {key.signature}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Diatonic triads -- the seven chords built from the scale\n", + "print(\"Diatonic triads in C major:\")\n", + "for i, name in enumerate(key.chords, 1):\n", + " print(f\" {i}. {name}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Seventh chords add richness and color\n", + "print(\"Diatonic seventh chords in C major:\")\n", + "for i, name in enumerate(key.seventh_chords, 1):\n", + " print(f\" {i}. {name}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Related keys\n", + "print(f\"Relative minor of C major: {key.relative}\")\n", + "print(f\"Parallel minor of C major: {key.parallel}\")\n", + "\n", + "# Key signatures for sharp and flat keys\n", + "for tonic in [\"G\", \"D\", \"F\", \"Bb\"]:\n", + " k = Key(tonic, \"major\")\n", + " sig = k.signature\n", + " print(f\"{k}: {sig['sharps']} sharps, {sig['flats']} flats -> {sig['accidentals']}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Chord Analysis\n", + "\n", + "Chords can be created from note names, intervals, chord symbols, or MIDI.\n", + "PyTheory can identify chord quality, measure tension and consonance,\n", + "and compute optimal voice leading between chords." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Multiple ways to create chords\n", + "c_major_chord = Chord.from_tones(\"C\", \"E\", \"G\")\n", + "g7 = Chord.from_intervals(\"G\", 4, 7, 10)\n", + "am = Chord.from_name(\"Am\")\n", + "\n", + "print(f\"{c_major_chord} (intervals: {c_major_chord.intervals})\")\n", + "print(f\"{g7} (intervals: {g7.intervals})\")\n", + "print(f\"{am} (intervals: {am.intervals})\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Analyze harmonic tension\n", + "# The dominant 7th chord is the most tension-filled chord in tonal music\n", + "print(\"Tension analysis:\")\n", + "for chord in [c_major_chord, am, g7]:\n", + " t = chord.tension\n", + " print(f\" {chord.identify():20s} -> score={t['score']:.2f}, \"\n", + " f\"tritones={t['tritones']}, dominant={t['has_dominant_function']}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Consonance vs dissonance (psychoacoustic measures)\n", + "print(f\"{'Chord':20s} {'Harmony':>10s} {'Dissonance':>12s}\")\n", + "print(\"-\" * 44)\n", + "for chord in [c_major_chord, am, g7]:\n", + " print(f\"{chord.identify():20s} {chord.harmony:10.4f} {chord.dissonance:12.4f}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Voice leading: how individual notes move between chords\n", + "f_major = Chord.from_tones(\"F\", \"A\", \"C\")\n", + "vl = c_major_chord.voice_leading(f_major)\n", + "\n", + "print(f\"Voice leading: {c_major_chord.identify()} -> {f_major.identify()}\")\n", + "for src, dst, movement in vl:\n", + " direction = \"up\" if movement > 0 else \"down\" if movement < 0 else \"stays\"\n", + " print(f\" {src} -> {dst} ({movement:+d} semitones, {direction})\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Inversions rearrange chord voicings\n", + "print(f\"Root position: {[t.full_name for t in c_major_chord.tones]}\")\n", + "print(f\"1st inversion: {[t.full_name for t in c_major_chord.inversion(1).tones]}\")\n", + "print(f\"2nd inversion: {[t.full_name for t in c_major_chord.inversion(2).tones]}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Chord Progressions\n", + "\n", + "Chord progressions are the harmonic backbone of songs. PyTheory supports\n", + "both **Roman numeral** analysis (classical/jazz) and the **Nashville number\n", + "system** (studio shorthand). It also ships with common progressions built in." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "key = Key(\"G\", \"major\")\n", + "\n", + "# Build a progression from Roman numerals\n", + "prog = key.progression(\"I\", \"V\", \"vi\", \"IV\")\n", + "print(\"I - V - vi - IV in G major (the 'four chord song'):\")\n", + "for chord in prog:\n", + " print(f\" {chord.identify()}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Nashville number system -- same thing, Arabic numerals\n", + "nashville = key.nashville(1, 5, 6, 4)\n", + "print(\"Nashville 1-5-6-4 in G major:\")\n", + "for chord in nashville:\n", + " print(f\" {chord.identify()}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Browse the built-in progression library\n", + "print(\"Built-in progressions:\")\n", + "for name, numerals in PROGRESSIONS.items():\n", + " print(f\" {name:25s} -> {' '.join(numerals)}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Analyze an existing chord progression\n", + "chords = [Chord.from_name(\"C\"), Chord.from_name(\"Am\"),\n", + " Chord.from_name(\"F\"), Chord.from_name(\"G\")]\n", + "numerals = analyze_progression(chords, key=\"C\")\n", + "print(\"Progression analysis in C:\")\n", + "for chord, numeral in zip(chords, numerals):\n", + " print(f\" {chord.identify():15s} -> {numeral}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. World Music Systems\n", + "\n", + "Music theory extends far beyond Western harmony. PyTheory includes scale\n", + "systems from several traditions:\n", + "\n", + "- **Indian** (raga/thaat) -- 10 parent scales covering all of Hindustani music\n", + "- **Arabic** (maqam) -- modal systems with characteristic augmented seconds\n", + "- **Japanese** -- pentatonic scales used in koto, shamisen, and folk music\n", + "- **Blues** -- the scales that built American popular music\n", + "- **Gamelan** -- Javanese/Balinese tuning systems (12-TET approximations)" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from pytheory import SYSTEMS\n", + "\n", + "# Indian thaat system\n", + "indian = TonedScale(tonic=\"C4\", system=SYSTEMS[\"indian\"])\n", + "print(\"Indian thaats from C:\")\n", + "for name in indian.scales:\n", + " scale = indian[name]\n", + " print(f\" {name:12s} -> {scale.note_names}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Arabic maqam -- the Hijaz scale has a distinctive augmented 2nd\n", + "arabic = TonedScale(tonic=\"D4\", system=SYSTEMS[\"arabic\"])\n", + "hijaz = arabic[\"hijaz\"]\n", + "print(f\"Maqam Hijaz from D: {hijaz.note_names}\")\n", + "\n", + "# Japanese hirajoshi -- hauntingly beautiful pentatonic\n", + "japanese = TonedScale(tonic=\"A4\", system=SYSTEMS[\"japanese\"])\n", + "hirajoshi = japanese[\"hirajoshi\"]\n", + "print(f\"Hirajoshi from A: {hirajoshi.note_names}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Blues scales -- the foundation of rock, jazz, and R&B\n", + "blues = TonedScale(tonic=\"A4\", system=SYSTEMS[\"blues\"])\n", + "print(\"Blues scales from A:\")\n", + "for name in blues.scales:\n", + " scale = blues[name]\n", + " print(f\" {name:20s} -> {scale.note_names}\")\n", + "\n", + "# Gamelan -- approximations of non-Western tuning\n", + "gamelan = TonedScale(tonic=\"C4\", system=SYSTEMS[\"gamelan\"])\n", + "slendro = gamelan[\"slendro\"]\n", + "print(f\"\\nGamelan slendro from C: {slendro.note_names}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 8. Guitar and Instruments\n", + "\n", + "The `Fretboard` class models stringed instruments. You can look up\n", + "chord fingerings, render tab diagrams, apply a capo, and visualize\n", + "scale patterns across the neck." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Standard guitar fretboard\n", + "guitar = Fretboard.guitar()\n", + "print(f\"Standard tuning: {guitar}\")\n", + "\n", + "# Look up chord fingerings from the chart\n", + "c_chart = CHARTS[\"western\"][\"C\"]\n", + "print(f\"\\n{c_chart.tab(fretboard=guitar)}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Show several common chord shapes\n", + "for chord_name in [\"G\", \"Am\", \"Em\", \"D\"]:\n", + " chart = CHARTS[\"western\"][chord_name]\n", + " print(chart.tab(fretboard=guitar))\n", + " print()" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Apply a capo -- raises all strings by N semitones\n", + "capo2 = Fretboard.guitar(capo=2)\n", + "print(f\"Capo on fret 2: {capo2}\")\n", + "print(\"Playing 'G shape' with capo 2 = A major voicing\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Scale diagram -- see where notes fall on the neck\n", + "c_major_scale = TonedScale(tonic=\"C4\")[\"major\"]\n", + "diagram = guitar.scale_diagram(c_major_scale, frets=12)\n", + "print(\"C major scale on guitar:\")\n", + "print(diagram)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 9. Building a Song\n", + "\n", + "Let's put it all together: pick a key, explore its chords, build a\n", + "progression, and analyze the harmonic movement." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Step 1: Choose a key\n", + "song_key = Key(\"E\", \"minor\")\n", + "print(f\"Key: {song_key}\")\n", + "print(f\"Notes: {song_key.note_names}\")\n", + "print(f\"Relative major: {song_key.relative}\")\n", + "print(f\"Signature: {song_key.signature}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Step 2: See what chords are available\n", + "print(\"Diatonic chords in E minor:\")\n", + "for i, name in enumerate(song_key.chords, 1):\n", + " print(f\" {i}. {name}\")\n", + "\n", + "print(\"\\nBorrowed chords from E major:\")\n", + "for name in song_key.borrowed_chords[:4]:\n", + " print(f\" {name}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Step 3: Build a progression\n", + "# i - VI - III - VII is a classic minor key progression\n", + "prog = song_key.progression(\"i\", \"VI\", \"III\", \"VII\")\n", + "\n", + "print(\"Progression: i - VI - III - VII\")\n", + "for chord in prog:\n", + " name = chord.identify()\n", + " numeral = chord.analyze(\"E\", \"minor\")\n", + " t = chord.tension\n", + " print(f\" {name:18s} [{numeral:5s}] tension={t['score']:.2f}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Step 4: Analyze voice leading through the progression\n", + "print(\"Voice leading through the progression:\\n\")\n", + "for i in range(len(prog) - 1):\n", + " src = prog[i]\n", + " dst = prog[i + 1]\n", + " vl = src.voice_leading(dst)\n", + " total = sum(abs(m) for _, _, m in vl)\n", + " print(f\"{src.identify()} -> {dst.identify()} (total movement: {total} semitones)\")\n", + " for s, d, m in vl:\n", + " print(f\" {s} -> {d} ({m:+d})\")\n", + " print()" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Step 5: Show the chords on guitar\n", + "guitar = Fretboard.guitar()\n", + "chord_names = [\"Em\", \"C\", \"G\", \"D\"]\n", + "\n", + "print(\"Guitar charts for the progression:\\n\")\n", + "for name in chord_names:\n", + " chart = CHARTS[\"western\"][name]\n", + " print(chart.tab(fretboard=guitar))\n", + " print()" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Bonus: Detect the key from a set of notes\n", + "detected = Key.detect(\"E\", \"G\", \"A\", \"B\", \"D\")\n", + "print(f\"Key detected from [E, G, A, B, D]: {detected}\")\n", + "\n", + "# Secondary dominant -- adds harmonic color\n", + "v_of_v = song_key.secondary_dominant(5)\n", + "print(f\"\\nSecondary dominant V/V in E minor: {v_of_v.identify()}\")\n", + "print(f\"Tension score: {v_of_v.tension['score']:.2f}\")" + ], + "outputs": [], + "execution_count": null + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 565f91a..d8ee6c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,9 @@ Documentation = "https://pytheory.kennethreitz.org" Repository = "https://github.com/kennethreitz/pytheory" Issues = "https://github.com/kennethreitz/pytheory/issues" +[project.scripts] +pytheory = "pytheory.cli:main" + [dependency-groups] dev = ["pytest"] docs = ["sphinx"] diff --git a/pytheory/cli.py b/pytheory/cli.py new file mode 100644 index 0000000..95626a8 --- /dev/null +++ b/pytheory/cli.py @@ -0,0 +1,157 @@ +"""PyTheory CLI — music theory from the command line.""" +from __future__ import annotations + +import argparse +import sys + + +def cmd_tone(args): + from .tones import Tone + tone = Tone.from_string(args.note, system="western") + print(f" Note: {tone.full_name}") + print(f" Frequency: {tone.frequency:.2f} Hz") + if tone.midi is not None: + print(f" MIDI: {tone.midi}") + if tone.enharmonic: + print(f" Enharmonic: {tone.enharmonic}") + print(f" Overtones: {', '.join(f'{h:.1f}' for h in tone.overtones(6))}") + + +def cmd_scale(args): + from .scales import TonedScale + ts = TonedScale(tonic=f"{args.tonic}4", system=args.system) + scale = ts[args.mode] + print(f" {args.tonic} {args.mode}: {' '.join(scale.note_names)}") + print(f" Intervals: {scale.tones[0].full_name}", end="") + for i in range(1, len(scale.tones)): + semitones = abs(scale.tones[i] - scale.tones[i-1]) + print(f" -{semitones}- {scale.tones[i].full_name}", end="") + print() + + +def cmd_chord(args): + from .tones import Tone + from .chords import Chord + tones = [Tone.from_string(f"{n}4", system="western") for n in args.notes] + chord = Chord(tones=tones) + name = chord.identify() or "Unknown" + print(f" Chord: {name}") + print(f" Tones: {' '.join(t.full_name for t in chord.tones)}") + print(f" Intervals: {chord.intervals}") + print(f" Harmony: {chord.harmony:.4f}") + print(f" Dissonance: {chord.dissonance:.4f}") + t = chord.tension + print(f" Tension: {t['score']:.2f} (tritones={t['tritones']})") + + +def cmd_key(args): + from .scales import Key + key = Key(args.tonic, args.mode) + sig = key.signature + acc = ", ".join(sig["accidentals"]) if sig["accidentals"] else "none" + print(f" Key: {key}") + print(f" Signature: {sig['sharps']} sharps, {sig['flats']} flats ({acc})") + print(f" Scale: {' '.join(key.note_names)}") + print(f" Triads:") + for chord in key.scale.harmonize(): + analysis = chord.analyze(args.tonic, args.mode) or "?" + print(f" {analysis:6s} {chord}") + print(f" 7th chords:") + for name in key.seventh_chords: + print(f" {name}") + print(f" Relative: {key.relative}") + print(f" Parallel: {key.parallel}") + + +def cmd_fingering(args): + from .charts import CHARTS + from .chords import Fretboard + chart = CHARTS.get("western", {}) + if args.chord not in chart: + print(f" Unknown chord: {args.chord}") + sys.exit(1) + fb = Fretboard.guitar(capo=args.capo) + print(chart[args.chord].tab(fretboard=fb)) + + +def cmd_progression(args): + from .scales import Key + key = Key(args.tonic, args.mode) + chords = key.progression(*args.numerals) + print(f" Key: {key}") + print(f" Progression: {' → '.join(args.numerals)}") + print() + for numeral, chord in zip(args.numerals, chords): + print(f" {numeral:6s} {chord}") + + +def cmd_detect(args): + from .scales import Key + key = Key.detect(*args.notes) + if key: + print(f" Detected key: {key}") + print(f" Scale: {' '.join(key.note_names)}") + else: + print(" Could not detect key") + + +def main(): + parser = argparse.ArgumentParser( + prog="pytheory", + description="Music Theory for Humans — from the command line", + ) + sub = parser.add_subparsers(dest="command") + + # tone + p = sub.add_parser("tone", help="Look up a tone (e.g. pytheory tone C4)") + p.add_argument("note", help="Note name with octave (e.g. C4, A#3)") + + # scale + p = sub.add_parser("scale", help="Show a scale (e.g. pytheory scale C major)") + p.add_argument("tonic", help="Tonic note (e.g. C, G, Sa)") + p.add_argument("mode", help="Scale/mode name (e.g. major, minor, dorian)") + p.add_argument("--system", default="western", help="Musical system (default: western)") + + # chord + p = sub.add_parser("chord", help="Identify a chord (e.g. pytheory chord C E G)") + p.add_argument("notes", nargs="+", help="Note names (e.g. C E G)") + + # key + p = sub.add_parser("key", help="Explore a key (e.g. pytheory key C major)") + p.add_argument("tonic", help="Tonic note (e.g. C, G)") + p.add_argument("mode", nargs="?", default="major", help="Mode (default: major)") + + # fingering + p = sub.add_parser("fingering", help="Guitar fingering (e.g. pytheory fingering Am)") + p.add_argument("chord", help="Chord name (e.g. C, Am, G7)") + p.add_argument("--capo", type=int, default=0, help="Capo fret (default: 0)") + + # progression + p = sub.add_parser("progression", help="Build a progression (e.g. pytheory progression C major I V vi IV)") + p.add_argument("tonic", help="Tonic note") + p.add_argument("mode", help="Mode (e.g. major, minor)") + p.add_argument("numerals", nargs="+", help="Roman numerals (e.g. I V vi IV)") + + # detect + p = sub.add_parser("detect", help="Detect key from notes (e.g. pytheory detect C E G)") + p.add_argument("notes", nargs="+", help="Note names") + + args = parser.parse_args() + if not args.command: + parser.print_help() + sys.exit(0) + + commands = { + "tone": cmd_tone, + "scale": cmd_scale, + "chord": cmd_chord, + "key": cmd_key, + "fingering": cmd_fingering, + "progression": cmd_progression, + "detect": cmd_detect, + } + commands[args.command](args) + + +if __name__ == "__main__": + main()