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:
@@ -445,8 +445,10 @@ class NativeAudioService:
|
||||
|
||||
def render_vst2_offline(self, plugin_path, notes, bpm=120.0, sr=44100,
|
||||
gain_db=0.0, pan=0.0, lim_active=False,
|
||||
threshold_db=-1.0, master_gain_db=0.0):
|
||||
"""Offline VST2: SF_VST2_Process (raw) + mixer_math (Python mirror)."""
|
||||
threshold_db=-1.0, master_gain_db=0.0, tail_sec=0.5):
|
||||
"""Offline VST2: SF_VST2_Process (raw) + mixer_math (Python mirror).
|
||||
|
||||
tail_sec: duoi im lang sau note cuoi (autosample muon release tail)."""
|
||||
with _LOCK:
|
||||
dll = self._load_vst2_dll()
|
||||
ctx = _Vst2Ctx(dll)
|
||||
@@ -466,7 +468,7 @@ class NativeAudioService:
|
||||
events.append((int((start_s + dur_s) * sr), "off", pitch, 0))
|
||||
total = max(total, start_s + dur_s)
|
||||
events.sort(key=lambda e: e[0])
|
||||
n = int((max(total, 0.25) + 0.5) * sr)
|
||||
n = int((max(total, 0.25) + float(tail_sec)) * sr)
|
||||
out = np.zeros((2, n), dtype=np.float32)
|
||||
block = 256
|
||||
pos = 0
|
||||
|
||||
@@ -5,6 +5,27 @@ import logging
|
||||
import wave
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
def _ensure_fluidsynth_runtime() -> None:
|
||||
"""Dua thu muc chua libfluidsynth DLL vao PATH de pyfluidsynth import duoc
|
||||
(find_library do theo PATH tren Windows). Khong lam gi tren non-Windows."""
|
||||
if os.name != "nt":
|
||||
return
|
||||
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
candidates = [
|
||||
os.path.join(root, "native_host", "build", "Release", "fluidsynth_runtime"),
|
||||
os.path.join(root, "native_host", "fluidsynth_runtime"),
|
||||
]
|
||||
path = os.environ.get("PATH", "")
|
||||
parts = [os.path.normcase(p) for p in path.split(os.pathsep)]
|
||||
for d in candidates:
|
||||
if os.path.isdir(d) and any(
|
||||
f.lower().startswith("libfluidsynth") and f.lower().endswith(".dll")
|
||||
for f in os.listdir(d)
|
||||
):
|
||||
if os.path.normcase(d) not in parts:
|
||||
os.environ["PATH"] = d + os.pathsep + path
|
||||
return
|
||||
|
||||
|
||||
SF_TARGET_DIRS = [
|
||||
"/opt/daw_engine/soundfonts",
|
||||
@@ -193,8 +214,8 @@ class SoundFontConverter:
|
||||
# OGG loop pointers are relative to the individual decompressed sample
|
||||
new_sloop = (startloop - start) if (startloop > start and startloop <= end) else 0
|
||||
new_eloop = (endloop - start) if (endloop > start and endloop <= end) else 0
|
||||
# Mark the sample as Ogg Vorbis compressed (FLUID_SAMPLETYPE_OGG_VORBIS = 0x20)
|
||||
new_stype = sampletype | 0x20
|
||||
# Mark the sample as Ogg Vorbis compressed (FLUID_SAMPLETYPE_OGG_VORBIS = 0x10)
|
||||
new_stype = sampletype | 0x10
|
||||
new_shdr += data[base:base + 20] # sample name
|
||||
new_shdr += struct.pack("<IIIIi", new_start, new_end, new_sloop, new_eloop, rate)
|
||||
new_shdr += data[base + 40:base + 44] # originalpitch, correction, samplelink
|
||||
@@ -282,35 +303,28 @@ class SoundFontConverter:
|
||||
"""Verify a SoundFont actually loads and renders audible audio (guards
|
||||
against shipping malformed SF3 files that silently play nothing).
|
||||
|
||||
Uses the low-level CFFI binding (new_fluid_synth / write_float) — the
|
||||
high-level Synth() class does not exist in this binding, so it is never
|
||||
used here.
|
||||
Uses the high-level Synth() binding (sfload/get_samples).
|
||||
"""
|
||||
if not os.path.exists(path):
|
||||
return False
|
||||
_ensure_fluidsynth_runtime()
|
||||
try:
|
||||
import fluidsynth as _fs
|
||||
import numpy as np
|
||||
_settings = _fs.new_fluid_settings()
|
||||
_fl = _fs.new_fluid_synth(_settings)
|
||||
_synth = _fs.Synth()
|
||||
try:
|
||||
h = _fs.fluid_synth_sfload(_fl, path.encode("utf-8"), 1)
|
||||
if h < 0:
|
||||
fid = _synth.sfload(path)
|
||||
if fid == -1:
|
||||
return False
|
||||
_fs.fluid_synth_program_select(_fl, 0, h, 0, 0)
|
||||
_fs.fluid_synth_noteon(_fl, 0, 60, 100)
|
||||
frames = 8820 # 0.2s
|
||||
buf = np.zeros(frames * 2, dtype=np.float32)
|
||||
_fs.fluid_synth_write_float(
|
||||
_fl, frames, buf.ctypes.data, 0, 1,
|
||||
buf.ctypes.data + frames * 4, 0, 1
|
||||
)
|
||||
_fs.fluid_synth_noteoff(_fl, 0, 60)
|
||||
rms = float(np.sqrt(np.mean(buf ** 2)))
|
||||
_synth.program_select(0, fid, 0, 0)
|
||||
_synth.noteon(0, 60, 100)
|
||||
buf = _synth.get_samples(8820) # int16 stereo, 0.2s
|
||||
_synth.noteoff(0, 60)
|
||||
rms = float(np.sqrt(np.mean((buf.astype(np.float32) / 32768.0) ** 2)))
|
||||
return rms > 1e-4
|
||||
finally:
|
||||
try:
|
||||
_fs.delete_fluid_synth(_fl)
|
||||
_synth.delete()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
@@ -453,7 +467,7 @@ class SoundFontConverter:
|
||||
new_loopstart = loopstart + new_start if (loopstart or loopend) else 0
|
||||
new_loopend = loopend + new_start if (loopstart or loopend) else 0
|
||||
# Clear the Ogg Vorbis flag; keep mono/left/right/linked flags
|
||||
new_stype = sampletype & ~0x20
|
||||
new_stype = sampletype & ~0x10
|
||||
new_shdr += name
|
||||
new_shdr += struct.pack("<IIIIi", new_start, new_end, new_loopstart, new_loopend, rate)
|
||||
new_shdr += data[base + 40:base + 44]
|
||||
|
||||
Reference in New Issue
Block a user