fix: SF render bridge-first (không fallback synth qua pyfluidsynth gain nhỏ)
This commit is contained in:
+68
-48
@@ -99,6 +99,55 @@ def _find_default_sf2() -> str:
|
||||
pass
|
||||
return ""
|
||||
|
||||
def _render_sf2_pyfluidsynth(sf_path: str, sample_rate: int, bpm: float, midi_channel: int,
|
||||
bank: int, program: int, midi_events) -> np.ndarray:
|
||||
# Fallback pyfluidsynth (RENDER_ENGINE != bridge, hoặc bridge fail).
|
||||
# Bắt buộc synth.gain=1.0: libfluidsynth mặc định 0.2 → âm nhỏ ~5x.
|
||||
import fluidsynth as _fs
|
||||
_settings = _fs.new_fluid_settings()
|
||||
_fs.fluid_settings_setnum(_settings, b'synth.sample-rate', float(sample_rate))
|
||||
_fs.fluid_settings_setnum(_settings, b'synth.gain', 1.0)
|
||||
_fl = _fs.new_fluid_synth(_settings)
|
||||
try:
|
||||
_fid = _fs.fluid_synth_sfload(_fl, sf_path.encode("utf-8"), 1)
|
||||
_fs.fluid_synth_program_select(_fl, midi_channel, _fid, bank, program)
|
||||
beat_sec = 60.0 / bpm
|
||||
total_sec = 0
|
||||
for ev in midi_events:
|
||||
end_sec = (ev.get("start_beat", 0) + ev.get("duration_beats", 1)) * beat_sec
|
||||
if end_sec > total_sec:
|
||||
total_sec = end_sec
|
||||
sf_total_samples = int((total_sec + 1.0) * sample_rate)
|
||||
midi_data = np.zeros((2, sf_total_samples), dtype=np.float32)
|
||||
_cursor = 0
|
||||
for ev in sorted(midi_events, key=lambda e: e.get("start_beat", 0)):
|
||||
note = ev.get("note", 60)
|
||||
velocity = ev.get("velocity", 100)
|
||||
start_beat = ev.get("start_beat", 0.0)
|
||||
dur_beats = ev.get("duration_beats", 1.0)
|
||||
start_sec = start_beat * beat_sec
|
||||
dur_sec = dur_beats * beat_sec
|
||||
start_s = int(start_sec * sample_rate)
|
||||
dur_s = int(dur_sec * sample_rate)
|
||||
# Advance synth time by rendering silence
|
||||
if start_s > _cursor:
|
||||
gap = start_s - _cursor
|
||||
_fs.fluid_synth_write_s16_stereo(_fl, gap)
|
||||
_cursor = start_s
|
||||
# Start note
|
||||
_fs.fluid_synth_noteon(_fl, midi_channel, note, min(velocity, 127))
|
||||
block_s16 = _fs.fluid_synth_write_s16_stereo(_fl, dur_s)
|
||||
_fs.fluid_synth_noteoff(_fl, midi_channel, note)
|
||||
block = block_s16.astype(np.float32).reshape(-1, 2).T / 32768.0
|
||||
end_s = min(_cursor + block.shape[1], sf_total_samples)
|
||||
actual = end_s - _cursor
|
||||
if actual > 0 and block.shape[1] > 0:
|
||||
midi_data[:, _cursor:end_s] += block[:, :actual]
|
||||
_cursor = end_s
|
||||
return midi_data
|
||||
finally:
|
||||
_fs.delete_fluid_synth(_fl)
|
||||
|
||||
def _apply_master_volume_automation(buf, sr, nodes):
|
||||
"""Gap 7: master volume automation lane (master.automation.volume_db).
|
||||
|
||||
@@ -689,51 +738,10 @@ class PythonRenderEngine:
|
||||
logger.warning(f"[RenderEngine] SoundFont not found for {soundfont_id or instrument_id}, trying default")
|
||||
sf_path = _find_default_sf2()
|
||||
|
||||
if sf_path and os.path.exists(sf_path) and HAS_PYFLUIDSYNTH:
|
||||
import fluidsynth as _fs
|
||||
_settings = _fs.new_fluid_settings()
|
||||
_fs.fluid_settings_setnum(_settings, b'synth.sample-rate', float(self.sample_rate))
|
||||
_fl = _fs.new_fluid_synth(_settings)
|
||||
_fid = _fs.fluid_synth_sfload(_fl, sf_path.encode("utf-8"), 1)
|
||||
_fs.fluid_synth_program_select(_fl, midi_channel, _fid, soundfont_bank, soundfont_program)
|
||||
beat_sec = 60.0 / bpm
|
||||
total_sec = 0
|
||||
for ev in midi_events:
|
||||
end_sec = (ev.get("start_beat", 0) + ev.get("duration_beats", 1)) * beat_sec
|
||||
if end_sec > total_sec:
|
||||
total_sec = end_sec
|
||||
sf_total_samples = int((total_sec + 1.0) * self.sample_rate)
|
||||
midi_data = np.zeros((2, sf_total_samples), dtype=np.float32)
|
||||
_cursor = 0
|
||||
for ev in sorted(midi_events, key=lambda e: e.get("start_beat", 0)):
|
||||
note = ev.get("note", 60)
|
||||
velocity = ev.get("velocity", 100)
|
||||
start_beat = ev.get("start_beat", 0.0)
|
||||
dur_beats = ev.get("duration_beats", 1.0)
|
||||
start_sec = start_beat * beat_sec
|
||||
dur_sec = dur_beats * beat_sec
|
||||
start_s = int(start_sec * self.sample_rate)
|
||||
dur_s = int(dur_sec * self.sample_rate)
|
||||
# Advance synth time by rendering silence
|
||||
if start_s > _cursor:
|
||||
gap = start_s - _cursor
|
||||
_fs.fluid_synth_write_s16_stereo(_fl, gap)
|
||||
_cursor = start_s
|
||||
# Start note
|
||||
_fs.fluid_synth_noteon(_fl, midi_channel, note, min(velocity, 127))
|
||||
block_s16 = _fs.fluid_synth_write_s16_stereo(_fl, dur_s)
|
||||
_fs.fluid_synth_noteoff(_fl, midi_channel, note)
|
||||
block = block_s16.astype(np.float32).reshape(-1, 2).T / 32768.0
|
||||
end_s = min(_cursor + block.shape[1], sf_total_samples)
|
||||
actual = end_s - _cursor
|
||||
if actual > 0 and block.shape[1] > 0:
|
||||
midi_data[:, _cursor:end_s] += block[:, :actual]
|
||||
_cursor = end_s
|
||||
synth_buffer = midi_data
|
||||
_fs.delete_fluid_synth(_fl)
|
||||
elif sf_path and os.path.exists(sf_path) and settings.RENDER_ENGINE == "bridge":
|
||||
# Gap 12: native bridge nhúng FluidSynth C API — render SF2/SF3 thật
|
||||
# khi pyfluidsynth thiếu (RENDER_ENGINE=bridge mặc định).
|
||||
if sf_path and os.path.exists(sf_path) and settings.RENDER_ENGINE == "bridge":
|
||||
# Bridge-first: native bridge nhúng FluidSynth C API render SF2/SF3 thật.
|
||||
# Frozen engine HAS_PYFLUIDSYNTH=True (libfluidsynth-3.dll trong _internal)
|
||||
# nhưng pyfluidsynth mặc định synth.gain=0.2 → âm nhỏ; bridge có makeup gain.
|
||||
try:
|
||||
from app.core import native_render
|
||||
out_path, _dur = native_render.render_soundfont_offline(
|
||||
@@ -760,9 +768,21 @@ class PythonRenderEngine:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.warning("[RenderEngine] native soundfont render failed: %s", e)
|
||||
synth_buffer = render_midi_events_to_audio(
|
||||
midi_events=midi_events, sr=self.sample_rate, bpm=bpm, instrument='synth'
|
||||
)
|
||||
if HAS_PYFLUIDSYNTH:
|
||||
synth_buffer = _render_sf2_pyfluidsynth(
|
||||
sf_path, self.sample_rate, bpm, midi_channel,
|
||||
soundfont_bank, soundfont_program, midi_events,
|
||||
)
|
||||
else:
|
||||
synth_buffer = render_midi_events_to_audio(
|
||||
midi_events=midi_events, sr=self.sample_rate, bpm=bpm, instrument='synth'
|
||||
)
|
||||
elif sf_path and os.path.exists(sf_path) and HAS_PYFLUIDSYNTH:
|
||||
# Fallback khi RENDER_ENGINE != bridge: pyfluidsynth với synth.gain=1.0.
|
||||
synth_buffer = _render_sf2_pyfluidsynth(
|
||||
sf_path, self.sample_rate, bpm, midi_channel,
|
||||
soundfont_bank, soundfont_program, midi_events,
|
||||
)
|
||||
else:
|
||||
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