"""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 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() 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)") 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()