fix: nhãn của soundfont hiển thị sai và không load được instrument
This commit is contained in:
+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.")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user