T16: autosample VST2 via native bridge + SF2/SF3 export hoàn chỉnh, UI chip size/note_count, re-sample on preset change, tests

This commit is contained in:
2026-08-11 18:04:34 +07:00
parent 0af834a8fa
commit 371b1ca665
8 changed files with 184 additions and 50 deletions
+70 -16
View File
@@ -64,20 +64,20 @@ def write_sf2(out_path, samples, sample_rate=44100, name="AutoSampled"):
phdr += name.encode("ascii", "replace")[:20].ljust(20, b"\x00")
phdr += struct.pack("<HHHIII", 0, 0, 0, 0, 0, 0) # preset=0 bank=0 bagNdx=0 libr genre morph
phdr += b"EOP".ljust(20, b"\x00") + struct.pack("<HHHIII", 0, 0, 1, 0, 0, 0)
pbag = struct.pack("<HH", 0, 0) + struct.pack("<HH", 0, 0)
pbag = struct.pack("<HH", 0, 0) + struct.pack("<HH", 1, 0)
pmod = b"\x00" * 10
pgen = struct.pack("<HH", 0, 0)
pgen = struct.pack("<HH", 41, 0) + struct.pack("<HH", 0, 0)
inst = bytearray()
inst += b"AutoSampled".ljust(20, b"\x00") + struct.pack("<H", 0)
inst += b"EOI".ljust(20, b"\x00") + struct.pack("<H", 1)
inst += b"EOI".ljust(20, b"\x00") + struct.pack("<H", n) # bagNdx = chi so record terminal zone (ibag co n+1 records)
ibag = b"".join(struct.pack("<HH", i * 3, 0) for i in range(n + 1))
imod = b"\x00" * 10
igen = bytearray()
for i, s in enumerate(samples):
note = int(s["note"]) & 0xFF
igen += struct.pack("<HH", 60, note | (note << 8)) # keyRange lo=hi=note
igen += struct.pack("<HH", 69, i) # sampleID
igen += struct.pack("<HH", 74, note) # overridingRootKey
igen += struct.pack("<HH", 43, note | (note << 8)) # keyRange lo=hi=note
igen += struct.pack("<HH", 58, note) # overridingRootKey
igen += struct.pack("<HH", 53, i) # sampleID (phai la gen cuoi zone)
igen += struct.pack("<HH", 0, 0)
shdr = bytearray()
for i, s in enumerate(samples):
@@ -118,34 +118,88 @@ def _render_note(vst, note, sr, dur, release, velocity):
return np.clip(audio * 32767, -32768, 32767).astype(np.int16)
def _postprocess(buf, sr):
"""Stereo float32 (2,N) -> int16 mono, peak 0.9 (chung cho 2 engine)."""
mono = buf.mean(axis=0)
peak = float(np.max(np.abs(mono)))
if peak < 1e-5:
return None
above = np.nonzero(np.abs(mono) > peak * 0.001)[0]
start = int(above[0]) if above.size else 0
audio = mono[start:] / peak * 0.9
return np.clip(audio * 32767, -32768, 32767).astype(np.int16)
def _is_vst2_path(path):
"""VST2 = .dll/.so dung rieng (Windows VST3 la FOLDER .vst3, .so VST3
co duoi .vst3.so) — native bridge render duoc; pedalboard thi khong."""
low = (path or "").lower()
if ".vst3" in low:
return False
return low.endswith(".dll") or low.endswith(".so")
def _render_note_native(plugin_path, note, sr, dur, release, velocity):
"""VST2 qua native bridge (Windows) — pedalboard khong ho tro VST2."""
from app.core.native_audio_service import get_service
bpm = 120.0
y = get_service().render_vst2_offline(
plugin_path,
[{"start_beat": 0.0, "duration_beats": dur * bpm / 60.0,
"note": int(note) & 0x7F, "velocity": int(velocity)}],
bpm=bpm, sr=sr, tail_sec=release,
)
return _postprocess(y, sr)
def autosample_sf2(instrument_id, out_path, preset_id=None, preset_path=None,
preset_data_b64=None, low=36, high=96, step=2, duration=2.5,
release=1.0, velocity=100, sample_rate=44100, name=None, log=None):
release=1.0, velocity=100, sample_rate=44100, name=None, log=None,
extra_vst_dirs=None):
"""Auto-sample VSTi -> SF2 (16-bit mono). Dung cho server endpoint
/api/v1/plugins/autosample va CLI main().
VST3 -> pedalboard (ap preset neu co); VST2 (.dll/.so khong .vst3) ->
native bridge (Windows) — pedalboard khong ho tro VST2, preset VST2 chua
qua bridge duoc nen bo qua.
Returns dict {note_count, out_path, size_bytes}.
Raises FileNotFoundError neu plugin khong tim thay, RuntimeError neu
pedalboard khong kha dung hoac khong render duoc not nao.
engine khong kha dung hoac khong render duoc not nao.
"""
from app.core.vst_engine import PluginManager, apply_preset_to_plugin, HAS_PEDALBOARD
if not HAS_PEDALBOARD:
raise RuntimeError("pedalboard khong kha dung - khong auto-sample duoc")
vst = PluginManager().load_vst(instrument_id)
if vst is None:
plugin_path = PluginManager(extra_vst_dirs=extra_vst_dirs or [])._scan_plugins().get(instrument_id)
if not plugin_path:
raise FileNotFoundError(f"Khong tim thay VSTi: {instrument_id} - hay Scan trong Plugin Manager truoc")
if preset_id or preset_path or preset_data_b64:
apply_preset_to_plugin(vst, preset_id=preset_id, preset_path=preset_path,
preset_data_b64=preset_data_b64)
def _log(msg):
if log:
log(msg)
if _is_vst2_path(plugin_path):
# VST2: native bridge (Windows) — pedalboard khong ho tro. Preset chua
# export qua bridge (VST2AudioEngine chua co chunk/preset) -> bo qua.
if preset_id or preset_path or preset_data_b64:
_log("VST2: native bridge chua ho tro preset - bo qua, sample preset mac dinh")
def _render(note):
return _render_note_native(plugin_path, note, sample_rate, duration,
release, velocity)
else:
if not HAS_PEDALBOARD:
raise RuntimeError("pedalboard khong kha dung - khong auto-sample VST3 duoc")
vst = PluginManager(extra_vst_dirs=extra_vst_dirs or []).load_vst(instrument_id)
if vst is None:
raise FileNotFoundError(f"Khong tim thay VSTi: {instrument_id} - hay Scan trong Plugin Manager truoc")
if preset_id or preset_path or preset_data_b64:
apply_preset_to_plugin(vst, preset_id=preset_id, preset_path=preset_path,
preset_data_b64=preset_data_b64)
def _render(note):
return _render_note(vst, note, sample_rate, duration, release, velocity)
samples = []
for note in range(low, high + 1, step):
frames = _render_note(vst, note, sample_rate, duration, release, velocity)
frames = _render(note)
if frames is None:
_log(f"note {note}: silent, bo qua")
continue