From d5beab46f29b7158a2a4300cda79e544f310eaad Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 22 Mar 2026 06:53:47 -0400 Subject: [PATCH] Add Fretboard.keyboard() for piano/synth controllers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fretboard.keyboard(keys=88, start="A0") — configurable key count: - 88 keys = full piano (A0-C8) - 61 keys = standard synth controller - 49/37/25 keys = compact MIDI controllers 25 instrument presets total. 368 tests. Co-Authored-By: Claude Opus 4.6 (1M context) --- pytheory/chords.py | 26 ++++++++++++++++++++++++++ test_pytheory.py | 19 ++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/pytheory/chords.py b/pytheory/chords.py index e2183c9..5889301 100644 --- a/pytheory/chords.py +++ b/pytheory/chords.py @@ -927,6 +927,32 @@ class Fretboard: Tone.from_string("E4", system="western"), ]) + @classmethod + def keyboard(cls, keys=88, start="A0"): + """Piano or keyboard with the given number of keys. + + Args: + keys: Number of keys (default 88 for a full piano). + Common sizes: 25, 37, 49, 61, 76, 88. + start: The lowest note (default ``"A0"`` for standard piano). + + A full 88-key piano spans A0 (27.5 Hz) to C8 (4186 Hz) — + the widest range of any standard acoustic instrument. + Smaller MIDI controllers typically start at C. + + Examples:: + + Fretboard.keyboard() # 88-key piano + Fretboard.keyboard(61, "C2") # 61-key controller + Fretboard.keyboard(25, "C3") # 25-key mini controller + """ + from .tones import Tone + start_tone = Tone.from_string(start, system="western") + tones = [] + for i in range(keys - 1, -1, -1): + tones.append(start_tone.add(i)) + return cls(tones=tones) + @classmethod def lute(cls): """Renaissance lute in G tuning (6 courses). diff --git a/test_pytheory.py b/test_pytheory.py index ee1d411..bd22734 100644 --- a/test_pytheory.py +++ b/test_pytheory.py @@ -1942,7 +1942,7 @@ def test_all_instruments_create(): "violin", "viola", "cello", "double_bass", "banjo", "harp", "pedal_steel", "bouzouki", "oud", "sitar", "shamisen", "erhu", - "charango", "pipa", "balalaika", "lute", + "charango", "pipa", "balalaika", "lute", "keyboard", ] for name in instruments: fb = getattr(Fretboard, name)() @@ -2002,6 +2002,23 @@ def test_fretboard_pipa(): assert len(fb) == 4 +def test_keyboard_88(): + kb = Fretboard.keyboard() + assert len(kb) == 88 + + +def test_keyboard_25(): + kb = Fretboard.keyboard(25, "C3") + assert len(kb) == 25 + assert kb.tones[-1].name == "C" + assert kb.tones[-1].octave == 3 + + +def test_keyboard_custom(): + kb = Fretboard.keyboard(61, "C2") + assert len(kb) == 61 + + # ── Ergonomic integration tests ───────────────────────────────────────────── def test_ergonomic_workflow():