"""Auto-sample VSTi presets -> SF2/SF3 cho client-side WASM live preview. Spec (Option 1): "a custom Python script" render preset qua tung not -> .sf3 (~3-8MB) de FluidSynth WASM phat realtime voi 0% IPC. Script nay render not bang pedalboard (DUNG VSTi + preset nhu export) -> SF2 (16-bit mono PCM, 1 zone/not). --sf3 chuyen tiep qua SoundFontConverter (can ffmpeg libvorbis). Usage: python tools/autosample_vsti.py --instrument --out out.sf2 \ [--preset ] [--low 36 --high 96 --step 2] [--sf3] Ket qua dat vao app/storage/soundfonts/ de client tai qua /soundfonts/download. """ import argparse import os import struct import sys ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, ROOT) import numpy as np def write_sf2(out_path, samples, sample_rate=44100, name="AutoSampled"): """Ghi SF2 toi gian hop le. samples: list[dict] voi note:int, frames: np.int16 mono 1D (audio da render cua not do).""" if not samples: raise ValueError("no samples") def chunk(cid, data): out = bytearray(cid) + struct.pack("= 0.9: MIDI messages la tuple (bytes raw MIDI, timestamp_seconds) messages = [ (bytes([0x90, int(note) & 0x7F, max(0, min(127, velocity))]), 0.0), (bytes([0x80, int(note) & 0x7F, 0]), dur), ] buf = vst(messages, sample_rate=sr, duration=dur + release, num_channels=2) 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 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)") ap.add_argument("--out", required=True, help="duong dan .sf2 (hoac .sf3 voi --sf3)") ap.add_argument("--preset", default=None, help="duong dan preset .vstpreset/.fxp/.fxb") ap.add_argument("--low", type=int, default=36) ap.add_argument("--high", type=int, default=96) ap.add_argument("--step", type=int, default=2, help="buoc not (2 = nua cung)") ap.add_argument("--duration", type=float, default=2.5, help="giay giu not") ap.add_argument("--release", type=float, default=1.0, help="giay duoi sau note-off") ap.add_argument("--velocity", type=int, default=100) ap.add_argument("--sample-rate", type=int, default=44100) ap.add_argument("--sf3", action="store_true", help="convert SF2 -> SF3 sau khi sample") args = ap.parse_args() 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()