FIX: khi click vào track number thì bị mất âm thanh, cho phép lưu màu của track
This commit is contained in:
@@ -45,6 +45,7 @@
|
||||
"id": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"type": { "type": "string", "enum": ["AUDIO", "MIDI", "SECTION"] },
|
||||
"color": { "type": ["string", "null"], "default": null },
|
||||
"volume_db": { "type": "number", "default": 0.0 },
|
||||
"pan": { "type": "number", "minimum": -1.0, "maximum": 1.0, "default": 0.0 },
|
||||
"mute": { "type": "boolean", "default": false },
|
||||
|
||||
+113
-39
@@ -1777,7 +1777,7 @@ const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal,
|
||||
};
|
||||
|
||||
// ── Track Strip Console Component (from md/48_TRACK_STRIP_COMPONENT.md) ──
|
||||
const TrackStripConsole = ({ track, index, onUpdateTrack, trackVuRefs }) => {
|
||||
const TrackStripConsole = ({ track, index, onUpdateTrack, trackVuRefs, style }) => {
|
||||
var vol = track.volumeDb != null ? track.volumeDb : 0;
|
||||
var trackColor = track.color || '#06b6d4';
|
||||
var isMuted = track.muted;
|
||||
@@ -1827,7 +1827,8 @@ const TrackStripConsole = ({ track, index, onUpdateTrack, trackVuRefs }) => {
|
||||
};
|
||||
|
||||
return React.createElement("div", {
|
||||
className: "flex flex-col items-stretch w-[145px] shrink-0 strip-bg-track rounded-md text-slate-300 select-none shadow-2xl border border-slate-900 overflow-hidden"
|
||||
style: style || undefined,
|
||||
className: "flex flex-col items-stretch w-[145px] shrink-0 strip-bg-track rounded-md text-slate-300 select-none shadow-2xl border border-slate-900 overflow-y-auto"
|
||||
},
|
||||
/* 1. Top Track Color Accent Bar */
|
||||
React.createElement("div", {
|
||||
@@ -1870,7 +1871,7 @@ const TrackStripConsole = ({ track, index, onUpdateTrack, trackVuRefs }) => {
|
||||
),
|
||||
|
||||
/* 3. Center Area: Peak dB + Fader + VU + Button Stack */
|
||||
React.createElement("div", { className: "flex-1 p-1 flex gap-1 justify-between items-stretch min-h-0" },
|
||||
React.createElement("div", { className: "flex-1 p-1 flex gap-1 justify-between items-stretch min-h-[170px]" },
|
||||
/* Left Fader & VU Column */
|
||||
React.createElement("div", {
|
||||
className: "flex-1 flex flex-col justify-between items-center bg-black/60 p-1 rounded border border-slate-800/80"
|
||||
@@ -2363,9 +2364,9 @@ const WaveformLane = ({
|
||||
const secWidth = sec.duration * zoom;
|
||||
if (secStartLocal + secWidth < 0 || secStartLocal > drawWidth) return;
|
||||
var isSecSelected = selectedItemIds && selectedItemIds.has(sec.id);
|
||||
ctx.fillStyle = isSecSelected ? 'rgba(245, 158, 11, 0.35)' : (sec.color ? sec.color + '44' : 'rgba(6, 182, 212, 0.25)');
|
||||
ctx.fillStyle = isSecSelected ? 'rgba(245, 158, 11, 0.35)' : ((sec.color || track.color || '#06b6d4') + '44');
|
||||
ctx.fillRect(secStartLocal, 2, secWidth, height - 4);
|
||||
ctx.strokeStyle = isSecSelected ? '#f59e0b' : (sec.color || '#06b6d4');
|
||||
ctx.strokeStyle = isSecSelected ? '#f59e0b' : (sec.color || track.color || '#06b6d4');
|
||||
ctx.lineWidth = isSecSelected ? 2.5 : 1;
|
||||
ctx.setLineDash(isSecSelected ? [] : [4, 4]);
|
||||
ctx.strokeRect(secStartLocal, 2, secWidth, height - 4);
|
||||
@@ -2455,12 +2456,12 @@ const WaveformLane = ({
|
||||
}
|
||||
|
||||
var isMidiSelected = selectedItemIds && selectedItemIds.has(midi.id);
|
||||
ctx.fillStyle = isMidiSelected ? 'rgba(245, 158, 11, 0.35)' : (isRecordingItem ? 'rgba(239, 68, 68, 0.2)' : '#a78bfa33');
|
||||
ctx.fillStyle = isMidiSelected ? 'rgba(245, 158, 11, 0.35)' : (isRecordingItem ? 'rgba(239, 68, 68, 0.2)' : (track.color || '#a78bfa') + '33');
|
||||
ctx.fillRect(midiStartLocal, 2, midiWidth, height - 4);
|
||||
ctx.strokeStyle = isMidiSelected ? '#f59e0b' : (isRecordingItem ? '#ef4444' : '#a78bfa');
|
||||
ctx.strokeStyle = isMidiSelected ? '#f59e0b' : (isRecordingItem ? '#ef4444' : (track.color || '#a78bfa'));
|
||||
ctx.lineWidth = isMidiSelected ? 2.5 : 1.5;
|
||||
ctx.strokeRect(midiStartLocal, 2, midiWidth, height - 4);
|
||||
ctx.fillStyle = isRecordingItem ? '#fca5a5' : '#c4b5fd';
|
||||
ctx.fillStyle = isRecordingItem ? '#fca5a5' : (track.color || '#a78bfa');
|
||||
ctx.font = 'bold 9px sans-serif';
|
||||
ctx.fillText(isRecordingItem ? '[Ghi MIDI...]' : (midi.name || 'MIDI'), Math.max(midiStartLocal + 4, 4), 14);
|
||||
|
||||
@@ -2480,7 +2481,7 @@ const WaveformLane = ({
|
||||
const ny = 18 + (1.0 - pitchFrac) * (height - 26);
|
||||
const nh = Math.max(6, (height - 26) / (pitchMax - pitchMin) * 4);
|
||||
|
||||
ctx.fillStyle = isRecordingItem ? '#10b981' : '#a78bfa';
|
||||
ctx.fillStyle = isRecordingItem ? '#10b981' : (track.color || '#a78bfa');
|
||||
ctx.fillRect(Math.max(noteStartLocal, midiStartLocal + 2), ny, Math.max(2, nw), nh);
|
||||
});
|
||||
}
|
||||
@@ -8733,6 +8734,7 @@ const serializeTracksList = (tracksList, secondsPerBar) => {
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
type: trackType,
|
||||
color: t.color || null,
|
||||
volume_db: t.volumeDb || 0.0,
|
||||
pan: t.pan || 0.0,
|
||||
mute: t.muted || false,
|
||||
@@ -14387,6 +14389,9 @@ const App = () => {
|
||||
// Last-known audibility per track id (used to detect inaudible→audible
|
||||
// transitions that need a playback re-schedule).
|
||||
const trackAudibleRef = useRef({});
|
||||
// Audio-relevant signature (mute/solo/vol/bypass/items count) — đổi màu/
|
||||
// rename KHÔNG trigger re-sync/re-route (tránh mất âm khi play).
|
||||
const trackAudioSyncSigRef = useRef(null);
|
||||
|
||||
// Keep the mastering-bypass routing map + realtime mute/solo in sync with the
|
||||
// tracks state (loads, undo, AI ops…). Placed AFTER the tracks/subTabs/
|
||||
@@ -14399,6 +14404,12 @@ const App = () => {
|
||||
trackMidiBypassMap[t.id] = !!t.midiBypass;
|
||||
});
|
||||
const list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : all;
|
||||
// Audio-relevant signature: đổi MÀU/rename track (không liên quan audio)
|
||||
// KHÔNG được trigger re-sync/re-route (tránh mất âm khi play sau đó —
|
||||
// updateSfRouting chạy thừa có thể đặt SF destination sai thời điểm).
|
||||
const audioSig = list.map(t => (t.muted ? '1' : '0') + (t.solo ? '1' : '0') + ':' + (t.volumeDb ?? 0) + ':' + (t.audioBypass ? '1' : '0') + (t.midiBypass ? '1' : '0') + ':' + (t.midiItems || []).length + ':' + (t.clips || []).length + ':' + (t.sections || []).length).join('|');
|
||||
if (trackAudioSyncSigRef.current === audioSig) return;
|
||||
trackAudioSyncSigRef.current = audioSig;
|
||||
list.forEach(t => {
|
||||
// ♪ state → SF mastering route (sfRouteGain/sfDryGain), live on existing nodes
|
||||
const sn = activeTrackNodesRef.current[t.id];
|
||||
@@ -17576,6 +17587,10 @@ const App = () => {
|
||||
const scheduledItemsSigRef = React.useRef('');
|
||||
const playheadFrameCountRef = React.useRef(0);
|
||||
const pendingRescheduleRef = React.useRef(null);
|
||||
// Master-silence watchdog counter: đếm frame im lặng liên tục khi đang play
|
||||
const masterSilenceFramesRef = React.useRef(0);
|
||||
// Cooldown rebuild (ms) — phòng watchdog trigger lặp trong đoạn im tự nhiên
|
||||
const lastMasterRebuildTimeRef = React.useRef(0);
|
||||
const lastRescheduleTimeRef = React.useRef(0);
|
||||
// Re-schedule NGAY (reset cooldown) sau khi THẢ chuột — check kế tiếp trong
|
||||
// updatePlayhead (~100ms) re-schedule luôn.
|
||||
@@ -17630,6 +17645,49 @@ const App = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Master-silence watchdog: đang play + có source ĐANG TRONG KHOẢNG PHÁT
|
||||
// (theo lịch) nhưng master output im lặng liên tục ~750ms → graph bị hỏng
|
||||
// (BiquadFilter "state is bad" — node cache dính) → rebuild nodes.
|
||||
// Đoạn im lặng TỰ NHIÊN (intro/rest — mọi source nằm ngoài khoảng phát)
|
||||
// KHÔNG trigger rebuild (cooldown 3s phòng trigger lặp).
|
||||
if (isPlaying && activeTabRef.current === 'main' && masterBus && masterBus.analyser && activeSourcesRef.current.length > 0) {
|
||||
try {
|
||||
const ctxNow = getAudioContext().currentTime;
|
||||
const anyPlaying = activeSourcesRef.current.some(s => typeof s.startTime === 'number' && ctxNow >= s.startTime && ctxNow <= s.startTime + (s.buffer ? s.buffer.duration : 0) + 0.1);
|
||||
if (anyPlaying) {
|
||||
const d = new Uint8Array(128);
|
||||
masterBus.analyser.getByteTimeDomainData(d);
|
||||
let pk = 0;
|
||||
for (let i = 0; i < d.length; i++) { const v = Math.abs(d[i] - 128) / 128; if (v > pk) pk = v; }
|
||||
if (pk < 0.001) {
|
||||
masterSilenceFramesRef.current++;
|
||||
const sinceRebuild = performance.now() - (lastMasterRebuildTimeRef.current || 0);
|
||||
if (masterSilenceFramesRef.current > 45 && sinceRebuild > 3000) {
|
||||
masterSilenceFramesRef.current = 0;
|
||||
lastMasterRebuildTimeRef.current = performance.now();
|
||||
console.warn('[Recovery] Master silent while sources active — rebuilding audio graph');
|
||||
try {
|
||||
const pt = currentTimeRef.current;
|
||||
stopAllPlayback();
|
||||
Object.keys(activeTrackNodesRef.current).forEach(k => { const n = activeTrackNodesRef.current[k]; try { if (n && n.gainNode && n.gainNode.disconnect) n.gainNode.disconnect(); } catch (e) {} });
|
||||
activeTrackNodesRef.current = {};
|
||||
try { initMasterBus(); } catch (e) { console.warn('[Recovery] initMasterBus error:', e); }
|
||||
setIsPlaying(true);
|
||||
startTrackPlayback(pt);
|
||||
} catch (e) { console.warn('[Recovery] rebuild error:', e); }
|
||||
}
|
||||
} else {
|
||||
masterSilenceFramesRef.current = 0;
|
||||
}
|
||||
} else {
|
||||
masterSilenceFramesRef.current = 0;
|
||||
}
|
||||
} catch (e) { masterSilenceFramesRef.current = 0; }
|
||||
} else {
|
||||
masterSilenceFramesRef.current = 0;
|
||||
}
|
||||
|
||||
if (recordingStateRef.current === 'RECORDING') {
|
||||
const audioCtx = getAudioContext();
|
||||
const lookahead = 0.1; // 100ms
|
||||
@@ -20526,6 +20584,14 @@ const App = () => {
|
||||
...t,
|
||||
color
|
||||
} : t));
|
||||
// Lưu màu NGAY (flush — không chờ debounce 2s — tránh mất màu khi reload nhanh)
|
||||
try {
|
||||
if (window.SonicStorage) {
|
||||
const nextTracks = (tracks || []).map(t => t.id === trackId ? { ...t, color } : t);
|
||||
window.SonicStorage.scheduleTempAutoSave(() => serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, nextTracks, subTabs, sessionTabs, masteringSettings));
|
||||
if (window.SonicStorage.flushTempAutoSave) window.SonicStorage.flushTempAutoSave();
|
||||
}
|
||||
} catch (e) {}
|
||||
setUndoStack(prev => {
|
||||
const next = [...prev, {
|
||||
action_type: 'RECOLOR',
|
||||
@@ -23743,6 +23809,22 @@ STRICT CONSTRAINTS:
|
||||
const masterVUAnimRef = useRef(null);
|
||||
useEffect(() => {
|
||||
function tick() {
|
||||
try {
|
||||
// Khi STOP (không play, không recording): vẽ VU RỖNG — không đọc analyser/
|
||||
// activity → hết animation dính + master không full sau khi dừng play.
|
||||
const isRecActive = activeAudioRecordersRef.current && Object.keys(activeAudioRecordersRef.current).length > 0;
|
||||
if (!isPlaying && !isRecActive) {
|
||||
setMasterVU(0);
|
||||
setMasterMeterPeak(0);
|
||||
Object.keys(trackVuRefs.current).forEach(k => {
|
||||
const c = trackVuRefs.current[k];
|
||||
if (!c) return;
|
||||
if (k.endsWith('_mixer')) drawMixerVuMeter(c, 0);
|
||||
else drawVuMeter(c, -60);
|
||||
});
|
||||
masterVUAnimRef.current = requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
// 1. Master VU Meter
|
||||
if (masterBus && masterBus.analyser) {
|
||||
const data = new Uint8Array(128);
|
||||
@@ -23785,7 +23867,10 @@ STRICT CONSTRAINTS:
|
||||
}
|
||||
}
|
||||
|
||||
let midiPeak = isPlaying && isAudible ? (midiVuActivityRef.current[trackId] || 0) : 0;
|
||||
// midiVuActivityRef được set bởi triggerMidiVuActivity — cả khi play
|
||||
// MIDI item LẪN khi ARM + nhấn phím MIDI keyboard (preview) — nên VU
|
||||
// nhảy trong cả 2 trường hợp (không chặn bởi isPlaying).
|
||||
let midiPeak = isAudible ? (midiVuActivityRef.current[trackId] || 0) : 0;
|
||||
if (midiPeak > 0) {
|
||||
midiVuActivityRef.current[trackId] = midiPeak * 0.90;
|
||||
if (midiVuActivityRef.current[trackId] < 0.01) {
|
||||
@@ -23811,6 +23896,9 @@ STRICT CONSTRAINTS:
|
||||
}
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.warn('VU tick error:', e);
|
||||
}
|
||||
masterVUAnimRef.current = requestAnimationFrame(tick);
|
||||
}
|
||||
masterVUAnimRef.current = requestAnimationFrame(tick);
|
||||
@@ -25461,14 +25549,9 @@ STRICT CONSTRAINTS:
|
||||
},
|
||||
className: `shrink-0 relative flex flex-col justify-between p-2.5 bg-[#1e1e1e] border-r border-zinc-900 cursor-pointer border-l-4 border-b border-[#141414] overflow-hidden ${isSelected ? 'border-cyan-500 bg-[#252525]' : 'border-transparent hover:bg-zinc-800/20'}`,
|
||||
onClick: () => {
|
||||
// Chỉ CHỌN track — KHÔNG tự dừng main play + chuyển sang piano roll
|
||||
// tab (trước đây gây "mất âm thanh khi click track number").
|
||||
setSelectedTrackId(track.id);
|
||||
var prTab = subTabs.find(function(s) { return s.type === 'PIANO_ROLL' && s.trackId === track.id; });
|
||||
if (prTab && prTab.notes && prTab.notes.length > 0) {
|
||||
stopAllPlayback();
|
||||
setSubTabs(function(prev) { return prev.map(function(s) { return s.id === prTab.id ? Object.assign({}, s, { isPlaying: true }) : s; }); });
|
||||
schedulePianoRollMidi(prTab, 0);
|
||||
startSubTabPlayback(prTab, 0);
|
||||
}
|
||||
}
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex items-start justify-between"
|
||||
@@ -25477,20 +25560,17 @@ STRICT CONSTRAINTS:
|
||||
}, /*#__PURE__*/React.createElement("span", {
|
||||
className: "text-xs font-bold text-zinc-500 font-mono"
|
||||
}, (idx + 1).toString().padStart(2, '0')), /*#__PURE__*/React.createElement("label", {
|
||||
onClick: e => {
|
||||
e.stopPropagation();
|
||||
const el = e.currentTarget.querySelector('input');
|
||||
if (el) el.click();
|
||||
},
|
||||
className: "cursor-pointer"
|
||||
className: "cursor-pointer relative block"
|
||||
}, /*#__PURE__*/React.createElement("input", {
|
||||
type: "color",
|
||||
value: track.color || '#0f766e',
|
||||
onClick: e => e.stopPropagation(),
|
||||
onMouseDown: e => e.stopPropagation(),
|
||||
onChange: e => {
|
||||
e.stopPropagation();
|
||||
updateTrackColor(track.id, e.target.value);
|
||||
},
|
||||
className: "w-0 h-0 opacity-0 absolute pointer-events-none"
|
||||
className: "absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||
}), /*#__PURE__*/React.createElement("div", {
|
||||
className: "w-3 h-3 rounded-full border border-zinc-600 hover:scale-110 transition-transform",
|
||||
style: {
|
||||
@@ -25678,21 +25758,18 @@ STRICT CONSTRAINTS:
|
||||
}, "Any MIDI Keyboard"), midiDevices.map(d => /*#__PURE__*/React.createElement("option", {
|
||||
key: d.id,
|
||||
value: `MIDI_KEYBOARD:${d.id}`
|
||||
}, d.name || `MIDI Input ${d.id.slice(0, 5)}`)))), track.isArmed && lastMidiNote && (lastMidiNote.length === 0 || Date.now() - lastMidiNote.time < 3000) && /*#__PURE__*/React.createElement("span", {
|
||||
className: "text-[9px] font-mono text-emerald-400 ml-0.5 truncate max-w-[60px] shrink-0",
|
||||
title: "MIDI Note:velocity:length"
|
||||
}, `${midiPitchToName(lastMidiNote.pitch)}:${lastMidiNote.velocity}:${lastMidiNote.length > 0 ? lastMidiNote.length.toFixed(2) + 's' : '...'}`)), track.isArmed && /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex items-center gap-1 mt-0.5"
|
||||
}, d.name || `MIDI Input ${d.id.slice(0, 5)}`)))), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex items-center gap-0.5 w-16 shrink-0 ml-1"
|
||||
}, /*#__PURE__*/React.createElement("span", {
|
||||
className: "w-8 text-right text-zinc-500 text-[9px]"
|
||||
}, "VU:"), /*#__PURE__*/React.createElement("canvas", {
|
||||
className: "text-zinc-500 text-[9px]"
|
||||
}, "VU"), /*#__PURE__*/React.createElement("canvas", {
|
||||
ref: el => {
|
||||
if (el) trackVuRefs.current[track.id] = el; else delete trackVuRefs.current[track.id];
|
||||
},
|
||||
width: 100,
|
||||
height: 4,
|
||||
className: "flex-1 bg-[#18181b] rounded h-1"
|
||||
}))), /*#__PURE__*/React.createElement("div", {
|
||||
})))), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex items-center gap-1.5 mt-1",
|
||||
onClick: e => e.stopPropagation()
|
||||
}, /*#__PURE__*/React.createElement("input", {
|
||||
@@ -26083,20 +26160,17 @@ STRICT CONSTRAINTS:
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex items-center gap-2"
|
||||
}, /*#__PURE__*/React.createElement("label", {
|
||||
onClick: e => {
|
||||
e.stopPropagation();
|
||||
const el = e.currentTarget.querySelector('input');
|
||||
if (el) el.click();
|
||||
},
|
||||
className: "cursor-pointer"
|
||||
className: "cursor-pointer relative block"
|
||||
}, /*#__PURE__*/React.createElement("input", {
|
||||
type: "color",
|
||||
value: vTrack.color || '#0f766e',
|
||||
onClick: e => e.stopPropagation(),
|
||||
onMouseDown: e => e.stopPropagation(),
|
||||
onChange: e => {
|
||||
e.stopPropagation();
|
||||
updateTrackColor(vTrack.id, e.target.value);
|
||||
},
|
||||
className: "w-0 h-0 opacity-0 absolute pointer-events-none"
|
||||
className: "absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||
}), /*#__PURE__*/React.createElement("div", {
|
||||
className: "w-3 h-3 rounded-full border border-zinc-600 hover:scale-110 transition-transform",
|
||||
style: {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -60,11 +60,13 @@
|
||||
}
|
||||
|
||||
let autoSaveTimer = null;
|
||||
let lastGetProjectStateCallback = null;
|
||||
function scheduleTempAutoSave(getProjectStateCallback) {
|
||||
if (getProjectStateCallback) lastGetProjectStateCallback = getProjectStateCallback;
|
||||
if (autoSaveTimer) clearTimeout(autoSaveTimer);
|
||||
autoSaveTimer = setTimeout(async () => {
|
||||
try {
|
||||
const state = getProjectStateCallback();
|
||||
const state = lastGetProjectStateCallback ? lastGetProjectStateCallback() : null;
|
||||
if (!state || (!state.tracks && !state.main_session)) return;
|
||||
const dataJson = JSON.stringify(state);
|
||||
localStorage.setItem('sonic_temp_project', dataJson);
|
||||
@@ -76,10 +78,26 @@
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
// Lưu NGAY (bỏ debounce 2s) — dùng cho thay đổi cần bền vững tức thì (đổi màu track)
|
||||
async function flushTempAutoSave() {
|
||||
if (autoSaveTimer) { clearTimeout(autoSaveTimer); autoSaveTimer = null; }
|
||||
try {
|
||||
const state = lastGetProjectStateCallback ? lastGetProjectStateCallback() : null;
|
||||
if (!state || (!state.tracks && !state.main_session)) return;
|
||||
const dataJson = JSON.stringify(state);
|
||||
localStorage.setItem('sonic_temp_project', dataJson);
|
||||
if (window.SonicAPI && localStorage.getItem('sonic_token')) {
|
||||
await window.SonicAPI.saveTempProject(dataJson).catch(() => {});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Flush temp project warning:", e);
|
||||
}
|
||||
}
|
||||
|
||||
window.SonicStorage = {
|
||||
exportProjectToSFS,
|
||||
importProjectFromSFSFile,
|
||||
scheduleTempAutoSave
|
||||
scheduleTempAutoSave,
|
||||
flushTempAutoSave
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<script src="/static/js/services/fluidsynthLoader.js?v=202607271245"></script>
|
||||
<script src="/static/js/services/api.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/audioEngine.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/storage.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/storage.js?v=202608038200"></script>
|
||||
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/soundfontPlayer.js?v=202608031400"></script>
|
||||
<script src="/static/js/services/aiGateway.js?v=202608037200"></script>
|
||||
@@ -24,7 +24,7 @@
|
||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608037600" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608038600" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
Reference in New Issue
Block a user