mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
45789f7af0
1. readline: try/except for Windows compatibility 2. drums: only persist preset after successful load 3. chords: use analyze() for correct Roman numerals in minor keys 4. clear: full reset to initial state (key, bpm, drums, parts) 5. progression: add as alias for prog 6. lint: split one-line if statements (ruff E701) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
698 lines
22 KiB
Python
698 lines
22 KiB
Python
"""PyTheory REPL — make music interactively.
|
|
|
|
Commands mirror the Python API so there's no new vocabulary to learn.
|
|
What you type in the REPL is what you'd type in a script.
|
|
|
|
Usage:
|
|
pytheory repl
|
|
"""
|
|
|
|
try:
|
|
import readline # noqa: F401 — enables line editing on Unix
|
|
except ImportError:
|
|
pass # Windows: REPL works without line editing
|
|
import sys
|
|
|
|
from .scales import Key, TonedScale
|
|
from .chords import Chord
|
|
from .tones import Tone
|
|
from .rhythm import Score, Pattern, Duration, Part
|
|
|
|
|
|
# ── State ──────────────────────────────────────────────────────────────────
|
|
|
|
class Session:
|
|
"""The live session state."""
|
|
|
|
def __init__(self):
|
|
self.key = Key("C", "major")
|
|
self.bpm = 120
|
|
self.time_sig = "4/4"
|
|
self.swing = 0.0
|
|
self.score = Score(self.time_sig, bpm=self.bpm)
|
|
self.current_part = None
|
|
self.parts = {}
|
|
self._drum_preset = None
|
|
|
|
def rebuild(self):
|
|
"""Rebuild score from settings."""
|
|
self.score = Score(self.time_sig, bpm=self.bpm, swing=self.swing)
|
|
self.parts = {}
|
|
self.current_part = None
|
|
if self._drum_preset:
|
|
self.score.drums(self._drum_preset, repeats=4)
|
|
|
|
def ensure_part(self, name="lead"):
|
|
if name not in self.parts:
|
|
self.parts[name] = self.score.part(name)
|
|
return self.parts[name]
|
|
|
|
|
|
# ── Commands ───────────────────────────────────────────────────────────────
|
|
|
|
def cmd_help(session, args):
|
|
print("""
|
|
PyTheory REPL — commands mirror the Python API
|
|
|
|
Theory:
|
|
key Am Key("A", "minor")
|
|
key G major Key("G", "major")
|
|
chords key.chords
|
|
prog I V vi IV key.progression(...)
|
|
modes show all modes
|
|
scales list available scales
|
|
circle [C] circle of fifths/fourths
|
|
interval C4 G4 name the interval
|
|
identify C E G identify a chord from notes
|
|
identify Cmaj7 analyze a chord symbol
|
|
system [indian] switch musical system
|
|
|
|
Score:
|
|
bpm 140 Score("4/4", bpm=140)
|
|
time 3/4 TimeSignature
|
|
swing 0.5 Score(swing=0.5)
|
|
drums bossa nova score.drums("bossa nova")
|
|
drums list all presets
|
|
|
|
Parts:
|
|
part lead saw pluck score.part("lead", synth="saw", envelope="pluck")
|
|
part bass sine score.part("bass", synth="sine")
|
|
part list all parts
|
|
|
|
Notes (on active part):
|
|
add C5 1 part.add("C5", 1.0)
|
|
add Am 4 part.add(Chord.from_symbol("Am"), 4.0)
|
|
rest 2 part.rest(2.0)
|
|
arp Am updown 2 2 part.arpeggio("Am", pattern="updown", bars=2, octaves=2)
|
|
prog I V vi IV part adds key.progression(...)
|
|
|
|
Effects (on active part):
|
|
reverb 0.4 reverb=0.4
|
|
delay 0.3 0.375 delay=0.3, delay_time=0.375
|
|
lowpass 2000 3 lowpass=2000, lowpass_q=3
|
|
distortion 0.5 distortion=0.5
|
|
chorus 0.3 chorus=0.3
|
|
sidechain 0.8 sidechain=0.8
|
|
humanize 0.3 humanize=0.3
|
|
volume 0.5 volume=0.5
|
|
legato on legato=True
|
|
glide 0.04 glide=0.04
|
|
set lowpass 3000 part.set(lowpass=3000)
|
|
lfo lowpass 0.5 400 3000 8 part.lfo("lowpass", rate=0.5, ...)
|
|
|
|
Playback:
|
|
play_score play the full score
|
|
play_pattern play just the drums
|
|
render sketch.wav render to WAV
|
|
save_midi sketch.mid save as MIDI
|
|
|
|
Guitar:
|
|
fingering Am guitar chord fingering
|
|
diagram [mode] [frets] scale diagram on guitar
|
|
|
|
Session:
|
|
show score info
|
|
status current state
|
|
clear reset everything
|
|
help this message
|
|
quit exit
|
|
""")
|
|
|
|
|
|
def cmd_key(session, args):
|
|
if not args:
|
|
notes = " ".join(session.key.note_names)
|
|
print(f" {session.key}: {notes}")
|
|
return
|
|
if len(args) == 1:
|
|
name = args[0]
|
|
if name.endswith("m") and len(name) <= 3:
|
|
tonic, mode = name[:-1], "minor"
|
|
else:
|
|
tonic, mode = name, "major"
|
|
else:
|
|
tonic, mode = args[0], " ".join(args[1:])
|
|
try:
|
|
session.key = Key(tonic, mode)
|
|
notes = " ".join(session.key.note_names)
|
|
print(f" {session.key}: {notes}")
|
|
except (KeyError, ValueError) as e:
|
|
print(f" error: {e}")
|
|
|
|
|
|
def cmd_bpm(session, args):
|
|
if not args:
|
|
print(f" bpm={session.bpm}")
|
|
return
|
|
session.bpm = int(args[0])
|
|
session.score.bpm = session.bpm
|
|
print(f" bpm={session.bpm}")
|
|
|
|
|
|
def cmd_swing(session, args):
|
|
if not args:
|
|
print(f" swing={session.swing}")
|
|
return
|
|
session.swing = float(args[0])
|
|
session.score.swing = session.swing
|
|
print(f" swing={session.swing}")
|
|
|
|
|
|
def cmd_drums(session, args):
|
|
if not args:
|
|
presets = Pattern.list_presets()
|
|
cols = 4
|
|
for i in range(0, len(presets), cols):
|
|
row = presets[i:i + cols]
|
|
print(" " + " ".join(f"{p:<18s}" for p in row))
|
|
return
|
|
preset = " ".join(args[:-1]) if args[-1].isdigit() else " ".join(args)
|
|
repeats = int(args[-1]) if args[-1].isdigit() else 4
|
|
try:
|
|
session.score.drums(preset, repeats=repeats)
|
|
session._drum_preset = preset # only persist after success
|
|
print(f" score.drums(\"{preset}\", repeats={repeats})")
|
|
except ValueError as e:
|
|
print(f" error: {e}")
|
|
|
|
|
|
def cmd_time(session, args):
|
|
if not args:
|
|
print(f" time={session.time_sig}")
|
|
return
|
|
session.time_sig = args[0]
|
|
session.rebuild()
|
|
print(f" time={session.time_sig}")
|
|
|
|
|
|
def cmd_part(session, args):
|
|
if not args:
|
|
if session.parts:
|
|
for name, part in session.parts.items():
|
|
active = " ←" if part is session.current_part else ""
|
|
print(f" {name}: synth={part.synth} envelope={part.envelope} "
|
|
f"vol={part.volume}{active}")
|
|
else:
|
|
print(" no parts (type: part lead saw pluck)")
|
|
return
|
|
|
|
name = args[0]
|
|
synth = args[1] if len(args) > 1 else "saw"
|
|
envelope = args[2] if len(args) > 2 else "pluck"
|
|
|
|
if name not in session.parts:
|
|
session.parts[name] = session.score.part(name, synth=synth, envelope=envelope)
|
|
print(f" score.part(\"{name}\", synth=\"{synth}\", envelope=\"{envelope}\")")
|
|
else:
|
|
print(f" → {name}")
|
|
session.current_part = session.parts[name]
|
|
|
|
|
|
def _require_part(session):
|
|
if session.current_part is None:
|
|
session.parts["lead"] = session.score.part("lead", synth="saw", envelope="pluck")
|
|
session.current_part = session.parts["lead"]
|
|
print(" (auto-created lead: saw + pluck)")
|
|
return session.current_part
|
|
|
|
|
|
def cmd_add(session, args):
|
|
if not args:
|
|
print(" usage: add C5 1 or add Am 4")
|
|
return
|
|
part = _require_part(session)
|
|
name = args[0]
|
|
beats = float(args[1]) if len(args) > 1 else 1.0
|
|
velocity = int(args[2]) if len(args) > 2 else 100
|
|
|
|
# Try as chord first, then as note
|
|
try:
|
|
chord = Chord.from_symbol(name)
|
|
part.add(chord, beats)
|
|
print(f" .add(Chord.from_symbol(\"{name}\"), {beats})")
|
|
return
|
|
except (ValueError, KeyError):
|
|
pass
|
|
|
|
try:
|
|
part.add(name, beats, velocity=velocity)
|
|
vel_str = f", velocity={velocity}" if velocity != 100 else ""
|
|
print(f" .add(\"{name}\", {beats}{vel_str})")
|
|
except Exception as e:
|
|
print(f" error: {e}")
|
|
|
|
|
|
def cmd_rest(session, args):
|
|
part = _require_part(session)
|
|
beats = float(args[0]) if args else 1.0
|
|
part.rest(beats)
|
|
print(f" .rest({beats})")
|
|
|
|
|
|
def cmd_arp(session, args):
|
|
if not args:
|
|
print(" usage: arp Am [pattern] [bars] [octaves]")
|
|
return
|
|
part = _require_part(session)
|
|
chord_name = args[0]
|
|
pattern = args[1] if len(args) > 1 else "up"
|
|
bars = float(args[2]) if len(args) > 2 else 2
|
|
octaves = int(args[3]) if len(args) > 3 else 1
|
|
try:
|
|
part.arpeggio(chord_name, bars=bars, pattern=pattern, octaves=octaves)
|
|
print(f" .arpeggio(\"{chord_name}\", pattern=\"{pattern}\", "
|
|
f"bars={bars}, octaves={octaves})")
|
|
except Exception as e:
|
|
print(f" error: {e}")
|
|
|
|
|
|
def cmd_prog(session, args):
|
|
if not args:
|
|
print(" usage: prog I V vi IV")
|
|
return
|
|
part = _require_part(session)
|
|
try:
|
|
chords = session.key.progression(*args)
|
|
for chord in chords:
|
|
part.add(chord, Duration.WHOLE)
|
|
symbols = [c.symbol or str(c) for c in chords]
|
|
print(f" {' → '.join(symbols)}")
|
|
except Exception as e:
|
|
print(f" error: {e}")
|
|
|
|
|
|
def _set_effect(session, param, args, default=0.3):
|
|
part = _require_part(session)
|
|
value = float(args[0]) if args else default
|
|
attr_map = {
|
|
"reverb": "reverb_mix", "delay": "delay_mix",
|
|
"distortion": "distortion_mix", "chorus": "chorus_mix",
|
|
}
|
|
attr = attr_map.get(param, param)
|
|
setattr(part, attr, value)
|
|
print(f" {part.name}: {param}={value}")
|
|
|
|
if param == "delay" and len(args) > 1:
|
|
part.delay_time = float(args[1])
|
|
print(f" {part.name}: delay_time={part.delay_time}")
|
|
if param == "lowpass" and len(args) > 1:
|
|
part.lowpass_q = float(args[1])
|
|
print(f" {part.name}: lowpass_q={part.lowpass_q}")
|
|
|
|
|
|
def cmd_set(session, args):
|
|
"""Automation: part.set() at current beat."""
|
|
if len(args) < 2:
|
|
print(" usage: set lowpass 3000")
|
|
return
|
|
part = _require_part(session)
|
|
param = args[0]
|
|
value = float(args[1])
|
|
part.set(**{param: value})
|
|
print(f" .set({param}={value})")
|
|
|
|
|
|
def cmd_lfo(session, args):
|
|
"""LFO automation."""
|
|
if len(args) < 4:
|
|
print(" usage: lfo lowpass 0.5 400 3000 [bars] [shape]")
|
|
return
|
|
part = _require_part(session)
|
|
param = args[0]
|
|
rate = float(args[1])
|
|
min_val = float(args[2])
|
|
max_val = float(args[3])
|
|
bars = float(args[4]) if len(args) > 4 else 4
|
|
shape = args[5] if len(args) > 5 else "sine"
|
|
part.lfo(param, rate=rate, min=min_val, max=max_val, bars=bars, shape=shape)
|
|
print(f" .lfo(\"{param}\", rate={rate}, min={min_val}, max={max_val}, "
|
|
f"bars={bars}, shape=\"{shape}\")")
|
|
|
|
|
|
def cmd_legato(session, args):
|
|
part = _require_part(session)
|
|
part.legato = not (args and args[0] == "off")
|
|
print(f" legato={'on' if part.legato else 'off'}")
|
|
|
|
|
|
def cmd_play_score(session, args):
|
|
try:
|
|
from .play import play_score
|
|
print(" ♫ play_score()")
|
|
play_score(session.score)
|
|
except Exception as e:
|
|
print(f" error: {e}")
|
|
|
|
|
|
def cmd_play_pattern(session, args):
|
|
if not session._drum_preset:
|
|
print(" no drums set")
|
|
return
|
|
try:
|
|
from .play import play_pattern
|
|
print(f" ♫ play_pattern(\"{session._drum_preset}\")")
|
|
play_pattern(Pattern.preset(session._drum_preset),
|
|
repeats=4, bpm=session.bpm)
|
|
except Exception as e:
|
|
print(f" error: {e}")
|
|
|
|
|
|
def cmd_render(session, args):
|
|
path = args[0] if args else "output.wav"
|
|
try:
|
|
from .play import render_score, SAMPLE_RATE
|
|
import scipy.io.wavfile
|
|
import numpy
|
|
buf = render_score(session.score)
|
|
pcm = (buf * 32767).astype(numpy.int16)
|
|
scipy.io.wavfile.write(path, SAMPLE_RATE, pcm)
|
|
print(f" saved: {path}")
|
|
except Exception as e:
|
|
print(f" error: {e}")
|
|
|
|
|
|
def cmd_save_midi(session, args):
|
|
path = args[0] if args else "output.mid"
|
|
session.score.save_midi(path)
|
|
print(f" save_midi(\"{path}\")")
|
|
|
|
|
|
def cmd_show(session, args):
|
|
s = session.score
|
|
print(f" {s}")
|
|
for name, part in session.parts.items():
|
|
active = " ←" if part is session.current_part else ""
|
|
fx = []
|
|
if part.reverb_mix > 0:
|
|
fx.append(f"reverb={part.reverb_mix}")
|
|
if part.delay_mix > 0:
|
|
fx.append(f"delay={part.delay_mix}")
|
|
if part.lowpass > 0:
|
|
fx.append(f"lp={part.lowpass}")
|
|
if part.distortion_mix > 0:
|
|
fx.append(f"dist={part.distortion_mix}")
|
|
if part.chorus_mix > 0:
|
|
fx.append(f"chorus={part.chorus_mix}")
|
|
if part.legato:
|
|
fx.append("legato")
|
|
if part.humanize > 0:
|
|
fx.append(f"humanize={part.humanize}")
|
|
fx_str = " " + " ".join(fx) if fx else ""
|
|
print(f" {name}: {part.synth}+{part.envelope} "
|
|
f"{len(part.notes)} notes{fx_str}{active}")
|
|
if s._drum_hits:
|
|
print(f" drums: {session._drum_preset} ({len(s._drum_hits)} hits)")
|
|
|
|
|
|
def cmd_chords(session, args):
|
|
chords = session.key.chords
|
|
for i, chord in enumerate(chords):
|
|
from .chords import Chord as ChordClass
|
|
# Build actual chord to get proper Roman numeral analysis
|
|
c = session.key.triad(i)
|
|
analysis = c.analyze(session.key.tonic_name, session.key.mode)
|
|
label = analysis or str(i + 1)
|
|
print(f" {label:6s} {chord}")
|
|
|
|
|
|
def cmd_modes(session, args):
|
|
ts = TonedScale(tonic=f"{session.key.tonic_name}4")
|
|
for mode in ["ionian", "dorian", "phrygian", "lydian",
|
|
"mixolydian", "aeolian", "locrian"]:
|
|
try:
|
|
print(f" {mode:<12s} {' '.join(ts[mode].note_names)}")
|
|
except KeyError:
|
|
continue
|
|
|
|
|
|
def cmd_scales(session, args):
|
|
ts = TonedScale(tonic=f"{session.key.tonic_name}4")
|
|
for name in ts.scales:
|
|
print(f" {name:<20s} {' '.join(ts[name].note_names)}")
|
|
|
|
|
|
def cmd_fingering(session, args):
|
|
"""Show guitar fingering for a chord."""
|
|
if not args:
|
|
print(" usage: fingering Am")
|
|
return
|
|
from .chords import Fretboard
|
|
from .charts import CHARTS
|
|
fb = Fretboard.guitar()
|
|
name = args[0]
|
|
chart = CHARTS.get("western", {})
|
|
if name in chart:
|
|
print(chart[name].tab(fretboard=fb))
|
|
else:
|
|
# Try from_symbol
|
|
try:
|
|
f = fb.chord(name)
|
|
print(f" {f}")
|
|
except (ValueError, KeyError) as e:
|
|
print(f" error: {e}")
|
|
|
|
|
|
def cmd_diagram(session, args):
|
|
"""Show a scale diagram on guitar."""
|
|
from .chords import Fretboard
|
|
fb = Fretboard.guitar()
|
|
mode = args[0] if args else session.key.mode
|
|
frets = int(args[1]) if len(args) > 1 else 12
|
|
|
|
ts = TonedScale(tonic=f"{session.key.tonic_name}4")
|
|
try:
|
|
scale = ts[mode]
|
|
print(fb.scale_diagram(scale, frets=frets))
|
|
except KeyError:
|
|
print(f" unknown scale: {mode}")
|
|
|
|
|
|
def cmd_system(session, args):
|
|
"""Switch musical system or show current."""
|
|
if not args:
|
|
from .systems import SYSTEMS
|
|
for name in SYSTEMS:
|
|
print(f" {name}")
|
|
return
|
|
system = args[0]
|
|
# Default tonics per system
|
|
default_tonics = {
|
|
"western": "C", "indian": "Sa", "arabic": "Do",
|
|
"japanese": "C", "blues": "C", "gamelan": "C",
|
|
}
|
|
tonic = args[1] if len(args) > 1 else default_tonics.get(system, "C")
|
|
try:
|
|
ts = TonedScale(tonic=f"{tonic}4", system=system)
|
|
available = list(ts.scales)[:10]
|
|
print(f" system: {system}")
|
|
print(f" scales: {', '.join(available)}")
|
|
if available:
|
|
first = ts[available[0]]
|
|
print(f" {available[0]}: {' '.join(first.note_names)}")
|
|
except Exception as e:
|
|
print(f" error: {e}")
|
|
|
|
|
|
def cmd_interval(session, args):
|
|
"""Show the interval between two notes."""
|
|
if len(args) < 2:
|
|
print(" usage: interval C4 G4")
|
|
return
|
|
try:
|
|
t1 = Tone.from_string(args[0], system="western")
|
|
t2 = Tone.from_string(args[1], system="western")
|
|
print(f" {t1.full_name} → {t2.full_name}: {t1.interval_to(t2)}")
|
|
print(f" {abs(t1 - t2)} semitones")
|
|
except Exception as e:
|
|
print(f" error: {e}")
|
|
|
|
|
|
def cmd_identify(session, args):
|
|
"""Identify a chord from notes or a symbol."""
|
|
if not args:
|
|
print(" usage: identify C E G or identify Cmaj7")
|
|
return
|
|
if len(args) == 1:
|
|
try:
|
|
chord = Chord.from_symbol(args[0])
|
|
print(f" {chord.identify()}")
|
|
print(f" symbol: {chord.symbol}")
|
|
print(f" tones: {' '.join(t.full_name for t in chord.tones)}")
|
|
print(f" intervals: {chord.intervals}")
|
|
return
|
|
except ValueError:
|
|
pass
|
|
# Try as individual notes
|
|
try:
|
|
tones = [Tone.from_string(f"{n}4", system="western") for n in args]
|
|
chord = Chord(tones=tones)
|
|
name = chord.identify() or "unknown"
|
|
print(f" {name}")
|
|
if chord.symbol:
|
|
print(f" symbol: {chord.symbol}")
|
|
except Exception as e:
|
|
print(f" error: {e}")
|
|
|
|
|
|
def cmd_circle(session, args):
|
|
"""Show circle of fifths."""
|
|
tonic = args[0] if args else session.key.tonic_name
|
|
tone = Tone.from_string(f"{tonic}4", system="western")
|
|
fifths = [t.name for t in tone.circle_of_fifths()]
|
|
fourths = [t.name for t in tone.circle_of_fourths()]
|
|
print(f" fifths: {' → '.join(fifths)}")
|
|
print(f" fourths: {' → '.join(fourths)}")
|
|
|
|
|
|
def cmd_clear(session, args):
|
|
"""Full reset — back to initial state."""
|
|
session.key = Key("C", "major")
|
|
session.bpm = 120
|
|
session.time_sig = "4/4"
|
|
session.swing = 0.0
|
|
session._drum_preset = None
|
|
session.score = Score(session.time_sig, bpm=session.bpm)
|
|
session.parts = {}
|
|
session.current_part = None
|
|
print(" cleared (C major, 120 bpm)")
|
|
|
|
|
|
def cmd_status(session, args):
|
|
parts = ", ".join(session.parts.keys()) if session.parts else "none"
|
|
active = session.current_part.name if session.current_part else "none"
|
|
print(f" key={session.key} bpm={session.bpm} swing={session.swing}")
|
|
print(f" drums={session._drum_preset or 'none'} parts=[{parts}] active={active}")
|
|
|
|
|
|
# ── Dispatch ───────────────────────────────────────────────────────────────
|
|
|
|
COMMANDS = {
|
|
"help": cmd_help, "?": cmd_help,
|
|
"key": cmd_key,
|
|
"bpm": cmd_bpm,
|
|
"swing": cmd_swing,
|
|
"drums": cmd_drums,
|
|
"time": cmd_time,
|
|
"part": cmd_part,
|
|
"add": cmd_add,
|
|
"rest": cmd_rest,
|
|
"arp": cmd_arp,
|
|
"prog": cmd_prog, "progression": cmd_prog,
|
|
"reverb": lambda s, a: _set_effect(s, "reverb", a),
|
|
"delay": lambda s, a: _set_effect(s, "delay", a),
|
|
"lowpass": lambda s, a: _set_effect(s, "lowpass", a, 2000),
|
|
"lp": lambda s, a: _set_effect(s, "lowpass", a, 2000),
|
|
"distortion": lambda s, a: _set_effect(s, "distortion", a),
|
|
"dist": lambda s, a: _set_effect(s, "distortion", a),
|
|
"chorus": lambda s, a: _set_effect(s, "chorus", a),
|
|
"sidechain": lambda s, a: _set_effect(s, "sidechain", a),
|
|
"humanize": lambda s, a: _set_effect(s, "humanize", a),
|
|
"volume": lambda s, a: _set_effect(s, "volume", a, 0.5),
|
|
"vol": lambda s, a: _set_effect(s, "volume", a, 0.5),
|
|
"glide": lambda s, a: _set_effect(s, "glide", a, 0.04),
|
|
"legato": cmd_legato,
|
|
"set": cmd_set,
|
|
"lfo": cmd_lfo,
|
|
"play_score": cmd_play_score,
|
|
"play_pattern": cmd_play_pattern,
|
|
"render": cmd_render,
|
|
"save_midi": cmd_save_midi,
|
|
"show": cmd_show,
|
|
"chords": cmd_chords,
|
|
"modes": cmd_modes,
|
|
"scales": cmd_scales,
|
|
"fingering": cmd_fingering, "f": cmd_fingering,
|
|
"diagram": cmd_diagram,
|
|
"system": cmd_system,
|
|
"interval": cmd_interval,
|
|
"identify": cmd_identify, "id": cmd_identify,
|
|
"circle": cmd_circle,
|
|
"clear": cmd_clear,
|
|
"status": cmd_status,
|
|
}
|
|
|
|
|
|
# ── Main ───────────────────────────────────────────────────────────────────
|
|
|
|
def _prompt(session):
|
|
"""Build a context-aware multiline prompt."""
|
|
key_str = f"{session.key.tonic_name}{('m' if session.key.mode == 'minor' else '')}"
|
|
ctx = [f"key={key_str}", f"bpm={session.bpm}"]
|
|
if session.swing > 0:
|
|
ctx.append(f"swing={session.swing}")
|
|
if session._drum_preset:
|
|
ctx.append(f"drums={session._drum_preset}")
|
|
if session.current_part is not None:
|
|
p = session.current_part
|
|
fx = []
|
|
if p.reverb_mix > 0:
|
|
fx.append(f"rev={p.reverb_mix}")
|
|
if p.delay_mix > 0:
|
|
fx.append(f"del={p.delay_mix}")
|
|
if p.lowpass > 0:
|
|
fx.append(f"lp={int(p.lowpass)}")
|
|
if p.distortion_mix > 0:
|
|
fx.append(f"dist={p.distortion_mix}")
|
|
if p.legato:
|
|
fx.append("legato")
|
|
part_str = f"{p.name}({p.synth})"
|
|
if fx:
|
|
part_str += f" {' '.join(fx)}"
|
|
ctx.append(f"→{part_str}")
|
|
|
|
# Single line if short, multiline if long
|
|
oneline = f"pytheory[{' | '.join(ctx)}]> "
|
|
if len(oneline) <= 60:
|
|
return oneline
|
|
|
|
# Multiline
|
|
lines = " " + " | ".join(ctx)
|
|
return f"{lines}\n♫> "
|
|
|
|
|
|
def main():
|
|
session = Session()
|
|
|
|
print()
|
|
print(" ♫ PyTheory REPL")
|
|
print(" ════════════════════════════════════════")
|
|
print()
|
|
print(" try: key Am — set a key")
|
|
print(" chords — see its chords")
|
|
print(" prog I V vi IV — hear a progression")
|
|
print(" drums bossa nova")
|
|
print(" play_score — hear it all")
|
|
print()
|
|
print(" help for all commands, quit to exit")
|
|
print()
|
|
|
|
while True:
|
|
try:
|
|
line = input(_prompt(session)).strip()
|
|
except (EOFError, KeyboardInterrupt):
|
|
print("\n ♫")
|
|
break
|
|
|
|
if not line:
|
|
continue
|
|
if line in ("quit", "exit", "q"):
|
|
print(" ♫")
|
|
break
|
|
|
|
tokens = line.split()
|
|
cmd = tokens[0].lower()
|
|
args = tokens[1:]
|
|
|
|
if cmd in COMMANDS:
|
|
try:
|
|
COMMANDS[cmd](session, args)
|
|
except Exception:
|
|
import traceback
|
|
traceback.print_exc()
|
|
else:
|
|
print(f" unknown: {cmd} (type 'help')")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|