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