Add SoundBuffer type: procedural audio, sfxr synthesis, DSP effects
New SoundBuffer Python type enables procedural audio generation: - Tone synthesis (sine, square, saw, triangle, noise) with ADSR envelopes - sfxr retro sound effect engine (7 presets, 24 params, mutation, seeding) - DSP effects chain: pitch_shift, low/high pass, echo, reverb, distortion, bit_crush, normalize, reverse, slice - Composition: concat (with crossfade overlap) and mix - Sound() now accepts SoundBuffer or filename string - Sound gains pitch property and play_varied() method - Platform stubs for HeadlessTypes and SDL2Types (loadFromSamples, pitch) - Interactive demo: sfxr clone UI + Animalese speech synthesizer - 62 unit tests across 6 test files (all passing) Refs #251 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bb72040396
commit
97dbec9106
20 changed files with 4793 additions and 197 deletions
85
tests/unit/soundbuffer_sound_test.py
Normal file
85
tests/unit/soundbuffer_sound_test.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
"""Test Sound integration with SoundBuffer."""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Test 1: Sound accepts SoundBuffer
|
||||
buf = mcrfpy.SoundBuffer.tone(440, 0.5, "sine")
|
||||
sound = mcrfpy.Sound(buf)
|
||||
assert sound is not None
|
||||
print("PASS: Sound(SoundBuffer) works")
|
||||
|
||||
# Test 2: Sound.buffer returns the SoundBuffer
|
||||
got_buf = sound.buffer
|
||||
assert got_buf is not None
|
||||
assert abs(got_buf.duration - buf.duration) < 0.02
|
||||
print("PASS: sound.buffer returns SoundBuffer")
|
||||
|
||||
# Test 3: Sound.pitch property
|
||||
assert sound.pitch == 1.0, f"Default pitch should be 1.0, got {sound.pitch}"
|
||||
sound.pitch = 1.5
|
||||
assert abs(sound.pitch - 1.5) < 0.001
|
||||
sound.pitch = 1.0
|
||||
print("PASS: sound.pitch get/set")
|
||||
|
||||
# Test 4: Sound.play_varied (in headless mode, just verifies no crash)
|
||||
sound.play_varied(pitch_range=0.1, volume_range=3.0)
|
||||
print("PASS: sound.play_varied() works")
|
||||
|
||||
# Test 5: Sound from SoundBuffer has duration
|
||||
assert sound.duration > 0
|
||||
print(f"PASS: Sound from SoundBuffer has duration {sound.duration:.3f}s")
|
||||
|
||||
# Test 6: Sound from SoundBuffer has source '<SoundBuffer>'
|
||||
assert sound.source == "<SoundBuffer>"
|
||||
print("PASS: Sound.source is '<SoundBuffer>' for buffer-created sounds")
|
||||
|
||||
# Test 7: Backward compatibility - Sound still accepts string
|
||||
# File may not exist, so we test that a string is accepted (not TypeError)
|
||||
# and that RuntimeError is raised for missing files
|
||||
sound2 = None
|
||||
try:
|
||||
sound2 = mcrfpy.Sound("test.ogg")
|
||||
print("PASS: Sound(str) backward compatible (file loaded)")
|
||||
except RuntimeError:
|
||||
# File doesn't exist - that's fine, the important thing is it accepted a string
|
||||
print("PASS: Sound(str) backward compatible (raises RuntimeError for missing file)")
|
||||
|
||||
# Test 8: Sound from SoundBuffer - standard playback controls
|
||||
sound.volume = 75.0
|
||||
assert abs(sound.volume - 75.0) < 0.1
|
||||
sound.loop = True
|
||||
assert sound.loop == True
|
||||
sound.loop = False
|
||||
print("PASS: Standard playback controls work with SoundBuffer")
|
||||
|
||||
# Test 9: sfxr buffer -> Sound pipeline
|
||||
sfx = mcrfpy.SoundBuffer.sfxr("coin", seed=42)
|
||||
coin_sound = mcrfpy.Sound(sfx)
|
||||
assert coin_sound is not None
|
||||
assert coin_sound.duration > 0
|
||||
print(f"PASS: sfxr -> Sound pipeline ({coin_sound.duration:.3f}s)")
|
||||
|
||||
# Test 10: Effect chain -> Sound pipeline
|
||||
processed = mcrfpy.SoundBuffer.tone(440, 0.3, "saw").low_pass(2000).normalize()
|
||||
proc_sound = mcrfpy.Sound(processed)
|
||||
assert proc_sound is not None
|
||||
assert proc_sound.duration > 0
|
||||
print(f"PASS: Effects -> Sound pipeline ({proc_sound.duration:.3f}s)")
|
||||
|
||||
# Test 11: Sound with invalid argument type
|
||||
try:
|
||||
mcrfpy.Sound(42)
|
||||
assert False, "Should have raised TypeError"
|
||||
except TypeError:
|
||||
pass
|
||||
print("PASS: Sound(int) raises TypeError")
|
||||
|
||||
# Test 12: Sound.buffer is None for file-loaded sounds
|
||||
if sound2 is not None:
|
||||
assert sound2.buffer is None
|
||||
print("PASS: Sound.buffer is None for file-loaded sounds")
|
||||
else:
|
||||
print("PASS: Sound.buffer test skipped (file not available)")
|
||||
|
||||
print("\nAll soundbuffer_sound tests passed!")
|
||||
sys.exit(0)
|
||||
Loading…
Add table
Add a link
Reference in a new issue