fix: nhãn của soundfont hiển thị sai và không load được instrument
This commit is contained in:
@@ -18,6 +18,7 @@ RUN apt-get update && apt-get install -y \
|
||||
xvfb \
|
||||
ffmpeg \
|
||||
libsndfile1 \
|
||||
libfluidsynth3 \
|
||||
build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
+45
-16
@@ -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)
|
||||
if fid < 0:
|
||||
fl.delete()
|
||||
continue
|
||||
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
|
||||
# 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:
|
||||
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
|
||||
return presets[:256]
|
||||
except Exception:
|
||||
pass
|
||||
import traceback; traceback.print_exc()
|
||||
return []
|
||||
|
||||
def list_available(self) -> dict:
|
||||
|
||||
+151
-67
@@ -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(() => {
|
||||
if (gridScrollRef.current) {
|
||||
gridScrollRef.current.scrollTop = (127 - 48) * NoteHeight;
|
||||
}
|
||||
const scrollToC3 = () => {
|
||||
if (gridScrollRef.current) {
|
||||
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,8 +4906,12 @@ 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();
|
||||
if (window.SonicSF) {
|
||||
window.SonicSF.playNote(pitch, 100, 500, undefined, st.instrumentProgram, null);
|
||||
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,12 +14909,11 @@ 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.")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
) : (
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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"}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"original_name": "7712630e-df7c-4b51-adbc-9a1cb0490907.sf2", "uuid": "7faf5aa0-87dd-4321-b729-7189e2f71bad", "file": "7faf5aa0-87dd-4321-b729-7189e2f71bad.sf2"}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user