|
| 1 | +import pathlib |
| 2 | + |
| 3 | +import numpy as np |
| 4 | +import torch |
| 5 | +import torch.nn.functional as F |
| 6 | +import torchaudio.transforms |
| 7 | +import yaml |
| 8 | +from librosa.filters import mel as librosa_mel_fn |
| 9 | + |
| 10 | +import utils |
| 11 | +from .nets import BiLSTMCurveEstimator |
| 12 | + |
| 13 | + |
| 14 | +class PitchAdjustableMelSpectrogram: |
| 15 | + def __init__( |
| 16 | + self, |
| 17 | + sample_rate=44100, |
| 18 | + n_fft=2048, |
| 19 | + win_length=2048, |
| 20 | + hop_length=512, |
| 21 | + f_min=40, |
| 22 | + f_max=16000, |
| 23 | + n_mels=128, |
| 24 | + center=False, |
| 25 | + ): |
| 26 | + self.sample_rate = sample_rate |
| 27 | + self.n_fft = n_fft |
| 28 | + self.win_size = win_length |
| 29 | + self.hop_length = hop_length |
| 30 | + self.f_min = f_min |
| 31 | + self.f_max = f_max |
| 32 | + self.n_mels = n_mels |
| 33 | + self.center = center |
| 34 | + |
| 35 | + self.mel_basis = {} |
| 36 | + self.hann_window = {} |
| 37 | + |
| 38 | + def __call__(self, y, key_shift=0, speed=1.0): |
| 39 | + factor = 2 ** (key_shift / 12) |
| 40 | + n_fft_new = int(np.round(self.n_fft * factor)) |
| 41 | + win_size_new = int(np.round(self.win_size * factor)) |
| 42 | + hop_length = int(np.round(self.hop_length * speed)) |
| 43 | + |
| 44 | + mel_basis_key = f"{self.f_max}_{y.device}" |
| 45 | + if mel_basis_key not in self.mel_basis: |
| 46 | + mel = librosa_mel_fn( |
| 47 | + sr=self.sample_rate, |
| 48 | + n_fft=self.n_fft, |
| 49 | + n_mels=self.n_mels, |
| 50 | + fmin=self.f_min, |
| 51 | + fmax=self.f_max, |
| 52 | + ) |
| 53 | + self.mel_basis[mel_basis_key] = torch.from_numpy(mel).float().to(y.device) |
| 54 | + |
| 55 | + hann_window_key = f"{key_shift}_{y.device}" |
| 56 | + if hann_window_key not in self.hann_window: |
| 57 | + self.hann_window[hann_window_key] = torch.hann_window( |
| 58 | + win_size_new, device=y.device |
| 59 | + ) |
| 60 | + |
| 61 | + y = torch.nn.functional.pad( |
| 62 | + y.unsqueeze(1), |
| 63 | + ( |
| 64 | + int((win_size_new - hop_length) // 2), |
| 65 | + int((win_size_new - hop_length+1) // 2), |
| 66 | + ), |
| 67 | + mode="reflect", |
| 68 | + ) |
| 69 | + y = y.squeeze(1) |
| 70 | + |
| 71 | + spec = torch.stft( |
| 72 | + y, |
| 73 | + n_fft_new, |
| 74 | + hop_length=hop_length, |
| 75 | + win_length=win_size_new, |
| 76 | + window=self.hann_window[hann_window_key], |
| 77 | + center=self.center, |
| 78 | + pad_mode="reflect", |
| 79 | + normalized=False, |
| 80 | + onesided=True, |
| 81 | + return_complex=True, |
| 82 | + ).abs() |
| 83 | + |
| 84 | + if key_shift != 0: |
| 85 | + size = self.n_fft // 2 + 1 |
| 86 | + resize = spec.size(1) |
| 87 | + if resize < size: |
| 88 | + spec = F.pad(spec, (0, 0, 0, size - resize)) |
| 89 | + |
| 90 | + spec = spec[:, :size, :] * self.win_size / win_size_new |
| 91 | + |
| 92 | + spec = torch.matmul(self.mel_basis[mel_basis_key], spec) |
| 93 | + |
| 94 | + return spec |
| 95 | + |
| 96 | + |
| 97 | +def dynamic_range_compression_torch(x, C=1, clip_val=1e-9): |
| 98 | + return torch.log(torch.clamp(x, min=clip_val) * C) |
| 99 | + |
| 100 | + |
| 101 | +class CurveEstimator: |
| 102 | + def __init__(self, model_path: pathlib.Path, device: str): |
| 103 | + if not isinstance(model_path, pathlib.Path): |
| 104 | + model_path = pathlib.Path(model_path) |
| 105 | + if not model_path.exists(): |
| 106 | + raise FileNotFoundError(f"Curve estimator model path {model_path} does not exist.") |
| 107 | + config_path = model_path.with_name("config.yaml") |
| 108 | + if not config_path.exists(): |
| 109 | + raise FileNotFoundError(f"Curve estimator config path {config_path} does not exist.") |
| 110 | + with open(config_path, "r", encoding="utf-8") as f: |
| 111 | + config = yaml.safe_load(f) |
| 112 | + dataset_args, model_args = config["dataset_args"], config["model_args"] |
| 113 | + self.device = device |
| 114 | + self.mel_spec_transform = PitchAdjustableMelSpectrogram( |
| 115 | + sample_rate=dataset_args["sample_rate"], |
| 116 | + n_fft=dataset_args["win_size"], |
| 117 | + win_length=dataset_args["win_size"], |
| 118 | + hop_length=dataset_args["hop_size"], |
| 119 | + f_min=dataset_args["f_min"], |
| 120 | + f_max=dataset_args["f_max"], |
| 121 | + n_mels=dataset_args["mel_bins"], |
| 122 | + center=True |
| 123 | + ) |
| 124 | + self.model = BiLSTMCurveEstimator( |
| 125 | + **utils.filter_kwargs(model_args, BiLSTMCurveEstimator) |
| 126 | + ) |
| 127 | + self.model.load_state_dict(torch.load(model_path, map_location="cpu")) |
| 128 | + self.model.eval() |
| 129 | + self.model.to(device) |
| 130 | + self.resample_kernels = {} |
| 131 | + |
| 132 | + @torch.no_grad() |
| 133 | + def estimate(self, waveform: np.ndarray, sr: int, length: int) -> np.ndarray: |
| 134 | + waveform = torch.from_numpy(waveform).float().to(self.device).unsqueeze(0) |
| 135 | + if sr != self.mel_spec_transform.sample_rate: |
| 136 | + if sr not in self.resample_kernels: |
| 137 | + self.resample_kernels[sr] = torchaudio.transforms.Resample( |
| 138 | + orig_freq=sr, |
| 139 | + new_freq=self.mel_spec_transform.sample_rate, |
| 140 | + lowpass_filter_width=128 |
| 141 | + ).to(self.device) |
| 142 | + waveform = self.resample_kernels[sr](waveform) |
| 143 | + mel = dynamic_range_compression_torch( |
| 144 | + self.mel_spec_transform(waveform), clip_val=1e-5 |
| 145 | + ).transpose(1, 2) |
| 146 | + pred_curve = self.model.infer(mel).squeeze(0).cpu().numpy() |
| 147 | + pred_curve = np.interp( |
| 148 | + np.linspace(0, len(pred_curve), length), |
| 149 | + np.arange(len(pred_curve)), |
| 150 | + pred_curve |
| 151 | + ).astype(np.float32) |
| 152 | + return pred_curve |
0 commit comments