fix: nhãn của soundfont hiển thị sai và không load được instrument

This commit is contained in:
2026-07-23 22:32:19 +07:00
parent 1a28ebabee
commit 6a17e7036c
14 changed files with 259 additions and 111 deletions
+1
View File
@@ -18,6 +18,7 @@ RUN apt-get update && apt-get install -y \
xvfb \
ffmpeg \
libsndfile1 \
libfluidsynth3 \
build-essential \
&& rm -rf /var/lib/apt/lists/*
+24
View File
@@ -72,6 +72,30 @@ async def upload_soundfont(
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"
+46 -17
View File
@@ -1,6 +1,7 @@
# SonicForge Studio VST / VSTi Engine Service
import os
import numpy as np
from ctypes import c_int, c_char_p, c_void_p
def midi_note_to_freq(note_number: int) -> float:
return 440.0 * (2.0 ** ((note_number - 69) / 12.0))
@@ -72,6 +73,12 @@ def check_pyfluidsynth_safe():
HAS_PEDALBOARD = check_pedalboard_safe()
HAS_PYFLUIDSYNTH = check_pyfluidsynth_safe()
def ensure_pyfluidsynth():
global HAS_PYFLUIDSYNTH
if not HAS_PYFLUIDSYNTH:
HAS_PYFLUIDSYNTH = check_pyfluidsynth_safe()
return HAS_PYFLUIDSYNTH
if HAS_PEDALBOARD:
try:
from pedalboard import VST3Plugin, Pedalboard, Gain, MidiMessage
@@ -165,32 +172,54 @@ class PluginManager:
return None
def list_soundfont_instruments(self, sf_id: str):
if not HAS_PYFLUIDSYNTH:
if not ensure_pyfluidsynth():
return []
dirs = [self.sf_dir]
if self.upload_sf_dir:
dirs.append(self.upload_sf_dir)
for d in dirs:
search_dirs = []
if os.path.isdir(self.sf_dir):
search_dirs.append(self.sf_dir)
if self.upload_sf_dir and os.path.isdir(self.upload_sf_dir) and self.upload_sf_dir != self.sf_dir:
search_dirs.append(self.upload_sf_dir)
for d in search_dirs:
for f in os.listdir(d):
if not (f.endswith(".sf2") or f.endswith(".sf3")):
continue
base = os.path.splitext(f)[0]
if base == sf_id or base == sf_id.replace("sf_", ""):
path = os.path.join(d, f)
try:
fl = fluidsynth.FluidSynth(sample_rate=44100, gain=0.2)
fl = fluidsynth.Synth()
fid = fl.sfload(path)
count = fl.sfont_get_preset_count(fid)
presets = []
for pi in range(count):
try:
name = fl.sfont_get_preset_name(fid, pi)
bank, prog = fl.sfont_get_preset_bank_num(fid, pi), fl.sfont_get_preset_prog_num(fid, pi)
presets.append({"bank": bank, "program": prog, "name": name})
except Exception:
pass
if fid < 0:
fl.delete()
return presets
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
_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:
pass
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()
return presets[:256]
except Exception:
import traceback; traceback.print_exc()
return []
def list_available(self) -> dict:
+146 -62
View File
@@ -3137,6 +3137,7 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => {
if (!isOpen) return null;
const [localData, setLocalData] = React.useState(pluginsData);
const [sfUploadStatus, setSfUploadStatus] = React.useState('');
const [sfToDelete, setSfToDelete] = React.useState(null);
React.useEffect(() => {
if (isOpen) {
window.SonicAPI.listPlugins()
@@ -3245,19 +3246,7 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => {
),
React.createElement('div', { className: 'flex items-center gap-2' },
React.createElement('button', {
onClick: async () => {
if (!confirm('Delete SoundFont ' + (sf.display || sf.name) + '?')) return;
try {
if (window.SonicAPI.deleteSoundFont) {
await window.SonicAPI.deleteSoundFont(sf.id);
}
const data = await window.SonicAPI.listPlugins();
setLocalData(data);
window.showToast && window.showToast('SoundFont deleted.', 'info');
} catch (err) {
window.showToast && window.showToast('Delete failed: ' + err.message, 'error');
}
},
onClick: () => setSfToDelete(sf),
className: 'text-[10px] text-zinc-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition px-2 py-1'
}, 'Delete')
)
@@ -3328,6 +3317,43 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => {
},
React.createElement('span', null, 'VST: ', localData?.vst_instruments?.length || 0, ' | SoundFonts: ', localData?.soundfonts?.length || 0),
React.createElement('span', null, 'Last scanned: ', new Date().toLocaleTimeString())
),
// Delete confirmation modal
sfToDelete && React.createElement('div', {
className: 'fixed inset-0 z-[60] flex items-center justify-center bg-black/70',
onClick: () => setSfToDelete(null)
},
React.createElement('div', {
className: 'bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-sm p-5 text-slate-200',
onClick: e => e.stopPropagation()
},
React.createElement('h3', { className: 'text-sm font-bold text-red-400 mb-3' }, 'Delete SoundFont?'),
React.createElement('p', { className: 'text-xs text-zinc-400 mb-1' }, 'Are you sure you want to delete:'),
React.createElement('p', { className: 'text-sm font-semibold text-slate-200 mb-4' }, sfToDelete.display || sfToDelete.name || sfToDelete.id),
React.createElement('div', { className: 'flex justify-end gap-2' },
React.createElement('button', {
onClick: () => setSfToDelete(null),
className: 'px-4 py-2 text-xs rounded bg-zinc-700 hover:bg-zinc-600 text-zinc-300 transition'
}, 'Cancel'),
React.createElement('button', {
onClick: async () => {
try {
if (window.SonicAPI.deleteSoundFont) {
await window.SonicAPI.deleteSoundFont(sfToDelete.id);
}
const data = await window.SonicAPI.listPlugins();
setLocalData(data);
setSfToDelete(null);
window.showToast && window.showToast('SoundFont deleted.', 'info');
} catch (err) {
window.showToast && window.showToast('Delete failed: ' + err.message, 'error');
setSfToDelete(null);
}
},
className: 'px-4 py-2 text-xs rounded bg-red-700 hover:bg-red-600 text-white font-semibold transition'
}, 'Delete')
)
)
)
));
};
@@ -4314,7 +4340,7 @@ const AIPresetModal = ({ isOpen, onClose }) => {
}, "Đóng"))));
};
const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNotes, onSaveNotes, setSubTabs, onPlayPause, onStop, isPlaying }) => {
const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNotes, onSaveNotes, setSubTabs, onPlayPause, onStop, isPlaying, playPreviewNote }) => {
const [activeRollTool, setActiveRollTool] = React.useState('select');
const [snapVal, setSnapVal] = React.useState('1/16');
const [ccMode, setCcMode] = React.useState('velocity');
@@ -4411,6 +4437,10 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
setNotes(prev => prev.map(n => {
if (n.id !== noteUnderCursor.id) return n;
const newVel = Math.max(0.1, Math.min(1.0, (n.velocity ?? 0.8) + delta));
if (window.SonicSF) {
const ctx = getAudioContext();
window.SonicSF.playNote(n.pitch, newVel * 127, 300, ctx.currentTime, st.instrumentProgram, null);
}
return { ...n, velocity: newVel };
}));
} else if (selectedNoteIds.length > 0) {
@@ -4581,9 +4611,15 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
}, [notes, ccMode, rollZoom]);
React.useEffect(() => {
const scrollToC3 = () => {
if (gridScrollRef.current) {
gridScrollRef.current.scrollTop = (127 - 48) * NoteHeight;
const ch = gridScrollRef.current.clientHeight || 400;
gridScrollRef.current.scrollTop = Math.max(0, (127 - 48) * NoteHeight + NoteHeight - ch);
}
};
scrollToC3();
const timer = setTimeout(scrollToC3, 100);
return () => clearTimeout(timer);
}, []);
const handleGridMouseDown = (e) => {
@@ -4617,6 +4653,14 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
return pitch === n.pitch && beat >= n.start_beat && beat < n.start_beat + n.duration_beats;
});
// Click on note play note with SoundFont
if (clickedNoteIdx !== -1 && !e.ctrlKey && !e.shiftKey) {
if (window.SonicSF) {
const ctx = getAudioContext();
window.SonicSF.playNote(notes[clickedNoteIdx].pitch, 100, 300, ctx.currentTime, st.instrumentProgram, null);
}
}
// Ctrl+click: selection mode (select notes by touching)
if (e.ctrlKey) {
if (clickedNoteIdx !== -1) {
@@ -4862,9 +4906,13 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
className: `w-[60px] shrink-0 border-b border-zinc-800 text-[9px] font-bold flex items-center justify-end pr-2 transition cursor-pointer ${isBlack ? 'bg-zinc-950 text-slate-500 border-zinc-900 hover:bg-zinc-800' : 'bg-white text-zinc-800 border-r border-zinc-400 hover:bg-amber-100'}`,
onMouseDown: (e) => {
e.stopPropagation();
try {
if (window.SonicSF) {
window.SonicSF.playNote(pitch, 100, 500, undefined, st.instrumentProgram, null);
}
} catch (err) {
console.error('playNote error:', err);
}
}
}, showLabel && label)
);
@@ -4951,18 +4999,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
}, mode)))), /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-1"
}, /*#__PURE__*/React.createElement("button", {
onClick: () => onPlayPause && onPlayPause(),
className: `px-2.5 py-1 rounded text-xs flex items-center gap-1 transition font-bold ${st.isPlaying ? 'bg-amber-600 text-white' : 'bg-zinc-700 hover:bg-zinc-600 text-zinc-200'}`
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": st.isPlaying ? "pause" : "play",
className: "w-3 h-3"
}), st.isPlaying ? "Pause" : "Play"), /*#__PURE__*/React.createElement("button", {
onClick: () => onStop && onStop(),
className: "px-2.5 py-1 bg-zinc-700 hover:bg-zinc-600 text-zinc-300 rounded text-xs flex items-center gap-1 transition"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "square",
className: "w-3 h-3"
}), "Stop"), /*#__PURE__*/React.createElement("button", {
onClick: () => onSaveNotes(st.id, st.trackId, st.target_id, notes),
className: "px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs flex items-center gap-1 transition font-semibold"
}, /*#__PURE__*/React.createElement("i", {
@@ -4980,7 +5016,16 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
className: "w-[60px] bg-[#222222] border-r border-zinc-900 shrink-0"
}), /*#__PURE__*/React.createElement("div", {
ref: rulerScrollRef,
className: "flex-1 overflow-hidden"
className: "flex-1 overflow-hidden",
onMouseDown: (e) => {
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left + e.currentTarget.scrollLeft;
const clickBeat = x / pixelsPerBeat;
const clickTime = clickBeat * beatSec;
if (clickTime >= 0) {
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: clickTime } : s));
}
}
}, /*#__PURE__*/React.createElement("div", {
style: {
width: `${drawWidth}px`,
@@ -5299,14 +5344,32 @@ const App = () => {
const [instrumentSelectorData, setInstrumentSelectorData] = useState(null);
const openInstrumentSelector = trackId => {
setInstrumentSelectorTrackId(trackId);
if (!instrumentSelectorData) {
window.SonicAPI.listPlugins().then(data => setInstrumentSelectorData(data)).catch(() => {});
const track = activeTracks.find(t => t.id === trackId);
if (track && track.instrumentId && track.instrumentId.startsWith('sf_')) {
// Track already has a SoundFont assigned open instrument selection directly
setSynthCategory('soundfont');
setSelectedSoundFontId(track.instrumentId);
setSfPresets(null);
const sfIdParam = track.instrumentId.replace('sf_', '');
window.SonicAPI.listSoundfontInstruments(sfIdParam)
.then(data => setSfPresets(data.presets || []))
.catch(() => setSfPresets([]));
} else {
// Reset synth state and always reload plugin data
setSynthCategory(null);
setSelectedSoundFontId(null);
window.SonicAPI.listPlugins().then(data => setInstrumentSelectorData(data)).catch(e => console.error('listPlugins failed:', e));
}
};
const closeInstrumentSelector = () => setInstrumentSelectorTrackId(null);
const closeInstrumentSelector = () => {
setInstrumentSelectorTrackId(null);
setSynthCategory(null);
setSelectedSoundFontId(null);
};
const [synthCategory, setSynthCategory] = useState(null); // 'vst' | 'soundfont'
const [selectedSoundFontId, setSelectedSoundFontId] = useState(null);
const [sfPresets, setSfPresets] = useState(null); // presets from SoundFont
const GM_INSTRUMENTS = [
"Acoustic Grand Piano","Bright Acoustic Piano","Electric Grand Piano","Honky-tonk Piano","Electric Piano 1","Electric Piano 2","Harpsichord","Clavi",
"Celesta","Glockenspiel","Music Box","Vibraphone","Marimba","Xylophone","Tubular Bells","Dulcimer",
"Drawbar Organ","Percussive Organ","Rock Organ","Church Organ","Reed Organ","Accordion","Harmonica","Tango Accordion",
@@ -5336,6 +5399,11 @@ const App = () => {
};
const setTrackInstrument = (trackId, instrumentId, displayName) => {
if (instrumentId && instrumentId.startsWith('sf_')) {
// Set instrument on track immediately so Synth button shows the name
updateActiveTracks(prev => prev.map(t => {
if (t.id !== trackId) return t;
return { ...t, instrumentId, instrumentProgram: undefined, instrumentName: displayName };
}));
setSelectedSoundFontId(instrumentId);
setSynthCategory('soundfont');
setInstrumentSelectorTrackId(trackId);
@@ -5344,7 +5412,7 @@ const App = () => {
const sfIdParam = instrumentId.replace('sf_', '');
window.SonicAPI.listSoundfontInstruments(sfIdParam)
.then(data => setSfPresets(data.presets || []))
.catch(() => setSfPresets([]));
.catch(e => { console.error('listSoundfontInstruments failed:', e); setSfPresets([]); });
} else {
setTrackInstrumentWithProgram(trackId, instrumentId, undefined, displayName);
}
@@ -8269,14 +8337,10 @@ const App = () => {
}, [isPlaying, subTabs, isLoopingSelection, selLeft, selRight, selectionMode, localSelectionTrackId, selectionCleared, activeTab]);
// Playback
const startTrackPlayback = offsetTime => {
const context = getAudioContext();
const hasSolo = tracks.some(t => t.solo) || soloedTrackId !== null;
tracks.forEach(track => {
const isPlayable = hasSolo ? track.id === soloedTrackId || track.solo : !track.muted;
if (!isPlayable) return;
// Create persistent gain & panner per track for real-time control
const getOrCreateTrackNode = (track, context) => {
if (!track) return null;
let node = activeTrackNodesRef.current[track.id];
if (!node) {
const gainNode = context.createGain();
const volDb = track.volumeDb ?? 0;
const volLinear = volDb <= -50 ? 0 : Math.pow(10, volDb / 20);
@@ -8284,12 +8348,40 @@ const App = () => {
const pannerNode = context.createStereoPanner();
pannerNode.pan.setValueAtTime((track.pan ?? 0) / 100, context.currentTime);
pannerNode.connect(context.destination);
// Connect gain panner for all tracks (audio clips AND MIDI items)
gainNode.connect(pannerNode);
activeTrackNodesRef.current[track.id] = {
gainNode,
pannerNode
node = { gainNode, pannerNode };
activeTrackNodesRef.current[track.id] = node;
}
return node.gainNode;
};
const playMidiPreviewNote = (pitch, velocity = 0.8, durationMs = 500) => {
if (!window.SonicSF) return;
const context = getAudioContext();
const tab = subTabs.find(s => s.id === activeTabRef.current);
const trackId = tab ? tab.trackId : selectedTrackId;
const track = activeTracks.find(t => t.id === trackId);
const destNode = getOrCreateTrackNode(track, context);
const program = track ? track.instrumentProgram : undefined;
window.SonicSF.playNote(
pitch,
velocity,
durationMs,
context.currentTime,
program,
destNode
);
};
const startTrackPlayback = offsetTime => {
const context = getAudioContext();
const hasSolo = tracks.some(t => t.solo) || soloedTrackId !== null;
tracks.forEach(track => {
const isPlayable = hasSolo ? track.id === soloedTrackId || track.solo : !track.muted;
if (!isPlayable) return;
const gainNode = getOrCreateTrackNode(track, context);
const pannerNode = activeTrackNodesRef.current[track.id].pannerNode;
const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{
id: 'default',
buffer: track.buffer,
@@ -8380,18 +8472,9 @@ const App = () => {
speed: track.speed || 1.0
}] : [];
// Create persistent gain & panner for real-time control
const gainNode = context.createGain();
const volDb = track.volumeDb ?? 0;
const volLinear = volDb <= -50 ? 0 : Math.pow(10, volDb / 20);
gainNode.gain.setValueAtTime(volLinear, context.currentTime);
const pannerNode = context.createStereoPanner();
pannerNode.pan.setValueAtTime((track.pan ?? 0) / 100, context.currentTime);
pannerNode.connect(context.destination);
activeTrackNodesRef.current[trackId] = {
gainNode,
pannerNode
};
// Get or create persistent gain & panner for real-time control
const gainNode = getOrCreateTrackNode(track, context);
const pannerNode = activeTrackNodesRef.current[track.id].pannerNode;
clips.forEach(clip => {
if (!clip.buffer) return;
const source = context.createBufferSource();
@@ -8479,6 +8562,7 @@ const App = () => {
// Find the track to get instrument settings
const track = activeTracks.find(t => t.id === st.trackId);
const destNode = getOrCreateTrackNode(track, context);
const instrumentProgram = track ? track.instrumentProgram : undefined;
const instrumentName = track ? track.instrumentName : undefined;
@@ -8501,7 +8585,7 @@ const App = () => {
durMs,
scheduledTime,
instrumentProgram,
null
destNode
);
}
}
@@ -13745,7 +13829,7 @@ const App = () => {
className: "w-3 h-3"
})), " Synth: ", /*#__PURE__*/React.createElement("span", {
className: "text-zinc-500 font-normal"
}, track.instrumentId ? (track.instrumentProgram !== undefined ? `${track.instrumentId} [${track.instrumentProgram}]` : track.instrumentId) : "None"))), /*#__PURE__*/React.createElement("div", {
}, track.instrumentName || track.instrumentId || "None"))), /*#__PURE__*/React.createElement("div", {
onMouseDown: e => handleTrackResizeMouseDown(e, track.id),
className: "absolute bottom-0 left-0 right-0 h-1 cursor-ns-resize z-30 hover:bg-cyan-500/50 transition-colors",
onClick: e => e.stopPropagation()
@@ -13946,7 +14030,8 @@ const App = () => {
setSubTabs: setSubTabs,
onPlayPause: handlePlayPause,
onStop: stopAllPlayback,
isPlaying: isPlaying
isPlaying: isPlaying,
playPreviewNote: playMidiPreviewNote
});
}
const subTrack = tracks.find(t => t.id === st.trackId);
@@ -14824,14 +14909,13 @@ const App = () => {
key: i,
onClick: () => setTrackInstrumentWithProgram(instrumentSelectorTrackId, selectedSoundFontId, p.program, p.name || 'Preset ' + p.program),
className: "text-left px-2 py-1 text-[10px] rounded bg-zinc-800/60 hover:bg-amber-800 text-zinc-300 truncate"
}, (p.bank || 0) + ":" + p.program + " " + (p.name || 'Preset ' + p.program)))
)
}, p.name || 'Preset ' + p.program)
))
) : (
React.createElement("p", { className: "text-[10px] text-zinc-500 py-2" }, "No presets found.")
)
)
)
)
) : (
/*#__PURE__*/React.createElement(React.Fragment, null,
/*#__PURE__*/React.createElement("div", { className: "flex justify-between items-center pb-3 border-b border-[#383838]" },
File diff suppressed because one or more lines are too long
+1
View File
@@ -66,6 +66,7 @@ window.API_BASE_URL = window.API_BASE_URL || window.location.origin;
listDefaultSoundfonts: () => apiRequest('/api/v1/plugins/default-soundfonts', { method: 'GET' }),
listSoundfontInstruments: (sfId) => apiRequest(`/api/v1/plugins/soundfont-instruments/${sfId}`, { method: 'GET' }),
renderProject: (projectJson, outputFilename) => apiRequest('/api/v1/plugins/render', { method: 'POST', body: JSON.stringify({ project_json: projectJson, output_filename: outputFilename }) }),
deleteSoundFont: (sfId) => apiRequest(`/api/v1/plugins/soundfont/${sfId}`, { method: 'DELETE' }),
uploadSoundFont: async (file) => {
const formData = new FormData();
formData.append('file', file);
@@ -19,6 +19,9 @@
if (!window.__sharedAudioCtx) {
window.__sharedAudioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if (window.__sharedAudioCtx.state === 'suspended') {
window.__sharedAudioCtx.resume();
}
if (!__gainNode) {
__gainNode = window.__sharedAudioCtx.createGain();
__gainNode.gain.value = 0.3;
Binary file not shown.
@@ -1 +0,0 @@
{"original_name": "General_GS_57e3", "uuid": "57e324e5-bea6-4937-88e0-cb6e4ef51832", "file": "57e324e5-bea6-4937-88e0-cb6e4ef51832.sf2"}
@@ -0,0 +1 @@
{"original_name": "test.sf2", "uuid": "7deec9c7-6567-4502-8ae8-ecbae0ffa3af", "file": "7deec9c7-6567-4502-8ae8-ecbae0ffa3af.sf2"}
@@ -0,0 +1 @@
{"original_name": "7712630e-df7c-4b51-adbc-9a1cb0490907.sf2", "uuid": "7faf5aa0-87dd-4321-b729-7189e2f71bad", "file": "7faf5aa0-87dd-4321-b729-7189e2f71bad.sf2"}
+7 -7
View File
@@ -10,13 +10,13 @@
<script src="https://unpkg.com/lucide@latest"></script>
<script src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
<script src="/static/js/services/api.js?v=202607232100"></script>
<script src="/static/js/services/audioEngine.js?v=202607232100"></script>
<script src="/static/js/services/storage.js?v=202607232100"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202607232100"></script>
<script src="/static/js/services/aiGateway.js?v=202607232100"></script>
<script src="/static/js/services/dawCommandDispatcher.js?v=202607232100"></script>
<script src="/static/js/app.precompiled.js?v=202607232100" defer></script>
<script src="/static/js/services/api.js?v=202607232105"></script>
<script src="/static/js/services/audioEngine.js?v=202607232105"></script>
<script src="/static/js/services/storage.js?v=202607232105"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202607232105"></script>
<script src="/static/js/services/aiGateway.js?v=202607232105"></script>
<script src="/static/js/services/dawCommandDispatcher.js?v=202607232105"></script>
<script src="/static/js/app.precompiled.js?v=202607232105" defer></script>
<style>
:root {
--right-sidebar-width: 320px;