fix: lỗi vẽ volume và panning trên waveform
This commit is contained in:
@@ -136,3 +136,111 @@ class SubTabDSPEngine:
|
||||
)
|
||||
|
||||
return output_audio
|
||||
|
||||
|
||||
class DSPAudioModulator:
|
||||
@staticmethod
|
||||
def apply_automation_and_panning(
|
||||
y_raw: np.ndarray,
|
||||
sr: int,
|
||||
volume_points: list, # [{"time": 0.5, "db": -6.0}, ...]
|
||||
panning_points: list, # [{"time": 1.0, "pan": -0.7}, ...]
|
||||
fade_in_sec: float = 0.0,
|
||||
fade_out_sec: float = 0.0
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Applies multi-point volume envelopes, constant-power panning, and trigonometric fades
|
||||
directly onto a 1D (Mono) or 2D (Stereo) acoustic NumPy signal array.
|
||||
|
||||
Input: y_raw maps to the raw sound array (Mono/Stereo matrix bounded inside [-1.0, 1.0]).
|
||||
Output: y_processed yields a 2D interleaved Stereo NumPy array (2, N) with baked modulations.
|
||||
"""
|
||||
total_samples = y_raw.shape[-1] if len(y_raw.shape) > 1 else len(y_raw)
|
||||
duration_sec = total_samples / sr
|
||||
|
||||
# 1. Guarantee Stereo geometry dimensions (2 discrete channels) for Panning operations
|
||||
if len(y_raw.shape) == 1:
|
||||
# For Mono arrays, clone sample metrics symmetrically to Left/Right matrices
|
||||
y_stereo = np.vstack((y_raw, y_raw))
|
||||
else:
|
||||
y_stereo = np.copy(y_raw)
|
||||
|
||||
# 2. Allocate Envelope Mask arrays matching total track samples limits
|
||||
volume_envelope = np.ones(total_samples, dtype=np.float32)
|
||||
pan_envelope = np.zeros(total_samples, dtype=np.float32) # Default initialization: Center (0.0)
|
||||
|
||||
# 3. Compile the Volume Envelope using linear interpolation bounds across nodes
|
||||
if volume_points and len(volume_points) > 0:
|
||||
# Enforce strict chronological sorting down the timeline axis
|
||||
points = sorted(volume_points, key=lambda x: x["time"])
|
||||
|
||||
# Pad introductory bounds if the initial point coordinate sits past t = 0.0s
|
||||
if points[0]["time"] > 0:
|
||||
first_gain = 10.0 ** (points[0]["db"] / 20.0)
|
||||
idx_end = int(points[0]["time"] * sr)
|
||||
volume_envelope[:idx_end] = first_gain
|
||||
|
||||
for i in range(len(points) - 1):
|
||||
p1, p2 = points[i], points[i+1]
|
||||
idx_start = int(p1["time"] * sr)
|
||||
idx_end = int(p2["time"] * sr)
|
||||
|
||||
gain_start = 10.0 ** (p1["db"] / 20.0)
|
||||
gain_end = 10.0 ** (p2["db"] / 20.0)
|
||||
|
||||
# Linearly interpolate vector increments between adjacent anchor positions
|
||||
volume_envelope[idx_start:idx_end] = np.linspace(gain_start, gain_end, idx_end - idx_start)
|
||||
|
||||
# Pad trailing bounds from the final milestone extending through end-of-file
|
||||
if points[-1]["time"] < duration_sec:
|
||||
last_gain = 10.0 ** (points[-1]["db"] / 20.0)
|
||||
idx_start = int(points[-1]["time"] * sr)
|
||||
volume_envelope[idx_start:] = last_gain
|
||||
|
||||
# 4. Compile the Panning Envelope using linear interpolation bounds across nodes
|
||||
if panning_points and len(panning_points) > 0:
|
||||
points = sorted(panning_points, key=lambda x: x["time"])
|
||||
|
||||
if points[0]["time"] > 0:
|
||||
pan_envelope[:int(points[0]["time"] * sr)] = points[0]["pan"]
|
||||
|
||||
for i in range(len(points) - 1):
|
||||
p1, p2 = points[i], points[i+1]
|
||||
idx_start = int(p1["time"] * sr)
|
||||
idx_end = int(p2["time"] * sr)
|
||||
pan_envelope[idx_start:idx_end] = np.linspace(p1["pan"], p2["pan"], idx_end - idx_start)
|
||||
|
||||
if points[-1]["time"] < duration_sec:
|
||||
pan_envelope[int(points[-1]["time"] * sr):] = points[-1]["pan"]
|
||||
|
||||
# 5. Apply Trigonometric Cosine Fade-In / Fade-Out functions onto the Volume Envelope mask
|
||||
if fade_in_sec > 0:
|
||||
fade_in_samples = min(total_samples, int(fade_in_sec * sr))
|
||||
x_fade = np.linspace(0.0, np.pi, fade_in_samples)
|
||||
cosine_ramp = (1.0 - np.cos(x_fade)) / 2.0
|
||||
volume_envelope[:fade_in_samples] *= cosine_ramp
|
||||
|
||||
if fade_out_sec > 0:
|
||||
fade_out_samples = min(total_samples, int(fade_out_sec * sr))
|
||||
x_fade = np.linspace(0.0, np.pi, fade_out_samples)
|
||||
cosine_ramp = (1.0 + np.cos(x_fade)) / 2.0
|
||||
volume_envelope[-fade_out_samples:] *= cosine_ramp
|
||||
|
||||
# 6. Bake Volume Envelope matrices onto the Left and Right discrete audio paths
|
||||
y_stereo[0, :] *= volume_envelope
|
||||
y_stereo[1, :] *= volume_envelope
|
||||
|
||||
# 7. Apply Constant-Power Stereo Panning allocations
|
||||
# Map panning metrics range [-1.0, 1.0] onto angular radians field array [0, pi/2]
|
||||
theta_envelope = ((pan_envelope + 1.0) / 2.0) * (np.pi / 2.0)
|
||||
|
||||
# Evaluate localized amplitude coefficients for physical channels split
|
||||
gain_left = np.cos(theta_envelope)
|
||||
gain_right = np.sin(theta_envelope)
|
||||
|
||||
# Multiply scaling factors directly across corresponding discrete matrices
|
||||
y_stereo[0, :] *= gain_left
|
||||
y_stereo[1, :] *= gain_right
|
||||
|
||||
return y_stereo
|
||||
|
||||
|
||||
Reference in New Issue
Block a user