FEAT: thêm nút bypass cho track strip để bypass không qua mastering panel
This commit is contained in:
+30
-1
@@ -10,7 +10,36 @@ from typing import Optional, Dict, Any
|
||||
from app.models.user import get_db_connection
|
||||
from app.config import settings
|
||||
|
||||
SECRET_KEY = os.getenv("SECRET_KEY", "sonicforge_secret_key_super_secure_2026")
|
||||
COOKIE_NAME = "sf_token"
|
||||
X_AUTH_HEADER = "X-Auth-Token"
|
||||
|
||||
def _load_or_create_secret_key() -> str:
|
||||
"""Persistent random SECRET_KEY.
|
||||
|
||||
Priority: env SECRET_KEY > {STORAGE_DIR}/.secret_key (auto-generated on
|
||||
first run). Never falls back to a hardcoded value: a known secret lets
|
||||
anyone forge admin tokens.
|
||||
"""
|
||||
env_key = os.getenv("SECRET_KEY", "").strip()
|
||||
if env_key:
|
||||
return env_key
|
||||
key_file = os.path.join(settings.STORAGE_DIR, ".secret_key")
|
||||
try:
|
||||
os.makedirs(settings.STORAGE_DIR, exist_ok=True)
|
||||
if os.path.exists(key_file):
|
||||
with open(key_file, "r") as f:
|
||||
key = f.read().strip()
|
||||
if len(key) >= 32:
|
||||
return key
|
||||
key = secrets.token_hex(32)
|
||||
with open(key_file, "w") as f:
|
||||
f.write(key)
|
||||
return key
|
||||
except Exception:
|
||||
# Last resort: ephemeral random key (all tokens invalid on restart).
|
||||
return secrets.token_hex(32)
|
||||
|
||||
SECRET_KEY = _load_or_create_secret_key()
|
||||
|
||||
def hash_password(password: str, salt: Optional[str] = None) -> str:
|
||||
"""
|
||||
|
||||
+43
-21
@@ -1,4 +1,4 @@
|
||||
import os, logging
|
||||
import os, logging, math
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
import scipy.signal as signal
|
||||
@@ -87,12 +87,18 @@ class PythonRenderEngine:
|
||||
return url_or_id
|
||||
return url_or_id
|
||||
|
||||
def render_session_container(self, session: dict, section_store: dict, bpm: float, time_sig_num: int, total_samples: int) -> np.ndarray:
|
||||
def render_session_container(self, session: dict, section_store: dict, bpm: float, time_sig_num: int, total_samples: int, _cache: dict = None) -> np.ndarray:
|
||||
session_buffer = np.zeros((2, total_samples), dtype=np.float32)
|
||||
|
||||
# Solo semantics: when any track is soloed, only soloed tracks sound.
|
||||
tracks = session.get("tracks", [])
|
||||
solo_ids = {t.get("id") for t in tracks if t.get("solo")}
|
||||
|
||||
_channel_counter = 0
|
||||
|
||||
for track in session.get("tracks", []):
|
||||
for track in tracks:
|
||||
if solo_ids and track.get("id") not in solo_ids:
|
||||
continue
|
||||
track_type = track.get("type", "AUDIO")
|
||||
track_buffer = np.zeros((2, total_samples), dtype=np.float32)
|
||||
|
||||
@@ -123,8 +129,17 @@ class PythonRenderEngine:
|
||||
try:
|
||||
audio_data, sr = sf.read(resolved_path, dtype='float32')
|
||||
if sr != self.sample_rate:
|
||||
# Resampling fallback if simple, otherwise skip
|
||||
pass
|
||||
# Proper resampling: previously a silent no-op that
|
||||
# played 48kHz audio at the wrong speed/pitch.
|
||||
from scipy.signal import resample_poly
|
||||
g = math.gcd(sr, self.sample_rate)
|
||||
audio_data = resample_poly(
|
||||
audio_data,
|
||||
up=self.sample_rate // g,
|
||||
down=sr // g,
|
||||
axis=-1,
|
||||
)
|
||||
sr = self.sample_rate
|
||||
|
||||
# Handle channel mapping (Mono/Stereo)
|
||||
if len(audio_data.shape) == 1:
|
||||
@@ -148,7 +163,7 @@ class PythonRenderEngine:
|
||||
if actual_len > 0:
|
||||
track_buffer[:, start_sample:write_end] += sliced_audio[:, :actual_len]
|
||||
except Exception as e:
|
||||
print(f"[RenderEngine] Error reading audio file {resolved_path}: {e}")
|
||||
logger.warning("[RenderEngine] Error reading audio file %s: %s", resolved_path, e)
|
||||
|
||||
elif item_type == "MIDI_ITEM":
|
||||
source_data = item.get("source_data", {})
|
||||
@@ -284,21 +299,27 @@ class PythonRenderEngine:
|
||||
actual_len = min(synth_buffer.shape[1], total_samples)
|
||||
track_buffer[:, :actual_len] += synth_buffer[:, :actual_len]
|
||||
except Exception as e:
|
||||
print(f"[RenderEngine] Error rendering MIDI: {e}")
|
||||
logger.warning("[RenderEngine] Error rendering MIDI: %s", e)
|
||||
|
||||
elif item_type == "SECTION_ITEM":
|
||||
source_data = item.get("source_data", {})
|
||||
sec_id = source_data.get("referenced_section_id", "")
|
||||
if sec_id and sec_id in section_store:
|
||||
# Render nested section recursively
|
||||
sec_container = section_store[sec_id]
|
||||
sec_buffer = self.render_session_container(
|
||||
session=sec_container,
|
||||
section_store=section_store,
|
||||
bpm=bpm,
|
||||
time_sig_num=time_sig_num,
|
||||
total_samples=total_samples
|
||||
)
|
||||
# Render nested section recursively, cached per section id
|
||||
# so repeated section instances don't re-render every time.
|
||||
cache = _cache if _cache is not None else {}
|
||||
if sec_id in cache:
|
||||
sec_buffer = cache[sec_id]
|
||||
else:
|
||||
sec_buffer = self.render_session_container(
|
||||
session=section_store[sec_id],
|
||||
section_store=section_store,
|
||||
bpm=bpm,
|
||||
time_sig_num=time_sig_num,
|
||||
total_samples=total_samples,
|
||||
_cache=cache,
|
||||
)
|
||||
cache[sec_id] = sec_buffer
|
||||
|
||||
# Apply non-destructive crop/slicing on section buffer
|
||||
if offset_sample < total_samples:
|
||||
@@ -327,7 +348,7 @@ class PythonRenderEngine:
|
||||
board = Pedalboard([Chorus(rate_hz=1.5, depth=0.25)])
|
||||
track_buffer = board(track_buffer, sample_rate=self.sample_rate)
|
||||
except Exception as e:
|
||||
print(f"[RenderEngine] Pedalboard Chorus failed: {e}")
|
||||
logger.warning("[RenderEngine] Pedalboard Chorus failed: %s", e)
|
||||
else:
|
||||
# Fallback chorus using simple LFO delay modulation in scipy/numpy
|
||||
try:
|
||||
@@ -341,14 +362,14 @@ class PythonRenderEngine:
|
||||
wet[ch, :] = track_buffer[ch, indices]
|
||||
track_buffer = dry + wet * 0.5
|
||||
except Exception as e:
|
||||
print(f"[RenderEngine] Fallback Chorus failed: {e}")
|
||||
logger.warning("[RenderEngine] Fallback Chorus failed: %s", e)
|
||||
elif fx_type == "reverb":
|
||||
if HAS_PEDALBOARD:
|
||||
try:
|
||||
board = Pedalboard([Reverb(room_size=0.5, wet_level=0.4, dry_level=0.6)])
|
||||
track_buffer = board(track_buffer, sample_rate=self.sample_rate)
|
||||
except Exception as e:
|
||||
print(f"[RenderEngine] Pedalboard Reverb failed: {e}")
|
||||
logger.warning("[RenderEngine] Pedalboard Reverb failed: %s", e)
|
||||
else:
|
||||
# Fallback reverb using exponentially decaying noise room impulse response
|
||||
try:
|
||||
@@ -368,7 +389,7 @@ class PythonRenderEngine:
|
||||
wet[ch, :] = conv
|
||||
track_buffer = dry + wet * 0.4
|
||||
except Exception as e:
|
||||
print(f"[RenderEngine] Fallback Reverb failed: {e}")
|
||||
logger.warning("[RenderEngine] Fallback Reverb failed: %s", e)
|
||||
|
||||
# Process track volume
|
||||
if HAS_PEDALBOARD:
|
||||
@@ -410,7 +431,8 @@ class PythonRenderEngine:
|
||||
section_store=section_store,
|
||||
bpm=bpm,
|
||||
time_sig_num=time_sig_num,
|
||||
total_samples=total_samples
|
||||
total_samples=total_samples,
|
||||
_cache={},
|
||||
)
|
||||
|
||||
# Normalization to prevent clipping
|
||||
|
||||
@@ -280,31 +280,37 @@ class SoundFontConverter:
|
||||
@staticmethod
|
||||
def _sf3_plays_audio(path: str) -> bool:
|
||||
"""Verify a SoundFont actually loads and renders audible audio (guards
|
||||
against shipping malformed SF3 files that silently play nothing)."""
|
||||
against shipping malformed SF3 files that silently play nothing).
|
||||
|
||||
Uses the low-level CFFI binding (new_fluid_synth / write_float) — the
|
||||
high-level Synth() class does not exist in this binding, so it is never
|
||||
used here.
|
||||
"""
|
||||
if not os.path.exists(path):
|
||||
return False
|
||||
try:
|
||||
import fluidsynth
|
||||
import fluidsynth as _fs
|
||||
import numpy as np
|
||||
fl = fluidsynth.Synth()
|
||||
_settings = _fs.new_fluid_settings()
|
||||
_fl = _fs.new_fluid_synth(_settings)
|
||||
try:
|
||||
h = fl.sfload(path)
|
||||
h = _fs.fluid_synth_sfload(_fl, path.encode("utf-8"), 1)
|
||||
if h < 0:
|
||||
return False
|
||||
fl.program_select(0, h, 0, 0)
|
||||
fl.noteon(0, 60, 100)
|
||||
_fs.fluid_synth_program_select(_fl, 0, h, 0, 0)
|
||||
_fs.fluid_synth_noteon(_fl, 0, 60, 100)
|
||||
frames = 8820 # 0.2s
|
||||
buf = np.zeros(frames * 2, dtype=np.float32)
|
||||
fluidsynth._fl.fluid_synth_write_float(
|
||||
fl.synth, frames, buf.ctypes.data, 0, 1,
|
||||
_fs.fluid_synth_write_float(
|
||||
_fl, frames, buf.ctypes.data, 0, 1,
|
||||
buf.ctypes.data + frames * 4, 0, 1
|
||||
)
|
||||
fl.noteoff(0, 60)
|
||||
_fs.fluid_synth_noteoff(_fl, 0, 60)
|
||||
rms = float(np.sqrt(np.mean(buf ** 2)))
|
||||
return rms > 1e-4
|
||||
finally:
|
||||
try:
|
||||
fl.delete()
|
||||
_fs.delete_fluid_synth(_fl)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
|
||||
+59
-39
@@ -2,7 +2,7 @@
|
||||
import os
|
||||
import numpy as np
|
||||
import functools
|
||||
from ctypes import c_int, c_char_p, c_void_p
|
||||
from ctypes import c_char_p
|
||||
|
||||
def midi_note_to_freq(note_number: int) -> float:
|
||||
return 440.0 * (2.0 ** ((note_number - 69) / 12.0))
|
||||
@@ -110,7 +110,12 @@ def get_plugin_manager(vst_dir="/opt/daw_engine/vst3", sf_dir="/opt/daw_engine/s
|
||||
return _PLUGIN_MANAGER_INSTANCE
|
||||
|
||||
def load_soundfont_cached(path: str):
|
||||
"""Return a cached FluidSynth instance for path, incrementing refcount."""
|
||||
"""Return a cached low-level FluidSynth instance for path, incrementing refcount.
|
||||
|
||||
Uses the CFFI binding API (new_fluid_synth / fluid_synth_sfload) — the same
|
||||
API render_engine relies on. The high-level `FluidSynth()`/`Synth()` classes
|
||||
do not exist in this binding, so they are never used here.
|
||||
"""
|
||||
global _FLUID_CACHE
|
||||
if not HAS_PYFLUIDSYNTH:
|
||||
return None
|
||||
@@ -119,10 +124,15 @@ def load_soundfont_cached(path: str):
|
||||
_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)
|
||||
import fluidsynth as _fs
|
||||
_settings = _fs.new_fluid_settings()
|
||||
_fs.fluid_settings_setnum(_settings, b'synth.sample-rate', 44100.0)
|
||||
fl = _fs.new_fluid_synth(_settings)
|
||||
font_id = _fs.fluid_synth_sfload(fl, path.encode("utf-8"), 1)
|
||||
if font_id < 0:
|
||||
_fs.delete_fluid_synth(fl)
|
||||
return None
|
||||
_fs.fluid_synth_program_select(fl, 0, font_id, 0, 0)
|
||||
_FLUID_CACHE[path] = (fl, 1)
|
||||
return fl
|
||||
except Exception:
|
||||
@@ -136,7 +146,8 @@ def release_soundfont(path: str):
|
||||
fl, ref = _FLUID_CACHE[path]
|
||||
if ref <= 1:
|
||||
try:
|
||||
fl.delete()
|
||||
import fluidsynth as _fs
|
||||
_fs.delete_fluid_synth(fl)
|
||||
except Exception:
|
||||
pass
|
||||
del _FLUID_CACHE[path]
|
||||
@@ -245,38 +256,47 @@ 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 = []
|
||||
_fl = fluidsynth._fl
|
||||
_fl.fluid_synth_get_sfont_by_id.restype = c_void_p
|
||||
_fl.fluid_preset_get_name.restype = c_char_p
|
||||
_fl.fluid_sfont_get_preset.restype = c_void_p
|
||||
sfont_ptr = _fl.fluid_synth_get_sfont_by_id(c_void_p(fl.synth), c_int(fid))
|
||||
if sfont_ptr:
|
||||
for bank in range(0, 2):
|
||||
for prog_num in range(0, 128):
|
||||
try:
|
||||
preset = fluidsynth.fluid_sfont_get_preset(sfont_ptr, c_int(bank), c_int(prog_num))
|
||||
except Exception:
|
||||
break
|
||||
if preset:
|
||||
name_ptr = fluidsynth.fluid_preset_get_name(preset)
|
||||
if name_ptr:
|
||||
name_val = c_char_p(name_ptr).value
|
||||
if name_val:
|
||||
presets.append({
|
||||
"bank": bank,
|
||||
"program": prog_num,
|
||||
"name": name_val.decode("utf-8", errors="replace")
|
||||
})
|
||||
fl.delete()
|
||||
_SF_INSTRUMENTS_CACHE[sf_id] = presets[:256]
|
||||
return presets[:256]
|
||||
import fluidsynth as _fs
|
||||
# Low-level CFFI API (same as render_engine); never use
|
||||
# the high-level Synth() class that this binding lacks.
|
||||
_settings = _fs.new_fluid_settings()
|
||||
_synth = _fs.new_fluid_synth(_settings)
|
||||
try:
|
||||
fid = _fs.fluid_synth_sfload(_synth, path.encode("utf-8"), 1)
|
||||
if fid < 0:
|
||||
continue
|
||||
sfont = _fs.fluid_synth_get_sfont_by_id(_synth, fid)
|
||||
presets = []
|
||||
if sfont:
|
||||
for bank in range(0, 2):
|
||||
for prog_num in range(0, 128):
|
||||
try:
|
||||
preset = _fs.fluid_sfont_get_preset(sfont, bank, prog_num)
|
||||
except Exception:
|
||||
break
|
||||
if preset:
|
||||
try:
|
||||
name_ptr = _fs.fluid_preset_get_name(preset)
|
||||
if name_ptr:
|
||||
if hasattr(_fs, "ffi"):
|
||||
raw = _fs.ffi.string(name_ptr)
|
||||
else:
|
||||
raw = c_char_p(name_ptr).value
|
||||
if raw:
|
||||
presets.append({
|
||||
"bank": bank,
|
||||
"program": prog_num,
|
||||
"name": raw.decode("utf-8", errors="replace")
|
||||
})
|
||||
except Exception:
|
||||
continue
|
||||
_SF_INSTRUMENTS_CACHE[sf_id] = presets[:256]
|
||||
return presets[:256]
|
||||
finally:
|
||||
try:
|
||||
_fs.delete_fluid_synth(_synth)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
import traceback; traceback.print_exc()
|
||||
_SF_INSTRUMENTS_CACHE[sf_id] = []
|
||||
|
||||
Reference in New Issue
Block a user