D3+D4: export stage PCM16/24 + TPDF dither + noise shaping, resample 44.1/48/96k, normalize -1dBTP/-14LUFS, mono side/mid check; Export modal mastering options + /plugins/render options (test 9 case, 23 passed)

This commit is contained in:
2026-08-23 07:47:27 +07:00
parent 35076f680b
commit 5d37c0683b
8 changed files with 372 additions and 16 deletions
+56 -3
View File
@@ -375,6 +375,31 @@ def _probe_track_latency(track, sample_rate):
except Exception:
pass
def _quantize_pcm(audio, bits, dither):
"""Quantize float [-1,1] to PCM grid (bits=16|24), optional TPDF dither +
1st-order noise shaping. Tra ve int array de sf.write ghi EXACT bits
(libsndfile float->PCM khong round dung 32768-grid - kiem chung D3)."""
N = 2 ** (bits - 1)
x = audio.astype(np.float64)
if dither and bits < 32:
lsb = 1.0 / N
rng = np.random.default_rng()
d = (rng.random(x.shape) + rng.random(x.shape) - 1.0) * lsb # TPDF [-lsb, lsb]
out = np.empty_like(x)
err = np.zeros(x.shape[0])
for i in range(x.shape[1]):
s = x[:, i] + d[:, i] + err
q = np.rint(s * N)
out[:, i] = q
err = q / N - s # 1st-order noise shaping feedback
q = np.clip(out, -N, N - 1)
else:
q = np.clip(np.rint(x * N), -N, N - 1)
if bits == 16:
return q.astype(np.int16)
# PCM_24: libsndfile luu top-24-bit cua int32 -> <<8
return (q.astype(np.int64) << 8).astype(np.int32)
class PythonRenderEngine:
def __init__(self, sample_rate=44100):
self.sample_rate = sample_rate
@@ -765,7 +790,10 @@ class PythonRenderEngine:
return session_buffer
def render_project(self, project_json: dict, output_filepath: str):
def render_project(self, project_json: dict, output_filepath: str,
bit_depth: int = 32, out_sample_rate: int = 0,
dither: bool = False, normalize: bool = False,
normalize_target: str = "-1dbtp"):
with RENDER_LOCK:
bpm = project_json["metadata"]["bpm"]
time_sig_num = project_json["metadata"].get("time_signature_numerator", 4)
@@ -854,6 +882,31 @@ class PythonRenderEngine:
if max_peak > 1.0:
master_buffer /= max_peak
# Write final output file
sf.write(output_filepath, master_buffer.T, self.sample_rate, subtype="FLOAT")
# ── D3 export stage: resample / normalize / dither / PCM ──
out_sr = out_sample_rate or self.sample_rate
if out_sr != self.sample_rate:
from scipy.signal import resample_poly
g = math.gcd(int(out_sr), int(self.sample_rate))
master_buffer = resample_poly(
master_buffer, int(out_sr) // g, int(self.sample_rate) // g, axis=1)
if normalize:
from app.core import loudness
if normalize_target == "-14lufs":
_l = loudness.integrated_lufs(master_buffer, out_sr)
if np.isfinite(_l):
master_buffer *= 10 ** ((-14.0 - _l) / 20.0)
else: # "-1dbtp"
_tp = loudness.true_peak_db(master_buffer, out_sr)
if np.isfinite(_tp):
master_buffer *= 10 ** ((-1.0 - _tp) / 20.0)
# Write final output file (D3: PCM_16/PCM_24 + dither, FLOAT mac dinh)
if bit_depth == 16:
sf.write(output_filepath, _quantize_pcm(master_buffer, 16, dither).T,
out_sr, subtype="PCM_16")
elif bit_depth == 24:
sf.write(output_filepath, _quantize_pcm(master_buffer, 24, dither).T,
out_sr, subtype="PCM_24")
else:
sf.write(output_filepath, master_buffer.T, out_sr, subtype="FLOAT")
return output_filepath