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