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:
John McCardle 2026-02-19 18:57:20 -05:00
commit 97dbec9106
20 changed files with 4793 additions and 197 deletions

View file

@ -0,0 +1,68 @@
"""Test SoundBuffer tone generation."""
import mcrfpy
import sys
# Test 1: Basic sine tone
buf = mcrfpy.SoundBuffer.tone(440, 0.5, "sine")
assert buf is not None
assert abs(buf.duration - 0.5) < 0.02, f"Expected ~0.5s, got {buf.duration}"
assert buf.sample_rate == 44100
assert buf.channels == 1
print("PASS: Sine tone 440Hz 0.5s")
# Test 2: Square wave
buf = mcrfpy.SoundBuffer.tone(220, 0.3, "square")
assert abs(buf.duration - 0.3) < 0.02
print("PASS: Square wave")
# Test 3: Saw wave
buf = mcrfpy.SoundBuffer.tone(330, 0.2, "saw")
assert abs(buf.duration - 0.2) < 0.02
print("PASS: Saw wave")
# Test 4: Triangle wave
buf = mcrfpy.SoundBuffer.tone(550, 0.4, "triangle")
assert abs(buf.duration - 0.4) < 0.02
print("PASS: Triangle wave")
# Test 5: Noise
buf = mcrfpy.SoundBuffer.tone(1000, 0.1, "noise")
assert abs(buf.duration - 0.1) < 0.02
print("PASS: Noise")
# Test 6: ADSR envelope
buf = mcrfpy.SoundBuffer.tone(440, 1.0, "sine",
attack=0.1, decay=0.2, sustain=0.5, release=0.3)
assert abs(buf.duration - 1.0) < 0.02
print("PASS: ADSR envelope")
# Test 7: Custom sample rate
buf = mcrfpy.SoundBuffer.tone(440, 0.5, "sine", sample_rate=22050)
assert buf.sample_rate == 22050
assert abs(buf.duration - 0.5) < 0.02
print("PASS: Custom sample rate")
# Test 8: Invalid waveform raises ValueError
try:
mcrfpy.SoundBuffer.tone(440, 0.5, "invalid_waveform")
assert False, "Should have raised ValueError"
except ValueError:
pass
print("PASS: Invalid waveform raises ValueError")
# Test 9: Negative duration raises ValueError
try:
mcrfpy.SoundBuffer.tone(440, -0.5, "sine")
assert False, "Should have raised ValueError"
except ValueError:
pass
print("PASS: Negative duration raises ValueError")
# Test 10: Samples are non-zero (tone actually generates audio)
buf = mcrfpy.SoundBuffer.tone(440, 0.1, "sine")
# In headless mode, sample_count should be nonzero
assert buf.sample_count > 0, "Expected non-zero sample count"
print(f"PASS: Tone has {buf.sample_count} samples")
print("\nAll soundbuffer_tone tests passed!")
sys.exit(0)