Files
2026-07-13 12:35:17 +08:00

283 lines
10 KiB
Python

from typing import Dict
import numpy as np
import pyworld as pw
import torch
from torch.nn import functional as F
from modules.hnsep.vr import load_sep_model
from utils import hparams
from utils.pitch_utils import interp_f0
class DecomposedWaveform:
def __new__(
cls, waveform, samplerate, f0,
*,
hop_size=None, fft_size=None, win_size=None,
algorithm='world', device=None
):
if algorithm == 'world':
obj = object.__new__(DecomposedWaveformPyWorld)
# noinspection PyProtectedMember
obj._init(
waveform=waveform, samplerate=samplerate, f0=f0,
hop_size=hop_size, fft_size=fft_size, win_size=win_size,
device=device
)
elif algorithm == 'vr':
obj = object.__new__(DecomposedWaveformVocalRemover)
hnsep_ckpt = hparams['hnsep_ckpt']
# noinspection PyProtectedMember
obj._init(
waveform=waveform, samplerate=samplerate, f0=f0, hop_size=hop_size,
fft_size=fft_size, win_size=win_size, model_path=hnsep_ckpt,
device=device
)
else:
raise ValueError(f" [x] Unknown harmonic-noise separator: {algorithm}")
return obj
@property
def samplerate(self):
raise NotImplementedError()
@property
def hop_size(self):
raise NotImplementedError()
@property
def fft_size(self):
raise NotImplementedError()
@property
def win_size(self):
raise NotImplementedError()
def harmonic(self, k: int = None) -> np.ndarray:
raise NotImplementedError()
def aperiodic(self) -> np.ndarray:
raise NotImplementedError()
class DecomposedWaveformPyWorld(DecomposedWaveform):
def _init(
self, waveform, samplerate, f0, # basic parameters
*,
hop_size=None, fft_size=None, win_size=None, base_harmonic_radius=3.5, # analysis parameters
device=None # computation parameters
):
# the source components
self._waveform = waveform
self._samplerate = samplerate
self._f0 = f0
# extraction parameters
self._hop_size = hop_size
self._fft_size = fft_size if fft_size is not None else win_size
self._win_size = win_size if win_size is not None else win_size
self._time_step = hop_size / samplerate
self._half_width = base_harmonic_radius
self._device = ('cuda' if torch.cuda.is_available() else 'cpu') if device is None else device
# intermediate variables
self._f0_world = None
self._sp = None
self._ap = None
# final components
self._harmonic_part: np.ndarray = None
self._aperiodic_part: np.ndarray = None
self._harmonics: Dict[int, np.ndarray] = {}
@property
def samplerate(self):
return self._samplerate
@property
def hop_size(self):
return self._hop_size
@property
def fft_size(self):
return self._fft_size
@property
def win_size(self):
return self._win_size
def _world_extraction(self):
# Add a tiny noise to the signal to avoid NaN results of D4C in rare edge cases
# References:
# - https://github.com/JeremyCCHsu/Python-Wrapper-for-World-Vocoder/issues/50
# - https://github.com/mmorise/World/issues/116
x = self._waveform.astype(np.double) + np.random.randn(*self._waveform.shape) * 1e-5
samplerate = self._samplerate
f0 = self._f0.astype(np.double)
hop_size = self._hop_size
fft_size = self._fft_size
wav_frames = (x.shape[0] + hop_size - 1) // hop_size
f0_frames = f0.shape[0]
if f0_frames < wav_frames:
f0 = np.pad(f0, (0, wav_frames - f0_frames), mode='constant', constant_values=(f0[0], f0[-1]))
elif f0_frames > wav_frames:
f0 = f0[:wav_frames]
time_step = hop_size / samplerate
t = np.arange(0, wav_frames) * time_step
self._f0_world = f0
self._sp = pw.cheaptrick(x, f0, t, samplerate, fft_size=fft_size) # extract smoothed spectrogram
self._ap = pw.d4c(x, f0, t, samplerate, fft_size=fft_size) # extract aperiodicity
def _kth_harmonic(self, k: int) -> np.ndarray:
"""
Extract the Kth harmonic (starting from 0) from the waveform. Author: @yxlllc
:param k: a non-negative integer
:return: kth_harmonic float32[T]
"""
if k in self._harmonics:
return self._harmonics[k]
hop_size = self._hop_size
win_size = self._win_size
samplerate = self._samplerate
half_width = self._half_width
device = self._device
waveform = torch.from_numpy(self.harmonic()).unsqueeze(0).to(device) # [B, n_samples]
n_samples = waveform.shape[1]
f0 = self._f0 * (k + 1)
pad_size = int(n_samples // hop_size) - len(f0) + 1
if pad_size > 0:
f0 = np.pad(f0, (0, pad_size), mode='constant', constant_values=(f0[0], f0[-1]))
f0, _ = interp_f0(f0, uv=f0 == 0)
f0 = torch.from_numpy(f0).to(device)[None, :, None] # [B, n_frames, 1]
n_f0_frames = f0.shape[1]
phase = torch.arange(win_size, dtype=waveform.dtype, device=device) / win_size * 2 * np.pi
nuttall_window = (
0.355768
- 0.487396 * torch.cos(phase)
+ 0.144232 * torch.cos(2 * phase)
- 0.012604 * torch.cos(3 * phase)
)
spec = torch.stft(
waveform,
n_fft=win_size,
win_length=win_size,
hop_length=hop_size,
window=nuttall_window,
center=True,
return_complex=True
).permute(0, 2, 1) # [B, n_frames, n_spec]
n_spec_frames, n_specs = spec.shape[1:]
idx = torch.arange(n_specs).unsqueeze(0).unsqueeze(0).to(f0) # [1, 1, n_spec]
center = f0 * win_size / samplerate
start = torch.clip(center - half_width, min=0)
end = torch.clip(center + half_width, max=n_specs)
idx_mask = (center >= 1) & (idx >= start) & (idx < end) # [B, n_frames, n_spec]
if n_f0_frames < n_spec_frames:
idx_mask = F.pad(idx_mask, [0, 0, 0, n_spec_frames - n_f0_frames])
spec = spec * idx_mask[:, :n_spec_frames, :]
self._harmonics[k] = torch.istft(
spec.permute(0, 2, 1),
n_fft=win_size,
win_length=win_size,
hop_length=hop_size,
window=nuttall_window,
center=True,
length=n_samples
).squeeze(0).cpu().numpy()
return self._harmonics[k]
def harmonic(self, k: int = None) -> np.ndarray:
"""
Extract the full harmonic part, or the Kth harmonic if `k` is not None, from the waveform.
:param k: an integer representing the harmonic index, starting from 0
:return: full_harmonics float32[T] or kth_harmonic float32[T]
"""
if k is not None:
return self._kth_harmonic(k)
if self._harmonic_part is not None:
return self._harmonic_part
if self._sp is None or self._ap is None:
self._world_extraction()
# noinspection PyAttributeOutsideInit
self._harmonic_part = pw.synthesize(
self._f0_world,
np.clip(self._sp * (1 - self._ap * self._ap), a_min=1e-16, a_max=None), # clip to avoid zeros
np.zeros_like(self._ap),
self._samplerate, frame_period=self._time_step * 1000
).astype(np.float32) # synthesize the harmonic part using the parameters
return self._harmonic_part
def aperiodic(self) -> np.ndarray:
"""
Extract the aperiodic part from the waveform.
:return: aperiodic_part float32[T]
"""
if self._aperiodic_part is not None:
return self._aperiodic_part
if self._sp is None or self._ap is None:
self._world_extraction()
# noinspection PyAttributeOutsideInit
self._aperiodic_part = pw.synthesize(
self._f0_world, self._sp * self._ap * self._ap, np.ones_like(self._ap),
self._samplerate, frame_period=self._time_step * 1000
).astype(np.float32) # synthesize the aperiodic part using the parameters
return self._aperiodic_part
SEP_MODEL = None
class DecomposedWaveformVocalRemover(DecomposedWaveformPyWorld):
def _init(
self, waveform, samplerate, f0,
hop_size=None, fft_size=None, win_size=None, base_harmonic_radius=3.5,
model_path=None, device=None
):
super()._init(
waveform, samplerate, f0, hop_size=hop_size, fft_size=fft_size,
win_size=win_size, base_harmonic_radius=base_harmonic_radius, device=device
)
global SEP_MODEL
if SEP_MODEL is None:
SEP_MODEL = load_sep_model(model_path, self._device)
self.sep_model = SEP_MODEL
def _infer(self):
with torch.no_grad():
x = torch.from_numpy(self._waveform).to(self._device).reshape(1, 1, -1)
if not self.sep_model.is_mono:
x = x.repeat(1, 2, 1)
x = self.sep_model.predict_from_audio(x)
x = torch.mean(x, dim=1)
self._harmonic_part = x.squeeze().cpu().numpy()
self._aperiodic_part = self._waveform - self._harmonic_part
def harmonic(self, k: int = None) -> np.ndarray:
"""
Extract the full harmonic part, or the Kth harmonic if `k` is not None, from the waveform.
:param k: an integer representing the harmonic index, starting from 0
:return: full_harmonics float32[T] or kth_harmonic float32[T]
"""
if k is not None:
return self._kth_harmonic(k)
if self._harmonic_part is not None:
return self._harmonic_part
self._infer()
return self._harmonic_part
def aperiodic(self) -> np.ndarray:
"""
Extract the aperiodic part from the waveform.
:return: aperiodic_part float32[T]
"""
if self._aperiodic_part is not None:
return self._aperiodic_part
self._infer()
return self._aperiodic_part