fix: không hiển thị UI trên browser sau khi cài đặt phím tắt cho menu hệ thống
This commit is contained in:
+182
@@ -0,0 +1,182 @@
|
|||||||
|
Dưới đây là toàn bộ nội dung tài liệu đặc tả kỹ thuật đã được chuyển đổi sang định dạng Markdown chuẩn, tối ưu hóa các khối mã nguồn (`python`, `text`), căn chỉnh bảng biểu, sơ đồ luồng ASCII và các công thức toán học dạng LaTeX:
|
||||||
|
|
||||||
|
# Technical Specification: Sub-Tab Audio Clip Editor & DSP Operations
|
||||||
|
|
||||||
|
This document outlines the software engineering specification for the temporary isolated Sub-Tab Audio Clip Editor. It defines internal clipboard mechanics, DSP algorithms for selection-based operations, Context Menu structures, and main application menu shortcuts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Clipboard & Cursor-Aligned Insertion Mechanics
|
||||||
|
|
||||||
|
The Sub-Tab workspace features an isolated, low-latency stereo/mono audio buffer. The editor tracks a local virtual playhead position $t_{\text{cursor}}$ and handles clipboard buffers using non-destructive splicing techniques.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Local Timeline Buffer
|
||||||
|
+-------------------------------------------------------+
|
||||||
|
| Track Waveform Segment │ |
|
||||||
|
+-------------------------------┼-----------------------+
|
||||||
|
▲
|
||||||
|
t_cursor (Insertion Point)
|
||||||
|
│
|
||||||
|
▼ [ PASTE TRIGGERED ]
|
||||||
|
+-------------------------------------------------------+
|
||||||
|
| Track Waveform Segment │ CLIPBOARD DATA │ |
|
||||||
|
+-------------------------------------------------------+
|
||||||
|
◄──────────────►
|
||||||
|
clip_duration
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.1. Cursor Paste Action
|
||||||
|
|
||||||
|
When a paste command is issued (either via Context Menu, Application Menu, or Hotkey):
|
||||||
|
|
||||||
|
* **Payload Extraction:** Retrieve the copied `AudioBufferSegment` from the system/application clipboard.
|
||||||
|
* **Splicing Boundary Calculations:** Slice the current active timeline buffer at $t_{\text{cursor}}$.
|
||||||
|
* **Re-allocation & Stitching:**
|
||||||
|
* Compute the new duration: $T_{\text{new}} = T_{\text{original}} + T_{\text{clipboard}}$.
|
||||||
|
* Allocate a new virtual audio array $Y_{\text{new}}$:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$$Y_{\text{new}}(t) = \begin{cases} Y_{\text{original}}(t) & 0 \le t < t_{\text{cursor}} \\ Y_{\text{clipboard}}(t - t_{\text{cursor}}) & t_{\text{cursor}} \le t < t_{\text{cursor}} + T_{\text{clipboard}} \\ Y_{\text{original}}(t - T_{\text{clipboard}}) & t_{\text{cursor}} + T_{\text{clipboard}} \le t \le T_{\text{new}} \end{cases}$$
|
||||||
|
|
||||||
|
* **Playhead Update:** Advance the active playhead $t_{\text{cursor}}$ immediately to $t_{\text{cursor}} + T_{\text{clipboard}}$.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Selection Context Menu & DSP Engine
|
||||||
|
|
||||||
|
Right-clicking inside a highlighted region $[T_{\text{start}}, T_{\text{end}}]$ of the Waveform Canvas triggers an overlay context menu containing the following DSP and editing commands.
|
||||||
|
|
||||||
|
```text
|
||||||
|
+---------------------------------------------+
|
||||||
|
| Selection: [ 01:02.100 - 01:05.400 ] |
|
||||||
|
+---------------------------------------------+
|
||||||
|
| Normalize Selection To Peak |
|
||||||
|
| Adjust Gain/Volume... |
|
||||||
|
| Adjust Panning (Stereo Balance)... |
|
||||||
|
| Fade In (Linear/Exponential) |
|
||||||
|
| Fade Out (Linear/Exponential) |
|
||||||
|
|---------------------------------------------|
|
||||||
|
| Cut Ctrl+X |
|
||||||
|
| Copy Ctrl+C |
|
||||||
|
| Paste Ctrl+V |
|
||||||
|
| Delete Selected Segment Del |
|
||||||
|
|---------------------------------------------|
|
||||||
|
| Loop Selection: [ ▲ ] [ 4 ] [ ▼ ] times |
|
||||||
|
+---------------------------------------------+
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.1. Normalize Selection
|
||||||
|
|
||||||
|
Scales the peak amplitude of the selected segment to a target ceiling $A_{\text{target}}$ (defaulting to $1.0$ or $0\text{ dBFS}$):
|
||||||
|
|
||||||
|
$$Y_{\text{norm}}(t) = Y(t) \cdot \frac{A_{\text{target}}}{\max_{u \in [T_{\text{start}}, T_{\text{end}}]} \vert{}Y(u)\vert{}} \quad \text{for } t \in [T_{\text{start}}, T_{\text{end}}]$$
|
||||||
|
|
||||||
|
### 2.2. Volume (Gain dB) Adjustment
|
||||||
|
|
||||||
|
Applies a static linear gain multiplier derived from user-specified decibel scaling values ($\Delta\text{dB}$):
|
||||||
|
|
||||||
|
$$G = 10^{\frac{\Delta\text{dB}}{20}}$$
|
||||||
|
|
||||||
|
$$Y_{\text{gained}}(t) = Y(t) \cdot G \quad \text{for } t \in [T_{\text{start}}, T_{\text{end}}]$$
|
||||||
|
|
||||||
|
### 2.3. Panning (Stereo Balance)
|
||||||
|
|
||||||
|
Applies a constant-power panning law across Left ($L$) and Right ($R$) channels based on the panning angle $\theta \in [0, \pi/2]$, where $\theta = \pi/4$ represents absolute center:
|
||||||
|
|
||||||
|
$$Y_L(t) = Y_{\text{mono}}(t) \cdot \cos(\theta), \quad Y_R(t) = Y_{\text{mono}}(t) \cdot \sin(\theta)$$
|
||||||
|
|
||||||
|
### 2.4. Fade-In and Fade-Out (Linear / Exponential)
|
||||||
|
|
||||||
|
* **Linear Fade-In Curve:**
|
||||||
|
|
||||||
|
$$f_{\text{in}}(t) = \frac{t - T_{\text{start}}}{T_{\text{end}} - T_{\text{start}}} \quad \text{for } t \in [T_{\text{start}}, T_{\text{end}}]$$
|
||||||
|
|
||||||
|
* **Linear Fade-Out Curve:**
|
||||||
|
|
||||||
|
$$f_{\text{out}}(t) = 1.0 - \frac{t - T_{\text{start}}}{T_{\text{end}} - T_{\text{start}}} \quad \text{for } t \in [T_{\text{start}}, T_{\text{end}}]$$
|
||||||
|
|
||||||
|
### 2.5. Delete, Cut, and Copy
|
||||||
|
|
||||||
|
* **Delete:** Erases the selected segment $[T_{\text{start}}, T_{\text{end}}]$ and shifts all subsequent samples leftward.
|
||||||
|
* **Cut:** Copies the selected samples to the clipboard, then executes the *Delete* routine.
|
||||||
|
* **Copy:** Writes the targeted buffer segment to the clip memory without modifying the timeline.
|
||||||
|
|
||||||
|
### 2.6. Segment Looping with Step Multiplier
|
||||||
|
|
||||||
|
Repeats the selected segment $[T_{\text{start}}, T_{\text{end}}]$ consecutively $N$ times. The menu provides a numeric spinner (Up/Down buttons) to adjust $N$:
|
||||||
|
|
||||||
|
1. Extract segment: $Y_{\text{segment}} = Y(t)$ for $t \in [T_{\text{start}}, T_{\text{end}}]$.
|
||||||
|
2. Compute new duration adjustment: $\Delta L = (N - 1) \cdot (T_{\text{end}} - T_{\text{start}})$.
|
||||||
|
3. Duplicate and insert $Y_{\text{segment}}$ array $N-1$ times directly after $T_{\text{end}}$.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Global Menu Bar & Keyboard Shortcut Matrix
|
||||||
|
|
||||||
|
All context-dependent sub-tab actions are mapped directly to the global Menu Bar at the top of the DAW window, as specified in `image_e0e462.png`.
|
||||||
|
|
||||||
|
```text
|
||||||
|
File Edit View Insert Track Options Actions Extensions Help
|
||||||
|
│
|
||||||
|
├── Normalize Selection [Ctrl+Alt+N]
|
||||||
|
├── Adjust Volume... [V]
|
||||||
|
├── Adjust Panning... [P]
|
||||||
|
├── Fade In [F]
|
||||||
|
├── Fade Out [G]
|
||||||
|
├── Cut [Ctrl+X]
|
||||||
|
├── Copy [Ctrl+C]
|
||||||
|
├── Paste [Ctrl+V]
|
||||||
|
├── Delete [Del]
|
||||||
|
└── Loop Clip... [Ctrl+L]
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.1. Keyboard Mapping Table
|
||||||
|
|
||||||
|
To maximize speed and accessibility, the system listens for global key event hooks within the Sub-Tab window focus:
|
||||||
|
|
||||||
|
| Action Command | Main Menu Category | Recommended Keyboard Shortcut | Python Event Trigger (`QKeyEvent`) |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| **Cut** | Edit -> Cut | `Ctrl + X` | `Qt.Key.Key_X` + `ControlModifier` |
|
||||||
|
| **Copy** | Edit -> Copy | `Ctrl + C` | `Qt.Key.Key_C` + `ControlModifier` |
|
||||||
|
| **Paste** | Edit -> Paste | `Ctrl + V` | `Qt.Key.Key_V` + `ControlModifier` |
|
||||||
|
| **Delete** | Edit -> Delete | `Del` / `Backspace` | `Qt.Key.Key_Delete` / `Key_Backspace` |
|
||||||
|
| **Normalize** | Actions -> Normalize | `Ctrl + Alt + N` | `Qt.Key.Key_N` + `ControlModifier` + `AltModifier` |
|
||||||
|
| **Fade In** | Actions -> Fade In | `F` | `Qt.Key.Key_F` |
|
||||||
|
| **Fade Out** | Actions -> Fade Out | `G` | `Qt.Key.Key_G` |
|
||||||
|
| **Loop Segment** | Actions -> Loop... | `Ctrl + L` | `Qt.Key.Key_L` + `ControlModifier` |
|
||||||
|
| **Adjust Volume** | Actions -> Gain... | `V` | `Qt.Key.Key_V` |
|
||||||
|
| **Adjust Panning** | Actions -> Panning... | `P` | `Qt.Key.Key_P` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Python Implementation Notes for Docker Server Porting
|
||||||
|
|
||||||
|
When porting these sub-tab operations to your Python DSP engine (`core/audio_editor.py`), use NumPy slice vectors to perform non-destructive edits on waveforms:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Prototype helper for non-destructive volume adjustment in Python
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
def apply_gain_on_segment(y: np.ndarray, sr: int, start_sec: float, end_sec: float, gain_db: float) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Applies gain in dB to a selected segment of a mono numpy audio array.
|
||||||
|
"""
|
||||||
|
# 1. Translate time coordinates securely with boundary checking
|
||||||
|
start_sample = max(0, int(start_sec * sr))
|
||||||
|
end_sample = min(len(y), int(end_sec * sr))
|
||||||
|
|
||||||
|
# 2. Convert dB value to linear multiplier
|
||||||
|
multiplier = 10.0 ** (gain_db / 20.0)
|
||||||
|
|
||||||
|
# 3. Create a deep copy and modify segment in-place
|
||||||
|
y_edited = np.copy(y)
|
||||||
|
y_edited[start_sample:end_sample] *= multiplier
|
||||||
|
|
||||||
|
return y_edited
|
||||||
|
|
||||||
|
```
|
||||||
+18
-53
@@ -6,9 +6,9 @@
|
|||||||
<title>SonicForge Studio - Professional DAW Editor</title>
|
<title>SonicForge Studio - Professional DAW Editor</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
<script src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
|
||||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
|
||||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
<script src="https://unpkg.com/@babel/standalone@7.26.0/babel.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
@@ -460,70 +460,35 @@
|
|||||||
const isDraggingRulerRef = useRef(false);
|
const isDraggingRulerRef = useRef(false);
|
||||||
const handlePlayPauseRef = useRef(null);
|
const handlePlayPauseRef = useRef(null);
|
||||||
|
|
||||||
// ── Keyboard Shortcuts (LOOP_EDITOR_2.md §4.3, menu shortcuts) ──
|
// ── Keyboard Shortcuts ──
|
||||||
const handleUndoRef = useRef(handleUndo);
|
const handleUndoRef = useRef(handleUndo);
|
||||||
const handleRedoRef = useRef(handleRedo);
|
const handleRedoRef = useRef(handleRedo);
|
||||||
const handlePlayPauseRef = useRef(null);
|
|
||||||
const keyHandlersRef = useRef({});
|
|
||||||
handleUndoRef.current = handleUndo;
|
handleUndoRef.current = handleUndo;
|
||||||
handleRedoRef.current = handleRedo;
|
handleRedoRef.current = handleRedo;
|
||||||
|
|
||||||
// Keep key handlers up-to-date via refs (avoids stale closures in useEffect)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
keyHandlersRef.current = {
|
const handler = (e) => {
|
||||||
addNewTrack, handleSplitTrack, handleMergeTracks, handleCopyTrack,
|
|
||||||
handleCutTrack, handleDeleteTrack, contextMenuPaste, openTempTab,
|
|
||||||
openTempTabImport: () => {
|
|
||||||
const input = document.createElement('input');
|
|
||||||
input.type = 'file'; input.accept = 'audio/*';
|
|
||||||
input.onchange = async (e) => {
|
|
||||||
if (e.target.files[0]) {
|
|
||||||
addNewTrack();
|
|
||||||
const newId = (tracks.length + 1).toString();
|
|
||||||
setTimeout(() => loadFileOnTrack(newId, e.target.files[0]), 100);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
input.click();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = (e) => {
|
|
||||||
const k = keyHandlersRef.current;
|
|
||||||
const ctrl = e.ctrlKey || e.metaKey;
|
const ctrl = e.ctrlKey || e.metaKey;
|
||||||
const alt = e.altKey;
|
const alt = e.altKey;
|
||||||
|
if (e.key === ' ' || e.code === 'Space') { e.preventDefault(); if (handlePlayPauseRef.current) handlePlayPauseRef.current(); return; }
|
||||||
// Space: play/pause
|
|
||||||
if (e.key === ' ' || e.code === 'Space') {
|
|
||||||
e.preventDefault();
|
|
||||||
if (handlePlayPauseRef.current) handlePlayPauseRef.current();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ctrl+Z: Undo, Ctrl+Y or Ctrl+Shift+Z: Redo
|
|
||||||
if (ctrl && e.key === 'z' && !e.shiftKey) { e.preventDefault(); handleUndoRef.current(); return; }
|
if (ctrl && e.key === 'z' && !e.shiftKey) { e.preventDefault(); handleUndoRef.current(); return; }
|
||||||
if (ctrl && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) { e.preventDefault(); handleRedoRef.current(); return; }
|
if (ctrl && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) { e.preventDefault(); handleRedoRef.current(); return; }
|
||||||
|
|
||||||
// File menu shortcuts
|
|
||||||
if (ctrl && !alt && e.key === 'o') { e.preventDefault(); showToast('Open Project dialog','info'); return; }
|
if (ctrl && !alt && e.key === 'o') { e.preventDefault(); showToast('Open Project dialog','info'); return; }
|
||||||
if (ctrl && !alt && e.key === 's') { e.preventDefault(); showToast('Project saved','success'); return; }
|
if (ctrl && !alt && e.key === 's') { e.preventDefault(); showToast('Project saved','success'); return; }
|
||||||
if (ctrl && alt && e.key === 's') { e.preventDefault(); showToast('Save As dialog','info'); return; }
|
if (ctrl && alt && e.key === 's') { e.preventDefault(); showToast('Save As dialog','info'); return; }
|
||||||
|
if (ctrl && !alt && e.key === 'i') { e.preventDefault(); addNewTrack(); return; }
|
||||||
// Edit menu shortcuts
|
if (ctrl && alt && e.key === 'i') { e.preventDefault(); showToast('Import audio','info'); return; }
|
||||||
if (ctrl && !alt && e.key === 'i') { e.preventDefault(); k.addNewTrack(); return; }
|
if (ctrl && !alt && e.key === 'e') { e.preventDefault(); openTempTab(); return; }
|
||||||
if (ctrl && alt && e.key === 'i') { e.preventDefault(); k.openTempTabImport(); return; }
|
if (ctrl && !alt && e.key === 'm') { e.preventDefault(); handleMergeTracks(); return; }
|
||||||
if (ctrl && !alt && e.key === 'e') { e.preventDefault(); k.openTempTab(); return; }
|
if (ctrl && !alt && e.key === 'c') { e.preventDefault(); handleCopyTrack(); return; }
|
||||||
if (!ctrl && !alt && (e.key === 's' || e.key === 'S')) { e.preventDefault(); k.handleSplitTrack(selectedTrackId); return; }
|
if (ctrl && !alt && e.key === 'x') { e.preventDefault(); handleCutTrack(); return; }
|
||||||
if (ctrl && !alt && e.key === 'm') { e.preventDefault(); k.handleMergeTracks(); return; }
|
if (ctrl && !alt && e.key === 'v') { e.preventDefault(); contextMenuPaste(); return; }
|
||||||
if (ctrl && !alt && e.key === 'c') { e.preventDefault(); k.handleCopyTrack(); return; }
|
if (e.key === 'Delete' || e.key === 'Del') { e.preventDefault(); handleDeleteTrack(); return; }
|
||||||
if (ctrl && !alt && e.key === 'x') { e.preventDefault(); k.handleCutTrack(); return; }
|
if (!ctrl && !alt && e.key === 's') { e.preventDefault(); handleSplitTrack(selectedTrackId); return; }
|
||||||
if (ctrl && !alt && e.key === 'v') { e.preventDefault(); k.contextMenuPaste(); return; }
|
|
||||||
if (e.key === 'Delete' || e.key === 'Del') { e.preventDefault(); k.handleDeleteTrack(); return; }
|
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handler);
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener('keydown', handler);
|
||||||
}, []); // empty deps: refs avoid stale closure
|
}, []);
|
||||||
|
|
||||||
// ── Temp Tab: draw isolated waveform ──
|
// ── Temp Tab: draw isolated waveform ──
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user