Files
SonicForgeStudio/md/10_SNAP_TOOL.md

8.3 KiB

Technical Specification: Grid Snapping System (Grid Snapping Specification)

This document defines the graphical user interface design and coordinate/signal processing algorithms required to build a synchronized grid snapping feature across both the Web Frontend and the Dockerized Python Desktop Backend.


1. Toolbar UI Design Upgrade

The toolbar layout has been expanded to double its physical vertical height. This increase in interactive space allows for larger navigation buttons and the integration of a dedicated Snap controller.

+───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────+
| [Pro Toolbar - Height: 64px]                                                                                          |
| +──────────+ +──────────+ +──────────+ +──────────+ +───────────────────────────────────────────────────────────────+ |
| |  Cut     | |  Copy    | |  Paste   | |  Snap:   | | Transport Monitor                                             | |
| |  [Ctrl+X]| |  [Ctrl+C]| |  [Ctrl+V]| | [1/4  ▼] | | [Tempo: 120 BPM]  [Time Signature: 4/4]  [Bar:Beat 1.3.00]      | |
| +──────────+ +──────────+ +──────────+ +──────────+ +───────────────────────────────────────────────────────────────+ |
+───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────+

1.1. Snap Dropdown Configuration

  • Placement: Located immediately following the Paste button on the primary toolbar row.
  • UI Syntax: Snap: <Dropdown_Widget>
  • Dropdown Option Matrix:
  • free: Disables snapping; allows unrestricted pixel-by-pixel dragging.
  • 1: Snaps to the beginning of each complete measure (Whole Bar / 1/1).
  • 1/2: Divides the bar into 2 subdivisions (Half Note).
  • 1/4: Divides the bar into 4 subdivisions (Quarter Note / 1 Beat).
  • 1/8: Divides the bar into 8 subdivisions (Eighth Note).
  • 1/16: Divides the bar into 16 subdivisions (Sixteenth Note).
  • 1/32: Divides the bar into 32 subdivisions (Thirty-second Note).

2. DSP Grid Math: Grid Subdivision & Time Interval Calculations

To calculate the absolute temporal duration between individual grid lanes, the time metrics must be derived dynamically from the project's master tempo.

Let:

  • B be the project tempo (Beats Per Minute, e.g., 120\text{ BPM}).
  • T_{\text{beat}} be the duration of a single beat (seconds).
  • T_{\text{bar}} be the duration of a full measure/bar (seconds)—assuming a standard 4/4 time signature (4 beats per bar).

The foundational constants are established as follows:

T_{\text{beat}} = \frac{60}{B} \quad (\text{seconds}) T_{\text{bar}} = 4 \times T_{\text{beat}} = \frac{240}{B} \quad (\text{seconds})

Example: At a tempo of 120\text{ BPM}, a single 1\text{ Bar} measure spans exactly 2.0\text{ seconds}.

2.1. Determining Grid Time Interval Modifiers (\Delta t)

Based on the user's active choice inside the snap dropdown selection (S \in \{\text{free}, 1, 1/2, 1/4, 1/8, 1/16, 1/32\}), the exact grid time step \Delta t (seconds) is mapped as follows:

\Delta t = \begin{cases} 0 & S = \text{free} \\ T_{\text{bar}} & S = 1 \\ \frac{T_{\text{bar}}}{2} & S = 1/2 \\ \frac{T_{\text{bar}}}{4} = T_{\text{beat}} & S = 1/4 \\ \frac{T_{\text{bar}}}{8} & S = 1/8 \\ \frac{T_{\text{bar}}}{16} & S = 1/16 \\ \frac{T_{\text{bar}}}{32} & S = 1/32 \end{cases}

3. Snapping Coordinate Calculation

When a user executes a drag-and-drop event on an audio clip, or updates a timeline marker position, the system continuously converts raw cursor values into aligned coordinates.

                      Grid Line 1 (k * dt)          Grid Line 2 ((k+1) * dt)
                             │                             │
                             ├───────────────○─────────────┤
                                             ▲
                                             │ [ Cursor dragging action ]
                                        Raw Time (t_raw)
                                             │
                                             ▼ [ Apply Snap round() function ]
                             ────────────────┼─────────────►
                                        Snapped Time (t_snapped)

3.1. Pixel to Snap-Time Translation Workflow

  1. Intercept the actual client-side horizontal cursor position X_{\text{raw}} (pixels).
  2. Convert it into a raw timeline duration metric t_{\text{raw}} (seconds) utilizing the current scaling zoom factor Z (pixels/second):
t_{\text{raw}} = \frac{X_{\text{raw}}}{Z}
  1. Apply the rounding constraint formula to lock the raw timestamp to the absolute nearest grid marker:
t_{\text{snapped}} = \begin{cases} t_{\text{raw}} & S = \text{free} \\ \text{round}\left( \frac{t_{\text{raw}}}{\Delta t} \right) \times \Delta t & S \neq \text{free} \end{cases}
  1. Map the snapped timeline index t_{\text{snapped}} back to the layout canvas system coordinates to paint the element at its snapped visual boundary:
X_{\text{snapped}} = t_{\text{snapped}} \times Z

4. Python Porting Blueprint

When translating this architectural logic into a desktop Python core environment using frameworks like PyQt6, the snapping evaluations are tied directly into the tracking loop inside the mouseMoveEvent handler.

# [PYTHON PORTING BLUEPRINT] - Integrating the Snap algorithm into Python UI layer
import numpy as np

class AudioSnapEngine:
    def __init__(self, bpm: float = 120.0):
        self.bpm = bpm
        
    def calculate_grid_step(self, snap_option: str) -> float:
        """
        Calculates the grid's target duration step (seconds) based on Tempo and Snap selection.
        """
        if snap_option == "free":
            return 0.0
            
        # 1 Bar in a standard 4/4 signature equals 240 / BPM seconds
        t_bar = 240.0 / self.bpm
        
        fraction_map = {
            "1": 1.0,
            "1/2": 2.0,
            "1/4": 4.0,
            "1/8": 8.0,
            "1/16": 16.0,
            "1/32": 32.0
        }
        
        division = fraction_map.get(snap_option, 4.0)
        return float(t_bar / division)

    def snap_time(self, raw_time_seconds: float, snap_option: str) -> float:
        """
        Hard-clamps a raw timestamp to the nearest grid milestone. Prevents negative index overflows.
        """
        dt = self.calculate_grid_step(snap_option)
        if dt == 0.0:
            return max(0.0, raw_time_seconds)
            
        # Find the nearest integer index k of the target grid lane: raw_time / dt
        k = round(raw_time_seconds / dt)
        snapped_time = k * dt
        
        return max(0.0, snapped_time)


5. Visual Grid Alignment

To maintain an intuitive environment for multi-channel editing, whenever a snapping constraint value other than free is engaged:

  • The rendering engine overlays thin, low-contrast vertical grid lines (rgba(255, 255, 255, 0.05)) over the background profile of every active Waveform Lane.
  • These marker lines are projected onto every timeline axis point that satisfies a whole multiple increment of \Delta t.
  • Displaying these alignment indicators ensures that users can visually anticipate bounding snapping positions before releasing their mouse track buttons.