fix: time selection click and shiftclick

This commit is contained in:
2026-07-20 19:44:23 +07:00
parent 271f0583f4
commit 85a8dc6d17
13 changed files with 1788 additions and 293 deletions
+43
View File
@@ -80,6 +80,49 @@ def apply_micro_fade(segment: AudioSegment, fade_duration_ms: int = 50) -> Audio
return segment
def apply_micro_crossfade(original: np.ndarray, edited: np.ndarray, start_sample: int, fade_len_ms: int = 10, sr: int = 44100) -> np.ndarray:
"""
Áp dụng bộ lọc mờ biên Micro-crossfade (10ms) tại hai đầu điểm ráp nối
để triệt tiêu tiếng click/pop khi Apply & Merge Back (22_CLIENT_DESK.md §2.2).
Output(t) = (1 - alpha(t)) * Original(t) + alpha(t) * Edited(t - T_start)
"""
fade_samples = int((fade_len_ms / 1000.0) * sr)
if fade_samples <= 0 or len(original) == 0:
return edited
output = np.copy(original)
edited_len = len(edited)
end_sample = min(len(original), start_sample + edited_len)
actual_len = end_sample - start_sample
if actual_len <= 0:
return output
fade_in_len = min(fade_samples, actual_len)
fade_out_len = min(fade_samples, actual_len)
alpha_in = np.linspace(0.0, 1.0, fade_in_len)
alpha_out = np.linspace(1.0, 0.0, fade_out_len)
output[start_sample:end_sample] = edited[:actual_len]
# Fade in at start splice point
for i in range(fade_in_len):
idx = start_sample + i
if idx < len(original):
output[idx] = (1.0 - alpha_in[i]) * original[idx] + alpha_in[i] * edited[i]
# Fade out at end splice point
for i in range(fade_out_len):
idx = end_sample - fade_out_len + i
edit_idx = actual_len - fade_out_len + i
if idx < len(original) and edit_idx < len(edited):
output[idx] = alpha_out[i] * edited[edit_idx] + (1.0 - alpha_out[i]) * original[idx]
return output
def generate_peak_waveform(file_path: str, num_peaks: int = 800) -> dict:
"""
Tạo dữ liệu peak waveform cho hiển thị đồ thị sóng âm trên Frontend.
+77
View File
@@ -0,0 +1,77 @@
# SonicForge Studio VST / VSTi Engine Service (22_CLIENT_DESK.md §3)
import numpy as np
def midi_note_to_freq(note_number: int) -> float:
"""Quy đổi số nốt MIDI (0 - 127) sang tần số Hertz (Hz)."""
return 440.0 * (2.0 ** ((note_number - 69) / 12.0))
def render_midi_events_to_audio(midi_events: list, sr: int = 44100, bpm: float = 120.0, instrument: str = 'synth') -> np.ndarray:
"""
Tổng hợp mảng âm thanh NumPy Stereo từ sự kiện MIDI Piano Roll (22_CLIENT_DESK.md §3.1 & §3.2).
Args:
midi_events: Danh sách nốt MIDI [{"note": 60, "start_beat": 0, "duration_beats": 1, "velocity": 100}, ...]
sr: Tần số lấy mẫu (Sample Rate)
bpm: Nhịp BPM của dự án
instrument: Loại nhạc cụ tổng hợp
Returns:
np.ndarray: Mảng 2D Stereo Float32 [2, num_samples]
"""
beat_duration_sec = 60.0 / max(30.0, bpm)
max_duration_sec = 2.0
for event in midi_events:
start_beat = event.get('start_beat', 0.0)
dur_beats = event.get('duration_beats', 1.0)
end_sec = (start_beat + dur_beats) * beat_duration_sec
if end_sec > max_duration_sec:
max_duration_sec = end_sec
total_samples = int((max_duration_sec + 0.5) * sr)
out_l = np.zeros(total_samples, dtype=np.float32)
out_r = np.zeros(total_samples, dtype=np.float32)
for event in midi_events:
note = event.get('note', 60)
velocity = event.get('velocity', 100) / 127.0
start_beat = event.get('start_beat', 0.0)
dur_beats = event.get('duration_beats', 1.0)
start_sample = int(start_beat * beat_duration_sec * sr)
dur_samples = int(dur_beats * beat_duration_sec * sr)
end_sample = min(total_samples, start_sample + dur_samples)
actual_len = end_sample - start_sample
if actual_len <= 0 or start_sample >= total_samples:
continue
freq = midi_note_to_freq(note)
t = np.arange(actual_len) / float(sr)
# Synth tone + fundamental harmonics
tone = 0.6 * np.sin(2 * np.pi * freq * t) + 0.3 * np.sin(2 * np.pi * freq * 2 * t) + 0.1 * np.sin(2 * np.pi * freq * 3 * t)
# ADSR Envelope
attack = min(int(0.01 * sr), actual_len // 4)
release = min(int(0.05 * sr), actual_len // 4)
sustain_len = actual_len - attack - release
env = np.ones(actual_len, dtype=np.float32)
if attack > 0:
env[:attack] = np.linspace(0.0, 1.0, attack)
if release > 0:
env[-release:] = np.linspace(1.0, 0.0, release)
signal = tone * env * velocity
out_l[start_sample:end_sample] += signal
out_r[start_sample:end_sample] += signal
# Clamping normalization to prevent clipping
max_peak = max(np.max(np.abs(out_l)), np.max(np.abs(out_r)))
if max_peak > 1.0:
out_l /= max_peak
out_r /= max_peak
return np.vstack([out_l, out_r])