From d2e0010d88b34600241450f1e5329b72f49f1e6d Mon Sep 17 00:00:00 2001 From: 3dtours Date: Sun, 19 Jul 2026 08:22:13 +0700 Subject: [PATCH] =?UTF-8?q?fix&feat:=2012=5FSUBTAB.md=20ch=E1=BB=89nh=20s?= =?UTF-8?q?=E1=BB=ADa=20audioclip=20=E1=BB=9F=20subtab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 12_SUBTAB.md | 287 +++ app/core/sub_tab_dsp.py | 87 + app/templates/index.html | 5 +- index.html | 3888 +++++++++++++++++++++++++++++-------- tests/test_sub_tab_dsp.py | 44 + 5 files changed, 3550 insertions(+), 761 deletions(-) create mode 100644 12_SUBTAB.md create mode 100644 app/core/sub_tab_dsp.py create mode 100644 tests/test_sub_tab_dsp.py diff --git a/12_SUBTAB.md b/12_SUBTAB.md new file mode 100644 index 0000000..23f5744 --- /dev/null +++ b/12_SUBTAB.md @@ -0,0 +1,287 @@ +# Technical Specification: Sandbox Isolation & Sub-Tab DSP Editing Algorithms + +This document defines the processing workflow design and digital signal processing (DSP) algorithms dedicated to localized clip editing within a temporary isolated document workspace (Sub-tab). + +--- + +## 1. Sandbox Splicing Workflow + +When a user highlights a time region on the Main Tab and triggers "Edit in Sub-tab" or presses the edit keyboard shortcut: + +```text + [ MAIN TAB - MULTITRACK ] + Track 01: ───[█████ Selected Segment █████]─── + │ + ▼ (Copy to Clipboard Buffer) + [ KHỔI TẠO TAB TẠM THỜI (SUB-TAB) ] +- Instantiates a single Track (Height bounds: 48px - 200px via ns-resize) +- Timeline Ruler axis resets to t = 0.0s + │ + ▼ (Automated Insertion - Auto-Paste) + Track 01 (Sub-tab): [█████ Isolated Segment █████] at t = 0s + +``` + +* **Extract Buffer:** The underlying engine extracts the binary sample array (`Float32Array`) of the highlighted region from the active track, caching it securely into the application's clipboard buffer memory. +* **Sandbox Environment Initialization:** +* Appends a temporary document window onto the global Tab Bar (e.g., `Tab: sẤit tiá...n` or `Sub_Edit_1`). +* Focuses the viewport down into the sandboxed tab. Here, a single standalone track lane is drawn, mapping the timeline ruler scale to start at $t = 0.0\text{ s}$ up to the absolute duration limit ($T_{\text{clip}}$) of the extracted audio asset. + + +* **Track Height Resizing:** +* Hovering the cursor over the lower layout bounding path of the track lane changes the style configuration to `ns-resize`. +* Dragging downward expands the vertical height ceiling (up to an upper bound of $200\text{ px}$), maximizing the waveform amplitude drawing path for precision clip editing. Dragging upward compresses the physical row dimensions (down to a lower constraint of $48\text{ px}$) to protect screen space. + + +* **Auto-Paste Routine:** The framework automates the insertion sequence, dropping the cached array block onto the root index milestone ($t = 0.0\text{ s}$) inside the isolated single-track layer. + +--- + +## 2. Apply & Sync-Back Workflow + +When an editor finishes processing steps inside the sandbox workspace and engages the *Apply* action: + +```text + [ SUB-TAB - AUDIO SANDBOX ] + y_sub = [█████ Edited Waveform █████] + │ + ▼ (Click "Apply" - Trigger Overwrite) + [ MAIN TAB - ORIGINAL TRACK ] + Track 01: ───[█████ Overwritten Segment █████]─── at t = t_start + │ + (Sub-tab remains open, Undo Stack kept) + ▼ + [User presses Undo (Ctrl+Z) inside Sub-tab to iterate] + y_sub = [█████ Rollbacked Waveform █████] + │ + (Click "Apply" again) + ▼ + Track 01: ───[█████ Corrected Segment █████]──── at t = t_start + +``` + +### 2.1. Target Mapping & Metadata Linkage + +Throughout its lifecycle, each sub-tab persistently locks standard metadata records linking back to the origin source elements: + +* `parent_track_id`: Unique identifier referencing the primary source track on the Main Tab. +* `parent_clip_id`: Unique identifier tracking the original source audio clip. +* `t_start` (seconds): The exact historical start time position of the sliced block on the Main Tab timeline view. +* `original_duration` (seconds): The baseline temporal duration of the region prior to modification. + +### 2.2. In-place Overwrite & Splicing + +* **Edited Buffer Extraction:** The system reads the active sample sequence from the sub-tab ($y_{\text{sub}}$) along with its updated duration boundary $T_{\text{sub}}$ (which fluctuates if time-stretching or rate scaling actions have occurred). +* **Main Session Integration:** +1. The core route mapper checks for the matching `parent_track_id` parameter on the Main Tab. +2. Purges the legacy audio segment stretching from $t_{\text{start}}$ through $t_{\text{start}} + T_{\text{original}}$. +3. Splices the updated signal array $y_{\text{sub}}$ precisely at the historical insertion index $t_{\text{start}}$. +4. **Micro-crossfade:** Executes a ultra-fast crossfade envelope ($10\text{ ms}$) across both the initial and terminating splice boundaries. Blending adjacent files prevents phase cancellation or signal breakage that manifests as transient clicks/pops. + + +* **Visual Update Tracking:** Commands the canvas engine to redraw the waveform visualization grid for the origin track lane inside the Main Tab view. + +### 2.3. Persistence for Iterative Editing + +* **Tab Lifetime:** Engaging the *Apply* trigger propagates data back to the primary environment but does **not** close down the active sub-tab view. +* **Undo Stack Isolation:** The tracking loop containing the localized *Undo/Redo History Stack* inside the sub-tab sandbox remains entirely preserved. +* **Iterative Loop Workflow:** +1. If monitoring the Main Tab arrangement uncovers an audio anomaly, the user switches focus back to the Sub-tab workspace. +2. Pressing `Ctrl + Z` (Undo) rollbacks the localized signal to its earlier state. +3. The editor runs separate DSP actions. +4. Hitting *Apply* overwrites the updated audio slice over the same target coordinates on the Main Tab. + + +* **Explicit Destruction Hook:** The sandboxed tab structure is only unmapped when the user clicks the explicit close icon ($\times$) on the horizontal tab bar. + +--- + +## 3. Sub-Tab DSP Algorithm Specification + +Editing operations executed inside the sub-tab environment calculate discrete changes over the amplitude sample arrays ($x[n]$). These map to Web Audio API routines on the client layer and standard NumPy/SciPy audio arrays on the Dockerized backend. + +### 3.1. Time-Stretching & Speed Math + +Alters the duration bounds of the audio clip with optional pitch-shifting linking logic: + +* **Pitch-preserving Time-stretching:** Utilizes the Phase Vocoder method to analyze the Short-Time Fourier Transform (STFT) of the signal, shifts spectral frames across the frequency domain, and reconstructs the audio via the Inverse Short-Time Fourier Transform (ISTFT) to align with a new playback velocity ratio $S$: + +$$S = \frac{D}{D'} \times 100\%$$ + + + +*Where:* $D$ corresponds to the legacy unscaled duration (seconds), and $D'$ maps to the updated value post-resizing (executed by holding down the `Alt` key and dragging the right boundary handle). +* **Resampling (Pitch-shifting Speed Scale):** Runs a standard linear interpolation algorithm to resample the core data array size: + +$$x_{\text{new}}[m] = x\left[ \frac{m \cdot D}{D'} \right]$$ + + + +### 3.2. Peak Normalization + +Amplifies the signal scale uniformly across the active block until the single maximum absolute sample peak reaches a specified ceiling parameter $A_{\text{target}}$ (typically locked at $1.0$ or $0\text{ dBFS}$): + +1. Evaluate the absolute maximum peak within the array bounds: + +$$A_{\text{max}} = \max_{n=0}^{N-1} \vert x[n] \vert$$ + + +2. Compute the static gain multiplier constant $G$: + +$$G = \frac{A_{\text{target}}}{A_{\text{max}}}$$ + + +3. Multiply the entire audio array values by $G$: + +$$x_{\text{norm}}[n] = x[n] \cdot G$$ + + + +### 3.3. Volume Gain Adjustment (dB Scaling) + +1. Capture the decibel variance target ($\Delta \text{dB}$). +2. Translate the logarithmic value into a standard linear scalar multiplier variable $G_{\text{linear}}$: + +$$G_{\text{linear}} = 10^{\frac{\Delta \text{dB}}{20}}$$ + + +3. Apply the gain multiplier directly into the sample values: + +$$x_{\text{gained}}[n] = x[n] \cdot G_{\text{linear}}$$ + + + +### 3.4. Pitch Shifting + +Shifts the fundamental frequencies of the signal up or down by a specific number of semitones ($n$) while keeping the temporal duration value completely intact. + +* **Frequency Transposition Ratio ($F_{\text{ratio}}$):** + +$$F_{\text{ratio}} = 2^{\frac{n}{12}}$$ + + +* **DSP Processing Pipeline:** Employs either a Pitch Synchronous Overlap and Add (PSOLA) routine or a spectral Phase Vocoder to expand/compress the frequency components, then passes the array into a time-stretching step to return the physical track length to its source metric $T_{\text{clip}}$. + +### 3.5. Linear Fade-In & Fade-Out Curves + +Applies a linear fading envelope over the boundaries of the audio data block. + +* **Linear Fade-In Envelope** (Across a duration bound of $L_{\text{fade}}$ samples): + +$$x_{\text{fade}}[n] = x[n] \cdot \left( \frac{n}{L_{\text{fade}}} \right) \quad \text{for } 0 \le n < L_{\text{fade}}$$ + + +* **Linear Fade-Out Envelope** (Across the final trailing $L_{\text{fade}}$ samples): + +$$x_{\text{fade}}[N - 1 - n] = x[N - 1 - n] \cdot \left( \frac{n}{L_{\text{fade}}} \right) \quad \text{for } 0 \le n < L_{\text{fade}}$$ + + + +### 3.6. Array Splitting & Merging + +* **Split at Position ($n_{\text{cut}}$):** Unlinks a single sample block $x[n]$ of size $N$ into two separate independent sub-arrays: + +$$x_1[n] = x[n] \quad (0 \le n < n_{\text{cut}})$$ + + +$$x_2[n] = x[n + n_{\text{cut}}] \quad (0 \le n < N - n_{\text{cut}})$$ + + +* **Merge Segments:** Concatenates separate sample sequences end-to-end. The stitching logic runs a $10\text{ ms}$ micro-crossfade overlay envelope at the junction to smooth out phase gaps that prompt click artifacts. + +--- + +## 4. Python Backend Implementation Manual + +This prototype Python class (`core/sub_tab_dsp.py`) handles the sandboxed operations and includes the crossfaded structural splicing algorithm designed to run inside the Docker engine: + +```python +import numpy as np +import scipy.signal as signal +import librosa + +class SubTabDSPEngine: + @staticmethod + def change_speed(y: np.ndarray, sr: int, speed_ratio: float, preserve_pitch: bool = True) -> np.ndarray: + """ + Alters the playback velocity (Time-Stretching) of a NumPy signal array. + """ + if speed_ratio == 1.0: + return y + + if preserve_pitch: + return librosa.effects.time_stretch(y, rate=speed_ratio) + else: + num_samples_new = int(len(y) / speed_ratio) + return signal.resample(y, num_samples_new) + + @staticmethod + def normalize(y: np.ndarray, target_db: float = 0.0) -> np.ndarray: + """ + Performs Peak Normalization on an array to scale it to the target decibel value. + """ + target_amplitude = 10.0 ** (target_db / 20.0) + max_amplitude = np.max(np.abs(y)) + + if max_amplitude == 0: + return y + + gain = target_amplitude / max_amplitude + return y * gain + + @staticmethod + def merge_back_to_parent( + parent_track_audio: np.ndarray, + sr: int, + edited_sub_audio: np.ndarray, + start_seconds: float, + original_duration_seconds: float + ) -> np.ndarray: + """ + Splices the modified audio segment from the Sub-tab back into the parent track array. + Applies a 10ms micro-crossfade at the boundaries to eliminate pop/click noise. + """ + start_sample = int(start_seconds * sr) + original_samples_len = int(original_duration_seconds * sr) + edited_samples_len = len(edited_sub_audio) + crossfade_samples = int(0.01 * sr) # 10ms crossfade window + + # 1. Allocate the target output array dimension bounds + new_total_len = len(parent_track_audio) - original_samples_len + edited_samples_len + output_audio = np.zeros(new_total_len, dtype=np.float32) + + # 2. Extract leading unedited block + output_audio[:start_sample] = parent_track_audio[:start_sample] + + # 3. Stitch the modified audio payload + output_audio[start_sample:start_sample + edited_samples_len] = edited_sub_audio + + # 4. Extract trailing unedited block + post_start_original = start_sample + original_samples_len + post_start_new = start_sample + edited_samples_len + output_audio[post_start_new:] = parent_track_audio[post_start_original:] + + # 5. Execute micro-crossfade across the initial splice junction + if start_sample > crossfade_samples: + fade_in_ramp = np.linspace(0.0, 1.0, crossfade_samples) + fade_out_ramp = np.linspace(1.0, 0.0, crossfade_samples) + + # Smooth 10ms interpolation overlay + output_audio[start_sample : start_sample + crossfade_samples] = ( + edited_sub_audio[:crossfade_samples] * fade_in_ramp + + parent_track_audio[start_sample : start_sample + crossfade_samples] * fade_out_ramp + ) + + # 6. Execute micro-crossfade across the trailing splice junction + if post_start_new + crossfade_samples < len(output_audio): + fade_in_ramp = np.linspace(0.0, 1.0, crossfade_samples) + fade_out_ramp = np.linspace(1.0, 0.0, crossfade_samples) + + output_audio[post_start_new : post_start_new + crossfade_samples] = ( + parent_track_audio[post_start_original : post_start_original + crossfade_samples] * fade_in_ramp + + edited_sub_audio[-crossfade_samples:] * fade_out_ramp + ) + + return output_audio + +``` \ No newline at end of file diff --git a/app/core/sub_tab_dsp.py b/app/core/sub_tab_dsp.py new file mode 100644 index 0000000..434eb33 --- /dev/null +++ b/app/core/sub_tab_dsp.py @@ -0,0 +1,87 @@ +import numpy as np +import scipy.signal as signal +import librosa + +class SubTabDSPEngine: + @staticmethod + def change_speed(y: np.ndarray, sr: int, speed_ratio: float, preserve_pitch: bool = True) -> np.ndarray: + """ + Alters the playback velocity (Time-Stretching) of a NumPy signal array. + """ + if speed_ratio == 1.0: + return y + + if preserve_pitch: + return librosa.effects.time_stretch(y, rate=speed_ratio) + else: + num_samples_new = int(len(y) / speed_ratio) + return signal.resample(y, num_samples_new) + + @staticmethod + def normalize(y: np.ndarray, target_db: float = 0.0) -> np.ndarray: + """ + Performs Peak Normalization on an array to scale it to the target decibel value. + """ + target_amplitude = 10.0 ** (target_db / 20.0) + max_amplitude = np.max(np.abs(y)) + + if max_amplitude == 0: + return y + + gain = target_amplitude / max_amplitude + return y * gain + + @staticmethod + def merge_back_to_parent( + parent_track_audio: np.ndarray, + sr: int, + edited_sub_audio: np.ndarray, + start_seconds: float, + original_duration_seconds: float + ) -> np.ndarray: + """ + Splices the modified audio segment from the Sub-tab back into the parent track array. + Applies a 10ms micro-crossfade at the boundaries to eliminate pop/click noise. + """ + start_sample = int(start_seconds * sr) + original_samples_len = int(original_duration_seconds * sr) + edited_samples_len = len(edited_sub_audio) + crossfade_samples = int(0.01 * sr) # 10ms crossfade window + + # 1. Allocate the target output array dimension bounds + new_total_len = len(parent_track_audio) - original_samples_len + edited_samples_len + output_audio = np.zeros(new_total_len, dtype=np.float32) + + # 2. Extract leading unedited block + output_audio[:start_sample] = parent_track_audio[:start_sample] + + # 3. Stitch the modified audio payload + output_audio[start_sample:start_sample + edited_samples_len] = edited_sub_audio + + # 4. Extract trailing unedited block + post_start_original = start_sample + original_samples_len + post_start_new = start_sample + edited_samples_len + output_audio[post_start_new:] = parent_track_audio[post_start_original:] + + # 5. Execute micro-crossfade across the initial splice junction + if start_sample > crossfade_samples: + fade_in_ramp = np.linspace(0.0, 1.0, crossfade_samples) + fade_out_ramp = np.linspace(1.0, 0.0, crossfade_samples) + + # Smooth 10ms interpolation overlay + output_audio[start_sample : start_sample + crossfade_samples] = ( + edited_sub_audio[:crossfade_samples] * fade_in_ramp + + parent_track_audio[start_sample : start_sample + crossfade_samples] * fade_out_ramp + ) + + # 6. Execute micro-crossfade across the trailing splice junction + if post_start_new + crossfade_samples < len(output_audio): + fade_in_ramp = np.linspace(0.0, 1.0, crossfade_samples) + fade_out_ramp = np.linspace(1.0, 0.0, crossfade_samples) + + output_audio[post_start_new : post_start_new + crossfade_samples] = ( + parent_track_audio[post_start_original : post_start_original + crossfade_samples] * fade_in_ramp + + edited_sub_audio[-crossfade_samples:] * fade_out_ramp + ) + + return output_audio diff --git a/app/templates/index.html b/app/templates/index.html index a117c35..fe0f6ba 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1194,8 +1194,9 @@ const afterSnap = captureTrackSnapshot(subTab.trackId); pushAction('EDIT_TAB', subTab.trackId, beforeSnap, afterSnap); - closeSubTab(tabId); - showToast('Đã áp dụng chỉnh sửa vào track chính.', 'success'); + // Tab Lifetime: Engaging the Apply trigger propagates data back to the primary environment but does not close down the active sub-tab view. + // closeSubTab(tabId); + showToast('Đã áp dụng chỉnh sửa vào track chính.', 'success'); }; const closeSubTab = (tabId) => { diff --git a/index.html b/index.html index 5941537..fe0f6ba 100644 --- a/index.html +++ b/index.html @@ -4,15 +4,11 @@ SonicForge Studio - Professional DAW Editor - - - - - - - + + + @@ -63,6 +46,15 @@