feat: VSTi live playback 100% client-side (không Carla) — autosample SF2 backend (pedalboard) → FluidSynth WASM qua masterBus → mastering → main out; SonicVstiAutosample ensure+dedup+cooldown; runtime gating __enableCarlaLivePlayback; âm bắt qua ensureMidiCapture → clientSideExport
This commit is contained in:
@@ -32,3 +32,85 @@ def test_render_project_bit_depth(tmp_path):
|
||||
out = str(tmp_path / f"render_{bd}.wav")
|
||||
engine.render_project(project, out, bit_depth=bd)
|
||||
assert sf.info(out).subtype == subtype
|
||||
|
||||
def _default_mastering():
|
||||
return {
|
||||
"masterConnected": True, "isBypassed": False,
|
||||
"eqActive": True, "eqLowGain": 1.5, "eqMid1Gain": -1.0, "eqMid2Gain": 2.0, "eqHighGain": 1.8,
|
||||
"imagerActive": True, "w1": 0, "w2": 115, "w3": 135, "w4": 150,
|
||||
"maximizerActive": True, "maxGain": 5.4, "maxUpward": 2.0, "maxSoftClip": 15, "ceiling": -0.1,
|
||||
"compActive": False, "compThreshold": -16, "compRatio": 3, "compMakeup": 2,
|
||||
"limActive": False, "limThreshold": -1.0,
|
||||
"excActive": False, "excDrive": 30,
|
||||
"rebalActive": False, "rebalMid": 0, "rebalSide": 0,
|
||||
"chain": [
|
||||
{"id": "mod_eq", "type": "eq", "active": True},
|
||||
{"id": "mod_imager", "type": "imager", "active": True},
|
||||
{"id": "mod_maximizer", "type": "maximizer", "active": True},
|
||||
],
|
||||
}
|
||||
|
||||
def test_apply_mastering_engine_gates_and_ceiling():
|
||||
"""apply_mastering: gate masterConnected/isBypassed/chain rỗng → copy;
|
||||
chain mặc định → khác input, peak ≤ ceiling (hard clip của Maximizer)."""
|
||||
import numpy as np
|
||||
from app.core.mastering_engine import apply_mastering
|
||||
sr = 44100
|
||||
t = np.arange(sr // 5) / sr
|
||||
buf = np.stack([np.sin(2 * np.pi * 220 * t) * 0.9,
|
||||
np.sin(2 * np.pi * 220 * t + 0.5) * 0.8]).astype(np.float32)
|
||||
s = _default_mastering()
|
||||
assert np.array_equal(apply_mastering(buf, None, sr), buf)
|
||||
assert np.array_equal(apply_mastering(buf, {**s, "masterConnected": False}, sr), buf)
|
||||
assert np.array_equal(apply_mastering(buf, {**s, "isBypassed": True}, sr), buf)
|
||||
assert np.array_equal(apply_mastering(buf, {**s, "chain": []}, sr), buf)
|
||||
out = apply_mastering(buf, s, sr)
|
||||
assert out.shape == buf.shape and not np.array_equal(out, buf)
|
||||
assert np.max(np.abs(out)) <= 10 ** (-0.1 / 20) + 1e-6 # ceiling -0.1 dB
|
||||
|
||||
def test_render_project_mastering(tmp_path):
|
||||
"""render_project: có mastering_settings → WAV khác bản không mastering,
|
||||
peak ≤ 1.0 (hard clip sau mastering như WAV encoder client)."""
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
from app.config import settings
|
||||
engine = PythonRenderEngine()
|
||||
# 1s 440Hz tone làm nguồn audio thật (project rỗng = silence → mastering
|
||||
# của silence = silence, không test được gì)
|
||||
sr = engine.sample_rate
|
||||
t = np.arange(sr) / sr
|
||||
tone = (0.9 * np.sin(2 * np.pi * 440 * t)).astype(np.float32)
|
||||
src_path = os.path.join(settings.UPLOADS_DIR, "mastering_test_tone.wav")
|
||||
sf.write(src_path, tone, sr)
|
||||
try:
|
||||
project = {
|
||||
"metadata": {"bpm": 120, "time_signature_numerator": 4},
|
||||
"main_session": {
|
||||
"length_bars": 4.0,
|
||||
"tracks": [{
|
||||
"type": "AUDIO", "volume_db": 0.0, "pan": 0.0, "mute": False,
|
||||
"items": [{
|
||||
"type": "AUDIO_ITEM", "start_bar": 0.0, "duration_bars": 4.0,
|
||||
"clip_start_offset_bars": 0.0,
|
||||
"source_data": {"audio_file_url": "/static/audio/uploads/mastering_test_tone.wav", "gain": 1.0},
|
||||
}],
|
||||
}],
|
||||
},
|
||||
"section_store": {},
|
||||
}
|
||||
out_plain = str(tmp_path / "plain.wav")
|
||||
engine.render_project(project, out_plain, bit_depth=16)
|
||||
plain, _ = sf.read(out_plain, dtype="float32")
|
||||
assert np.max(np.abs(plain)) > 0.01
|
||||
|
||||
out_mast = str(tmp_path / "mastered.wav")
|
||||
engine.render_project({**project, "mastering_settings": _default_mastering()},
|
||||
out_mast, bit_depth=16)
|
||||
mast, _ = sf.read(out_mast, dtype="float32")
|
||||
assert not np.array_equal(mast, plain)
|
||||
assert np.max(np.abs(mast)) <= 1.0 + 1e-6
|
||||
finally:
|
||||
try:
|
||||
os.remove(src_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user