- SoundBuffer.gain(factor): new DSP method for amplitude scaling before mixing (0.5 = half volume, 2.0 = double, clamped to int16 range) - Fix sfxr square/saw waveform artifacts: phase now wraps at period boundary instead of growing unbounded; noise buffer refreshes per period - Fix PySound construction from SoundBuffer on SDL2 backend: use loadFromSamples() directly instead of copy-assign (deleted on SDL2) - Add Image::create(w, h, pixels) overload to HeadlessTypes and SDL2Types for pixel data initialization - Waveform test suite (62 lines) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
62 lines
3 KiB
Python
62 lines
3 KiB
Python
"""Test that sfxr waveforms produce sustained audio (not single-cycle pops).
|
|
|
|
Before the phase-wrap fix, square and sawtooth waveforms would only produce
|
|
one cycle of audio then become DC, resulting in very quiet output with pops.
|
|
After the fix, all waveforms should produce comparable output levels.
|
|
"""
|
|
import mcrfpy
|
|
import sys
|
|
|
|
# Generate each waveform with identical envelope params
|
|
WAVEFORMS = {0: "square", 1: "sawtooth", 2: "sine", 3: "noise"}
|
|
durations = {}
|
|
sample_counts = {}
|
|
|
|
for wt, name in WAVEFORMS.items():
|
|
buf = mcrfpy.SoundBuffer.sfxr(wave_type=wt, base_freq=0.3,
|
|
env_attack=0.0, env_sustain=0.3, env_decay=0.4)
|
|
durations[name] = buf.duration
|
|
sample_counts[name] = buf.sample_count
|
|
print(f"{name}: {buf.sample_count} samples, {buf.duration:.4f}s")
|
|
|
|
# All waveforms should produce similar duration (same envelope)
|
|
# Before fix, they all had the same envelope params so durations should match
|
|
for name, dur in durations.items():
|
|
assert dur > 0.1, f"FAIL: {name} duration too short ({dur:.4f}s)"
|
|
print(f" {name} duration OK: {dur:.4f}s")
|
|
|
|
# Test that normalize() on a middle slice doesn't massively amplify
|
|
# (If the signal is DC/near-silent, normalize would boost enormously,
|
|
# changing sample values from near-0 to near-max. With sustained waveforms,
|
|
# the signal is already substantial so normalize has less effect.)
|
|
for wt, name in [(0, "square"), (1, "sawtooth")]:
|
|
buf = mcrfpy.SoundBuffer.sfxr(wave_type=wt, base_freq=0.3,
|
|
env_attack=0.0, env_sustain=0.3, env_decay=0.4)
|
|
# Slice the sustain portion (not attack/decay edges)
|
|
mid = buf.slice(0.05, 0.15)
|
|
if mid.sample_count > 0:
|
|
# Apply pitch_shift as a transformation test - should change duration
|
|
shifted = mid.pitch_shift(2.0)
|
|
expected_count = mid.sample_count // 2
|
|
actual_count = shifted.sample_count
|
|
ratio = actual_count / max(1, expected_count)
|
|
print(f" {name} pitch_shift(2.0): {mid.sample_count} -> {shifted.sample_count} "
|
|
f"(expected ~{expected_count}, ratio={ratio:.2f})")
|
|
assert 0.8 < ratio < 1.2, f"FAIL: {name} pitch shift ratio off ({ratio:.2f})"
|
|
else:
|
|
print(f" {name} slice returned empty (skipping pitch test)")
|
|
|
|
# Generate a tone and sfxr with same waveform to compare
|
|
# The tone generator was already working, sfxr was broken
|
|
tone_sq = mcrfpy.SoundBuffer.tone(440, 0.3, "square")
|
|
sfxr_sq = mcrfpy.SoundBuffer.sfxr(wave_type=0, base_freq=0.5,
|
|
env_attack=0.0, env_sustain=0.3, env_decay=0.0)
|
|
print(f"\nComparison - tone square: {tone_sq.sample_count} samples, {tone_sq.duration:.4f}s")
|
|
print(f"Comparison - sfxr square: {sfxr_sq.sample_count} samples, {sfxr_sq.duration:.4f}s")
|
|
|
|
# Both should have substantial sample counts
|
|
assert tone_sq.sample_count > 10000, f"FAIL: tone square too short"
|
|
assert sfxr_sq.sample_count > 5000, f"FAIL: sfxr square too short"
|
|
|
|
print("\nPASS: All waveform tests passed")
|
|
sys.exit(0)
|