diff --git a/app/core/render_engine.py b/app/core/render_engine.py index eb01476..95d4253 100644 --- a/app/core/render_engine.py +++ b/app/core/render_engine.py @@ -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'