import os import json import pytest from app.core.render_engine import PythonRenderEngine def test_render_engine_init(): engine = PythonRenderEngine() assert engine.sample_rate == 44100 def test_bars_to_samples(): engine = PythonRenderEngine() samples = engine.bars_to_samples(4.0, 120.0, 4) assert samples == 352800 def test_render_session_container_empty(): engine = PythonRenderEngine() session = {"tracks": []} buf = engine.render_session_container(session, {}, 120.0, 4, 1000) assert buf.shape == (2, 1000) assert (buf == 0.0).all() def test_render_project_bit_depth(tmp_path): """bit_depth 24/32 → WAV PCM_24/PCM_32; mặc định vẫn PCM_16.""" import soundfile as sf engine = PythonRenderEngine() project = { "metadata": {"bpm": 120, "time_signature_numerator": 4}, "main_session": {"length_bars": 1.0, "tracks": []}, "section_store": {}, } for bd, subtype in [(16, "PCM_16"), (24, "PCM_24"), (32, "PCM_32")]: 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