117 lines
4.9 KiB
Python
117 lines
4.9 KiB
Python
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
|