Note.beats now returns 0.0 for held notes (_hold=True), matching the
renderer which already skipped advancing the beat position. Previously
every hold() call added its full duration to the part's total, causing
duration reports to be 2-3x too long on tracks with drone notes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Single tanh was too mild. Now chains preamp gain → power amp clip →
asymmetric rectifier sag for proper overdrive/fuzz character.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Threshold 0.5 → 0.7 so more dynamics survive. Makeup gain capped
at 3x so sparse arrangements (solo singing bowl, etc.) don't get
over-amplified to clipping.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two variants modeling Himalayan singing bowl acoustics:
- Strike: mallet hit with chirp from inharmonic partials, long ring
- Ring: rim-rubbed sustained tone with slow build and beating modes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reverted all frequency/harmonic changes — original thump, metal, sub,
click all restored. Only change from original: envelope uses exp(-0.8*t)
instead of _exp_decay(n_samples, 6) so the sweep sustains long enough
to be audible. Dispatch override for sound_value 108 kept to bypass
stale closure.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The dispatch dict lambda was holding a stale function reference that
survived module reloads. Added explicit override for sound_value 108
to always call the current _synth_tabla_ge_bend directly.
Synth improvements:
- Sweep: 50→450Hz with slow exp(-1.5t) so ear tracks the bend
- Sustain: exp(-0.8t) envelope gives ~1.2s of audible signal
- Removed static sub/metal that masked the sweep
- Added 2nd harmonic for richness
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaced _exp_decay (which uses sample-count-based decay and was too fast)
with a direct time-based exponential: exp(-0.8*t) giving ~1.2s of signal.
The sweep from 50→450Hz is now actually hearable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Sweep range: 50→450Hz (was 60→240Hz)
- Slower sweep rate: exp(-1.5t) so ear can track it (was -4t)
- Longer sustain: decay rate 2.5 (was 6) — bend lives long enough to hear
- Removed static sub and metal that masked the sweep
- Added 2nd harmonic for richness
- Louder body (1.2 gain)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pytheory-live is now a proper command after pip install pytheory[live].
TUI moved to pytheory/live_tui.py, registered as console script.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tracks held keys. OS keyboard repeat is ignored (same key refreshes
timer). Note releases 150ms after last repeat stops — approximates
key-up detection. All notes released on Esc.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Piano-style layout across the full QWERTY keyboard:
- Bottom rows (ZXCVBNM + ASDFGHJKL): lower octave, white+black keys
- Top rows (QWERTYUIOP + 1234567890): upper octave, white+black keys
- Every letter mapped, ~3.5 octaves total
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MIDI port is now optional. If no device found or port fails,
the audio stream still starts so keyboard mode works. Cleanup
handles missing MIDI gracefully.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Keyboard mode is now a proper modal: kbd enters, Esc exits
- All keys go to MIDI while in keyboard mode, Up/Down change octave
- Header shows KBD and REC indicators
- VU meters use ASCII-safe characters
- play_recording.py: render MIDI through full engine
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Tab completes commands, instruments, patterns, fx params
- Left/Right arrows move cursor, insert mid-line
- Home/Ctrl-A, End/Ctrl-E for jump to start/end
- fx <ch> <param> <val> for live effect tweaking
- fx alone lists all params, fx <ch> shows current values
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_Channel now stores and applies: chorus, detune, distortion,
saturation, tremolo, analog, delay, phaser, sub_osc, noise_mix.
TUI fx command: fx <ch> <param> <val> to tweak any effect live.
fx alone lists all available params. fx <ch> shows current values.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes CI failure — rtmidi needs ALSA headers on Linux which
aren't available in the test runner. Now optional: import is
lazy with clear error message if missing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Channel validation: ch must be int 1-16, raises ValueError
- Port validation: string port raises ValueError if not found
- Exception-safe MIDI: open_port wrapped in try/except, cleanup on failure
- Reverb CC clears cache (was missing)
- stop() uses _stop_event to unblock start()
- _all_notes_off clears drum channel too
- Sorted __slots__
- Fixed en-dash in docstring
- Documented 3-second wavetable limitation
- Unused loop var fixed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MIDI pitch bend (0xE0) adjusts playback rate of active voices
via linear interpolation through the wavetable. ±2 semitone range.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- MIDI clock (0xF8) tracking for BPM detection
- Start/Stop/Continue transport handling
- engine.drums("rock") plays pattern synced to MIDI clock
- Pre-render all wavetables (MIDI 36-96) on startup for zero-glitch playback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Simple feedback delay network reverb applied during wavetable
rendering. 3 early reflection taps + 6-pass feedback loop for tail.
Extended wavetable to 3 seconds for reverb room.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
engine.cc(0, "lowpass", min_val=300, max_val=8000) maps any MIDI CC
to any channel parameter. Supports per-channel or global mapping.
Invalidates synth cache when filter params change.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
LiveEngine listens for MIDI input and synthesizes audio in real-time.
Each MIDI channel maps to a pytheory instrument with its own synth,
envelope, and effects. Supports polyphony, voice stealing, and
GM drum channel (10).
Adds python-rtmidi as a dependency.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ensemble rendering no longer re-synthesizes every note N times.
Renders the part once, then creates N copies with per-player
time shifts and velocity variation (cheap buffer ops).
Benchmarks:
- Heavy (ens=10+effects): 12.7s → 3.0s (4.2x faster)
- Ensemble=20: 4.3s → 0.43s (10x faster)
Also: vectorized strings_wave body_response, synth output cache.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Wurlitzer: reed-based, nasal, biting — bark on hard hits
- Vibraphone: aluminum bars with motor tremolo (spinning disc)
- Pipe organ: multi-rank (8'+4'+2'), constant air, wind chiff
- Choir: formant-filtered glottal source, vowel control via lyric=,
no vibrato (ensemble handles voice variation)
- All four with instrument presets, audio demos, and docs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CAJON_SLAP is now dry wood crack (no wires). New CAJON_SLAP_SNARE
has the buzzy version for cajóns with snare wires engaged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added hand-on-wood transient (lowpassed noise for soft palm feel)
before the box resonance kicks in. Deeper sub, longer air cavity
thump. You hear the hand, then the box.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>