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
80
tests/unit/soundbuffer_compose_test.py
Normal file
80
tests/unit/soundbuffer_compose_test.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
"""Test SoundBuffer composition (concat, mix)."""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Create test buffers
|
||||
a = mcrfpy.SoundBuffer.tone(440, 0.3, "sine")
|
||||
b = mcrfpy.SoundBuffer.tone(880, 0.2, "sine")
|
||||
c = mcrfpy.SoundBuffer.tone(660, 0.4, "square")
|
||||
|
||||
# Test 1: concat two buffers
|
||||
result = mcrfpy.SoundBuffer.concat([a, b])
|
||||
assert result is not None
|
||||
expected = a.duration + b.duration
|
||||
assert abs(result.duration - expected) < 0.02, f"Expected ~{expected:.3f}s, got {result.duration:.3f}s"
|
||||
print(f"PASS: concat([0.3s, 0.2s]) -> {result.duration:.3f}s")
|
||||
|
||||
# Test 2: concat three buffers
|
||||
result3 = mcrfpy.SoundBuffer.concat([a, b, c])
|
||||
expected3 = a.duration + b.duration + c.duration
|
||||
assert abs(result3.duration - expected3) < 0.03
|
||||
print(f"PASS: concat([0.3s, 0.2s, 0.4s]) -> {result3.duration:.3f}s")
|
||||
|
||||
# Test 3: concat with crossfade overlap
|
||||
overlapped = mcrfpy.SoundBuffer.concat([a, b], overlap=0.05)
|
||||
# Duration should be about 0.05s shorter than without overlap
|
||||
expected_overlap = a.duration + b.duration - 0.05
|
||||
assert abs(overlapped.duration - expected_overlap) < 0.03, \
|
||||
f"Expected ~{expected_overlap:.3f}s, got {overlapped.duration:.3f}s"
|
||||
print(f"PASS: concat with overlap=0.05 -> {overlapped.duration:.3f}s")
|
||||
|
||||
# Test 4: mix two buffers
|
||||
mixed = mcrfpy.SoundBuffer.mix([a, b])
|
||||
assert mixed is not None
|
||||
# mix pads to longest buffer
|
||||
assert abs(mixed.duration - max(a.duration, b.duration)) < 0.02
|
||||
print(f"PASS: mix([0.3s, 0.2s]) -> {mixed.duration:.3f}s (padded to longest)")
|
||||
|
||||
# Test 5: mix same duration buffers
|
||||
d = mcrfpy.SoundBuffer.tone(440, 0.5, "sine")
|
||||
e = mcrfpy.SoundBuffer.tone(660, 0.5, "sine")
|
||||
mixed2 = mcrfpy.SoundBuffer.mix([d, e])
|
||||
assert abs(mixed2.duration - 0.5) < 0.02
|
||||
print(f"PASS: mix([0.5s, 0.5s]) -> {mixed2.duration:.3f}s")
|
||||
|
||||
# Test 6: concat empty list raises ValueError
|
||||
try:
|
||||
mcrfpy.SoundBuffer.concat([])
|
||||
assert False, "Should have raised ValueError"
|
||||
except ValueError:
|
||||
pass
|
||||
print("PASS: concat([]) raises ValueError")
|
||||
|
||||
# Test 7: mix empty list raises ValueError
|
||||
try:
|
||||
mcrfpy.SoundBuffer.mix([])
|
||||
assert False, "Should have raised ValueError"
|
||||
except ValueError:
|
||||
pass
|
||||
print("PASS: mix([]) raises ValueError")
|
||||
|
||||
# Test 8: concat with non-SoundBuffer raises TypeError
|
||||
try:
|
||||
mcrfpy.SoundBuffer.concat([a, "not a buffer"])
|
||||
assert False, "Should have raised TypeError"
|
||||
except TypeError:
|
||||
pass
|
||||
print("PASS: concat with invalid types raises TypeError")
|
||||
|
||||
# Test 9: concat single buffer returns copy
|
||||
single = mcrfpy.SoundBuffer.concat([a])
|
||||
assert abs(single.duration - a.duration) < 0.02
|
||||
print("PASS: concat single buffer works")
|
||||
|
||||
# Test 10: mix single buffer returns copy
|
||||
single_mix = mcrfpy.SoundBuffer.mix([a])
|
||||
assert abs(single_mix.duration - a.duration) < 0.02
|
||||
print("PASS: mix single buffer works")
|
||||
|
||||
print("\nAll soundbuffer_compose tests passed!")
|
||||
sys.exit(0)
|
||||
Loading…
Add table
Add a link
Reference in a new issue