Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ADD RVC Voice Changer #1096

Closed
kyakuno opened this issue Apr 8, 2023 · 42 comments
Closed

ADD RVC Voice Changer #1096

kyakuno opened this issue Apr 8, 2023 · 42 comments
Assignees

Comments

@kyakuno
Copy link
Collaborator

kyakuno commented Apr 8, 2023

AIを使用したボイスチェンジャー。
https://github.com/liujing04/Retrieval-based-Voice-Conversion-WebUI

@kyakuno
Copy link
Collaborator Author

kyakuno commented Apr 8, 2023

@yuki399 yuki399 assigned yuki399 and unassigned yuki399 Apr 8, 2023
@srpkdyy srpkdyy self-assigned this May 4, 2023
@srpkdyy
Copy link
Contributor

srpkdyy commented Jul 5, 2023

fairseqのhubertモデルが内部で用いられているが、ONNXエクスポートが難しい

@srpkdyy srpkdyy removed their assignment Jul 5, 2023
@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 8, 2023

RVCのコアモデルはONNXに変換できそうに思われる
https://github.com/w-okada/voice-changer/blob/master/tutorials/tutorial_rvc_ja_latest.md

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 8, 2023

推論サンプルの中でhubertをloadしている
https://github.com/RVC-Project/Retrieval-based-Voice-Conversion-WebUI/blob/main/infer-web.py

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 8, 2023

hubertは音声におけるbertのような表現学習モデル
https://techblog.yahoo.co.jp/entry/2023032930418574/

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 8, 2023

hubertをonnxにエクスポートすると推論できない
onnx/onnx#3907

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 8, 2023

latestのtorchだとエクスポートと推論ができるとのこと
pytorch/pytorch#70289

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 8, 2023

@ooe1123 現在、取り組まれているモデルの後に、こちらのモデルの変換をお願いしてもよいでしょうか。

@ooe1123
Copy link
Contributor

ooe1123 commented Jul 23, 2023

F.scaled_dot_product_attentionのエクスポートは、Nightly版で対応されたとのこと。

torch 2.1.0.dev20230722+cu118
torchaudio 2.1.0.dev20230722+cu118
torchvision 0.16.0.dev20230722+cu118

@ooe1123
Copy link
Contributor

ooe1123 commented Jul 23, 2023

RVC WebUIの導入
https://zenn.dev/kaeru39/articles/c13b45726ca668

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

アーキテクチャとしてはPCM入力、PCM出力となっている。

  1. 入力音声を16kHzのrange(-1,+1)のモノラル音声にする(4secの場合、(60736,))
  2. highpassフィルタを0位相フィルタで適用(filtfilt)
  3. hubertで特徴ベクトルを取得(feat (1, 156736), output (1,489,256))
  4. 特徴ベクトルにinterpolateを適用して2倍の長さにする
  5. vcで音声変換 (feat (1, 978, 256), output (1, 1, 391200)
  6. 出力音量を正規化

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

リアルタイムモードにも対応したいため、バッチとリアルタイムで同じONNXが使用できるかを確認する必要がある。
https://github.com/RVC-Project/Retrieval-based-Voice-Conversion-WebUI/blob/main/rvc_for_realtime.py

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

VCモデルのONNX変換はWEBUIにも搭載されている。
https://github.com/RVC-Project/Retrieval-based-Voice-Conversion-WebUI

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

MITライセンスのVCモデル。
https://chihaya369.booth.pm/items/4701666

fairseq.models.hubert.hubert | HubertModel Config: {'_name': 'hubert', 'label_rate': 50.0, 'extractor_mode': default, 'encoder_layers': 12, 'encoder_embed_dim': 768, 'encoder_ffn_embed_dim': 3072, 'encoder_attention_heads': 12, 'activation_fn': gelu, 'layer_type': transformer, 'dropout': 0.1, 'attention_dropout': 0.1, 'activation_dropout': 0.0, 'encoder_layerdrop': 0.05, 'dropout_input': 0.1, 'dropout_features': 0.1, 'final_dim': 256, 'untie_final_proj': True, 'layer_norm_first': False, 'conv_feature_layers': '[(512,10,5)] + [(512,3,2)] * 4 + [(512,2,2)] * 2', 'conv_bias': False, 'logit_temp': 0.1, 'target_glu': False, 'feature_grad_mult': 0.1, 'mask_length': 10, 'mask_prob': 0.8, 'mask_selection': static, 'mask_other': 0.0, 'no_mask_overlap': False, 'mask_min_space': 1, 'mask_channel_length': 10, 'mask_channel_prob': 0.0, 'mask_channel_selection': static, 'mask_channel_other': 0.0, 'no_mask_channel_overlap': False, 'mask_channel_min_space': 1, 'conv_pos': 128, 'conv_pos_groups': 16, 'latent_temp': [2.0, 0.5, 0.999995], 'skip_masked': False, 'skip_nomask': False, 'checkpoint_activations': False, 'required_seq_len_multiple': 2, 'depthwise_conv_kernel_size': 31, 'attn_type': '', 'pos_enc_type': 'abs', 'fp16': False}

if_f0はweightの中に入っている。下記のようにcptを取得し、cpt.f0で参照している。このモデルはf0を使用していない。

cpt = torch.load(person, map_location="cpu")
if_f0 = cpt.get("f0", 1)

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

if_f0はpitch guidanceで歌声用にpitchを追加情報として与えるために追加されている気がする。
(模型是否带音高指导 = pitch guidance = if_f0が対応している)

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

4secの音声ファイルのM2での処理時間。ailia SDK 1.2.15

モデル MPS BLAS
hubert 1st 8655ms 2561ms
hubert 2nd 8278ms 2306ms
vc 1st 8800ms 7737ms
vc 2nd 7952ms 6299ms

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

MPS Profile

hubert

====Profile(Grouped by LayerType)====
LayerType	TotalPredictTime(Average)[us]	TimeRatio[%]
MatMul_DNN	7843389	93.31
Convolution1D/Gelu[Fused]_DNN	329624	3.92
Convolution1D/Gelu[Fused]	309551	3.68
Eltwise_DNN	84765	1.01
Convolution1D_DNN	64120	0.76
ReduceMean_DNN	62086	0.74

vc

====Profile(Grouped by LayerType)====
LayerType	TotalPredictTime(Average)[us]	TimeRatio[%]
Convolution1D_DNN	3377697	40.56
Convolution1D	3290021	39.50
Convolution1D/PReLU[Fused]_DNN	3207996	38.52
Convolution1D/PReLU[Fused]	2983992	35.83
Eltwise	492447	5.91
PReLU	297115	3.57
Deconvolution_DNN	262809	3.16
Eltwise_DNN	245986	2.95

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

BLAS Profile

hubert

====Profile(Grouped by LayerType)====
LayerType	TotalPredictTime(Average)[us]	TimeRatio[%]
MatMul_DNN	1920693	79.86
MatMul	1876271	78.01
Convolution1D/Gelu[Fused]	335134	13.93
Eltwise_DNN	94547	3.93
ReduceMean_DNN	47363	1.97
Convolution1D_DNN	45820	1.91
Gemm_DNN	44634	1.86

vc

====Profile(Grouped by LayerType)====
LayerType	TotalPredictTime(Average)[us]	TimeRatio[%]
Convolution1D	3063279	46.36
Convolution1D/PReLU[Fused]	2948313	44.62
Deconvolution	188019	2.85
Eltwise	128596	1.95
MatMul	112141	1.70
PReLU	59852	0.91

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

hubertはmatmulネック、vcはConv1Dネック。

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

4secの音声ファイルのRTX3080での処理時間。ailia SDK 1.2.15。

モデル cuDNN FP32
hubert 1st 1437ms
hubert 2nd 275ms
vc 1st 1144ms
vc 2nd 288ms

PCであればリアルタイム処理が可能。

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

WebUIの起動。

git clone https://github.com/RVC-Project/Retrieval-based-Voice-Conversion-WebUI
pip3 install -r requirements.txt
python3 infer_web.py

事前に下記のweightをダウンロードしておく必要がある。
https://huggingface.co/lj1995/VoiceConversionWebUI/tree/main

hubert_base.pt

声のモデルはweightsフォルダに配置する。

macOSの場合はCPU実行にする必要がある。

export PYTORCH_ENABLE_MPS_FALLBACK=1

webui

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

・filtfiltで16kHzのhigh pass filterをかけているが入力音声が16kHzだと不要ではないか?

コメントアウトしても音質に問題はなさそうなので、C++実装では外しても良さそう。

・長い音声で窓分割した場合のオーバラップはどのようにしているか?

audioのshapeが(60736,)の場合、前後にt_padを加えて、audio_padのshapeは(156736,) = 60736 + 48000*2になる。
推論すると、(156736,)が(1, 1, 391200)になる。そこから、前後、t_pad_tgtを削除すると、151200のPCMが得られる。
入力が16kHzで、出力は40kHzになるので、2.5倍にサンプル数が広がる。
入力サンプル数がt_maxを超える場合、160サンプルずつずらしながら推論する。
その場合、[prev=48000sample, main=160sample, next=48000sample]で推論して、400sampleを取得する。

パラメータ

vc_param.window 160
vc_param.t_max 1040000 : 65sec
vc_param.t_pad 48000 : 16kHz
vc_param.t_pad2 96000 : t_pad * 2
vc_param.t_pad_tgt 120000 : 40kHz (48000 * 2.5)

windowサイズがpadに比べて小さすぎて、ペナルティが大きい気もするが、デフォルト値は160になっている。
ただ、現実的にはt_maxが65secと大きいので、分割するパスに入る方が珍しいのかもしれない。

・rms_mix_rateの効果は?

rms_mix_rateのデフォルトは0.25。rmsは音の短時間のエネルギー。
入力PCMのエネルギーと出力PCMのエネルギーが合うようにブロック(0.5sec単位)で音量調整を行う。
C++のサンプルとしては省略しても音としては正しい音になる。
Root Mean Squareなので、実装はそんなに難しくはない。

・リアルタイムモードの動作は?

realtimeモードでは、gui_v1.pyにおいて、
 self.block_frame = int(self.config.block_time * self.config.samplerate)
単位で推論をしている。
block_timeはデフォルトで1secである。
先頭のPaddingを行わず、末尾にcrossfade_time分だけpaddingして推論して、出力をクロスフェードしている。
crossfade_timeはデフォルトで0.08secである。

indataがpushされると、input_wavにappendする。
input_wavは推論すると、block_frameだけpopする。

ONNXモデルとしては、リアルタイムモードとバッチモードで共通のモデルを使用できる。

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

C++から呼び出す場合、if_f0がfalseであれば、16kHzのPCMから1つ目のモデルでEmbeddingを取得して、2つ目のモデルでPCMを取得するだけでシンプルに実装できそう。

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

 feats = F.interpolate(feats.permute(0, 2, 1), scale_factor=2).permute(0, 2, 1)
は、
 interpolate in torch.Size([1, 489, 256])
 interpolate out torch.Size([1, 978, 256])
のように、axis=1をnearestで2倍にコピーする操作である。

下記でも等価になる。

    new_feats = np.zeros((feats.shape[0], feats.shape[1]*2, feats.shape[2]))
    for i in range(feats.shape[1]):
        new_feats[:,i*2+0,:] = feats[:,i,:]
        new_feats[:,i*2+1,:] = feats[:,i,:]
    feats = new_feats

下記で計算した誤差は0になる。

print("diff", (interp_feats - feats).mean())

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

RMSは下記のコードで計算可能。

    rms1 = librosa.feature.rms(
        y=audio0, frame_length=16000 // 2 * 2, hop_length=16000 // 2
    )  # 每半秒一个点

    import math
    rms2 = []
    i = 0
    while i < len(audio0):
        sum = 0
        for j in range(16000 // 2 * 2):
            if i + j - 16000 // 2 >= 0 and i + j - 16000 // 2 < len(audio0):
                sum = sum + audio0[i + j - 16000 // 2] * audio0[i + j - 16000 // 2]
        i = i + 16000 //2
        rms2.append(math.sqrt(sum / (16000 // 2 * 2)))
rms1 [[0.01186692 0.03842403 0.07738604 0.09538703 0.11221342 0.09029565
  0.00377364 0.09029192 0.11220899 0.09538583 0.07739243 0.03843572
  0.01580535 0.01053577 0.0083064  0.01405562 0.03136    0.06526884
  0.09713326 0.11689314]]
recalc rms1 [0.011866915683046389, 0.03842402383352823, 0.07738604487145795, 0.09538702655233344, 0.1122134129345262, 0.09029564310712612, 0.0037736419053470208, 0.09029190999923685, 0.11220899426454611, 0.09538583212138613, 0.07739242836008103, 0.03843572278584739, 0.015805346160338015, 0.010535767466201143, 0.008306403196957655, 0.014055619783892297, 0.03135999664088785, 0.06526883390625657, 0.09713326339263806, 0.11689313597392988]

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

RVCのモデルサイズはhubert_baseが293.5MB、vcが110.2MB。
memory_mode=11で、
4secの音声で消費メモリは1.49GB程度。
9secの音声で消費メモリは1.82GB程度。

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

音声ファイルによってはailiaでもonnx runtimeでもhubertが下記のエラーになることがある。入力長に何か制約がありそう。onnxruntimeはhubertの出力の次元が0になって、その後のinterpolateでエラーになる。

+ error detail : Layer:/final_proj/MatMul_output_0(MatMul_DNN) Error:This layer is not support inference with empty blobs.

RuntimeError: Input and output sizes should be greater than 0, but got input (W: 0) and output (W: 0)

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

サンプル数ごとに推論が成功するかどうか。

156736 -> OK
233440 -> OK
233937 -> NG
731890 -> NG

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

40secの音声でもエラーになるので、入力長が一定以上になると動かない気がする。

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

hubertのONNXエクスポート時の音声ファイルの長さに影響されている可能性があるかどうか。

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

Transposeまでは正しい値が取れていて、Sliceで次元数が0になってしまっている。

Idx 2131 /encoder/Transpose_5_output_0 (1, 730, 768)
Idx 2132 /encoder/Neg_output_0 ()
Idx 2133 /encoder/Constant_51_output_0 (1,)
Idx 2134 /encoder/Unsqueeze_6_output_0 (1,)
Idx 2135 /encoder/Constant_50_output_0 (1,)
Idx 2136 /encoder/Constant_49_output_0 (1,)
Idx 2137 /encoder/Constant_52_output_0 (1,)
Idx 2138 /encoder/Slice_5_output_0 (1, 0, 768)
Idx 2139 onnx::MatMul_2842 (768, 256)

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

下記の値が壊れていそうなのだけれど、グラフ的には壊れるのが不思議。
推論に失敗していると内部blobの値がinvalid_stateで取得できないので、ailiaをビルドしてblobをダンプする。

/encoder/Unsqueeze_6_output_0 <- "/encoder/Neg_output_0" <- "/encoder/Cast_2_output_0" <- "/encoder/Sub_output_0" <- "/encoder/Mul_3_output_0"

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

推論できないサンプルだと、/encoder/Mul_3_output_0は730.0が入っていて、/encoder/Sub_output_0が0.0になっている。
/encoder/Cast_1_output_0が730.0なので、Subで0になってしまう。

mod1

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

正常に推論できるサンプルだと、/encoder/Unsqueeze_6_output_0は-1 (全要素を指定する意味を持つ)が入っていて、/encoder/Sub_output_0は1.0。
/encoder/Mul_3_output_0が730.0で、/encoder/Cast_1_output_0が729.0になっている。

mod2

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

A % 2 - Aをして、無理やり-1を作って、Sliceのend_indicesに入れて、全ての出力を出すという不思議なONNXになっている。この最後のSliceは入力Shapeをそのまま出力するものであり、不要ではないかと思われる。

slice

@kyakuno
Copy link
Collaborator Author

kyakuno commented Jul 26, 2023

@ooe1123 hubertのエクスポート時に、何らかのコードで、MatMulの前にSliceが入ってしまっているようでして、エクスポート時にこれを削除することは可能でしょうか?

@ooe1123
Copy link
Contributor

ooe1123 commented Jul 26, 2023

@kyakuno
当該のSlice処理は、モデルが利用している外部モジュールの以下の箇所に該当するようです。
https://github.com/facebookresearch/fairseq/blob/main/fairseq/models/wav2vec/wav2vec2.py#L1144-L1154

paddingがある場合とない場合とでif文による分岐が発生しており、どちらかのパターンにしか対応できていないモデルになってしまっているようですね。
分岐が発生しない形に修正できないか、検討してみますが、難しい場合はモデルに入力するテンソルのサイズを固定するという対応が必要かもしれません。

@ooe1123
Copy link
Contributor

ooe1123 commented Jul 26, 2023

hubertモデルのエクスポートにあたってfairseqの修正箇所

○ fairseq/utils.py

def index_put(tensor, indices, value):
    if is_xla_tensor(tensor):
        ...
    else:
        tensor[indices] = value
    return tensor

def index_put(tensor, indices, value):
    if is_xla_tensor(tensor):
        ...
    else:
        # tensor[indices] = value
        tensor[0][indices[0]] = value
    return tensor

○ fairseq/models/wav2vec/utils.py

def pad_to_multiple(x, multiple, dim=-1, value=0):
    # Inspired from https://github.com/lucidrains/local-attention/blob/master/local_attention/local_attention.py#L41
    if x is None:
        return None, 0
    tsz = x.size(dim)
    m = tsz / multiple
    remainder = math.ceil(m) * multiple - tsz
    if m.is_integer():
        return x, 0
    pad_offset = (0,) * (-1 - dim) * 2

    return F.pad(x, (*pad_offset, 0, remainder), value=value), remainder

def pad_to_multiple(x, multiple, dim=-1, value=0):
    # Inspired from https://github.com/lucidrains/local-attention/blob/master/local_attention/local_attention.py#L41
    if x is None:
        return None, 0
    tsz = x.size(dim)
    m = tsz / multiple
    if torch.onnx.is_in_onnx_export():
        remainder = torch.ceil(m) * multiple - tsz
        remainder = remainder.type(torch.int64)
        pad_offset = (0,) * (-1 - dim) * 2
    else:
        remainder = math.ceil(m) * multiple - tsz
        if m.is_integer():
            return x, 0
        pad_offset = (0,) * (-1 - dim) * 2

    return F.pad(x, (*pad_offset, 0, remainder), value=value), remainder

○ fairseq/models/wav2vec/wav2vec2.py

class TransformerEncoder(nn.Module):
    ...
    def extract_features(
        self,
        x,
        padding_mask=None,
        tgt_layer=None,
        min_layer=0,
    ):
        ...
        # undo paddding
        if pad_length > 0:
            x = x[:, :-pad_length]

            def undo_pad(a, b, c):
                return (
                    a[:-pad_length],
                    b[:-pad_length] if b is not None else b,
                    c[:-pad_length],
                )

            layer_results = [undo_pad(*u) for u in layer_results]

        return x, layer_results

class TransformerEncoder(nn.Module):
    ...
    def extract_features(
        self,
        x,
        padding_mask=None,
        tgt_layer=None,
        min_layer=0,
    ):
        ...
        # undo paddding
        if True:
            x = x[:, :x.shape[1]-pad_length]

            def undo_pad(a, b, c):
                return (
                    a[:a.shape[0]-pad_length],
                    b[:b.shape[0]-pad_length] if b is not None else b,
                    c[:c.shape[0]-pad_length],
                )

            layer_results = [undo_pad(*u) for u in layer_results]

        return x, layer_results

@kyakuno
Copy link
Collaborator Author

kyakuno commented Aug 9, 2023

if_f0 = Trueの場合は、net_gにpitchとpitchfの引数が加わる。
pitchはhubertのfeatureのlengthだけ使用する。

@kyakuno
Copy link
Collaborator Author

kyakuno commented Aug 9, 2023

protectが有効な場合、下記の式で補正する。この時、featsはfaissを適用したもの、feats0はfaissを適用しないものである。

    if protect < 0.5 and pitch is not None and pitchf is not None:
        pitchff = np.copy(pitchf)
        pitchff[pitchf > 0] = 1
        pitchff[pitchf < 1] = protect
        pitchff = np.expand_dims(pitchff, axis=-1)
        feats = feats * pitchff + feats0 * (1 - pitchff)

@kyakuno
Copy link
Collaborator Author

kyakuno commented Aug 9, 2023

faissは、k=8の近いfeature vectorを取得し、元のfeature vectorに重み付けする機能。

ykk648 added a commit to ykk648/fairseq that referenced this issue Nov 17, 2023
@ArEnSc
Copy link

ArEnSc commented Feb 25, 2024

    #feats = hubert.get_blob_data(hubert.find_blob_index_by_name("/encoder/Slice_5_output_0")) # v2 : 768
        elif args.version == 2:

bug in the rvc.py

does not actually get that layer from onxx model

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants