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:
2026-08-11 14:28:42 +07:00
parent 7293d7ac7e
commit cadb5402a3
12 changed files with 676 additions and 44 deletions
+53 -24
View File
@@ -118,6 +118,49 @@ def _render_note(vst, note, sr, dur, release, velocity):
return np.clip(audio * 32767, -32768, 32767).astype(np.int16)
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):
"""Auto-sample VSTi -> SF2 (16-bit mono). Dung cho server endpoint
/api/v1/plugins/autosample va CLI main().
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.
"""
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:
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)
samples = []
for note in range(low, high + 1, step):
frames = _render_note(vst, note, sample_rate, duration, release, velocity)
if frames is None:
_log(f"note {note}: silent, bo qua")
continue
samples.append({"note": note, "frames": frames})
_log(f"note {note}: {frames.shape[0] / sample_rate:.2f}s")
if not samples:
raise RuntimeError("Khong render duoc not nao (plugin silent?)")
if name is None:
name = os.path.basename(instrument_id)
write_sf2(out_path, samples, sample_rate, name=name)
size = os.path.getsize(out_path)
_log(f"wrote {out_path} ({size // 1024} KB, {len(samples)} not)")
return {"note_count": len(samples), "out_path": out_path, "size_bytes": size}
def main():
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
ap.add_argument("--instrument", required=True, help="plugin_id da scan (Plugin Manager)")
@@ -133,35 +176,21 @@ def main():
ap.add_argument("--sf3", action="store_true", help="convert SF2 -> SF3 sau khi sample")
args = ap.parse_args()
from app.core.vst_engine import PluginManager, apply_preset_to_plugin, HAS_PEDALBOARD
if not HAS_PEDALBOARD:
sys.exit("pedalboard khong kha dung - khong auto-sample duoc")
vst = PluginManager().load_vst(args.instrument)
if vst is None:
sys.exit(f"Khong tim thay VSTi: {args.instrument} - hay Scan trong Plugin Manager truoc")
if args.preset:
apply_preset_to_plugin(vst, preset_path=args.preset)
samples = []
for note in range(args.low, args.high + 1, args.step):
frames = _render_note(vst, note, args.sample_rate, args.duration, args.release, args.velocity)
if frames is None:
print(f"note {note}: silent, bo qua")
continue
samples.append({"note": note, "frames": frames})
print(f"note {note}: {frames.shape[0] / args.sample_rate:.2f}s")
if not samples:
sys.exit("Khong render duoc not nao (plugin silent?)")
write_sf2(args.out, samples, args.sample_rate, name=os.path.basename(args.instrument))
print(f"wrote {args.out} ({os.path.getsize(args.out) // 1024} KB, {len(samples)} not)")
try:
result = autosample_sf2(
args.instrument, args.out, preset_path=args.preset,
low=args.low, high=args.high, step=args.step,
duration=args.duration, release=args.release,
velocity=args.velocity, sample_rate=args.sample_rate,
)
except (FileNotFoundError, RuntimeError) as e:
sys.exit(str(e))
print(f"wrote {result['out_path']} ({result['size_bytes'] // 1024} KB, {result['note_count']} not)")
if args.sf3:
from app.core.soundfont_converter import SoundFontConverter
p = SoundFontConverter().convert_sf2_to_sf3(args.out)
print("sf3:", p)
if __name__ == "__main__":
main()