mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 06:46:14 +00:00
Add audio samples for documentation
- docs/generate_audio.py renders 12 code examples as WAV files - Audio players embedded in sequencing and drums docs via raw HTML - Covers: piano hold, articulations, dynamics, filter ramp, rock, bossa nova, djembe, tabla, marching snare, ensemble, strum, swell - WAV files gitignored — generated at build time Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,3 +7,4 @@ t2.py
|
||||
__pycache__
|
||||
pytheory.egg-info
|
||||
docs/_build
|
||||
docs/_static/audio/*.wav
|
||||
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
<audio controls style="width: 100%; margin: 0.5em 0 1.5em 0;">
|
||||
<source src="{{ pathto('_static/audio/' + file, 1) }}" type="audio/wav">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
@@ -0,0 +1,281 @@
|
||||
"""Generate audio samples for documentation.
|
||||
|
||||
Renders code examples from the docs as WAV files so they can be
|
||||
embedded as <audio> players on the website.
|
||||
|
||||
Usage:
|
||||
uv run python docs/generate_audio.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from pytheory import Score, Duration, Key, Chord, Fretboard, DrumSound
|
||||
from pytheory.play import render_score, SAMPLE_RATE
|
||||
|
||||
import numpy
|
||||
import struct
|
||||
|
||||
AUDIO_DIR = os.path.join(os.path.dirname(__file__), "_static", "audio")
|
||||
os.makedirs(AUDIO_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def save_wav(buf, path):
|
||||
"""Save a float32 buffer as 16-bit WAV."""
|
||||
# Normalize
|
||||
peak = numpy.abs(buf).max()
|
||||
if peak > 0:
|
||||
buf = buf / peak * 0.9
|
||||
samples = (buf * 32767).astype(numpy.int16)
|
||||
with open(path, "wb") as f:
|
||||
n = len(samples)
|
||||
f.write(b"RIFF")
|
||||
f.write(struct.pack("<I", 36 + n * 2))
|
||||
f.write(b"WAVE")
|
||||
f.write(b"fmt ")
|
||||
f.write(struct.pack("<IHHIIHH", 16, 1, 1, SAMPLE_RATE, SAMPLE_RATE * 2, 2, 16))
|
||||
f.write(b"data")
|
||||
f.write(struct.pack("<I", n * 2))
|
||||
f.write(samples.tobytes())
|
||||
print(f" {os.path.basename(path)} ({len(buf)/SAMPLE_RATE:.1f}s)")
|
||||
|
||||
|
||||
def render(name, score):
|
||||
"""Render a score to WAV in the audio directory."""
|
||||
buf = render_score(score)
|
||||
save_wav(buf, os.path.join(AUDIO_DIR, f"{name}.wav"))
|
||||
|
||||
|
||||
# ── Piano hold (polyphonic overlap) ──────────────────────────────────────
|
||||
|
||||
def gen_piano_hold():
|
||||
score = Score("4/4", bpm=85)
|
||||
piano = score.part("piano", instrument="piano", reverb=0.3)
|
||||
piano.hold("C3", Duration.WHOLE * 2, velocity=60)
|
||||
piano.hold("E3", Duration.WHOLE * 2, velocity=55)
|
||||
piano.hold("G3", Duration.WHOLE * 2, velocity=55)
|
||||
for n in ["E4", "G4", "C5", "G4", "E4", "D4", "C4", "E4"]:
|
||||
piano.add(n, Duration.QUARTER, velocity=80)
|
||||
render("piano_hold", score)
|
||||
|
||||
|
||||
# ── Articulations ────────────────────────────────────────────────────────
|
||||
|
||||
def gen_articulations():
|
||||
score = Score("4/4", bpm=90)
|
||||
piano = score.part("piano", instrument="piano", reverb=0.25)
|
||||
for n in ["C4", "E4", "G4", "C5"]:
|
||||
piano.add(n, Duration.QUARTER, velocity=80)
|
||||
for n in ["C4", "E4", "G4", "C5"]:
|
||||
piano.add(n, Duration.QUARTER, velocity=80, articulation="staccato")
|
||||
for n in ["C5", "G4", "E4", "C4"]:
|
||||
piano.add(n, Duration.QUARTER, velocity=80, articulation="legato")
|
||||
for n in ["C4", "E4", "G4", "C5"]:
|
||||
piano.add(n, Duration.QUARTER, velocity=80, articulation="marcato")
|
||||
render("articulations", score)
|
||||
|
||||
|
||||
# ── Dynamic curves ───────────────────────────────────────────────────────
|
||||
|
||||
def gen_dynamics():
|
||||
score = Score("4/4", bpm=90)
|
||||
piano = score.part("piano", instrument="piano", reverb=0.3)
|
||||
piano.crescendo(["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"],
|
||||
Duration.QUARTER, start_vel=30, end_vel=110)
|
||||
piano.decrescendo(["C5", "B4", "A4", "G4", "F4", "E4", "D4", "C4"],
|
||||
Duration.QUARTER, start_vel=110, end_vel=30)
|
||||
render("dynamics", score)
|
||||
|
||||
|
||||
# ── Filter ramp ──────────────────────────────────────────────────────────
|
||||
|
||||
def gen_filter_ramp():
|
||||
score = Score("4/4", bpm=130)
|
||||
score.drums("house", repeats=8, fill="house", fill_every=8)
|
||||
score.set_drum_effects(volume=0.4)
|
||||
acid = score.part("acid", synth="saw", volume=0.6,
|
||||
lowpass=300, lowpass_q=8.0, distortion=0.3,
|
||||
legato=True, glide=0.03)
|
||||
acid.ramp(over=Duration.WHOLE * 4, curve="ease_in", lowpass=5000)
|
||||
for _ in range(4):
|
||||
for n in ["C2", "C3", "C2", "Eb2", "C2", "G2", "Bb2", "C2"]:
|
||||
acid.add(n, Duration.EIGHTH, velocity=90)
|
||||
acid.ramp(over=Duration.WHOLE * 4, curve="ease_out", lowpass=300)
|
||||
for _ in range(4):
|
||||
for n in ["C2", "C3", "C2", "Eb2", "C2", "G2", "Bb2", "C2"]:
|
||||
acid.add(n, Duration.EIGHTH, velocity=88)
|
||||
render("filter_ramp", score)
|
||||
|
||||
|
||||
# ── Rock beat ────────────────────────────────────────────────────────────
|
||||
|
||||
def gen_rock_beat():
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("rock", repeats=4, fill="rock", fill_every=4)
|
||||
render("rock_beat", score)
|
||||
|
||||
|
||||
# ── Bossa nova ───────────────────────────────────────────────────────────
|
||||
|
||||
def gen_bossa_nova():
|
||||
score = Score("4/4", bpm=140)
|
||||
score.drums("bossa nova", repeats=4)
|
||||
rhodes = score.part("rhodes", synth="fm", envelope="piano", volume=0.3,
|
||||
reverb=0.4, reverb_decay=1.8)
|
||||
for sym in ["Am", "Am", "Dm", "Dm", "E7", "E7", "Am", "Am"]:
|
||||
rhodes.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
render("bossa_nova", score)
|
||||
|
||||
|
||||
# ── Djembe ───────────────────────────────────────────────────────────────
|
||||
|
||||
def gen_djembe():
|
||||
score = Score("4/4", bpm=110)
|
||||
score.drums("tiriba", repeats=4, fill="djembe call", fill_every=4)
|
||||
score.set_drum_effects(reverb=0.2)
|
||||
render("djembe", score)
|
||||
|
||||
|
||||
# ── Tabla ────────────────────────────────────────────────────────────────
|
||||
|
||||
def gen_tabla():
|
||||
score = Score("4/4", bpm=80)
|
||||
score.drums("teental", repeats=2)
|
||||
score.drums("chakradar", repeats=1)
|
||||
score.set_drum_effects(reverb=0.2)
|
||||
render("tabla", score)
|
||||
|
||||
|
||||
# ── Marching snare ───────────────────────────────────────────────────────
|
||||
|
||||
def gen_march_snare():
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("snare", synth="sine", volume=0.8, reverb=0.15)
|
||||
S = DrumSound.MARCH_SNARE
|
||||
R = DrumSound.MARCH_RIMSHOT
|
||||
C = DrumSound.MARCH_CLICK
|
||||
|
||||
for _ in range(4):
|
||||
p.hit(C, Duration.QUARTER, velocity=95)
|
||||
|
||||
for _ in range(4):
|
||||
p.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
p.hit(R, Duration.SIXTEENTH, velocity=115)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
p.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=35)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(R, Duration.SIXTEENTH, velocity=120)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
p.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
render("march_snare", score)
|
||||
|
||||
|
||||
# ── Ensemble comparison ──────────────────────────────────────────────────
|
||||
|
||||
def gen_ensemble():
|
||||
score = Score("4/4", bpm=120)
|
||||
S = DrumSound.MARCH_SNARE
|
||||
R = DrumSound.MARCH_RIMSHOT
|
||||
|
||||
# Solo first
|
||||
solo = score.part("solo", synth="sine", volume=0.7, reverb=0.15)
|
||||
for _ in range(2):
|
||||
solo.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
solo.hit(R, Duration.SIXTEENTH, velocity=115)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
solo.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=35)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
solo.hit(R, Duration.SIXTEENTH, velocity=120)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
solo.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
|
||||
# Then ensemble
|
||||
line = score.part("line", synth="sine", volume=0.7, reverb=0.15, ensemble=8)
|
||||
for _ in range(8):
|
||||
line.rest(Duration.QUARTER)
|
||||
for _ in range(4):
|
||||
line.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
line.hit(R, Duration.SIXTEENTH, velocity=115)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
line.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=35)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
line.hit(R, Duration.SIXTEENTH, velocity=120)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
line.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
render("ensemble", score)
|
||||
|
||||
|
||||
# ── Guitar strum ─────────────────────────────────────────────────────────
|
||||
|
||||
def gen_strum():
|
||||
score = Score("4/4", bpm=100)
|
||||
guitar = score.part("guitar", instrument="acoustic_guitar",
|
||||
fretboard=Fretboard.guitar())
|
||||
for ch in ["G", "D", "Em", "C"] * 2:
|
||||
guitar.strum(ch, duration=Duration.WHOLE, velocity=75)
|
||||
render("strum", score)
|
||||
|
||||
|
||||
# ── Swell ────────────────────────────────────────────────────────────────
|
||||
|
||||
def gen_swell():
|
||||
score = Score("4/4", bpm=80)
|
||||
strings = score.part("strings", instrument="string_ensemble",
|
||||
reverb=0.4, ensemble=12)
|
||||
strings.swell(["C4", "D4", "E4", "F4", "G4", "F4", "E4", "D4",
|
||||
"C4", "D4", "E4", "F4", "G4", "A4", "G4", "E4"],
|
||||
Duration.QUARTER, low_vel=30, peak_vel=100)
|
||||
render("swell", score)
|
||||
|
||||
|
||||
# ── Generate all ─────────────────────────────────────────────────────────
|
||||
|
||||
GENERATORS = [
|
||||
gen_piano_hold,
|
||||
gen_articulations,
|
||||
gen_dynamics,
|
||||
gen_filter_ramp,
|
||||
gen_rock_beat,
|
||||
gen_bossa_nova,
|
||||
gen_djembe,
|
||||
gen_tabla,
|
||||
gen_march_snare,
|
||||
gen_ensemble,
|
||||
gen_strum,
|
||||
gen_swell,
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Generating audio samples for docs...")
|
||||
print()
|
||||
for gen in GENERATORS:
|
||||
gen()
|
||||
print()
|
||||
print(f"Done. {len(GENERATORS)} files in {AUDIO_DIR}")
|
||||
@@ -246,6 +246,10 @@ Playing Patterns
|
||||
|
||||
play_pattern(Pattern.preset("rock"), repeats=4, bpm=120)
|
||||
play_pattern(Pattern.preset("bossa nova"), repeats=4, bpm=140)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/rock_beat.wav" type="audio/wav"></audio>
|
||||
play_pattern(Pattern.preset("salsa"), repeats=4, bpm=180)
|
||||
play_pattern(Pattern.preset("afrobeat"), repeats=8, bpm=110)
|
||||
|
||||
@@ -458,6 +462,10 @@ West African-style break).
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("djembe", repeats=8, fill="djembe call", fill_every=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/djembe.wav" type="audio/wav"></audio>
|
||||
|
||||
Metal Kit
|
||||
~~~~~~~~~
|
||||
|
||||
@@ -548,6 +556,10 @@ voice with per-player timing tendencies and micro pitch drift.
|
||||
# Or use patterns
|
||||
score.drums("drumline", repeats=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/march_snare.wav" type="audio/wav"></audio>
|
||||
|
||||
**Sympathetic resonance:** The marching snare builds up snare wire
|
||||
buzz as hits accumulate, and the buzz decays during rests — just like
|
||||
a real drum.
|
||||
|
||||
@@ -431,6 +431,10 @@ Pass ``articulation=`` to ``Part.add()``:
|
||||
piano.add("G4", Duration.QUARTER, articulation="accent") # louder
|
||||
piano.add("C5", Duration.HALF, articulation="fermata") # held longer
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/articulations.wav" type="audio/wav"></audio>
|
||||
|
||||
What each articulation does:
|
||||
|
||||
- **staccato** — plays ~40% of the note duration with a quick fade-out. Short and detached.
|
||||
@@ -467,6 +471,10 @@ of notes instead of setting each one manually.
|
||||
piano.dynamics(["C4","E4","G4","C5"], Duration.QUARTER,
|
||||
velocities=[50, 80, 110, 90])
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/dynamics.wav" type="audio/wav"></audio>
|
||||
|
||||
Four methods:
|
||||
|
||||
- **crescendo()** — linear velocity ramp from ``start_vel`` to ``end_vel``.
|
||||
@@ -548,6 +556,12 @@ Each ensemble voice gets a consistent timing personality (some rush,
|
||||
some drag) plus small per-note wobble, and slightly different tuning.
|
||||
The result sounds like a real section — together but alive.
|
||||
|
||||
Solo snare, then an 8-player section plays the same pattern:
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/ensemble.wav" type="audio/wav"></audio>
|
||||
|
||||
Swing and Groove
|
||||
----------------
|
||||
|
||||
@@ -663,6 +677,10 @@ Four interpolation curves:
|
||||
# Smooth reverb wash fading in and settling
|
||||
pad.ramp(over=Duration.WHOLE * 4, curve="ease_in_out", reverb=0.6)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/filter_ramp.wav" type="audio/wav"></audio>
|
||||
|
||||
``ramp()`` generates automation points every quarter-beat by default.
|
||||
Set ``resolution=0.125`` for smoother curves (every 32nd note), or
|
||||
``resolution=1.0`` for lighter automation (every beat).
|
||||
|
||||
Reference in New Issue
Block a user