import os, uuid, json, tempfile from fastapi import APIRouter, HTTPException, Depends, UploadFile, File from pydantic import BaseModel from typing import Optional, Any from app.config import settings from app.core.vst_engine import PluginManager, HAS_PEDALBOARD, HAS_PYFLUIDSYNTH from app.core.render_engine import PythonRenderEngine from app.api.v1.auth import get_current_user router = APIRouter() UPLOAD_SF_DIR = os.path.join(settings.STORAGE_DIR, "soundfonts") os.makedirs(UPLOAD_SF_DIR, exist_ok=True) @router.get("/available") async def list_plugins(current_user: dict = Depends(get_current_user)): pm = PluginManager(upload_sf_dir=UPLOAD_SF_DIR) return pm.list_available() @router.get("/default-soundfonts") async def list_default_soundfonts(): static_sf_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "static", "soundfonts") result = [] if os.path.isdir(static_sf_dir): for f in os.listdir(static_sf_dir): if f.endswith(".sf2") or f.endswith(".sf3"): result.append({ "id": os.path.splitext(f)[0], "name": f, "file": f, "url": f"/soundfonts/{f}" }) return result @router.get("/soundfont-instruments/{sf_id}") async def list_soundfont_instruments(sf_id: str, current_user: dict = Depends(get_current_user)): pm = PluginManager(upload_sf_dir=UPLOAD_SF_DIR) presets = pm.list_soundfont_instruments(sf_id) return {"presets": presets, "count": len(presets)} @router.post("/upload-soundfont") async def upload_soundfont( file: UploadFile = File(...), current_user: dict = Depends(get_current_user) ): if not (file.filename and (file.filename.endswith(".sf2") or file.filename.endswith(".sf3"))): raise HTTPException(status_code=400, detail="Only .sf2 / .sf3 files are allowed") contents = await file.read() if not PluginManager.validate_sf2_header(contents): raise HTTPException(status_code=400, detail="Invalid SoundFont file: missing RIFF/sfbk header") file_ext = os.path.splitext(file.filename)[1] file_uuid = str(uuid.uuid4()) # Store original name in a sidecar file base_name = os.path.splitext(file.filename)[0].replace('/', '_').replace('\\', '_') file_id = file_uuid + file_ext dest_path = os.path.join(UPLOAD_SF_DIR, file_id) with open(dest_path, "wb") as f: f.write(contents) # Save metadata with original name meta_path = os.path.join(UPLOAD_SF_DIR, file_uuid + ".meta") with open(meta_path, "w", encoding="utf-8") as f: import json json.dump({"original_name": file.filename, "uuid": file_uuid, "file": file_id}, f) return {"id": file_id, "name": file.filename, "path": dest_path, "size_bytes": len(contents)} @router.delete("/soundfont/{sf_id}") async def delete_soundfont(sf_id: str, current_user: dict = Depends(get_current_user)): base_id = sf_id.replace("sf_", "") deleted = False for d in [UPLOAD_SF_DIR, os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "static", "soundfonts")]: if not os.path.isdir(d): continue for f in os.listdir(d): if os.path.splitext(f)[0] == base_id: path = os.path.join(d, f) os.remove(path) # Remove associated .meta file meta_path = os.path.join(d, os.path.splitext(f)[0] + ".meta") if os.path.isfile(meta_path): os.remove(meta_path) deleted = True break if deleted: break if not deleted: raise HTTPException(status_code=404, detail="SoundFont not found") return {"deleted": True, "sf_id": sf_id} class RenderRequest(BaseModel): project_json: dict output_filename: Optional[str] = "render_output.wav" @router.post("/render") async def render_project( req: RenderRequest, current_user: dict = Depends(get_current_user) ): engine = PythonRenderEngine() output_path = os.path.join(settings.PROCESSED_DIR, req.output_filename or "render_output.wav") try: result_path = engine.render_project(req.project_json, output_path) return {"url": f"/static/audio/processed/{os.path.basename(result_path)}", "path": result_path} except Exception as e: raise HTTPException(status_code=500, detail=f"Render failed: {str(e)}")