fix(engine): replace pyfluidsynth high-level API with low-level CFFI

Export installed pyfluidsynth is actually the low-level ctypes
wrapper (fluidsynth), not the high-level FluidSynth class. The
broken call fluidsynth.FluidSynth() caused silent fallback to
oscillator synth for ALL SoundFont renders. Replace with:
new_fluid_settings / new_fluid_synth / fluid_synth_sfload /
fluid_synth_program_select / fluid_synth_write_s16_stereo.
Also fix synth time advancement: call write_s16_stereo for
silence gaps instead of just advancing a cursor.
This commit is contained in:
2026-07-26 17:08:40 +07:00
parent 1059f67979
commit 013465f27a
+24 -14
View File
@@ -230,10 +230,12 @@ class PythonRenderEngine:
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)
fl.program_select(midi_channel, fid, soundfont_bank, soundfont_program)
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:
@@ -242,25 +244,33 @@ class PythonRenderEngine:
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)
for ev in midi_events:
_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
fl.noteon(midi_channel, note, velocity)
start_s = int(start_sec * self.sample_rate)
dur_s = int(dur_sec * self.sample_rate)
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], sf_total_samples)
actual = end_s - start_s
if actual > 0:
midi_data[:, start_s:end_s] += block[:, :actual]
# 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
fl.delete()
_fs.delete_fluid_synth(_fl)
else:
if not HAS_PYFLUIDSYNTH:
logger.warning("[RenderEngine] pyfluidsynth not available, falling back to oscillator synth")