fix: piano roll zoom/scroll + AI copilot + SF cache + auto-scroll drag

This commit is contained in:
2026-07-25 07:37:27 +07:00
parent 1c0d35f69b
commit b04a76ebfb
6 changed files with 406 additions and 101 deletions
+64 -10
View File
@@ -1,6 +1,7 @@
# SonicForge Studio VST / VSTi Engine Service
import os
import numpy as np
import functools
from ctypes import c_int, c_char_p, c_void_p
def midi_note_to_freq(note_number: int) -> float:
@@ -92,11 +93,62 @@ if HAS_PYFLUIDSYNTH:
HAS_PYFLUIDSYNTH = False
# ── Module-level caches ──
_FLUID_CACHE = {} # path → (fluidsynth.FluidSynth, refcount)
_PLUGIN_MANAGER_INSTANCE = None
_PLUGIN_MANAGER_ARGS = None
_SF_INSTRUMENTS_CACHE = {} # sf_id → list[presets]
def get_plugin_manager(vst_dir="/opt/daw_engine/vst3", sf_dir="/opt/daw_engine/soundfonts", upload_sf_dir=None) -> "PluginManager":
"""Singleton: reuse PluginManager when args match, else create new."""
global _PLUGIN_MANAGER_INSTANCE, _PLUGIN_MANAGER_ARGS
args = (vst_dir, sf_dir, upload_sf_dir)
if _PLUGIN_MANAGER_INSTANCE is not None and _PLUGIN_MANAGER_ARGS == args:
return _PLUGIN_MANAGER_INSTANCE
_PLUGIN_MANAGER_ARGS = args
_PLUGIN_MANAGER_INSTANCE = PluginManager(vst_dir, sf_dir, upload_sf_dir)
return _PLUGIN_MANAGER_INSTANCE
def load_soundfont_cached(path: str):
"""Return a cached FluidSynth instance for path, incrementing refcount."""
global _FLUID_CACHE
if not HAS_PYFLUIDSYNTH:
return None
if path in _FLUID_CACHE:
fl, ref = _FLUID_CACHE[path]
_FLUID_CACHE[path] = (fl, ref + 1)
return fl
try:
import fluidsynth
fl = fluidsynth.FluidSynth(sample_rate=44100, gain=0.5)
font_id = fl.sfload(path)
fl.program_select(0, font_id, 0, 0)
_FLUID_CACHE[path] = (fl, 1)
return fl
except Exception:
return None
def release_soundfont(path: str):
"""Decrement refcount; delete FluidSynth when count reaches 0."""
global _FLUID_CACHE
if path not in _FLUID_CACHE:
return
fl, ref = _FLUID_CACHE[path]
if ref <= 1:
try:
fl.delete()
except Exception:
pass
del _FLUID_CACHE[path]
else:
_FLUID_CACHE[path] = (fl, ref - 1)
class PluginManager:
def __init__(self, vst_dir="/opt/daw_engine/vst3", sf_dir="/opt/daw_engine/soundfonts", upload_sf_dir=None):
self.vst_dir = vst_dir
self.sf_dir = sf_dir
self.upload_sf_dir = upload_sf_dir
self._sf_scan_cache = None # cache for _scan_soundfonts()
def _scan_plugins(self) -> dict:
plugins = {}
@@ -160,20 +212,20 @@ class PluginManager:
pass
return vst
def _scan_soundfonts_cached(self):
if self._sf_scan_cache is not None:
return self._sf_scan_cache
self._sf_scan_cache = self._scan_soundfonts()
return self._sf_scan_cache
def load_soundfont(self, path: str):
if not HAS_PYFLUIDSYNTH:
return None
try:
fl = fluidsynth.FluidSynth(sample_rate=44100, gain=0.5)
font_id = fl.sfload(path)
fl.program_select(0, font_id, 0, 0)
return fl
except Exception:
return None
return load_soundfont_cached(path)
def list_soundfont_instruments(self, sf_id: str):
if not ensure_pyfluidsynth():
return []
if sf_id in _SF_INSTRUMENTS_CACHE:
return _SF_INSTRUMENTS_CACHE[sf_id]
search_dirs = []
if os.path.isdir(self.sf_dir):
search_dirs.append(self.sf_dir)
@@ -187,13 +239,13 @@ class PluginManager:
if base == sf_id or base == sf_id.replace("sf_", ""):
path = os.path.join(d, f)
try:
import fluidsynth
fl = fluidsynth.Synth()
fid = fl.sfload(path)
if fid < 0:
fl.delete()
continue
presets = []
# Use fluid_sfont_get_preset + fluid_preset_get_name C API
_fl = fluidsynth._fl
_fl.fluid_synth_get_sfont_by_id.restype = c_void_p
_fl.fluid_preset_get_name.restype = c_char_p
@@ -217,9 +269,11 @@ class PluginManager:
"name": name_val.decode("utf-8", errors="replace")
})
fl.delete()
_SF_INSTRUMENTS_CACHE[sf_id] = presets[:256]
return presets[:256]
except Exception:
import traceback; traceback.print_exc()
_SF_INSTRUMENTS_CACHE[sf_id] = []
return []
def list_available(self) -> dict: