# SonicForge Studio VST / VSTi Engine Service import os import numpy as np def midi_note_to_freq(note_number: int) -> float: return 440.0 * (2.0 ** ((note_number - 69) / 12.0)) def render_midi_events_to_audio(midi_events: list, sr: int = 44100, bpm: float = 120.0, instrument: str = 'synth') -> np.ndarray: beat_duration_sec = 60.0 / max(30.0, bpm) max_duration_sec = 2.0 for event in midi_events: start_beat = event.get('start_beat', 0.0) dur_beats = event.get('duration_beats', 1.0) end_sec = (start_beat + dur_beats) * beat_duration_sec if end_sec > max_duration_sec: max_duration_sec = end_sec total_samples = int((max_duration_sec + 0.5) * sr) out_l = np.zeros(total_samples, dtype=np.float32) out_r = np.zeros(total_samples, dtype=np.float32) for event in midi_events: note = event.get('note', 60) velocity = event.get('velocity', 100) / 127.0 start_beat = event.get('start_beat', 0.0) dur_beats = event.get('duration_beats', 1.0) start_sample = int(start_beat * beat_duration_sec * sr) dur_samples = int(dur_beats * beat_duration_sec * sr) end_sample = min(total_samples, start_sample + dur_samples) actual_len = end_sample - start_sample if actual_len <= 0 or start_sample >= total_samples: continue freq = midi_note_to_freq(note) t = np.arange(actual_len) / float(sr) tone = 0.6 * np.sin(2 * np.pi * freq * t) + 0.3 * np.sin(2 * np.pi * freq * 2 * t) + 0.1 * np.sin(2 * np.pi * freq * 3 * t) attack = min(int(0.01 * sr), actual_len // 4) release = min(int(0.05 * sr), actual_len // 4) env = np.ones(actual_len, dtype=np.float32) if attack > 0: env[:attack] = np.linspace(0.0, 1.0, attack) if release > 0: env[-release:] = np.linspace(1.0, 0.0, release) signal = tone * env * velocity out_l[start_sample:end_sample] += signal out_r[start_sample:end_sample] += signal max_peak = max(np.max(np.abs(out_l)), np.max(np.abs(out_r))) if max_peak > 1.0: out_l /= max_peak out_r /= max_peak return np.vstack([out_l, out_r]) def check_pedalboard_safe(): import subprocess, sys try: res = subprocess.run( [sys.executable, "-c", "import pedalboard"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=2.0 ) return res.returncode == 0 except Exception: return False def check_pyfluidsynth_safe(): import subprocess, sys try: res = subprocess.run( [sys.executable, "-c", "import fluidsynth"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=2.0 ) return res.returncode == 0 except Exception: return False HAS_PEDALBOARD = check_pedalboard_safe() HAS_PYFLUIDSYNTH = check_pyfluidsynth_safe() if HAS_PEDALBOARD: try: from pedalboard import VST3Plugin, Pedalboard, Gain, MidiMessage except Exception: HAS_PEDALBOARD = False if HAS_PYFLUIDSYNTH: try: import fluidsynth except Exception: HAS_PYFLUIDSYNTH = False 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 def _scan_plugins(self) -> dict: plugins = {} if not os.path.isdir(self.vst_dir): return plugins for root, dirs, files in os.walk(self.vst_dir): for file in files: if file.endswith(".vst3") or file.endswith(".so"): plugin_path = os.path.join(root, file) plugin_name = os.path.splitext(file)[0] plugins[plugin_name] = plugin_path return plugins def _scan_soundfonts(self) -> list: sfonts = [] dirs = [self.sf_dir] if self.upload_sf_dir and self.upload_sf_dir != self.sf_dir: dirs.append(self.upload_sf_dir) for d in dirs: if not os.path.isdir(d): continue for f in os.listdir(d): if f.endswith(".sf2") or f.endswith(".sf3"): sfonts.append({"id": os.path.splitext(f)[0], "name": f, "file": f}) return sfonts def load_vst(self, plugin_name: str, preset_data: dict = None): if not HAS_PEDALBOARD: return None plugins = self._scan_plugins() if plugin_name not in plugins: return None path = plugins[plugin_name] vst = VST3Plugin(path) if preset_data: for k, v in preset_data.items(): try: setattr(vst, k, v) except Exception: pass return vst 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 def list_available(self) -> dict: return { "vst_instruments": [ {"id": k, "name": k, "type": "VST3", "has_native_support": HAS_PEDALBOARD} for k in self._scan_plugins().keys() ], "soundfonts": self._scan_soundfonts() } @staticmethod def midi_events_to_messages(midi_events: list, bpm: float, sr: int) -> list: if not HAS_PEDALBOARD: return [] beat_duration_sec = 60.0 / max(30.0, bpm) messages = [] for ev in midi_events: 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_duration_sec dur_sec = dur_beats * beat_duration_sec sample_offset = int(start_sec * sr) end_sample_offset = int((start_sec + dur_sec) * sr) messages.append(MidiMessage(note_on=note, velocity=velocity, sample_offset=sample_offset)) messages.append(MidiMessage(note_off=note, velocity=0, sample_offset=end_sample_offset)) return messages @staticmethod def validate_sf2_header(data: bytes) -> bool: if len(data) < 12: return False if data[0:4] != b'RIFF': return False if data[8:12] != b'sfbk': return False return True