From 013465f27a432d27299b0d29722c3db9cf76b7c8 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Sun, 26 Jul 2026 17:08:40 +0700 Subject: [PATCH] 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. --- app/core/render_engine.py | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/app/core/render_engine.py b/app/core/render_engine.py index 2bdb4b4..a337a4d 100644 --- a/app/core/render_engine.py +++ b/app/core/render_engine.py @@ -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")