fix: lỗi ghi note midi vào track
This commit is contained in:
+135
-137
@@ -64,7 +64,7 @@ const formatBeat = (secs, bpmVal) => {
|
||||
return `${bar}.${beat}.${sub}`;
|
||||
};
|
||||
const midiPitchToName = pitch => {
|
||||
const names = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
|
||||
const names = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
||||
const octave = Math.floor(pitch / 12) - 1;
|
||||
return names[pitch % 12] + octave;
|
||||
};
|
||||
@@ -116,6 +116,7 @@ class ClientMIDIRecorder {
|
||||
this.recordedNotes = [];
|
||||
this.recStartAudioTime = 0.0;
|
||||
this.recStartBar = 0.0;
|
||||
this.selectedMidiInputId = null;
|
||||
|
||||
// Compute round-trip browser latency
|
||||
this.latencyCompSec = (this.audioCtx.baseLatency || 0) + (this.audioCtx.outputLatency || 0);
|
||||
@@ -127,23 +128,15 @@ class ClientMIDIRecorder {
|
||||
this.activeNotes.clear();
|
||||
this.recStartBar = startBar;
|
||||
this.recStartAudioTime = this.audioCtx.currentTime;
|
||||
|
||||
this.bindMIDIInputs(selectedMidiInputId);
|
||||
this.selectedMidiInputId = selectedMidiInputId;
|
||||
}
|
||||
|
||||
bindMIDIInputs(selectedMidiInputId = null) {
|
||||
if (navigator.requestMIDIAccess) {
|
||||
navigator.requestMIDIAccess().then(midiAccess => {
|
||||
for (let input of midiAccess.inputs.values()) {
|
||||
if (selectedMidiInputId && input.id !== selectedMidiInputId) continue;
|
||||
input.onmidimessage = (event) => this.handleMIDIMessage(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleMIDIMessage(event) {
|
||||
handleMIDIMessage(event, sourceInputId = null) {
|
||||
if (!this.isRecording) return;
|
||||
if (this.selectedMidiInputId && this.selectedMidiInputId !== 'ALL' && sourceInputId && sourceInputId !== this.selectedMidiInputId) {
|
||||
console.log(`[DevLog] [MIDI Rec] Ignoring input message from "${sourceInputId}" (Selected: "${this.selectedMidiInputId}")`);
|
||||
return;
|
||||
}
|
||||
|
||||
const [status, pitch, velocity] = event.data;
|
||||
const command = status >> 4;
|
||||
@@ -151,17 +144,19 @@ class ClientMIDIRecorder {
|
||||
// Apply latency compensation formula
|
||||
const currentTimeSec = Math.max(0, this.audioCtx.currentTime - this.recStartAudioTime - this.latencyCompSec);
|
||||
const secondsPerBeat = 60.0 / this.bpm;
|
||||
const currentBeat = (currentTimeSec / secondsPerBeat) + (this.recStartBar * this.timeSigNum);
|
||||
const currentBeat = currentTimeSec / secondsPerBeat;
|
||||
|
||||
// Command 0x9: Note On
|
||||
if (command === 0x9 && velocity > 0) {
|
||||
const noteId = `rec_${Date.now()}_${pitch}`;
|
||||
this.activeNotes.set(pitch, {
|
||||
const newActiveNote = {
|
||||
id: noteId,
|
||||
pitch: pitch,
|
||||
start_beat: currentBeat,
|
||||
velocity: velocity / 127.0
|
||||
});
|
||||
};
|
||||
this.activeNotes.set(pitch, newActiveNote);
|
||||
console.log(`[DevLog] [MIDI Rec] Note On - Pitch: ${pitch}, Velocity: ${velocity}, StartBeat: ${currentBeat.toFixed(3)}, latencyCompSec: ${this.latencyCompSec.toFixed(3)}, noteObj:`, newActiveNote);
|
||||
|
||||
// Fire visual feedback callback
|
||||
if (this.onNoteOn) {
|
||||
@@ -185,6 +180,7 @@ class ClientMIDIRecorder {
|
||||
|
||||
this.recordedNotes.push(finishedNote);
|
||||
this.activeNotes.delete(pitch);
|
||||
console.log(`[DevLog] [MIDI Rec] Note Off - Pitch: ${pitch}, DurationBeats: ${durationBeats.toFixed(3)}, FinishedNote:`, finishedNote);
|
||||
|
||||
if (this.onNoteOff) {
|
||||
this.onNoteOff(pitch, finishedNote);
|
||||
@@ -198,29 +194,24 @@ class ClientMIDIRecorder {
|
||||
|
||||
// Flush remaining active keypresses when stop is triggered
|
||||
const currentTimeSec = Math.max(0, this.audioCtx.currentTime - this.recStartAudioTime - this.latencyCompSec);
|
||||
const currentBeat = (currentTimeSec / (60.0 / this.bpm)) + (this.recStartBar * this.timeSigNum);
|
||||
const currentBeat = currentTimeSec / (60.0 / this.bpm);
|
||||
|
||||
for (let [pitch, note] of this.activeNotes.entries()) {
|
||||
this.recordedNotes.push({
|
||||
const durationBeats = Math.max(0.25, currentBeat - note.start_beat);
|
||||
const finishedNote = {
|
||||
id: note.id,
|
||||
pitch: note.pitch,
|
||||
start_beat: note.start_beat,
|
||||
duration_beats: Math.max(0.25, currentBeat - note.start_beat),
|
||||
duration_beats: durationBeats,
|
||||
velocity: note.velocity,
|
||||
pan: 0.0
|
||||
});
|
||||
};
|
||||
this.recordedNotes.push(finishedNote);
|
||||
console.log(`[DevLog] [MIDI Rec] Flushing active keypress on stop - Pitch: ${pitch}, DurationBeats: ${durationBeats.toFixed(3)}, FinishedNote:`, finishedNote);
|
||||
}
|
||||
this.activeNotes.clear();
|
||||
|
||||
// Disconnect midi Access callbacks
|
||||
if (navigator.requestMIDIAccess) {
|
||||
navigator.requestMIDIAccess().then(midiAccess => {
|
||||
for (let input of midiAccess.inputs.values()) {
|
||||
input.onmidimessage = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[DevLog] [MIDI Rec] Recording stopped. Total notes: ${this.recordedNotes.length}. List:`, this.recordedNotes);
|
||||
return this.recordedNotes;
|
||||
}
|
||||
}
|
||||
@@ -295,7 +286,7 @@ class ClientAudioRecorder {
|
||||
if (this.sourceNode && this.workletNode) {
|
||||
try {
|
||||
this.sourceNode.disconnect(this.workletNode);
|
||||
} catch (e) {}
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
if (this.mediaStream) {
|
||||
@@ -453,7 +444,7 @@ const WaveformLane = ({
|
||||
if (snapValue && snapValue !== 'free') {
|
||||
const beatDuration = 60 / parseFloat(bpm || 120);
|
||||
let divisor = 1;
|
||||
if (snapValue === '1') divisor = 1;else if (snapValue === '1/2') divisor = 0.5;else if (snapValue === '1/4') divisor = 0.25;else if (snapValue === '1/8') divisor = 0.125;else if (snapValue === '1/16') divisor = 0.0625;else if (snapValue === '1/32') divisor = 0.03125;
|
||||
if (snapValue === '1') divisor = 1; else if (snapValue === '1/2') divisor = 0.5; else if (snapValue === '1/4') divisor = 0.25; else if (snapValue === '1/8') divisor = 0.125; else if (snapValue === '1/16') divisor = 0.0625; else if (snapValue === '1/32') divisor = 0.03125;
|
||||
gridSpacing = beatDuration * divisor;
|
||||
} else {
|
||||
gridSpacing = 60 / parseFloat(bpm || 120);
|
||||
@@ -707,14 +698,21 @@ const WaveformLane = ({
|
||||
const midiStartLocal = midi.startTime * zoom - scrollLeft;
|
||||
const midiWidth = midi.duration * zoom;
|
||||
if (midiStartLocal + midiWidth < 0 || midiStartLocal > drawWidth) return;
|
||||
ctx.fillStyle = '#a78bfa33';
|
||||
|
||||
const isRecordingItem = midi.name === 'Recording...';
|
||||
|
||||
if (isRecordingItem && Math.random() < 0.05) {
|
||||
console.log(`[DevLog] [Canvas Draw] Rendering MIDI item: ID=${midi.id}, Name="${midi.name}", Start=${midiStartLocal.toFixed(1)}px, Width=${midiWidth.toFixed(1)}px, NotesCount=${midi.notes?.length || 0}`);
|
||||
}
|
||||
|
||||
ctx.fillStyle = isRecordingItem ? 'rgba(239, 68, 68, 0.2)' : '#a78bfa33';
|
||||
ctx.fillRect(midiStartLocal, 2, midiWidth, height - 4);
|
||||
ctx.strokeStyle = '#a78bfa';
|
||||
ctx.strokeStyle = isRecordingItem ? '#ef4444' : '#a78bfa';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.strokeRect(midiStartLocal, 2, midiWidth, height - 4);
|
||||
ctx.fillStyle = '#c4b5fd';
|
||||
ctx.fillStyle = isRecordingItem ? '#fca5a5' : '#c4b5fd';
|
||||
ctx.font = 'bold 9px sans-serif';
|
||||
ctx.fillText(midi.name || 'MIDI', Math.max(midiStartLocal + 4, 4), 14);
|
||||
ctx.fillText(isRecordingItem ? '[Ghi MIDI...]' : (midi.name || 'MIDI'), Math.max(midiStartLocal + 4, 4), 14);
|
||||
|
||||
const midiNotes = midi.notes || [];
|
||||
if (midiNotes.length > 0) {
|
||||
@@ -732,47 +730,12 @@ const WaveformLane = ({
|
||||
const ny = 18 + (1.0 - pitchFrac) * (height - 26);
|
||||
const nh = Math.max(6, (height - 26) / (pitchMax - pitchMin) * 4);
|
||||
|
||||
ctx.fillStyle = '#a78bfa';
|
||||
ctx.fillStyle = isRecordingItem ? '#10b981' : '#a78bfa';
|
||||
ctx.fillRect(Math.max(noteStartLocal, midiStartLocal + 2), ny, Math.max(2, nw), nh);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (recordingState === 'RECORDING' && track.isArmed && track.inputSource?.deviceType === 'MIDI_KEYBOARD') {
|
||||
const liveMidiDuration = currentTime - recStartTimelineTime;
|
||||
const recStartLocal = recStartTimelineTime * zoom - scrollLeft;
|
||||
const recWidth = liveMidiDuration * zoom;
|
||||
if (recWidth > 0 && recStartLocal + recWidth >= 0 && recStartLocal <= drawWidth) {
|
||||
ctx.fillStyle = 'rgba(239, 68, 68, 0.2)';
|
||||
ctx.fillRect(recStartLocal, 2, recWidth, height - 4);
|
||||
ctx.strokeStyle = '#ef4444';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.strokeRect(recStartLocal, 2, recWidth, height - 4);
|
||||
ctx.fillStyle = '#fca5a5';
|
||||
ctx.font = 'bold 9px sans-serif';
|
||||
ctx.fillText('[GHI MIDI...]', Math.max(recStartLocal + 4, 4), 14);
|
||||
|
||||
if (recTempMidiNotes && recTempMidiNotes.length > 0) {
|
||||
const secondsPerBeat = 60.0 / (parseInt(bpm) || 120);
|
||||
recTempMidiNotes.forEach(note => {
|
||||
const noteStartSec = note.start_beat * secondsPerBeat;
|
||||
const noteDurSec = note.duration_beats * secondsPerBeat;
|
||||
const nxLocal = noteStartSec * zoom - scrollLeft;
|
||||
const nw = noteDurSec * zoom;
|
||||
|
||||
const pitchMin = 36;
|
||||
const pitchMax = 84;
|
||||
const pitchFrac = Math.max(0, Math.min(1, (note.pitch - pitchMin) / (pitchMax - pitchMin)));
|
||||
const ny = 6 + (1.0 - pitchFrac) * (height - 18);
|
||||
const nh = Math.max(6, (height - 18) / (pitchMax - pitchMin) * 4);
|
||||
|
||||
ctx.fillStyle = '#10b981';
|
||||
ctx.fillRect(nxLocal, ny, Math.max(2, nw), nh);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Selection highlight - local selection on this track
|
||||
if (selectionMode === 'local' && localSelectionTrackId === track.id && localSelLeft !== null && localSelRight !== null && localSelRight > localSelLeft) {
|
||||
const hlLeftLocal = localSelLeft * zoom - scrollLeft;
|
||||
@@ -783,7 +746,7 @@ const WaveformLane = ({
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(hlLeftLocal, 0, hlWidth, height);
|
||||
}
|
||||
}, [track, zoom, timelineWidth, viewportWidth, isSelected, markers, selectionMode, localSelectionTrackId, localSelLeft, localSelRight, snapValue, bpm, scrollLeft, recordingState, recTempMidiNotes, recStartTimelineTime, canvasRedrawCount]);
|
||||
}, [track, zoom, timelineWidth, viewportWidth, isSelected, markers, selectionMode, localSelectionTrackId, localSelLeft, localSelRight, snapValue, bpm, scrollLeft, recordingState, recTempMidiNotes, recStartTimelineTime, canvasRedrawCount, currentTime]);
|
||||
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
|
||||
key: "virtual-spacer",
|
||||
style: {
|
||||
@@ -1238,7 +1201,7 @@ const TempoTrackLane = ({
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
|
||||
ctx.lineWidth = 0.8;
|
||||
let divisor = 1;
|
||||
if (snapValue === '1') divisor = 1;else if (snapValue === '1/2') divisor = 0.5;else if (snapValue === '1/4') divisor = 0.25;else if (snapValue === '1/8') divisor = 0.125;else if (snapValue === '1/16') divisor = 0.0625;else if (snapValue === '1/32') divisor = 0.03125;
|
||||
if (snapValue === '1') divisor = 1; else if (snapValue === '1/2') divisor = 0.5; else if (snapValue === '1/4') divisor = 0.25; else if (snapValue === '1/8') divisor = 0.125; else if (snapValue === '1/16') divisor = 0.0625; else if (snapValue === '1/32') divisor = 0.03125;
|
||||
const snapInterval = beatDuration * divisor;
|
||||
if (snapInterval * zoom >= 4) {
|
||||
const firstSnap = Math.floor(tStart / snapInterval) * snapInterval;
|
||||
@@ -1748,7 +1711,7 @@ const SubTabWaveform = ({
|
||||
for (let px = Math.floor(x0); px < Math.ceil(x1); px++) {
|
||||
const t = px / zoom;
|
||||
const y = hermiteY(t, t0, y0, m0, t1, y1, m1);
|
||||
if (px === Math.floor(x0) && i === 0) ctx.moveTo(px, y);else ctx.lineTo(px, y);
|
||||
if (px === Math.floor(x0) && i === 0) ctx.moveTo(px, y); else ctx.lineTo(px, y);
|
||||
}
|
||||
}
|
||||
ctx.stroke();
|
||||
@@ -2674,7 +2637,7 @@ const GraphEditorCanvas = ({
|
||||
nodes.forEach((n, i) => {
|
||||
const x = n.time / buffer.duration * drawWidth;
|
||||
const y = nodeY(n, h);
|
||||
if (i === 0) ctx.moveTo(x, y);else ctx.lineTo(x, y);
|
||||
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
||||
});
|
||||
ctx.stroke();
|
||||
nodes.forEach((n, i) => {
|
||||
@@ -3102,11 +3065,11 @@ const AIConfigModal = ({
|
||||
},
|
||||
className: "px-2 py-1 bg-red-800 hover:bg-red-700 text-white text-xs font-bold rounded"
|
||||
}, "Xóa"), providers.length > 1 && /*#__PURE__*/React.createElement("button", {
|
||||
onClick: function(e) { e.stopPropagation(); var provs = providers.slice(); var pos = -1; for (var pi = 0; pi < provs.length; pi++) { if (provs[pi].id === selectedId) { pos = pi; break; } } if (pos > 0) { var tmp = provs[pos]; provs[pos] = provs[pos-1]; provs[pos-1] = tmp; setProviders(provs); } },
|
||||
onClick: function (e) { e.stopPropagation(); var provs = providers.slice(); var pos = -1; for (var pi = 0; pi < provs.length; pi++) { if (provs[pi].id === selectedId) { pos = pi; break; } } if (pos > 0) { var tmp = provs[pos]; provs[pos] = provs[pos - 1]; provs[pos - 1] = tmp; setProviders(provs); } },
|
||||
className: "px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-white text-xs font-bold rounded",
|
||||
title: "Di chuyển lên"
|
||||
}, "▲"), providers.length > 1 && /*#__PURE__*/React.createElement("button", {
|
||||
onClick: function(e) { e.stopPropagation(); var provs = providers.slice(); var pos = -1; for (var pi = 0; pi < provs.length; pi++) { if (provs[pi].id === selectedId) { pos = pi; break; } } if (pos < provs.length - 1) { var tmp = provs[pos]; provs[pos] = provs[pos+1]; provs[pos+1] = tmp; setProviders(provs); } },
|
||||
onClick: function (e) { e.stopPropagation(); var provs = providers.slice(); var pos = -1; for (var pi = 0; pi < provs.length; pi++) { if (provs[pi].id === selectedId) { pos = pi; break; } } if (pos < provs.length - 1) { var tmp = provs[pos]; provs[pos] = provs[pos + 1]; provs[pos + 1] = tmp; setProviders(provs); } },
|
||||
className: "px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-white text-xs font-bold rounded",
|
||||
title: "Di chuyển xuống"
|
||||
}, "▼"))), activeProvider && /*#__PURE__*/React.createElement("form", {
|
||||
@@ -4325,10 +4288,10 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
|
||||
name: m.name || "MIDI Item",
|
||||
type: "MIDI_ITEM",
|
||||
start_bar: m.startTime / secondsPerBar,
|
||||
duration_bars: m.duration || 4.0,
|
||||
duration_bars: m.duration ? (m.duration / secondsPerBar) : 4.0,
|
||||
clip_start_offset_bars: 0.0,
|
||||
source_data: {
|
||||
total_buffer_bars: m.duration || 8.0,
|
||||
total_buffer_bars: m.duration ? (m.duration / secondsPerBar) : 8.0,
|
||||
notes: (m.notes || []).map(n => ({
|
||||
id: n.id || 'note_' + Math.random().toString(36).substr(2, 9),
|
||||
pitch: n.pitch || 60,
|
||||
@@ -4442,7 +4405,7 @@ const deserializeProjectFromSchema = (schemaObj) => {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
startTime: item.start_bar * secondsPerBar,
|
||||
duration: item.duration_bars,
|
||||
duration: item.duration_bars * secondsPerBar,
|
||||
notes: src.notes || []
|
||||
});
|
||||
} else if (item.type === "SECTION_ITEM") {
|
||||
@@ -4551,7 +4514,7 @@ const App = () => {
|
||||
const [draggedClip, setDraggedClip] = useState(null); // { trackId, clickOffset, buffer, name, volume, color }
|
||||
const [hoveredTrackId, setHoveredTrackId] = useState(null);
|
||||
const openPanel = id => {
|
||||
if (id === 'export') setShowExportPanel(true);else if (id === 'ai') setShowAIPanel(true);else if (id === 'python_tools') setShowPythonToolsPanel(true);else if (id === 'selection') setShowSelectionPanel(true);
|
||||
if (id === 'export') setShowExportPanel(true); else if (id === 'ai') setShowAIPanel(true); else if (id === 'python_tools') setShowPythonToolsPanel(true); else if (id === 'selection') setShowSelectionPanel(true);
|
||||
};
|
||||
const [activeTool, setActiveTool] = useState('select'); // 'select' | 'grab' | 'razor'
|
||||
const [snapValue, setSnapValue] = useState('free'); // 'free', '1', '1/2', '1/4', '1/8', '1/16', '1/32'
|
||||
@@ -4560,7 +4523,7 @@ const App = () => {
|
||||
if (snapVal === 'free') return time;
|
||||
const beatDuration = 60 / parseFloat(bpmVal || 120);
|
||||
let divisor = 1;
|
||||
if (snapVal === '1') divisor = 1;else if (snapVal === '1/2') divisor = 0.5;else if (snapVal === '1/4') divisor = 0.25;else if (snapVal === '1/8') divisor = 0.125;else if (snapVal === '1/16') divisor = 0.0625;else if (snapVal === '1/32') divisor = 0.03125;
|
||||
if (snapVal === '1') divisor = 1; else if (snapVal === '1/2') divisor = 0.5; else if (snapVal === '1/4') divisor = 0.25; else if (snapVal === '1/8') divisor = 0.125; else if (snapVal === '1/16') divisor = 0.0625; else if (snapVal === '1/32') divisor = 0.03125;
|
||||
const gridSpacing = beatDuration * divisor;
|
||||
return Math.round(time / gridSpacing) * gridSpacing;
|
||||
};
|
||||
@@ -4591,7 +4554,8 @@ const App = () => {
|
||||
const [currentProjectId, setCurrentProjectId] = useState(() => localStorage.getItem('sonic_project_id') || null);
|
||||
const [saveProjectModalOpen, setSaveProjectModalOpen] = useState(false);
|
||||
const [saveAsModalOpen, setSaveAsModalOpen] = useState(false);
|
||||
const [soloedTrackId, setSoloedTrackId] = useState(null);
|
||||
const soloedTrack = tracks.find(t => t.solo);
|
||||
const soloedTrackId = soloedTrack ? soloedTrack.id : null;
|
||||
const [toastMessage, setToastMessage] = useState(null);
|
||||
const [audioDevices, setAudioDevices] = useState([]);
|
||||
const [midiDevices, setMidiDevices] = useState([]);
|
||||
@@ -4608,6 +4572,7 @@ const App = () => {
|
||||
for (let input of access.inputs.values()) {
|
||||
inputs.push(input);
|
||||
input.onmidimessage = msg => {
|
||||
console.log(`[DevLog] [Raw MIDI] Received from "${input.name}" (ID: ${input.id}) - Data:`, Array.from(msg.data));
|
||||
if (msg.data.length < 3) return;
|
||||
const cmd = msg.data[0] >> 4;
|
||||
const pitch = msg.data[1];
|
||||
@@ -4623,6 +4588,16 @@ const App = () => {
|
||||
setLastMidiNote(prev => prev && prev.pitch === pitch ? { ...prev, length: lenSec, time: Date.now() } : prev);
|
||||
}
|
||||
}
|
||||
|
||||
// Forward to active MIDI recorders
|
||||
if (activeMIDIRecordersRef.current) {
|
||||
for (let trackId in activeMIDIRecordersRef.current) {
|
||||
const rec = activeMIDIRecordersRef.current[trackId];
|
||||
if (rec) {
|
||||
rec.handleMIDIMessage(msg, input.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
setMidiDevices(inputs);
|
||||
@@ -4777,7 +4752,7 @@ const App = () => {
|
||||
showToast(`Redo: ${last.action_type}`, 'info');
|
||||
};
|
||||
const applyTrackState = (trackId, state) => {
|
||||
setTracks(prev => prev.map(t => {
|
||||
updateActiveTracks(prev => prev.map(t => {
|
||||
if (t.id !== trackId) return t;
|
||||
return {
|
||||
...t,
|
||||
@@ -4786,7 +4761,7 @@ const App = () => {
|
||||
}));
|
||||
};
|
||||
const captureTrackSnapshot = trackId => {
|
||||
const track = tracks.find(t => t.id === trackId);
|
||||
const track = activeTracks.find(t => t.id === trackId);
|
||||
if (!track) return null;
|
||||
return {
|
||||
volumeDb: track.volumeDb,
|
||||
@@ -4873,7 +4848,7 @@ const App = () => {
|
||||
} catch (err) {
|
||||
const cached = localStorage.getItem('sonic_user');
|
||||
if (cached) {
|
||||
try { setCurrentUser(JSON.parse(cached)); } catch (_) {}
|
||||
try { setCurrentUser(JSON.parse(cached)); } catch (_) { }
|
||||
setIsMandatoryLogin(false);
|
||||
setAuthModalOpen(false);
|
||||
} else {
|
||||
@@ -4963,7 +4938,7 @@ const App = () => {
|
||||
const active = data.providers.find(p => p.is_active) || data.providers[0];
|
||||
if (active) setSelectedProviderId(active.id);
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e) { }
|
||||
})();
|
||||
}
|
||||
};
|
||||
@@ -5050,7 +5025,7 @@ const App = () => {
|
||||
const h = rect.height;
|
||||
const margin = 60;
|
||||
let zone = null;
|
||||
if (x < margin && x > 10) zone = 'left';else if (x > w - margin && x < w - 10) zone = 'right';else if (y < margin && y > 10) zone = 'top';else if (y > h - margin && y < h - 10) zone = 'bottom';
|
||||
if (x < margin && x > 10) zone = 'left'; else if (x > w - margin && x < w - 10) zone = 'right'; else if (y < margin && y > 10) zone = 'top'; else if (y > h - margin && y < h - 10) zone = 'bottom';
|
||||
panelDropZoneRef.current = zone;
|
||||
setPanelDropZone(zone);
|
||||
setDragGhostPos({
|
||||
@@ -5869,7 +5844,9 @@ const App = () => {
|
||||
clips: [],
|
||||
sections: [],
|
||||
midiItems: [],
|
||||
markers: []
|
||||
markers: [],
|
||||
isArmed: false,
|
||||
monitoringEnabled: true
|
||||
}));
|
||||
setSessionTabs(prev => [...prev, { id: tabId, name: tabName, sectionId: sectionId, tracks: clonedTracks }]);
|
||||
setActiveTab(tabId);
|
||||
@@ -6654,7 +6631,7 @@ const App = () => {
|
||||
speed
|
||||
} = clipboardRef.current;
|
||||
const ctx = getAudioContext();
|
||||
const targetTrack = tracks.find(t => t.id === targetTrackId);
|
||||
const targetTrack = activeTracks.find(t => t.id === targetTrackId);
|
||||
const newClip = {
|
||||
id: nextClipId(),
|
||||
startTime: pasteTime,
|
||||
@@ -6666,7 +6643,7 @@ const App = () => {
|
||||
...(speed !== undefined ? { speed } : {})
|
||||
};
|
||||
if (targetTrack) {
|
||||
setTracks(p => p.map(t => {
|
||||
updateActiveTracks(p => p.map(t => {
|
||||
if (t.id === targetTrackId) {
|
||||
const existingClips = t.clips && t.clips.length > 0 ? t.clips : t.buffer ? [{
|
||||
id: 'default_' + t.id,
|
||||
@@ -6696,7 +6673,7 @@ const App = () => {
|
||||
// No matching track — create a new one
|
||||
const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
|
||||
const newId = 'track_pasted_' + Date.now();
|
||||
setTracks(prev => [...prev, {
|
||||
updateActiveTracks(prev => [...prev, {
|
||||
id: newId,
|
||||
name: `Pasted_${name || 'track'}`,
|
||||
buffer: clipBuffer,
|
||||
@@ -6815,7 +6792,7 @@ const App = () => {
|
||||
showToast('Merged all unmuted tracks.', 'success');
|
||||
};
|
||||
const handleCopyTrack = () => {
|
||||
const t = tracks.find(x => x.id === selectedTrackId);
|
||||
const t = activeTracks.find(x => x.id === selectedTrackId);
|
||||
if (!t || !t.buffer) return;
|
||||
const sr = t.buffer.sampleRate;
|
||||
const data = t.buffer.getChannelData(0);
|
||||
@@ -6843,19 +6820,19 @@ const App = () => {
|
||||
}
|
||||
// No selection: copy entire track
|
||||
clipboardRef.current = {
|
||||
buffer: clipBuffer,
|
||||
buffer: t.buffer,
|
||||
name: t.name,
|
||||
volumeDb: t.volumeDb,
|
||||
pan: t.pan,
|
||||
color: t.color,
|
||||
sampleRate: clipBuffer.sampleRate,
|
||||
channels: clipBuffer.numberOfChannels,
|
||||
sampleRate: t.buffer.sampleRate,
|
||||
channels: t.buffer.numberOfChannels,
|
||||
speed: t.speed || 1.0
|
||||
};
|
||||
showToast('Copied track to clipboard.', 'info');
|
||||
};
|
||||
const handleCutTrack = () => {
|
||||
const t = tracks.find(x => x.id === selectedTrackId);
|
||||
const t = activeTracks.find(x => x.id === selectedTrackId);
|
||||
if (!t || !t.buffer) return;
|
||||
const beforeSnap = captureTrackSnapshot(selectedTrackId);
|
||||
const sr = t.buffer.sampleRate;
|
||||
@@ -6886,7 +6863,7 @@ const App = () => {
|
||||
let idx = 0;
|
||||
for (let i = 0; i < startSample; i++) newData[idx++] = data[i];
|
||||
for (let i = endSample; i < data.length; i++) newData[idx++] = data[i];
|
||||
setTracks(p => p.map(tr => tr.id === selectedTrackId ? {
|
||||
updateActiveTracks(p => p.map(tr => tr.id === selectedTrackId ? {
|
||||
...tr,
|
||||
buffer: newBuffer
|
||||
} : tr));
|
||||
@@ -6933,7 +6910,7 @@ const App = () => {
|
||||
}, [timelineWrapperNode]);
|
||||
useEffect(() => {
|
||||
fetch(API_BASE_URL).then(r => {
|
||||
if (r.ok) setServerStatus('connected');else setServerStatus('error');
|
||||
if (r.ok) setServerStatus('connected'); else setServerStatus('error');
|
||||
}).catch(() => setServerStatus('offline'));
|
||||
}, []);
|
||||
|
||||
@@ -7175,7 +7152,7 @@ const App = () => {
|
||||
volNodes.forEach((n, i) => {
|
||||
const t = context.currentTime + n.time / speed;
|
||||
const linearGain = Math.pow(10, n.db / 20);
|
||||
if (i === 0) volumeGainNode.gain.setValueAtTime(linearGain, t);else volumeGainNode.gain.linearRampToValueAtTime(linearGain, t);
|
||||
if (i === 0) volumeGainNode.gain.setValueAtTime(linearGain, t); else volumeGainNode.gain.linearRampToValueAtTime(linearGain, t);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7186,7 +7163,7 @@ const App = () => {
|
||||
panNodes.forEach((n, i) => {
|
||||
const t = context.currentTime + n.time / speed;
|
||||
const clamped = Math.max(-1, Math.min(1, n.pan));
|
||||
if (i === 0) pannerNode.pan.setValueAtTime(clamped, t);else pannerNode.pan.linearRampToValueAtTime(clamped, t);
|
||||
if (i === 0) pannerNode.pan.setValueAtTime(clamped, t); else pannerNode.pan.linearRampToValueAtTime(clamped, t);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7274,21 +7251,28 @@ const App = () => {
|
||||
}
|
||||
|
||||
// MIDI preview
|
||||
let combinedNotes = [];
|
||||
const armedTracks = activeTracksRef.current.filter(t => t.isArmed);
|
||||
for (let track of armedTracks) {
|
||||
const midiRec = activeMIDIRecordersRef.current[track.id];
|
||||
if (midiRec) {
|
||||
const currentBeat = (audioCtx.currentTime - midiRec.recStartAudioTime - midiRec.latencyCompSec) / secondsPerBeat + (midiRec.recStartBar * 4);
|
||||
setRecTempMidiNotes([...midiRec.recordedNotes, ...Array.from(midiRec.activeNotes.values()).map(n => ({
|
||||
const currentBeat = (audioCtx.currentTime - midiRec.recStartAudioTime - midiRec.latencyCompSec) / secondsPerBeat;
|
||||
const notes = [...midiRec.recordedNotes, ...Array.from(midiRec.activeNotes.values()).map(n => ({
|
||||
...n,
|
||||
duration_beats: currentBeat - n.start_beat
|
||||
}))]);
|
||||
}))];
|
||||
combinedNotes = combinedNotes.concat(notes);
|
||||
}
|
||||
}
|
||||
if (combinedNotes.length > 0 || armedTracks.some(t => activeMIDIRecordersRef.current[t.id])) {
|
||||
setRecTempMidiNotes(combinedNotes);
|
||||
setCanvasRedrawCount(n => n + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (activeTabRef.current !== 'main') {
|
||||
const isSubTab = subTabsRef.current.some(sub => sub.id === activeTabRef.current);
|
||||
if (isSubTab) {
|
||||
const st = subTabsRef.current.find(s => s.id === activeTabRef.current);
|
||||
if (!st || !st.isPlaying || !st.buffer) return;
|
||||
const context = getAudioContext();
|
||||
@@ -7537,7 +7521,7 @@ const App = () => {
|
||||
activeSourcesRef.current.forEach(src => {
|
||||
try {
|
||||
src.stop();
|
||||
} catch (e) {}
|
||||
} catch (e) { }
|
||||
});
|
||||
activeSourcesRef.current = [];
|
||||
activeTrackNodesRef.current = {};
|
||||
@@ -7665,7 +7649,7 @@ const App = () => {
|
||||
id: tempMidiItemId,
|
||||
name: 'Recording...',
|
||||
startTime: startTimelineTime,
|
||||
duration: 4,
|
||||
duration: 4 * (secondsPerBeat * 4),
|
||||
notes: []
|
||||
}]
|
||||
};
|
||||
@@ -7677,8 +7661,9 @@ const App = () => {
|
||||
drawVuMeter(canvas, 0);
|
||||
setTimeout(() => drawVuMeter(canvas, -60), 100);
|
||||
}
|
||||
const elapsedBeats = Math.max(0, currentBeat - midiRec.recStartBar * 4);
|
||||
const durBars = Math.max(1, Math.ceil(elapsedBeats / 4) + 1);
|
||||
const elapsedBeats = Math.max(0, currentBeat);
|
||||
const elapsedSec = elapsedBeats * secondsPerBeat;
|
||||
const durationSec = Math.max(4 * (secondsPerBeat * 4), elapsedSec);
|
||||
const allNotes = [...midiRec.recordedNotes, ...Array.from(midiRec.activeNotes.values()).map(n => ({
|
||||
...n,
|
||||
duration_beats: currentBeat - n.start_beat
|
||||
@@ -7690,16 +7675,18 @@ const App = () => {
|
||||
if (t.id !== track.id) return t;
|
||||
return {
|
||||
...t,
|
||||
midiItems: t.midiItems.map(m => m.id === midiRec.tempMidiItemId ? { ...m, notes: allNotes, duration: durBars } : m)
|
||||
midiItems: t.midiItems.map(m => m.id === midiRec.tempMidiItemId ? { ...m, notes: allNotes, duration: durationSec } : m)
|
||||
};
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
midiRec.onNoteOff = () => {
|
||||
const currentBeat = (context.currentTime - midiRec.recStartAudioTime - midiRec.latencyCompSec) / (60.0 / midiRec.bpm) + (midiRec.recStartBar * 4);
|
||||
const elapsedBeats = Math.max(0, currentBeat - midiRec.recStartBar * 4);
|
||||
const durBars = Math.max(1, Math.ceil(elapsedBeats / 4) + 1);
|
||||
const currentBeat = (context.currentTime - midiRec.recStartAudioTime - midiRec.latencyCompSec) / (60.0 / midiRec.bpm);
|
||||
|
||||
const elapsedBeats = Math.max(0, currentBeat);
|
||||
const elapsedSec = elapsedBeats * secondsPerBeat;
|
||||
const durationSec = Math.max(4 * (secondsPerBeat * 4), elapsedSec);
|
||||
const activeNotesArray = Array.from(midiRec.activeNotes.values()).map(n => ({
|
||||
...n,
|
||||
duration_beats: currentBeat - n.start_beat
|
||||
@@ -7712,7 +7699,7 @@ const App = () => {
|
||||
if (t.id !== track.id) return t;
|
||||
return {
|
||||
...t,
|
||||
midiItems: t.midiItems.map(m => m.id === midiRec.tempMidiItemId ? { ...m, notes: allNotes, duration: durBars } : m)
|
||||
midiItems: t.midiItems.map(m => m.id === midiRec.tempMidiItemId ? { ...m, notes: allNotes, duration: durationSec } : m)
|
||||
};
|
||||
}));
|
||||
}
|
||||
@@ -7761,19 +7748,22 @@ const App = () => {
|
||||
for (let { trackId, midiRec } of midiRecList) {
|
||||
if (!midiRec.isRecording || !midiRec.tempMidiItemId) continue;
|
||||
const currentTimeSec = Math.max(0, getAudioContext().currentTime - midiRec.recStartAudioTime - midiRec.latencyCompSec);
|
||||
const currentBeat = currentTimeSec / secondsPerBeatInt + midiRec.recStartBar * 4;
|
||||
const currentBeat = currentTimeSec / secondsPerBeatInt;
|
||||
const allNotes = [...midiRec.recordedNotes, ...Array.from(midiRec.activeNotes.values()).map(n => ({
|
||||
...n,
|
||||
duration_beats: currentBeat - n.start_beat
|
||||
}))];
|
||||
const elapsedBeats = Math.max(0, currentBeat - midiRec.recStartBar * 4);
|
||||
const durBars = Math.max(1, Math.ceil(elapsedBeats / 4) + 1);
|
||||
const durationSec = Math.max(4 * (secondsPerBeatInt * 4), currentTimeSec);
|
||||
if (Math.random() < 0.2) { // Throttle log to prevent flooding (approx 2 logs/sec)
|
||||
console.log(`[DevLog] [MIDI Rec Sync] Temp Item ID: ${midiRec.tempMidiItemId}, Duration: ${durationSec.toFixed(2)}s, ActiveNotes: ${midiRec.activeNotes.size}, RecordedNotes: ${midiRec.recordedNotes.length}`);
|
||||
}
|
||||
setRecTempMidiNotes(allNotes);
|
||||
setCanvasRedrawCount(n => n + 1);
|
||||
updateActiveTracks(prev => prev.map(t => {
|
||||
if (t.id !== trackId) return t;
|
||||
return {
|
||||
...t,
|
||||
midiItems: t.midiItems.map(m => m.id === midiRec.tempMidiItemId ? { ...m, notes: allNotes, duration: durBars } : m)
|
||||
midiItems: t.midiItems.map(m => m.id === midiRec.tempMidiItemId ? { ...m, notes: allNotes, duration: durationSec } : m)
|
||||
};
|
||||
}));
|
||||
}
|
||||
@@ -7788,6 +7778,7 @@ const App = () => {
|
||||
|
||||
const context = getAudioContext();
|
||||
const secondsPerBeat = 60.0 / (parseInt(bpm) || 120);
|
||||
const secondsPerBar = secondsPerBeat * 4;
|
||||
|
||||
const midiRecorders = activeMIDIRecordersRef.current;
|
||||
const audioRecorders = activeAudioRecordersRef.current;
|
||||
@@ -7817,7 +7808,7 @@ const App = () => {
|
||||
...updatedItems[itemIndex],
|
||||
name: recordedNotes.length > 0 ? 'Recorded MIDI' : 'Empty MIDI',
|
||||
notes: recordedNotes,
|
||||
duration: Math.ceil(totalDurationBeats / 4)
|
||||
duration: Math.ceil(totalDurationBeats / 4) * secondsPerBar
|
||||
};
|
||||
return { ...t, midiItems: updatedItems };
|
||||
}
|
||||
@@ -7825,7 +7816,7 @@ const App = () => {
|
||||
id: 'midi_rec_' + Date.now(),
|
||||
name: 'Recorded MIDI',
|
||||
startTime: recordingStartTimeRef.current,
|
||||
duration: Math.ceil(totalDurationBeats / 4),
|
||||
duration: Math.ceil(totalDurationBeats / 4) * secondsPerBar,
|
||||
notes: recordedNotes
|
||||
};
|
||||
return {
|
||||
@@ -7844,7 +7835,7 @@ const App = () => {
|
||||
id: 'midi_rec_' + Date.now(),
|
||||
name: 'Recorded MIDI',
|
||||
startTime: recordingStartTimeRef.current,
|
||||
duration: Math.ceil(totalDurationBeats / 4),
|
||||
duration: Math.ceil(totalDurationBeats / 4) * secondsPerBar,
|
||||
notes: recordedNotes
|
||||
};
|
||||
updateActiveTracks(prev => prev.map(t => {
|
||||
@@ -8387,6 +8378,7 @@ const App = () => {
|
||||
const time = mouseX / zoom;
|
||||
const newStart = Math.max(0, time - drag.clickOffset);
|
||||
const targetTrackId = hoveredTrackIdRef.current || drag.trackId;
|
||||
const secondsPerBar = (60.0 / (parseInt(bpm) || 120)) * 4;
|
||||
updateActiveTracks(prev => prev.map(t => {
|
||||
const items = drag.itemType === 'section' ? (t.sections || []) : (t.midiItems || []);
|
||||
const updatedItems = items.filter(it => it.id !== drag.itemId);
|
||||
@@ -8398,9 +8390,9 @@ const App = () => {
|
||||
: { ...movedItem, startTime: newStart });
|
||||
} else {
|
||||
if (drag.itemType === 'section') {
|
||||
updatedItems.push({ id: drag.itemId, name: 'Section', start: newStart, duration: 4, color: '#06b6d4' });
|
||||
updatedItems.push({ id: drag.itemId, name: 'Section', start: newStart, duration: 4 * secondsPerBar, color: '#06b6d4' });
|
||||
} else {
|
||||
updatedItems.push({ id: drag.itemId, name: 'MIDI Item', startTime: newStart, duration: 4, notes: [], color: '#a78bfa' });
|
||||
updatedItems.push({ id: drag.itemId, name: 'MIDI Item', startTime: newStart, duration: 4 * secondsPerBar, notes: [], color: '#a78bfa' });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8605,7 +8597,6 @@ const App = () => {
|
||||
solo: false
|
||||
};
|
||||
}));
|
||||
setSoloedTrackId(prev => prev === trackId ? null : trackId);
|
||||
if (wasPlaying) {
|
||||
setTimeout(() => {
|
||||
startOffsetTimeRef.current = currentTime;
|
||||
@@ -8905,11 +8896,12 @@ const App = () => {
|
||||
const curTracks = activeTracks;
|
||||
const track = curTracks.find(t => t.id === selectedTrackId);
|
||||
if (!track) { showToast('Chọn track trước', 'warning'); return; }
|
||||
const secondsPerBar = (60.0 / (parseInt(bpm) || 120)) * 4;
|
||||
const midiItem = {
|
||||
id: `midi_${Date.now()}`,
|
||||
name: 'MIDI Item',
|
||||
startTime: currentTime,
|
||||
duration: 4,
|
||||
duration: 4 * secondsPerBar,
|
||||
notes: [],
|
||||
color: '#a78bfa'
|
||||
};
|
||||
@@ -9918,7 +9910,7 @@ const App = () => {
|
||||
const active = data.providers.find(p => p.is_active) || data.providers[0];
|
||||
if (active) setSelectedProviderId(active.id);
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e) { }
|
||||
}
|
||||
const prv = aiProviders.find(p => p.id === selectedProviderId) || (aiProviders.length > 0 ? aiProviders[0] : null);
|
||||
const provider = prv || aiConfig;
|
||||
@@ -10493,7 +10485,7 @@ const App = () => {
|
||||
const smp = Math.max(-1, Math.min(1, renderedBuffer.getChannelData(ch)[i]));
|
||||
if (bd === 8) vw.setUint8(ofs, Math.floor((smp + 1) * 127.5));
|
||||
else if (bd === 16) vw.setInt16(ofs, Math.floor(smp < 0 ? smp * 0x8000 : smp * 0x7FFF), true);
|
||||
else { const v24 = Math.floor(smp < 0 ? smp * 0x800000 : smp * 0x7FFFFF); vw.setUint8(ofs, v24 & 0xFF); vw.setUint8(ofs+1, v24 >> 8 & 0xFF); vw.setUint8(ofs+2, v24 >> 16 & 0xFF); }
|
||||
else { const v24 = Math.floor(smp < 0 ? smp * 0x800000 : smp * 0x7FFFFF); vw.setUint8(ofs, v24 & 0xFF); vw.setUint8(ofs + 1, v24 >> 8 & 0xFF); vw.setUint8(ofs + 2, v24 >> 16 & 0xFF); }
|
||||
ofs += bps;
|
||||
}
|
||||
}
|
||||
@@ -10775,7 +10767,7 @@ const App = () => {
|
||||
localStorage.setItem('sonic_preferences', JSON.stringify(prefs));
|
||||
if (!currentUser || currentUser === 'cached') return;
|
||||
const timer = setTimeout(async () => {
|
||||
try { await window.SonicAPI.savePreferences(prefs); } catch (e) {}
|
||||
try { await window.SonicAPI.savePreferences(prefs); } catch (e) { }
|
||||
}, 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [showAIPanel, showExportPanel, showSelectionPanel, showPythonToolsPanel,
|
||||
@@ -11331,7 +11323,8 @@ const App = () => {
|
||||
className: "w-[1px] h-5 bg-zinc-800 mx-0.5"
|
||||
}), /*#__PURE__*/React.createElement("button", {
|
||||
onClick: () => {
|
||||
if (activeTab !== 'main') {
|
||||
const isSubTab = subTabs.some(sub => sub.id === activeTab);
|
||||
if (isSubTab) {
|
||||
setSubTabs(prev => prev.map(s => s.id === activeTab ? {
|
||||
...s,
|
||||
currentTime: 0
|
||||
@@ -11349,7 +11342,8 @@ const App = () => {
|
||||
className: "w-3.5 h-3.5"
|
||||
}))), /*#__PURE__*/React.createElement("button", {
|
||||
onClick: () => {
|
||||
if (activeTab !== 'main') {
|
||||
const isSubTab = subTabs.some(sub => sub.id === activeTab);
|
||||
if (isSubTab) {
|
||||
setSubTabs(prev => prev.map(s => {
|
||||
if (s.id !== activeTab) return s;
|
||||
const left = s.selectionStart !== null && s.selectionEnd !== null ? Math.min(s.selectionStart, s.selectionEnd) : null;
|
||||
@@ -11398,7 +11392,8 @@ const App = () => {
|
||||
className: "w-3.5 h-3.5 fill-current"
|
||||
}))), /*#__PURE__*/React.createElement("button", {
|
||||
onClick: () => {
|
||||
if (activeTab !== 'main') {
|
||||
const isSubTab = subTabs.some(sub => sub.id === activeTab);
|
||||
if (isSubTab) {
|
||||
setSubTabs(prev => prev.map(s => {
|
||||
if (s.id !== activeTab) return s;
|
||||
const right = s.selectionStart !== null && s.selectionEnd !== null ? Math.max(s.selectionStart, s.selectionEnd) : null;
|
||||
@@ -11420,7 +11415,8 @@ const App = () => {
|
||||
className: "w-3.5 h-3.5"
|
||||
}))), /*#__PURE__*/React.createElement("button", {
|
||||
onClick: () => {
|
||||
if (activeTab !== 'main') {
|
||||
const isSubTab = subTabs.some(sub => sub.id === activeTab);
|
||||
if (isSubTab) {
|
||||
setSubTabs(prev => prev.map(s => {
|
||||
if (s.id !== activeTab) return s;
|
||||
const duration = s.buffer ? s.buffer.duration / (s.speed || 1.0) : 0;
|
||||
@@ -12293,7 +12289,7 @@ const App = () => {
|
||||
},
|
||||
title: "Solo",
|
||||
className: `px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 ${soloedTrackId === track.id || track.solo ? 'bg-amber-950 text-amber-400 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`
|
||||
}, /*#__PURE__*/React.createElement("i", { "data-lucide": soloedTrackId === track.id || track.solo ? "headphones" : "headphones-off", className: "w-3 h-3" })), /*#__PURE__*/React.createElement("button", {
|
||||
}, /*#__PURE__*/React.createElement("i", { "data-lucide": soloedTrackId === track.id || track.solo ? "headphones" : "headphone-off", className: "w-3 h-3" })), /*#__PURE__*/React.createElement("button", {
|
||||
onClick: e => {
|
||||
e.stopPropagation();
|
||||
toggleTrackArm(track.id);
|
||||
@@ -12378,7 +12374,9 @@ const App = () => {
|
||||
value: `MICROPHONE:${d.deviceId}`
|
||||
}, d.label || `Microphone ${d.deviceId.slice(0, 5)}`))), /*#__PURE__*/React.createElement("optgroup", {
|
||||
label: "MIDI Keyboards"
|
||||
}, midiDevices.map(d => /*#__PURE__*/React.createElement("option", {
|
||||
}, /*#__PURE__*/React.createElement("option", {
|
||||
value: "MIDI_KEYBOARD:ALL"
|
||||
}, "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", {
|
||||
@@ -12390,7 +12388,7 @@ const App = () => {
|
||||
className: "w-8 text-right 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];
|
||||
if (el) trackVuRefs.current[track.id] = el; else delete trackVuRefs.current[track.id];
|
||||
},
|
||||
width: 100,
|
||||
height: 4,
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
+79
-21
@@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -14,7 +15,7 @@
|
||||
<script src="/static/js/services/storage.js"></script>
|
||||
<script src="/static/js/services/aiGateway.js"></script>
|
||||
<script src="/static/js/services/dawCommandDispatcher.js"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=20260723" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202607231122" defer></script>
|
||||
<style>
|
||||
:root {
|
||||
--right-sidebar-width: 320px;
|
||||
@@ -31,26 +32,71 @@
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
.daw-bg { background-color: #1e1e1e; }
|
||||
.daw-panel { background-color: #262626; }
|
||||
.daw-header { background-color: #2e2e2e; }
|
||||
.daw-border { border-color: #181818; }
|
||||
.daw-track-active { background-color: #333333; }
|
||||
::-webkit-scrollbar { width: 10px; height: 10px; }
|
||||
::-webkit-scrollbar-track { background: #141414; }
|
||||
::-webkit-scrollbar-thumb { background: #3a3a3a; border: 2px solid #141414; border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #4a4a4a; }
|
||||
.knob-container { position: relative; width: 28px; height: 28px; }
|
||||
.knob-dial { transform-origin: center; transition: transform 0.1s ease; }
|
||||
|
||||
.daw-bg {
|
||||
background-color: #1e1e1e;
|
||||
}
|
||||
|
||||
.daw-panel {
|
||||
background-color: #262626;
|
||||
}
|
||||
|
||||
.daw-header {
|
||||
background-color: #2e2e2e;
|
||||
}
|
||||
|
||||
.daw-border {
|
||||
border-color: #181818;
|
||||
}
|
||||
|
||||
.daw-track-active {
|
||||
background-color: #333333;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #141414;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #3a3a3a;
|
||||
border: 2px solid #141414;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #4a4a4a;
|
||||
}
|
||||
|
||||
.knob-container {
|
||||
position: relative;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.knob-dial {
|
||||
transform-origin: center;
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.selection-interactive-box {
|
||||
min-width: 4px;
|
||||
}
|
||||
|
||||
.no-scrollbar {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE 10+ */
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
-ms-overflow-style: none;
|
||||
/* IE 10+ */
|
||||
}
|
||||
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none; /* Safari and Chrome */
|
||||
display: none;
|
||||
/* Safari and Chrome */
|
||||
}
|
||||
|
||||
/* Fullscreen Fixed App Shell */
|
||||
@@ -101,12 +147,14 @@
|
||||
}
|
||||
|
||||
#panel-media-explorer {
|
||||
height: 50%; /* Default 50/50 split */
|
||||
height: 50%;
|
||||
/* Default 50/50 split */
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
#panel-ai {
|
||||
flex: 1; /* Fills remaining height */
|
||||
flex: 1;
|
||||
/* Fills remaining height */
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
@@ -118,7 +166,8 @@
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px;
|
||||
overflow-x: auto; /* Enables horizontal scroll when panels overflow */
|
||||
overflow-x: auto;
|
||||
/* Enables horizontal scroll when panels overflow */
|
||||
overflow-y: hidden;
|
||||
background-color: #161616;
|
||||
border-top: 1px solid var(--panel-border-color);
|
||||
@@ -129,17 +178,20 @@
|
||||
.daw-bottom-strip::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.daw-bottom-strip::-webkit-scrollbar-thumb {
|
||||
background: #3a3a3a;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.daw-bottom-strip::-webkit-scrollbar-thumb:hover {
|
||||
background: #00ffcc;
|
||||
}
|
||||
|
||||
/* Sub-panels inside the bottom strip */
|
||||
.bottom-panel {
|
||||
flex: 0 0 auto; /* Prevents shrinking, locks content dimensions */
|
||||
flex: 0 0 auto;
|
||||
/* Prevents shrinking, locks content dimensions */
|
||||
width: 320px;
|
||||
height: 100%;
|
||||
background-color: #222;
|
||||
@@ -151,11 +203,13 @@
|
||||
/* RESIZER HANDLES */
|
||||
.resizer-col-handle {
|
||||
width: 5px;
|
||||
cursor: ew-resize; /* Horizontal resize cursor */
|
||||
cursor: ew-resize;
|
||||
/* Horizontal resize cursor */
|
||||
background: transparent;
|
||||
transition: background 0.2s;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.resizer-col-handle:hover,
|
||||
.resizer-col-handle:active {
|
||||
background: #00ffcc;
|
||||
@@ -163,18 +217,22 @@
|
||||
|
||||
.resizer-row-handle {
|
||||
height: 5px;
|
||||
cursor: ns-resize; /* Vertical resize cursor */
|
||||
cursor: ns-resize;
|
||||
/* Vertical resize cursor */
|
||||
background: transparent;
|
||||
transition: background 0.2s;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.resizer-row-handle:hover,
|
||||
.resizer-row-handle:active {
|
||||
background: #00ffcc;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="h-screen w-screen flex flex-col">
|
||||
<div id="root" class="h-full w-full flex flex-col"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user