feat(engine): fix SF2 path resolution, add synth_engine struct and fallback chain
- Fix SF2 path: search upload dir, system dir, static dir (backward compat) - Add synth_engine struct parsing with flat field fallback - Add 3-level fallback: selected SF -> default SF -> oscillator synth - Write synth_engine in setTrackInstrument UI setters - Pass synth_engine context to client SoundFontPlayer and AI gateway
This commit is contained in:
+60
-17
@@ -1,4 +1,4 @@
|
||||
import os
|
||||
import os, logging
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
import scipy.signal as signal
|
||||
@@ -11,6 +11,43 @@ from app.core.vst_engine import (
|
||||
HAS_PYFLUIDSYNTH,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
UPLOAD_SF_DIR = os.path.join(settings.STORAGE_DIR, "soundfonts")
|
||||
SYSTEM_SF_DIR = "/opt/daw_engine/soundfonts"
|
||||
|
||||
SYS_SOUNDFONTS = [
|
||||
("GeneralUser_GS.sf2", "GeneralUser GS"),
|
||||
("SGM_v2.01.sf2", "SGM v2.01"),
|
||||
("SGM-V2.01.sf2", "SGM v2.01"),
|
||||
]
|
||||
|
||||
def _find_sf2_path(sf_id: str) -> str:
|
||||
clean_id = sf_id.replace("sf_", "") if sf_id.startswith("sf_") else sf_id
|
||||
for base_dir in [UPLOAD_SF_DIR, SYSTEM_SF_DIR]:
|
||||
if not os.path.isdir(base_dir):
|
||||
continue
|
||||
for fname in os.listdir(base_dir):
|
||||
fbase, fext = os.path.splitext(fname)
|
||||
if fext.lower() in (".sf2", ".sf3") and (fbase == clean_id or fbase == sf_id):
|
||||
return os.path.join(base_dir, fname)
|
||||
# Backward compat: static/soundfonts
|
||||
static_dir = os.path.join(settings.APP_DIR, "static", "soundfonts")
|
||||
if os.path.isdir(static_dir):
|
||||
for fname in os.listdir(static_dir):
|
||||
fbase, fext = os.path.splitext(fname)
|
||||
if fext.lower() in (".sf2", ".sf3") and (fbase == clean_id or fbase == sf_id):
|
||||
return os.path.join(static_dir, fname)
|
||||
return ""
|
||||
|
||||
def _find_default_sf2() -> str:
|
||||
for sf_name, _ in SYS_SOUNDFONTS:
|
||||
for base_dir in [SYSTEM_SF_DIR, UPLOAD_SF_DIR]:
|
||||
p = os.path.join(base_dir, sf_name)
|
||||
if os.path.exists(p):
|
||||
return p
|
||||
return ""
|
||||
|
||||
if HAS_PEDALBOARD:
|
||||
try:
|
||||
from pedalboard import Pedalboard, Gain, Chorus, Reverb
|
||||
@@ -58,8 +95,13 @@ class PythonRenderEngine:
|
||||
track_type = track.get("type", "AUDIO")
|
||||
track_buffer = np.zeros((2, total_samples), dtype=np.float32)
|
||||
|
||||
soundfont_bank = track.get("soundfont_bank", 0)
|
||||
soundfont_program = track.get("soundfont_program", 0)
|
||||
# Parse synth_engine struct (Task C) — fall back to flat fields
|
||||
se = track.get("synth_engine", {}) or {}
|
||||
instrument_id = se.get("plugin_id") or track.get("instrument_id", "") or track.get("instrument", "")
|
||||
instrument_source = se.get("type") or track.get("instrument_source", "soundfont")
|
||||
soundfont_bank = se.get("soundfont_bank") if se.get("soundfont_bank") is not None else track.get("soundfont_bank", 0)
|
||||
soundfont_program = se.get("soundfont_program") if se.get("soundfont_program") is not None else track.get("soundfont_program", 0)
|
||||
soundfont_id = se.get("soundfont_id") or track.get("soundfont_id", "")
|
||||
is_percussion = track.get("is_percussion", False) or (soundfont_bank == 128)
|
||||
midi_channel = 9 if is_percussion else (_channel_counter % 9)
|
||||
if not is_percussion:
|
||||
@@ -130,12 +172,9 @@ class PythonRenderEngine:
|
||||
|
||||
if midi_events:
|
||||
try:
|
||||
instrument_id = track.get("instrument_id", "") or track.get("instrument", "")
|
||||
plugin_mgr = PluginManager()
|
||||
vst = plugin_mgr.load_vst(instrument_id) if instrument_id else None
|
||||
|
||||
instrument_source = track.get("instrument_source", "soundfont")
|
||||
|
||||
if instrument_source == "pianobook":
|
||||
dspreset_path = track.get("dspreset_path", "")
|
||||
if dspreset_path and os.path.exists(dspreset_path) and HAS_PEDALBOARD:
|
||||
@@ -157,7 +196,7 @@ class PythonRenderEngine:
|
||||
board = Pedalboard([vst])
|
||||
synth_buffer = board(silent, sample_rate=self.sample_rate, midi_messages=midi_messages)
|
||||
except Exception as e:
|
||||
print(f"[RenderEngine] DecentSampler/Pianobook error: {e}")
|
||||
logger.warning(f"[RenderEngine] DecentSampler/Pianobook error: {e}")
|
||||
synth_buffer = render_midi_events_to_audio(
|
||||
midi_events=midi_events, sr=self.sample_rate, bpm=bpm, instrument='synth'
|
||||
)
|
||||
@@ -167,7 +206,6 @@ class PythonRenderEngine:
|
||||
)
|
||||
elif vst and HAS_PEDALBOARD:
|
||||
from pedalboard import Pedalboard
|
||||
# Convert MIDI events with precise sample offset
|
||||
midi_messages = PluginManager.midi_events_to_messages(
|
||||
midi_events, bpm, self.sample_rate,
|
||||
bank=soundfont_bank, program=soundfont_program
|
||||
@@ -182,12 +220,15 @@ class PythonRenderEngine:
|
||||
silent = np.zeros((2, total_needed), dtype=np.float32)
|
||||
board = Pedalboard([vst])
|
||||
synth_buffer = board(silent, sample_rate=self.sample_rate, midi_messages=midi_messages)
|
||||
elif instrument_id and instrument_id.startswith("sf_"):
|
||||
sf_path = os.path.join(
|
||||
os.path.dirname(os.path.dirname(__file__)), "static", "soundfonts",
|
||||
instrument_id.replace("sf_", "") + ".sf2"
|
||||
)
|
||||
if os.path.exists(sf_path) and HAS_PYFLUIDSYNTH:
|
||||
elif instrument_id and (instrument_id.startswith("sf_") or soundfont_id):
|
||||
sf_path = _find_sf2_path(soundfont_id or instrument_id)
|
||||
# 3-level fallback: selected SF → default SF → oscillator synth
|
||||
if not sf_path or not os.path.exists(sf_path) or not HAS_PYFLUIDSYNTH:
|
||||
if not sf_path or not os.path.exists(sf_path):
|
||||
logger.warning(f"[RenderEngine] SoundFont not found for {soundfont_id or instrument_id}, trying default")
|
||||
sf_path = _find_default_sf2() if HAS_PYFLUIDSYNTH else ""
|
||||
|
||||
if sf_path and os.path.exists(sf_path) and HAS_PYFLUIDSYNTH:
|
||||
import fluidsynth
|
||||
fl = fluidsynth.FluidSynth(sample_rate=self.sample_rate, gain=0.5)
|
||||
fid = fl.sfload(sf_path)
|
||||
@@ -198,8 +239,8 @@ class PythonRenderEngine:
|
||||
end_sec = (ev.get("start_beat", 0) + ev.get("duration_beats", 1)) * beat_sec
|
||||
if end_sec > total_sec:
|
||||
total_sec = end_sec
|
||||
total_samples = int((total_sec + 1.0) * self.sample_rate)
|
||||
midi_data = np.zeros((2, total_samples), dtype=np.float32)
|
||||
sf_total_samples = int((total_sec + 1.0) * self.sample_rate)
|
||||
midi_data = np.zeros((2, sf_total_samples), dtype=np.float32)
|
||||
for ev in midi_events:
|
||||
note = ev.get("note", 60)
|
||||
velocity = ev.get("velocity", 100)
|
||||
@@ -213,13 +254,15 @@ class PythonRenderEngine:
|
||||
block = fl.get_samples(int(dur_s)) if hasattr(fl, 'get_samples') else np.zeros((2, dur_s), dtype=np.float32)
|
||||
fl.noteoff(midi_channel, note)
|
||||
if block.shape[1] > 0:
|
||||
end_s = min(start_s + block.shape[1], total_samples)
|
||||
end_s = min(start_s + block.shape[1], sf_total_samples)
|
||||
actual = end_s - start_s
|
||||
if actual > 0:
|
||||
midi_data[:, start_s:end_s] += block[:, :actual]
|
||||
synth_buffer = midi_data
|
||||
fl.delete()
|
||||
else:
|
||||
if not HAS_PYFLUIDSYNTH:
|
||||
logger.warning("[RenderEngine] pyfluidsynth not available, falling back to oscillator synth")
|
||||
synth_buffer = render_midi_events_to_audio(
|
||||
midi_events=midi_events, sr=self.sample_rate, bpm=bpm, instrument='synth'
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user