316 lines
12 KiB
Python
316 lines
12 KiB
Python
from functools import partial
|
|
from multiprocessing.pool import ThreadPool
|
|
import os
|
|
import numpy as np
|
|
from scipy import signal
|
|
import torch
|
|
|
|
from .lib.rmvpe import RMVPE
|
|
from .lib.audio import autotune_f0, pad_audio
|
|
from .lib import BASE_MODELS_DIR
|
|
from .lib.utils import gc_collect, get_merge_func, get_optimal_threads, get_optimal_torch_device
|
|
|
|
class FeatureExtractor:
|
|
def __init__(self, tgt_sr, config, onnx=False):
|
|
self.x_pad, self.x_query, self.x_center, self.x_max, self.is_half = (
|
|
config.x_pad,
|
|
config.x_query,
|
|
config.x_center,
|
|
config.x_max,
|
|
config.is_half,
|
|
)
|
|
|
|
self.sr = 16000 # hubert输入采样率
|
|
self.window = 160 # 每帧点数
|
|
self.t_pad = self.sr * self.x_pad # 每条前后pad时间
|
|
self.t_pad_tgt = tgt_sr * self.x_pad
|
|
self.t_pad2 = self.t_pad * 2
|
|
self.t_query = self.sr * self.x_query # 查询切点前后查询时间
|
|
self.t_center = self.sr * self.x_center # 查询切点位置
|
|
self.t_max = self.sr * self.x_max # 免查询时长阈值
|
|
self.device = config.device
|
|
self.onnx = onnx
|
|
self.f0_method_dict = {
|
|
"pm": self.get_pm,
|
|
"harvest": self.get_harvest,
|
|
"dio": self.get_dio,
|
|
"rmvpe": self.get_rmvpe,
|
|
"rmvpe_onnx": self.get_rmvpe,
|
|
"rmvpe+": self.get_pitch_dependant_rmvpe,
|
|
"crepe": self.get_f0_official_crepe_computation,
|
|
"crepe-tiny": partial(self.get_f0_official_crepe_computation, model='model'),
|
|
"mangio-crepe": self.get_f0_crepe_computation,
|
|
"mangio-crepe-tiny": partial(self.get_f0_crepe_computation, model='model'),
|
|
|
|
}
|
|
|
|
def __del__(self):
|
|
if hasattr(self,"model_rmvpe"):
|
|
del self.model_rmvpe
|
|
gc_collect()
|
|
|
|
def load_index(self, file_index):
|
|
try:
|
|
if not type(file_index)==str: # loading file index to save time
|
|
print("Using preloaded file index.")
|
|
index = file_index
|
|
big_npy = index.reconstruct_n(0, index.ntotal)
|
|
elif file_index == "":
|
|
print("File index was empty.")
|
|
index = None
|
|
big_npy = None
|
|
else:
|
|
if os.path.isfile(file_index):
|
|
print(f"Attempting to load {file_index}....")
|
|
else:
|
|
print(f"{file_index} was not found...")
|
|
import faiss
|
|
index = faiss.read_index(file_index)
|
|
print(f"loaded index: {index}")
|
|
big_npy = index.reconstruct_n(0, index.ntotal)
|
|
except Exception as e:
|
|
print(f"Could not open Faiss index file for reading. {e}")
|
|
index = None
|
|
big_npy = None
|
|
return index, big_npy
|
|
|
|
# Fork Feature: Compute f0 with the crepe method
|
|
def get_f0_crepe_computation(
|
|
self,
|
|
x,
|
|
f0_min,
|
|
f0_max,
|
|
*args, # 512 before. Hop length changes the speed that the voice jumps to a different dramatic pitch. Lower hop lengths means more pitch accuracy but longer inference time.
|
|
**kwargs, # Either use crepe-tiny "tiny" or crepe "full". Default is full
|
|
):
|
|
import torchcrepe
|
|
x = x.astype(
|
|
np.float32
|
|
) # fixes the F.conv2D exception. We needed to convert double to float.
|
|
x /= np.quantile(np.abs(x), 0.999)
|
|
torch_device = get_optimal_torch_device()
|
|
audio = torch.from_numpy(x).to(torch_device, copy=True)
|
|
audio = torch.unsqueeze(audio, dim=0)
|
|
if audio.ndim == 2 and audio.shape[0] > 1:
|
|
audio = torch.mean(audio, dim=0, keepdim=True).detach()
|
|
audio = audio.detach()
|
|
hop_length = kwargs.get('crepe_hop_length', 160)
|
|
model = kwargs.get('model', 'full')
|
|
print("Initiating prediction with a crepe_hop_length of: " + str(hop_length))
|
|
pitch: torch.Tensor = torchcrepe.predict(
|
|
audio,
|
|
self.sr,
|
|
hop_length,
|
|
f0_min,
|
|
f0_max,
|
|
model,
|
|
batch_size=hop_length * 2,
|
|
device=torch_device,
|
|
pad=True,
|
|
)
|
|
p_len = x.shape[0] // hop_length
|
|
# Resize the pitch for final f0
|
|
source = np.array(pitch.squeeze(0).cpu().float().numpy())
|
|
source[source < 0.001] = np.nan
|
|
target = np.interp(
|
|
np.arange(0, len(source) * p_len, len(source)) / p_len,
|
|
np.arange(0, len(source)),
|
|
source,
|
|
)
|
|
f0 = np.nan_to_num(target)
|
|
return f0 # Resized f0
|
|
|
|
def get_f0_official_crepe_computation(
|
|
self,
|
|
x,
|
|
f0_min,
|
|
f0_max,
|
|
*args,
|
|
**kwargs
|
|
):
|
|
import torchcrepe
|
|
# Pick a batch size that doesn't cause memory errors on your gpu
|
|
batch_size = 512
|
|
# Compute pitch using first gpu
|
|
audio = torch.tensor(np.copy(x))[None].float()
|
|
model = kwargs.get('model', 'full')
|
|
f0, pd = torchcrepe.predict(
|
|
audio,
|
|
self.sr,
|
|
self.window,
|
|
f0_min,
|
|
f0_max,
|
|
model,
|
|
batch_size=batch_size,
|
|
device=self.device,
|
|
return_periodicity=True,
|
|
)
|
|
pd = torchcrepe.filter.median(pd, 3)
|
|
f0 = torchcrepe.filter.mean(f0, 3)
|
|
f0[pd < 0.1] = 0
|
|
f0 = f0[0].cpu().numpy()
|
|
return f0
|
|
|
|
def get_pm(self, x, *args, **kwargs):
|
|
import parselmouth
|
|
p_len = x.shape[0] // 160 + 1
|
|
f0 = parselmouth.Sound(x, self.sr).to_pitch_ac(
|
|
time_step=0.01,
|
|
voicing_threshold=0.6,
|
|
pitch_floor=kwargs.get('f0_min'),
|
|
pitch_ceiling=kwargs.get('f0_max'),
|
|
).selected_array["frequency"]
|
|
|
|
pad_size = (p_len - len(f0) + 1) // 2
|
|
if pad_size > 0 or p_len - len(f0) - pad_size > 0:
|
|
# print(pad_size, p_len - len(f0) - pad_size)
|
|
f0 = np.pad(f0, [[pad_size, p_len - len(f0) - pad_size]], mode="constant")
|
|
return f0
|
|
|
|
def get_harvest(self, x, *args, **kwargs):
|
|
import pyworld
|
|
f0_spectral = pyworld.harvest(
|
|
x.astype(np.double),
|
|
fs=self.sr,
|
|
f0_ceil=kwargs.get('f0_max'),
|
|
f0_floor=kwargs.get('f0_min'),
|
|
frame_period=1000 * kwargs.get('hop_length', 160) / self.sr,
|
|
)
|
|
return pyworld.stonemask(x.astype(np.double), *f0_spectral, self.sr)
|
|
|
|
def get_dio(self, x, *args, **kwargs):
|
|
import pyworld
|
|
f0_spectral = pyworld.dio(
|
|
x.astype(np.double),
|
|
fs=self.sr,
|
|
f0_ceil=kwargs.get('f0_max'),
|
|
f0_floor=kwargs.get('f0_min'),
|
|
frame_period=1000 * kwargs.get('hop_length', 160) / self.sr,
|
|
)
|
|
return pyworld.stonemask(x.astype(np.double), *f0_spectral, self.sr)
|
|
|
|
|
|
def get_rmvpe(self, x, *args, **kwargs):
|
|
if not hasattr(self,"model_rmvpe"):
|
|
self.model_rmvpe = RMVPE(os.path.join(BASE_MODELS_DIR,f"rmvpe.{'onnx' if self.onnx else 'pt'}"), is_half=self.is_half, device=self.device, onnx=self.onnx)
|
|
|
|
return self.model_rmvpe.infer_from_audio(x, thred=0.03)
|
|
# else:
|
|
# f0 = self.model_rmvpe.infer_from_audio(x, thred=0.03)
|
|
# if "privateuseone" in str(self.device):
|
|
# del self.model_rmvpe.model
|
|
# del self.model_rmvpe
|
|
# print("cleaning ortruntime memory")
|
|
# return f0
|
|
|
|
def get_pitch_dependant_rmvpe(self, x, f0_min=1, f0_max=40000, *args, **kwargs):
|
|
if not hasattr(self,"model_rmvpe"):
|
|
self.model_rmvpe = RMVPE(os.path.join(BASE_MODELS_DIR,f"rmvpe.{'onnx' if self.onnx else 'pt'}"), is_half=self.is_half, device=self.device, onnx=self.onnx)
|
|
|
|
return self.model_rmvpe.infer_from_audio_with_pitch(x, thred=0.03, f0_min=f0_min, f0_max=f0_max)
|
|
|
|
|
|
# Fork Feature: Acquire median hybrid f0 estimation calculation
|
|
def get_f0_hybrid_computation(
|
|
self,
|
|
methods_list,
|
|
merge_type,
|
|
x,
|
|
f0_min,
|
|
f0_max,
|
|
filter_radius,
|
|
crepe_hop_length,
|
|
time_step,
|
|
**kwargs
|
|
):
|
|
# Get various f0 methods from input to use in the computation stack
|
|
params = {'x': x, 'f0_min': f0_min,
|
|
'f0_max': f0_max, 'time_step': time_step, 'filter_radius': filter_radius,
|
|
'crepe_hop_length': crepe_hop_length, 'model': "full"
|
|
}
|
|
|
|
f0_computation_stack = []
|
|
|
|
print(f"Calculating f0 pitch estimations for methods: {methods_list}")
|
|
x = x.astype(np.float32)
|
|
x /= np.quantile(np.abs(x), 0.999)
|
|
# Get f0 calculations for all methods specified
|
|
|
|
def _get_f0(method,params):
|
|
if method not in self.f0_method_dict:
|
|
raise Exception(f"Method {method} not found.")
|
|
f0 = self.f0_method_dict[method](**params)
|
|
if method == 'harvest' and filter_radius > 2:
|
|
f0 = signal.medfilt(f0, filter_radius)
|
|
f0 = f0[1:] # Get rid of first frame.
|
|
return f0
|
|
|
|
with ThreadPool(max(1,get_optimal_threads())) as pool:
|
|
f0_computation_stack = pool.starmap(_get_f0,[(method,params) for method in methods_list])
|
|
|
|
f0_computation_stack = pad_audio(*f0_computation_stack) # prevents uneven f0
|
|
|
|
print(f"Calculating hybrid median f0 from the stack of: {methods_list} using {merge_type} merge")
|
|
merge_func = get_merge_func(merge_type)
|
|
f0_median_hybrid = merge_func(f0_computation_stack, axis=0)
|
|
|
|
return f0_median_hybrid
|
|
|
|
def get_f0(
|
|
self,
|
|
x,
|
|
f0_up_key,
|
|
f0_method,
|
|
merge_type="median",
|
|
filter_radius=3,
|
|
crepe_hop_length=160,
|
|
f0_autotune=False,
|
|
rmvpe_onnx=False,
|
|
inp_f0=None,
|
|
f0_min=50,
|
|
f0_max=1100,
|
|
**kwargs
|
|
):
|
|
time_step = self.window / self.sr * 1000
|
|
f0_mel_min = 1127 * np.log(1 + f0_min / 700)
|
|
f0_mel_max = 1127 * np.log(1 + f0_max / 700)
|
|
params = {'x': x, 'f0_up_key': f0_up_key, 'f0_min': f0_min,
|
|
'f0_max': f0_max, 'time_step': time_step, 'filter_radius': filter_radius,
|
|
'crepe_hop_length': crepe_hop_length, 'model': "full", 'onnx': rmvpe_onnx
|
|
}
|
|
print(f"get_f0 {f0_method} unused params: {kwargs}")
|
|
if hasattr(f0_method,"pop") and len(f0_method)==1: f0_method = f0_method.pop()
|
|
if type(f0_method) == list:
|
|
# Perform hybrid median pitch estimation
|
|
f0 = self.get_f0_hybrid_computation(f0_method,merge_type,**params)
|
|
else:
|
|
f0 = self.f0_method_dict[f0_method](**params)
|
|
|
|
if f0_autotune:
|
|
f0 = autotune_f0(f0)
|
|
|
|
f0 *= pow(2, f0_up_key / 12)
|
|
# with open("test.txt","w")as f:f.write("\n".join([str(i)for i in f0.tolist()]))
|
|
tf0 = self.sr // self.window # 每秒f0点数
|
|
if inp_f0 is not None:
|
|
delta_t = np.round(
|
|
(inp_f0[:, 0].max() - inp_f0[:, 0].min()) * tf0 + 1
|
|
).astype("int16")
|
|
replace_f0 = np.interp(
|
|
list(range(delta_t)), inp_f0[:, 0] * 100, inp_f0[:, 1]
|
|
)
|
|
shape = f0[self.x_pad * tf0 : self.x_pad * tf0 + len(replace_f0)].shape[0]
|
|
f0[self.x_pad * tf0 : self.x_pad * tf0 + len(replace_f0)] = replace_f0[
|
|
:shape
|
|
]
|
|
|
|
# f0bak = f0.copy()
|
|
f0_mel = 1127 * np.log(1 + f0 / 700)
|
|
f0_mel[f0_mel > 0] = (f0_mel[f0_mel > 0] - f0_mel_min) * 254 / (
|
|
f0_mel_max - f0_mel_min
|
|
) + 1
|
|
f0_mel[f0_mel <= 1] = 1
|
|
f0_mel[f0_mel > 255] = 255
|
|
f0_coarse = np.rint(f0_mel).astype(np.int16)
|
|
|
|
return f0_coarse, f0 # 1-0 |