mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-07-21 18:19:30 +00:00
dc70eab069
- LiveEngine.export_recording() referenced an undefined NOTE_NAMES and crashed with NameError on every call. Define NOTE_NAMES in live.py. - The cabinet (amp-sim) effect was applied for plain melodic parts but silently skipped for (a) parts with automation points and (b) drum parts — both effect-presence checks omitted it. Add cabinet to both. Both are the same "a code path handles only part of the structure" class as the recent MIDI/render/notation fixes. New tests: export_recording roundtrip, cabinet-on-drums, cabinet-with-automation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
126 lines
4.2 KiB
Python
126 lines
4.2 KiB
Python
import pytest
|
|
import numpy
|
|
|
|
import pytheory
|
|
from pytheory import Tone, TonedScale, Fretboard, Chord, Key, Note, TET
|
|
from pytheory.charts import CHARTS, NamedChord, charts_for_fretboard, QUALITIES
|
|
from pytheory.systems import System, SYSTEMS
|
|
from pytheory.rhythm import Duration, TimeSignature, Note as RhythmNote, Rest, Score
|
|
|
|
from _util import (HAS_PORTAUDIO, needs_portaudio, _write_test_wav,
|
|
_roundtrip_melody, _render_test_mix, _chords_roundtrip,
|
|
_chord_buffer, _progression_score)
|
|
|
|
|
|
def test_live_percussive_instrument_stays_one_shot():
|
|
from pytheory.live import _Channel
|
|
sr = 44100
|
|
ch = _Channel(synth_name="piano", envelope_name="piano", volume=0.5)
|
|
ch.note_on(60, 100)
|
|
assert ch.voices[0].loop_end is None
|
|
for _ in range(int(4.0 * sr / 512)):
|
|
last = ch.render_stereo(512)
|
|
assert len(ch.voices) == 0 # decayed and cleaned up
|
|
assert numpy.abs(last).max() == 0.0
|
|
|
|
|
|
def test_live_note_off_releases_looping_voice():
|
|
from pytheory.live import _Channel
|
|
ch = _Channel(synth_name="organ", envelope_name="organ")
|
|
ch.note_on(60, 100)
|
|
for _ in range(10):
|
|
ch.render_stereo(512)
|
|
ch.note_off(60)
|
|
for _ in range(20):
|
|
tail = ch.render_stereo(512)
|
|
assert len(ch.voices) == 0
|
|
assert numpy.abs(tail).max() == 0.0
|
|
|
|
|
|
def test_live_cc_does_not_clear_cache_for_bus_params():
|
|
from pytheory.live import LiveEngine
|
|
engine = LiveEngine()
|
|
engine.channel(1, instrument="electric_piano")
|
|
ch = engine.channels[1]
|
|
ch._get_wave(60, 44100 * 3)
|
|
assert len(ch._cache) == 1
|
|
engine.cc(11, "lowpass", min_val=200, max_val=8000)
|
|
engine._apply_cc(1, 11, 64)
|
|
assert len(ch._cache) == 1 # no re-render needed
|
|
assert ch.lowpass > 200
|
|
engine.cc(12, "detune", min_val=0, max_val=20)
|
|
engine._apply_cc(1, 12, 64)
|
|
assert len(ch._cache) == 0 # baked param → cache cleared
|
|
|
|
|
|
def test_live_engine_link_sync():
|
|
pytest.importorskip("link")
|
|
import time
|
|
from pytheory.live import LiveEngine
|
|
|
|
e = LiveEngine()
|
|
e.channel(1, instrument="electric_piano")
|
|
e.drums("rock", volume=0.5)
|
|
e.enable_link(quantum=4)
|
|
try:
|
|
state = e._link.captureSessionState()
|
|
now = e._link.clock().micros()
|
|
state.setIsPlaying(True, now)
|
|
state.setTempo(140, now)
|
|
e._link.commitSessionState(state)
|
|
time.sleep(0.05)
|
|
|
|
hits = []
|
|
e._drum_channel.note_on = lambda n, v: hits.append(n)
|
|
for _ in range(120):
|
|
e._on_link_audio(512)
|
|
time.sleep(0.005)
|
|
assert abs(e._bpm - 140) < 0.5 # tempo followed the session
|
|
assert hits # drums fired on the Link grid
|
|
finally:
|
|
e.stop()
|
|
assert e._link is None
|
|
|
|
|
|
def test_live_engine_link_stopped_transport_is_silent():
|
|
pytest.importorskip("link")
|
|
import time
|
|
from pytheory.live import LiveEngine
|
|
|
|
e = LiveEngine()
|
|
e.channel(1, instrument="electric_piano")
|
|
e.drums("rock", volume=0.5)
|
|
e.enable_link(quantum=4)
|
|
try:
|
|
state = e._link.captureSessionState()
|
|
state.setIsPlaying(False, e._link.clock().micros())
|
|
e._link.commitSessionState(state)
|
|
time.sleep(0.05)
|
|
hits = []
|
|
e._drum_channel.note_on = lambda n, v: hits.append(n)
|
|
for _ in range(40):
|
|
e._on_link_audio(512)
|
|
time.sleep(0.002)
|
|
assert hits == []
|
|
finally:
|
|
e.stop()
|
|
|
|
|
|
def test_export_recording_does_not_crash():
|
|
"""export_recording() must convert MIDI numbers to note names without a
|
|
NameError (NOTE_NAMES was undefined in live.py)."""
|
|
from pytheory.live import LiveEngine
|
|
|
|
e = LiveEngine()
|
|
# (timestamp, channel, midi_note, velocity, is_on)
|
|
e._record_events = [
|
|
(0.0, 1, 60, 100, True), (0.5, 1, 60, 0, False), # C4
|
|
(0.5, 1, 64, 90, True), (1.0, 1, 64, 0, False), # E4
|
|
(1.0, 1, 67, 80, True), (1.5, 1, 67, 0, False), # G4
|
|
]
|
|
e._bpm = 120
|
|
score = e.export_recording() # no filename -> returns Score
|
|
names = [t.name for p in score.parts.values()
|
|
for n in p.notes if n.tone is not None for t in [n.tone]]
|
|
assert names == ["C", "E", "G"]
|