fix: SF render bridge-first (không fallback synth qua pyfluidsynth gain nhỏ)

This commit is contained in:
2026-08-25 12:01:17 +07:00
parent ebf0828c55
commit 6cee46d82a
+68 -48
View File
@@ -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'