From 864d4f077b824f4d32f41c81cd93c7391f46ea0e Mon Sep 17 00:00:00 2001 From: oatsu Date: Mon, 23 Sep 2024 22:02:14 +0900 Subject: [PATCH 1/6] =?UTF-8?q?=E6=8B=A1=E5=BC=B5=E6=A9=9F=E8=83=BD=20acou?= =?UTF-8?q?stic=5Feditor=20=E3=81=A7=E3=81=AE=E7=B7=A8=E9=9B=86=E6=A9=9F?= =?UTF-8?q?=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_enunu.py | 119 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 110 insertions(+), 9 deletions(-) diff --git a/simple_enunu.py b/simple_enunu.py index d8f7a0f..676512f 100755 --- a/simple_enunu.py +++ b/simple_enunu.py @@ -1,6 +1,6 @@ #! /usr/bin/env python3 # coding: utf-8 -# Copyright (c) 2023 oatsu +# Copyright (c) 2023-2024 oatsu """ 1. UTAUプラグインのテキストファイルを読み取る。 2. LABファイル→WAVファイル @@ -177,8 +177,8 @@ def adjust_wav_gain_for_float32(wav: np.ndarray): return wav -class SimpleEnunu(SPSVS): - """SimpleEnunuで合成するとき用に、外部ソフトでタイミング補正ができるようにする。 +class ENUNU(SPSVS): + """ENUNU で合成するするときのクラス。 Args: model_dir (str): NNSVSのモデルがあるフォルダ @@ -199,6 +199,10 @@ def __init__(self, self.path_mono_score = None self.path_full_timing = None self.path_mono_timing = None + self.path_mgc = None + self.path_f0 = None + self.path_vuv = None + self.path_bap = None # self.path_wav = None def set_paths(self, temp_dir, songname): @@ -210,6 +214,10 @@ def set_paths(self, temp_dir, songname): self.path_mono_score = join(temp_dir, f'{songname}_score.lab') self.path_full_timing = join(temp_dir, f'{songname}_timing.full') self.path_mono_timing = join(temp_dir, f'{songname}_timing.lab') + self.path_mgc = join(temp_dir, f'{songname}_acoustic_mgc.csv') + self.path_f0 = join(temp_dir, f'{songname}_acoustic_f0.csv') + self.path_vuv = join(temp_dir, f'{songname}_acoustic_vuv.csv') + self.path_bap = join(temp_dir, f'{songname}_acoustic_bap.csv') def get_extension_path_list(self, key) -> List[str]: """ @@ -324,6 +332,81 @@ def edit_timing(self, duration_modified_labels, key='timing_editor'): duration_modified_labels = hts.load(self.path_full_timing).round_() return duration_modified_labels + def edit_acoustic(self, multistream_features, feature_type, key='acoustic_editor'): + """ + 外部ツールでピッチなどを編集する。 + """ + # acoustic加工ツールのパスを取得 + extension_list = self.get_extension_path_list(key) + # ツールが指定されていない場合はSkip + if len(extension_list) == 0: + return multistream_features + + # 想定外のボコーダが指定された場合もSkip + if feature_type not in ['world', 'melf0']: + self.logger.warning( + 'Unknown feature_type "%s" is selected. Skipping acoustic editor.', + feature_type) + return multistream_features + + # ツールが指定されている場合はCSV書き出し + if feature_type == 'world': + assert len(multistream_features) == 4 + mgc, lf0, vuv, bap = multistream_features + f0 = np.exp(lf0) + np.savetxt(self.path_mgc, mgc, fmt="%.16f", delimiter=",") + np.savetxt(self.path_f0, f0, fmt="%.16f", delimiter=",") + np.savetxt(self.path_vuv, vuv, fmt="%.16f", delimiter=",") + np.savetxt(self.path_bap, bap, fmt="%.16f", delimiter=",") + elif feature_type == 'melf0': + assert len(multistream_features) == 3 + mgc, lf0, vuv = multistream_features + f0 = np.exp(lf0) + # CSV書き出し + np.savetxt(self.path_mgc, mgc, fmt="%.16f", delimiter=",") + np.savetxt(self.path_f0, f0, fmt="%.16f", delimiter=",") + np.savetxt(self.path_vuv, vuv, fmt="%.16f", delimiter=",") + + # 複数ツールのすべてについて処理実施する + for path_extension in extension_list: + print(f'Editing acoustic features with {path_extension}') + enulib.extensions.run_extension( + path_extension, + ust=self.path_ust, + table=self.path_table, + full_score=self.path_full_score, + mono_score=self.path_mono_score, + full_timing=self.path_full_timing, + mono_timing=self.path_mono_timing, + mgc=self.path_mgc, + f0=self.path_f0, + vuv=self.path_vuv, + bap=self.path_bap + ) + + # 編集が終わったらCSV読み取り + if feature_type == 'world': + mgc = np.loadtxt(self.path_mgc, delimiter=',', dtype=np.float64) + lf0 = np.log(np.loadtxt(self.path_f0, delimiter=',', dtype=np.float64) + ).reshape(-1, 1) + vuv = np.loadtxt(self.path_vuv, delimiter=',', + dtype=np.float64).reshape(-1, 1) + bap = np.loadtxt(self.path_bap, delimiter=',', dtype=np.float64) + # 統合 + multistream_features = (mgc, lf0, vuv, bap) + elif feature_type == 'melf0': + # 編集が終わったらCSV読み取り + mgc = np.loadtxt(self.path_mgc, delimiter=',', dtype=np.float64) + lf0 = np.log(np.loadtxt(self.path_f0, delimiter=',', dtype=np.float64) + ).reshape(-1, 1) + vuv = np.loadtxt(self.path_vuv, delimiter=',', dtype=np.float64 + ).reshape(-1, 1) + # 統合 + multistream_features = (mgc, lf0, vuv) + else: + raise Exception('Unexpected Error') + return multistream_features + def svs( self, labels, @@ -413,8 +496,8 @@ def tqdm(x, **kwargs): # Run acoustic model and vocoder hts_frame_shift = int(self.config.frame_period * 1e4) wavs = [] - self.logger.info( - f"Number of segments: {len(duration_modified_labels_segs)}") + self.logger.info("Number of segments: %s", + len(duration_modified_labels_segs)) for duration_modified_labels_seg in tqdm( duration_modified_labels_segs, desc="[segment]", @@ -444,6 +527,12 @@ def tqdm(x, **kwargs): f0_shift_in_cent=-style_shift * 100, ) + # NOTE: ここにピッチ補正のための割り込み処理を追加----------- + multistream_features = self.edit_acoustic( + multistream_features, + feature_type=self.feature_type + ) + # Generate waveform by vocoder wav = self.predict_waveform( multistream_features=multistream_features, @@ -464,9 +553,11 @@ def tqdm(x, **kwargs): loudness_norm=loudness_norm, target_loudness=target_loudness, ) + # pylint: disable=W1203 self.logger.info(f"Total time: {time.time() - start_time:.3f} sec") RT = (time.time() - start_time) / (len(wav) / self.sample_rate) self.logger.info(f"Total real-time factor: {RT:.3f}") + # pylint: enable=W1203 return wav, self.sample_rate @@ -475,7 +566,7 @@ def main(path_plugin: str, path_wav: Union[str, None] = None, play_wav=True) -> UTAUプラグインのファイルから音声を生成する """ # USTの形式のファイルでなければエラー - if not path_plugin.endswith('.tmp') or path_plugin.endswith('.ust'): + if not (path_plugin.endswith('.tmp') or path_plugin.endswith('.ust')): raise ValueError('Input file must be UST or TMP(plugin).') # UTAUの一時ファイルに書いてある設定を読み取る logging.info('reading settings in TMP') @@ -542,13 +633,23 @@ def main(path_plugin: str, path_wav: Union[str, None] = None, play_wav=True) -> # モデルを読み取る logging.info('Loading models') - engine = SimpleEnunu( - model_dir, device='cuda' if torch.cuda.is_available() else 'cpu') + engine = ENUNU( + model_dir, + device='cuda' if torch.cuda.is_available() else 'cpu') engine.set_paths(temp_dir=temp_dir, songname=songname) + # NOTE: 後方互換のため + # enuconfigが存在する場合、そこに記載されている拡張機能のパスをconfigに追加する + if exists(join(voice_dir, 'enuconfig.yaml')): + with open(join(voice_dir, 'enuconfig.yaml'), encoding='utf-8') as f: + enuconfig = yaml.safe_load(f) + engine.config['extensions'] = enuconfig.get('extensions') + del enuconfig + # USTを一時フォルダに複製 print(f'{datetime.now()} : copying UST') shutil.copy2(path_plugin, engine.path_ust) + # Tableファイルを一時フォルダに複製 print(f'{datetime.now()} : copying Table') shutil.copy2(find_table(model_dir), engine.path_table) @@ -582,7 +683,7 @@ def main(path_plugin: str, path_wav: Union[str, None] = None, play_wav=True) -> vocoder_type='auto', post_filter_type='gv', force_fix_vuv=True, - segmented_synthesis=True, + segmented_synthesis=False, ) # wav出力のフォーマットを確認する From 503a851ba4caf02d8c912c53abae8f572ce81966 Mon Sep 17 00:00:00 2001 From: oatsu Date: Mon, 23 Sep 2024 22:02:38 +0900 Subject: [PATCH 2/6] Create f0_smoother.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ENUNUのリポジトリからコピー --- extensions/f0_smoother.py | 361 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 extensions/f0_smoother.py diff --git a/extensions/f0_smoother.py b/extensions/f0_smoother.py new file mode 100644 index 0000000..6008fa6 --- /dev/null +++ b/extensions/f0_smoother.py @@ -0,0 +1,361 @@ +#!/usr/bin/env python3 +# Copyright (c) 2022 oatsu +""" +f0の極端に急峻な変化をなめらかにする拡張機能。 +""" +from argparse import ArgumentParser +from copy import copy +from math import cos, log10, pi +from pprint import pprint + +SMOOTHEN_WIDTH = 6 # 3から9くらいが良さそう。 +DETECT_THRESHOLD = 0.6 +IGNORE_THRESHOLD = 0.01 + + +def repair_sudden_zero_f0(f0_list): + """ + 前後がどちらもf0=0ではないのに、急に出現したf0=0な点を修正する。 + >>> repair_sudden_zero_f0([1, 2, 3, 0, 5, 6]) + [1, 2, 3, 4, 5, 6] + """ + newf0_list = copy(f0_list) + for i, f0 in enumerate(f0_list[1:-2], 1): + if all((f0 == 0, f0_list[i-1] != 0, f0_list[i+1] != 0)): + newf0_list[i] = (f0_list[i-1] + f0_list[i+1]) / 2 + return newf0_list + + +def repair_jaggy_f0(f0_list, ignore_threshold): + """明らかにギザギザしているf0を修復する。 + + 周囲の動きと明らかに逆の推移をしている区間を修復する。 + relative_f0 関連でノート境界で起こっている可能性がある。 + """ + newf0_list = copy(f0_list) + indices = [] + + # 不良検出 + for i, _ in enumerate(f0_list[2:-2], 2): + # 計算する区間の両端のf0が無効なときはスキップ + if any((f0_list[i-1] == 0, f0_list[i] == 0, f0_list[i+1] == 0, f0_list[i+2] == 0)): + continue + # 1区間の音程変化 + delta_1 = f0_list[i+1] - f0_list[i] + # 3区間の音程変化 + delta_3 = f0_list[i+2] - f0_list[i-1] + # ゼロ除算しそうなときはスキップ + if delta_3 == 0: + continue + # f0変化が小さいときに誤判定しないようにスキップ + if abs(delta_1) < ignore_threshold: + continue + # 2点間の変化が、その前後の点の変化と逆を向いている場合を検出する。 + if delta_1 * delta_3 < 0: + indices.append(i) + print("f0遷移方向が逆になっている区間: ", indices) + + # 修正すべき区間の周辺の点を直線を引いて(半ば強引に)修復 + for idx in indices: + newf0_list[idx - 1] = \ + 0.75 * newf0_list[idx - 2] + 0.25 * newf0_list[idx + 2] + newf0_list[idx] = \ + 0.5 * newf0_list[idx - 2] + 0.5 * newf0_list[idx + 2] + newf0_list[idx + 1] = \ + 0.25 * newf0_list[idx - 2] + 0.75 * f0_list[idx + 2] + + return newf0_list + + +def get_rapid_f0_change_indices(f0_list: list, detect_threshold: list, ignore_threshold): + """急峻なf0変化を検出する。 + + 1区間での変化量が前後を含めた3区間の変化量の半分を上回る場合、急峻な変化とみなす。 + """ + indices = [] + # f0のリスト内ループ + for i, _ in enumerate(f0_list[1:-2], 1): + # 計算する区間の両端のf0が無効なときはスキップ + if any((f0_list[i-1] == 0, f0_list[i] == 0, f0_list[i+1] == 0, f0_list[i+2] == 0)): + continue + # 1区間の音程変化 + delta_1 = f0_list[i+1] - f0_list[i] + # 3区間の音程変化 + delta_3 = f0_list[i+2] - f0_list[i-1] + # ゼロ除算しそうなときはスキップ + if delta_3 == 0: + continue + # f0変化が小さいときに誤判定しないようにスキップ + if abs(delta_1) < ignore_threshold: + continue + # 一定以上の急峻さで検出 + if delta_1 / delta_3 > detect_threshold: + indices.append(i) + return indices + + # def get_rapid_f0_change_indices(f0_list: list, detect_threshold: list, ignore_threshold, width=SMOOTHEN_WIDTH): + # """急峻なf0変化を検出する。 + + # 1区間での変化量が前後を含めた3区間の変化量の半分を上回る場合、急峻な変化とみなす。 + # 3区間ではなく任意の区間で設定できるようにした。 + # """ + # indices = [] + # # f0のリスト内ループ + # for i, _ in enumerate(f0_list[width:-(width+1)], width): + # # 計算する区間の中に休符があるときはスキップ + # if any((f0_list[i-1] == 0, f0_list[i] == 0, f0_list[i+width] == 0, f0_list[i+width] == 0)): + # continue + # # 1区間の音程変化 + # delta_1 = f0_list[i+1] - f0_list[i] + # # 3区間の音程変化 + # delta_wide = f0_list[i+width+1] - f0_list[i-width] + # # ゼロ除算しそうなときはスキップ + # if delta_wide == 0: + # continue + # # f0変化が小さいときに誤判定しないようにスキップ + # if abs(delta_1) < ignore_threshold: + # continue + # # 一定以上の急峻さで検出 + # if delta_1 / delta_wide > detect_threshold: + # indices.append(i) + # return indices + + +def reduce_indices(indices): + """時間的に近い2点で急速なf0変化が検出された場合、両方補正すると変になるので削減する。 + + # 連続して検出された場合 + >>> reduce_indices([10, 11]) + [10] + + # 1つだけ間をあけて検出された場合 + >>> reduce_indices([10, 12]) + [11] + >>> reduce_indices([10, 12, 16]) + [11, 16] + + >>> reduce_indices([10, 13]) + [11] + + # 2回連続で処理する場合(1) + >>> reduce_indices([10, 11, 12]) + [11] + >>> reduce_indices([10, 12, 14]) + [12] + >>> reduce_indices([10, 13, 14]) + [12] + >>> reduce_indices([10, 11, 13]) + [11] + >>> reduce_indices([10, 12, 13]) + [12] + + """ + indices = copy(indices) + + for i, _ in enumerate(indices[:-1]): + delta = indices[i+1] - indices[i] + if delta == 1: + indices[i] = None + indices[i + 1] = indices[i + 1] - 1 + elif delta == 2: + indices[i] = None + indices[i + 1] = indices[i + 1] - 1 + elif delta == 3: + indices[i] = None + indices[i + 1] = indices[i + 1] - 2 + else: + pass + indices = [idx for idx in indices if idx is not None] + + return indices + + +def get_adjusted_widths(f0_list: list, rapid_f0_change_indices: list, default_width: int): + """基準値を計算するための値に0が含まれてしまうときに幅を狭くして返す。 + + 返すリストは 0 以上の整数からなるリストで、 + 0の時は補正を行わないことになるのでスキップしていいと思う。 + """ + # 万が一負の値が入ったていたら止める + assert default_width >= 0 + + # 結果を格納するリスト + adjusted_widths = [] + len_f0_list = len(f0_list) + + # 指定されたf0の点の周辺を順に調べる + for f0_idx in rapid_f0_change_indices: + # 急峻な変化をする2点の前後を近い順に調査 + width = default_width + # そもそも両端がIndexErrorになってしまうのを回避する必要がある。 + # f0の長さが足りない場合は補正幅を狭める。 + while (f0_idx - width) < 0 or (f0_idx + width + 1) > len_f0_list: + width -= 1 + # 両端のf0が0な場合は、平滑化の幅を狭める。 + # ただし、wが負になって右側と左側のf0の位置が逆転する前にループを止める。 + # while width > 0 and (f0_list[f0_idx - width] == 0 or f0_list[f0_idx + width + 1] == 0): + while width > 0 and (0 in f0_list[f0_idx - width: f0_idx + width + 2]): + width -= 1 + # 調整後の値をリストに追加 + adjusted_widths.append(width) + + # 一応長さ確認 + assert len(adjusted_widths) == len(rapid_f0_change_indices) + + # 調整した幅の一覧をリストにして返す + return adjusted_widths + + +def get_target_f0_list(f0_list: list, rapid_f0_change_indices: list, adjusted_widths: list): + """補正に用いる基準値(平均値)を計算する。 + + 中心となる2点からwidth分だけ前後の両端の点の平均値を、補正に用いる値とする。 + その値をリストにして返す。 + """ + # 念のため + assert len(rapid_f0_change_indices) == len(adjusted_widths) + + # 補正の基準にするf0の値 + target_f0_list = [] + + # 急峻な変化がある場所について、前から順に平均値を計算していく。 + for f0_idx, width in zip(rapid_f0_change_indices, adjusted_widths): + f0_left = f0_list[f0_idx - width] + f0_right = f0_list[f0_idx + width + 1] + target_f0 = (f0_left + f0_right) / 2 + target_f0_list.append(target_f0) + + # こんな感じのリストを返す → [(f0_idx, width), ...] + return target_f0_list + + +def get_smoothened_f0_list(f0_list, width, detect_threshold, ignore_threshold): + """修正すべき区間と、修正の基準として使う値を返す。 + [(idx, target_f0), ...] + + """ + # もとのf0を残すために複製して使う。 + f0_list = copy(f0_list) + + # 補正したほうがいい場所を検出する。 + rapid_f0_change_indices = get_rapid_f0_change_indices( + f0_list, + detect_threshold, + ignore_threshold + ) + # rapid_f0_change_indices = reduce_indices(rapid_f0_change_indices) + + # 不具合が起きないように補正幅を調整 + adjusted_widths = get_adjusted_widths( + f0_list, + rapid_f0_change_indices, + width + ) + assert len(rapid_f0_change_indices) == len(adjusted_widths) + + # 該当箇所の9区間の最初と最後の平均 (元の長さ: N-9, 追加後長さ: N-1) + # ・-・-・-・-・=・-・-・-・-・ + target_f0_list = get_target_f0_list( + f0_list, + rapid_f0_change_indices, + adjusted_widths + ) + assert len(rapid_f0_change_indices) == len(target_f0_list) + + # 動作内容確認用に出力 + # pprint(list(zip(rapid_f0_change_indices, adjusted_widths, target_f0_list))) + + # 検出済みの場所と、その周辺に適用するターゲット値の組でループする + for (f0_idx, width, target_f0) in zip(rapid_f0_change_indices, adjusted_widths, target_f0_list): + # 調整不要(不可能)な場合はスキップ + if width <= 0: + continue + # 修正必要なf0点の前後数点を、近くから順に補正する。 + for i in range(width): + # 元の値をどのくらい使うか + ratio_of_original_f0 = cos( + pi * ((width - i) / (2 * width + 1))) + # ターゲット値にどのくらい寄せるか + ratio_of_target_f0 = 1 - ratio_of_original_f0 + # 過去側のf0の点を補正する + f0_list[f0_idx - i] = ( + ratio_of_target_f0 * target_f0 + + ratio_of_original_f0 * f0_list[f0_idx - i] + ) + # 未来側のf0の点を補正する + f0_list[f0_idx + i + 1] = ( + ratio_of_target_f0 * target_f0 + + ratio_of_original_f0 * f0_list[f0_idx + i + 1] + ) + + print(f'Smoothed {len(rapid_f0_change_indices)} points') + # [(idx, target_f0), ...] の形にして返す + return f0_list + + +def main(): + """全体時の処理をやる + """ + parser = ArgumentParser() + parser.add_argument('--f0', help='f0の情報を持ったCSVファイルのパス') + + # 使わない引数は無視して、必要な情報だけ取り出す。 + args, _ = parser.parse_known_args() + + # f0ファイルの入出力パス + # ENUNUからの呼び出しがうまくいっていないか、テスト実行の場合 + if args.f0 is None: + path_in = input('path: ').strip('\'\"') + path_out = path_in.replace('.csv', '_out.csv') + # ENUNUから呼び出しているとき + else: + path_in = str(args.f0).strip('\'"') + path_out = path_in + + # f0のファイルを読み取る + with open(path_in, 'r', encoding='utf-8') as f: + f0_list = list(map(float, f.read().splitlines())) + + # 底を10とした対数に変換する (長さ: N) + # f0が負や0だと対数変換できないのを回避しつつ、log(f0)>0 となるようにする。 + log_f0_list = [log10(max(f0, 1)) for f0 in f0_list] + + # 突発的な0Hzを直す。 + print('Repairing unnaturally sudden 0Hz in f0') + log_f0_list = repair_sudden_zero_f0(log_f0_list) + + # ギザギザしてるのを直す + # log_f0_list = repair_jaggy_f0( + # log_f0_list, ignore_threshold=IGNORE_THRESHOLD) + + # なめらかにする + print('Smoothening f0') + new_log_f0_list = get_smoothened_f0_list( + log_f0_list, + width=SMOOTHEN_WIDTH, + detect_threshold=DETECT_THRESHOLD, + ignore_threshold=IGNORE_THRESHOLD + ) + + # log(f0) でエラーが出ないためにf0=1Hzにしてあるのを0Hzに戻す。 + new_f0_list = [] + for log_f0 in new_log_f0_list: + # log10(f0)=0 のときに f0=1Hz ではなく 0Hz にする。 + if log_f0 == 0: + f0 = 0 + else: + f0 = 10 ** log_f0 + new_f0_list.append(f0) + + # 文字列にする + s = '\n'.join(list(map(str, new_f0_list))) + + # 出力 + with open(path_out, 'w', encoding='utf-8') as f: + f.write(s) + + +if __name__ == "__main__": + print('f0_smoother.py (2022-04-24) ---------------------------') + main() + print('-------------------------------------------------------') From 3381de616ced2fe4d118bf4dbd48902162536c85 Mon Sep 17 00:00:00 2001 From: oatsu Date: Mon, 23 Sep 2024 22:02:42 +0900 Subject: [PATCH 3/6] Create style_shifter.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ENUNUのリポジトリからコピー --- extensions/style_shifter.py | 141 ++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 extensions/style_shifter.py diff --git a/extensions/style_shifter.py b/extensions/style_shifter.py new file mode 100644 index 0000000..9fef8ea --- /dev/null +++ b/extensions/style_shifter.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# Copyright (c) 2022 oatsu +""" +USTの音高をずらして読み込んで、合成時にf0を本体の高さに戻すことで歌い癖をずらす。 + +1個のファイルでUST編集とf0編集ができるようにしたい。 +UST読み込んで加工する際に、各ノートに独自エントリを書き込む。 +USTにの [#SETTING] にすでに独自エントリがある場合はf0ファイルを編集する。 +""" +import re +from argparse import ArgumentParser +from copy import copy +from math import log2 +from pprint import pprint + +import utaupy + +STYLE_SHIFT_FLAG_PATTERN = re.compile(r'S(\d+|\+\d+|-\d+)') + + +def shift_ust_notes(ust) -> utaupy.ust.Ust: + """フラグに基づいてUST内のノート番号をずらし、その分を独自エントリに追加する。 + """ + ust = copy(ust) + key = '$EnunuStyleShift' + ust.setting[key] = True + for note in ust.notes: + # フラグ内のスタイルシフトのパラメータを取得する + style_shift = re.search(STYLE_SHIFT_FLAG_PATTERN, note.flags) + # フラグにスタイルシフトのパラメータがあるとき + if style_shift is not None: + # フルラベルにするときの不具合の原因にならないように、フラグのスタイルシフト部分を削除する。 + note.flags = note.flags.replace(style_shift.group(), '') + # 数値部分を取り出す + style_shift_amount = int(style_shift.group(1)) + # スタイルシフト設定値の分だけノートの音高を下げる。 + note.notenum += int(style_shift_amount) + # フラグのスタイルシフト値を独自エントリとしてノートに登録する。 + note[key] = '{:+}'.format(style_shift_amount) + else: + note[key] = 0 + + return ust + + +def shift_f0(ust, full_timing, f0_list: list) -> list: + """f0をいい感じに編集する + """ + ust_notes = ust.notes + hts_notes = full_timing.song.all_notes + # ノート数が一致することを確認しておく + if len(ust_notes) != len(hts_notes): + raise ValueError( + f'USTのノート数({len(ust_notes)}) と フルラベルのノート数({len(hts_notes)}) が一致していません。') + + # 各ノートのf0開始スライスと終了スライス + f0_point_slices = [ + (round(note.start / 50000), round(note.end / 50000)) for note in hts_notes] + + # スタイルシフトの量をUSTのノートから取り出してリストにする + style_shift_list = [ + int(note.get('$EnunuStyleShift', 0)) for note in ust_notes] + + # 計算しやすいように対数に変換 + log2_f0_list = [log2(hz) if hz > 0 else 0 for hz in f0_list] + + # f0のリストをノートごとに区切って2次元にする + log2_f0_list_2d = [ + log2_f0_list[slice_start: slice_end] for (slice_start, slice_end) in f0_point_slices + ] + + # ノート区切りごとにf0を調製して、新しいf0のリストを作る + # このとき一番最初の開始時刻が0出ない時にf0点数が合わなくなるのを回避する。 + offset = round(hts_notes[0].start / 50000) + new_log2_f0_list = log2_f0_list[0:offset] + for f0_list_for_note, shift_amount in zip(log2_f0_list_2d, style_shift_list): + delta_log2_f0 = shift_amount / (-12) + new_log2_f0_list += [f0 + delta_log2_f0 if f0 > + 0 else 0 for f0 in f0_list_for_note] + # 書き換えたやつ対数から元に戻す + new_f0_list = [ + (2 ** log2_f0 if log2_f0 > 0 else 0) for log2_f0 in new_log2_f0_list] + return new_f0_list + + +def switch_mode(ust) -> str: + """どのタイミングで起動されたかを、USTから調べて動作モードを切り替える。 + """ + if '$EnunuStyleShift' in ust.setting: + return 'f0_editor' + return 'ust_editor' + + +def main(): + parser = ArgumentParser() + parser.add_argument('--ust', help='選択部分のノートのUSTファイルのパス') + parser.add_argument('--f0', help='f0の情報を持ったCSVファイルのパス') + parser.add_argument('--full_timing', help='タイミング推定済みのフルラベルファイルのパス') + + # 使わない引数は無視して、必要な情報だけ取り出す。 + args, _ = parser.parse_known_args() + path_ust = args.ust + + ust = utaupy.ust.load(path_ust) + + # ust_editor として起動されたか、acoustic_editor として起動されたかを取得して動作切り替える + mode = switch_mode(ust) + + # ust編集のステップで実行された場合、ustの音高操作などをする。 + if mode == 'ust_editor': + print('USTの音高を加工します。/ Shifting notes in UST.') + ust = shift_ust_notes(ust) + ust.write(path_ust) + print('USTの音高を加工しました。/ Shifted notes in UST.') + + # f0加工用に呼び出された場合、f0加工をする。 + elif mode == 'f0_editor': + print('f0を加工します。/ Shifting f0.') + # f0のファイルを読み取る + path_f0 = args.f0 + with open(path_f0, 'r', encoding='utf-8') as f: + f0_list = list(map(float, f.read().splitlines())) + # フルラベルファイルを読み取る + full_timing = utaupy.hts.load(args.full_timing) + # f0を編集する + new_f0_list = shift_f0(ust, full_timing, f0_list) + new_f0_list = list(map(str, new_f0_list)) + s_f0 = '\n'.join(new_f0_list) + '\n' + with open(path_f0, 'w', encoding='utf-8') as f: + f.write(s_f0) + print('f0を加工しました。/ Shifted f0.') + + # それ以外 + else: + raise Exception('動作モードを判別できませんでした。') + + +if __name__ == "__main__": + print('style_shifter.py (2022-09-24) -------------------------') + main() + print('-------------------------------------------------------') From 294d938b0ef23a3ec3205f42a7fe8829a567cd2e Mon Sep 17 00:00:00 2001 From: oatsu Date: Tue, 24 Sep 2024 00:39:15 +0900 Subject: [PATCH 4/6] =?UTF-8?q?=E6=8B=A1=E5=BC=B5=E6=A9=9F=E8=83=BD?= =?UTF-8?q?=E3=81=AB=20path=5Ffeedback=20=E5=BC=95=E6=95=B0=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HISTORY.md | 19 +++ extensions/f0_feedbacker.py | 253 ++++++++++++++++++++++++++++++++++++ simple_enunu.py | 16 ++- 3 files changed, 284 insertions(+), 4 deletions(-) create mode 100644 extensions/f0_feedbacker.py diff --git a/HISTORY.md b/HISTORY.md index a49e81b..51330c2 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -68,3 +68,22 @@ extensions: - PyTorch のインストールに失敗する不具合を修正。 - 「名前を付けて保存」のタイミングを音声合成前ではなく音声合成後に変更。 - 保存わすれで時間を無駄にするのを防ぐため。 + +## v0.4.0 + +- 拡張機能機能実行時に、UTAUにフィードバックするためのTMPファイルのパスを `--feedback` の引数で渡すようにした。 +- ピッチ加工ツールやスタイルシフトツールなどの拡張機能を指定する、`acoustic_editor` を使えるようにした。 + +```yaml +# sample of config.yaml to activate extensions +extensions: + ust_editor: + - "%e/extensions/voicecolor_applier/voicecolor_applier.py" + - "%e/extensions/voicecolor_applier/lyric_nyaizer.py" + - "%e/extensions/style_shifter.py" + timing_editor: + - "%e/extensions/velocity_applier.py" + acoustic_editor: + - "%e/extensions/style_shifter.py" + - "%e/extensions/f0_smoother.py" +``` diff --git a/extensions/f0_feedbacker.py b/extensions/f0_feedbacker.py new file mode 100644 index 0000000..4440447 --- /dev/null +++ b/extensions/f0_feedbacker.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 +# Copyright (c) 2022 oatsu +""" +ENUNUで合成したf0をUTAUのピッチ曲線としてフィードバックする。 +""" +from argparse import ArgumentParser +from math import log2 + +import numpy as np +import utaupy +from scipy.signal import argrelmax, argrelmin + +FRAME_PERIOD = 5 # ms +F0_FLOOR = 32 + + +def load_f0(path_f0, frame_period=FRAME_PERIOD): + """f0のファイルを読み取って、周波数と時刻(ms)の一覧を返す。 + """ + with open(path_f0, 'r', encoding='utf-8') as f: + freq_list = list(map(float, f.read().splitlines())) + time_list = [i*frame_period for i in range(len(freq_list))] + return freq_list, time_list + + +def distribute_f0(freq_list, time_list, ust): + """周波数とその時刻の情報をノートごとに分割する。 + """ + # 要素数が一致していることを確認しておく。 + assert len(freq_list) == len(time_list) + len_f0 = len(freq_list) + + # ループ時にノートの終了時刻を記憶するための変数 + t_note_end = 0 + # ループ時にf0のインデックスを記憶するための変数 + idx_f0 = 0 + # 各ノートごとに分割されたf0とその時刻を保持するためのリスト。 + f0_freq_for_each_note = [] + f0_time_for_each_note = [] + + # ノートごとにループする。 + # 最後のf0点が使用されない可能性があることに注意。 + for note in ust.notes: + t_note_end += note.length_ms + temp_f0_freq = [] + temp_f0_time = [] + # f0の時刻を前から順番に調べて、ノート内だったら一時リストに追加 + while time_list[idx_f0] < t_note_end and idx_f0 < len_f0: + temp_f0_freq.append(freq_list[idx_f0]) + temp_f0_time.append(time_list[idx_f0]) + idx_f0 += 1 + # 最後の点を処理したらループを抜ける + if idx_f0 == len_f0: + break + # 最後の点を次のノートにも重複して追加するために、インデックスを一つ下げる + idx_f0 -= 1 + # 現在のノートに対するf0とその時刻のリストを、全体のリストに追加 + f0_freq_for_each_note.append(temp_f0_freq) + f0_time_for_each_note.append(temp_f0_time) + + return f0_freq_for_each_note, f0_time_for_each_note + + +def reduce_f0_points_for_a_note(f0_list, time_list): + """ノート内のf0点を削減する。 + + UTAUのGUI上に表示できるピッチ密度には限界があるため、 + 各ノートの以下のピッチ点になるものだけ残す。 + - ノート内で最初の点 + - ノート内で最後の点 + - 極大値と極小値と変曲点 + """ + # 点数が一致することを確認しておく + assert len(f0_list) == len(time_list) + + # 1階微分 + delta_f0_freq = [0] # 最初の点は勾配を計算できないので0 + delta_f0_freq += [ + next_freq - prev_freq for next_freq, prev_freq + in zip(f0_list[:-1], f0_list[1:]) + ] + delta_f0_freq += [0] # 最後の点も勾配を計算できないので0 + + # 極値のindexを取り出す + extremum_f0_indices = \ + list(argrelmax(np.array(f0_list))[0]) + \ + list(argrelmin(np.array(f0_list))[0]) + # 最初と最後と極値のindex (残すf0点のみ) + reduced_f0_indices = [0] + extremum_f0_indices + [len(f0_list) - 1] + + # 変曲点を使う場合↓------------------------------------ + # # 変曲点のindexを取り出す + # inflection_f0_indices = \ + # list(argrelmax(np.array(delta_f0_freq))[0]) + \ + # list(argrelmin(np.array(delta_f0_freq))[0]) + # inflection_f0_indices = [] # NOTE:極値を無視 + + # # 最初と最後と極値と変曲点のindex (残すf0点のみ) + # reduced_f0_indices = \ + # [0] + extremum_f0_indices + inflection_f0_indices + [len(f0_list) - 1] + # ------------------------------------------------------- + + # 重複する要素を削除 + reduced_f0_indices = list(set(reduced_f0_indices)) + # 順番がめちゃくちゃなので並べなおす + reduced_f0_indices.sort() + + # 残したいf0の周波数 + l_reduced_f0_freq = [f0_list[i] for i in reduced_f0_indices] + # 残したいf0の時刻 + l_reduced_f0_time = [time_list[i] for i in reduced_f0_indices] + + return l_reduced_f0_freq, l_reduced_f0_time + + +def notenum2hz(notenum: int, concert_pitch=440) -> float: + """UTAUの音階番号を周波数に変換する + """ + return concert_pitch * (2 ** ((notenum - 69) / 12)) + + +def hz2cent(freq: float, notenum: int): + """f0の周波数をUST用のPBY用の数値に変換する + """ + base_hz = notenum2hz(notenum) + if freq == 0: + cent = 0 + else: + cent = 120.0 * (log2(freq) - log2(base_hz)) + return cent + + +def note_times_ms(ust): + """ノートの開始時刻(ms)と終了時刻(ms)のリストを返す。 + [[start, end], ...] + """ + t_start = 0 # ノート開始時刻 + t_end = 0 # ノート終了時刻 + l_start_end = [] # 開始時刻と終了時刻のリスト + + # 各ノートの長さから、開始時刻と終了時刻を計算する + for note in ust.notes: + t_end += note.length_ms + l_start_end.append([t_start, t_end]) + + # リストを返す + return l_start_end + + +def repair_pitch_fall_near_restnote(ust): + """休符前で音程が低くなるのを直す。 + 休符直前のノートの最後のピッチ点のPBYと、 + 休符の最初のピッチ点のPBYを0にする + """ + for note_now, note_next in zip(ust.notes[:-1], ust.notes[1:]): + if 'R' in note_next.lyric: + note_now.pby = \ + note_now.pby[:-2] + [max(0, note_now.pby[-2]), 0] + note_next.pby = \ + [max(0, note_next.pby[0])] + note_next.pby[1:] + + +def round_pitches(plugin): + """音高を丸める。半音(1ノートぶん)で丸める。 + """ + for note in plugin.notes: + note.pbs = [note.pbs[0], round(note.pbs[1] / 10) * 10] + note.pby = [round(x/10) * 10 for x in note.pby] + + +def main(path_f0, path_plugin): + """Test + """ + # USTファイルを読み取る + # path_ust = input('USTファイルを指定してください: ').strip('"') + # ust = utaupy.ust.load(path_ust) + ust = utaupy.utauplugin.load(path_plugin) + + # f0ファイルを読み取る + # path_f0 = input('f0ファイルを指定してください: ').strip('"') + freq_list, time_list = load_f0(path_f0) + + # ノートごとになるようにf0を分割して2次元リストにする + print('ピッチ点をノートごとに分割します。') + freq_list_2d, time_list_2d = distribute_f0(freq_list, time_list, ust) + + # 削減後のリストとか + reduced_freq_list_2d = [] + reduced_time_list_2d = [] + + # 各ノートのf0点を削減する + print('ピッチ点を削減します。') + for freq_list_for_a_note, time_list_for_a_note in zip(freq_list_2d, time_list_2d): + l_freq, l_time = reduce_f0_points_for_a_note( + freq_list_for_a_note, time_list_for_a_note + ) + reduced_freq_list_2d.append(l_freq) + reduced_time_list_2d.append(l_time) + + # 各ノートのピッチ点を登録する + assert len(ust.notes) == len( + reduced_freq_list_2d) == len(reduced_time_list_2d) + print('各ノートにPBYとPBWとPBMを登録します。') + for note, l_freq, l_time in zip(ust.notes, reduced_freq_list_2d, reduced_time_list_2d): + notenum = note.notenum + # PBSを仮登録 + note.pbs = [0, 0] + # 相対音高(cent)を計算してPBYを登録 + note.pby = [hz2cent(freq, notenum) for freq in l_freq] + [0] + # 時刻を計算してPBWを登録 + note.pbw = [0] + [t_next - t_now for t_now, t_next + in zip(l_time[:-1], l_time[1:])] + [0] + # 全てS字で登録 + note.pbm = [''] * (len(l_freq) + 1) + + # PBSを計算して適切に登録しなおす + print('各ノートにPBYとPBWとPBMを登録します。') + for note, note_prev in zip(ust.notes[1:], ust.notes[:-1]): + # 直前のノートのピッチ点が終わる時刻と 直前のノートが終わる時刻の差が + # 今のノートのPBS + offset = (note_prev.pbs[0] + sum(note_prev.pbw)) - note_prev.length_ms + note.pbs = [offset, 0] + + # 休符前で音程が下がるのを修正 + repair_pitch_fall_near_restnote(ust) + + # ピッチ点を丸める + round_pitches(ust) + + # 休符からピッチ点を削除する + print('休符のピッチを削除します') + for note in ust.notes: + if 'R' in note.lyric: + note.pbw = [] + note.pbm = [] + note.pby = [] + + # ファイル出力 + print('完了しました。上書き保存します。') + # path_ust_out = path_ust.replace('.ust', '_out.ust') + # ust.setting['Mode2'] = True + # ust.write(path_ust_out) + ust.write(path_plugin) + + +if __name__ == '__main__': + parser = ArgumentParser() + parser.add_argument('--f0', help='f0.csvのパス') + parser.add_argument('--feedback', help='UTAUにフィードバックするために上書きするtmpファイル') + # 使わない引数は無視 + args, _ = parser.parse_known_args() + # 実行引数を渡して処理 + main(args.f0, args.feedback) diff --git a/simple_enunu.py b/simple_enunu.py index 676512f..c411338 100755 --- a/simple_enunu.py +++ b/simple_enunu.py @@ -203,9 +203,10 @@ def __init__(self, self.path_f0 = None self.path_vuv = None self.path_bap = None + self.path_feedback = None # self.path_wav = None - def set_paths(self, temp_dir, songname): + def set_paths(self, temp_dir, songname, path_feedback=None): """ファイル入出力のPATHを設定する """ self.path_ust = join(temp_dir, f'{songname}_temp.ust') @@ -218,6 +219,8 @@ def set_paths(self, temp_dir, songname): self.path_f0 = join(temp_dir, f'{songname}_acoustic_f0.csv') self.path_vuv = join(temp_dir, f'{songname}_acoustic_vuv.csv') self.path_bap = join(temp_dir, f'{songname}_acoustic_bap.csv') + if path_feedback is not None: + self.path_feedback = path_feedback def get_extension_path_list(self, key) -> List[str]: """ @@ -263,7 +266,8 @@ def edit_ust(self, ust: utaupy.ust.Ust, key='ust_editor') -> utaupy.ust.Ust: enulib.extensions.run_extension( path_extension, ust=self.path_ust, - table=self.path_table + table=self.path_table, + feedback=self.path_feedback ) # 編集後のustファイルを読み取る ust = utaupy.ust.load(self.path_ust) @@ -285,6 +289,7 @@ def edit_score(self, score_labels, key='score_editor'): path_extension, ust=self.path_ust, table=self.path_table, + feedback=self.path_feedback, full_score=self.path_full_score ) score_labels = hts.load(self.path_full_score).round_() @@ -310,6 +315,7 @@ def edit_timing(self, duration_modified_labels, key='timing_editor'): path_extension, ust=self.path_ust, table=self.path_table, + feedback=self.path_feedback, full_score=self.path_full_score, mono_score=self.path_mono_score, full_timing=self.path_full_timing, @@ -374,6 +380,7 @@ def edit_acoustic(self, multistream_features, feature_type, key='acoustic_editor path_extension, ust=self.path_ust, table=self.path_table, + feedback=self.path_feedback, full_score=self.path_full_score, mono_score=self.path_mono_score, full_timing=self.path_full_timing, @@ -636,7 +643,8 @@ def main(path_plugin: str, path_wav: Union[str, None] = None, play_wav=True) -> engine = ENUNU( model_dir, device='cuda' if torch.cuda.is_available() else 'cpu') - engine.set_paths(temp_dir=temp_dir, songname=songname) + engine.set_paths(temp_dir=temp_dir, songname=songname, + path_feedback=path_plugin) # NOTE: 後方互換のため # enuconfigが存在する場合、そこに記載されている拡張機能のパスをconfigに追加する @@ -683,7 +691,7 @@ def main(path_plugin: str, path_wav: Union[str, None] = None, play_wav=True) -> vocoder_type='auto', post_filter_type='gv', force_fix_vuv=True, - segmented_synthesis=False, + segmented_synthesis=True ) # wav出力のフォーマットを確認する From 26818eda3b182f8420f78c90fafa6ba1847706a2 Mon Sep 17 00:00:00 2001 From: oatsu Date: Tue, 24 Sep 2024 00:44:48 +0900 Subject: [PATCH 5/6] Update README.md --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 41e7931..ae10c0c 100644 --- a/README.md +++ b/README.md @@ -34,16 +34,24 @@ extensions: - voicecolor_applier (ust_editor) - `あ強` などの表情サフィックスを使用可能にします。(例:`強` が含まれる場合は `Power` をフラグ欄に追記します。) - - dummy (-) - とくに何もしません。デバッグ用です。 +- f0_feedbacker (acoustic_editor) + - ENUNUモデルで合成したピッチ線を UST のピッチにフィードバックします。EnuPitch のようなことができます。 + +- f0_smoother (acoustic_editor) + - 急峻なピッチ変化を滑らかにします。 - lyric_nyaizer (ust_editor) - 歌詞を `ny a` にします。主にデバッグ用です。 +- score_myaizer (score_editor) + - 歌詞を `my a` にします。主にデバッグ用です。 + +- style_shifter (ust_editor, acoustic_editor) + - USTのフラグ に `S5` や `S-3` のように記入することで、スタイルシフトのようなことができます。 - timing_repairer (timing_editor) - ラベル内の音素の発声時間に不具合がある場合に自動修正を試みます。 - - velocity_applier (timing_editor) - USTの子音速度をもとに子音の長さを調節します。 From 9c932f13289c58da6e4e0a652aaa0b401276c3ac Mon Sep 17 00:00:00 2001 From: oatsu Date: Tue, 24 Sep 2024 00:48:04 +0900 Subject: [PATCH 6/6] Update HISTORY.md --- HISTORY.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 51330c2..09776d0 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -73,6 +73,9 @@ extensions: - 拡張機能機能実行時に、UTAUにフィードバックするためのTMPファイルのパスを `--feedback` の引数で渡すようにした。 - ピッチ加工ツールやスタイルシフトツールなどの拡張機能を指定する、`acoustic_editor` を使えるようにした。 + - 拡張機能 f0_smoother を追加 (ENUNUからのコピー) + - 拡張機能 f0_feedbacker を追加 (EnuPitchからコピーしてすこし改良) + - 拡張機能 style_shifter を追加 (ENUNUからのコピー) ```yaml # sample of config.yaml to activate extensions