|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# Copyright (c) 2022 oatsu |
| 3 | +""" |
| 4 | +USTの音高をずらして読み込んで、合成時にf0を本体の高さに戻すことで歌い癖をずらす。 |
| 5 | +
|
| 6 | +1個のファイルでUST編集とf0編集ができるようにしたい。 |
| 7 | +UST読み込んで加工する際に、各ノートに独自エントリを書き込む。 |
| 8 | +USTにの [#SETTING] にすでに独自エントリがある場合はf0ファイルを編集する。 |
| 9 | +""" |
| 10 | +import re |
| 11 | +from argparse import ArgumentParser |
| 12 | +from copy import copy |
| 13 | +from math import log2 |
| 14 | +from pprint import pprint |
| 15 | + |
| 16 | +import utaupy |
| 17 | + |
| 18 | +STYLE_SHIFT_FLAG_PATTERN = re.compile(r'S(\d+|\+\d+|-\d+)') |
| 19 | + |
| 20 | + |
| 21 | +def shift_ust_notes(ust) -> utaupy.ust.Ust: |
| 22 | + """フラグに基づいてUST内のノート番号をずらし、その分を独自エントリに追加する。 |
| 23 | + """ |
| 24 | + ust = copy(ust) |
| 25 | + key = '$EnunuStyleShift' |
| 26 | + ust.setting[key] = True |
| 27 | + for note in ust.notes: |
| 28 | + # フラグ内のスタイルシフトのパラメータを取得する |
| 29 | + style_shift = re.search(STYLE_SHIFT_FLAG_PATTERN, note.flags) |
| 30 | + # フラグにスタイルシフトのパラメータがあるとき |
| 31 | + if style_shift is not None: |
| 32 | + # フルラベルにするときの不具合の原因にならないように、フラグのスタイルシフト部分を削除する。 |
| 33 | + note.flags = note.flags.replace(style_shift.group(), '') |
| 34 | + # 数値部分を取り出す |
| 35 | + style_shift_amount = int(style_shift.group(1)) |
| 36 | + # スタイルシフト設定値の分だけノートの音高を下げる。 |
| 37 | + note.notenum += int(style_shift_amount) |
| 38 | + # フラグのスタイルシフト値を独自エントリとしてノートに登録する。 |
| 39 | + note[key] = '{:+}'.format(style_shift_amount) |
| 40 | + else: |
| 41 | + note[key] = 0 |
| 42 | + |
| 43 | + return ust |
| 44 | + |
| 45 | + |
| 46 | +def shift_f0(ust, full_timing, f0_list: list) -> list: |
| 47 | + """f0をいい感じに編集する |
| 48 | + """ |
| 49 | + ust_notes = ust.notes |
| 50 | + hts_notes = full_timing.song.all_notes |
| 51 | + # ノート数が一致することを確認しておく |
| 52 | + if len(ust_notes) != len(hts_notes): |
| 53 | + raise ValueError( |
| 54 | + f'USTのノート数({len(ust_notes)}) と フルラベルのノート数({len(hts_notes)}) が一致していません。') |
| 55 | + |
| 56 | + # 各ノートのf0開始スライスと終了スライス |
| 57 | + f0_point_slices = [ |
| 58 | + (round(note.start / 50000), round(note.end / 50000)) for note in hts_notes] |
| 59 | + |
| 60 | + # スタイルシフトの量をUSTのノートから取り出してリストにする |
| 61 | + style_shift_list = [ |
| 62 | + int(note.get('$EnunuStyleShift', 0)) for note in ust_notes] |
| 63 | + |
| 64 | + # 計算しやすいように対数に変換 |
| 65 | + log2_f0_list = [log2(hz) if hz > 0 else 0 for hz in f0_list] |
| 66 | + |
| 67 | + # f0のリストをノートごとに区切って2次元にする |
| 68 | + log2_f0_list_2d = [ |
| 69 | + log2_f0_list[slice_start: slice_end] for (slice_start, slice_end) in f0_point_slices |
| 70 | + ] |
| 71 | + |
| 72 | + # ノート区切りごとにf0を調製して、新しいf0のリストを作る |
| 73 | + # このとき一番最初の開始時刻が0出ない時にf0点数が合わなくなるのを回避する。 |
| 74 | + offset = round(hts_notes[0].start / 50000) |
| 75 | + new_log2_f0_list = log2_f0_list[0:offset] |
| 76 | + for f0_list_for_note, shift_amount in zip(log2_f0_list_2d, style_shift_list): |
| 77 | + delta_log2_f0 = shift_amount / (-12) |
| 78 | + new_log2_f0_list += [f0 + delta_log2_f0 if f0 > |
| 79 | + 0 else 0 for f0 in f0_list_for_note] |
| 80 | + # 書き換えたやつ対数から元に戻す |
| 81 | + new_f0_list = [ |
| 82 | + (2 ** log2_f0 if log2_f0 > 0 else 0) for log2_f0 in new_log2_f0_list] |
| 83 | + return new_f0_list |
| 84 | + |
| 85 | + |
| 86 | +def switch_mode(ust) -> str: |
| 87 | + """どのタイミングで起動されたかを、USTから調べて動作モードを切り替える。 |
| 88 | + """ |
| 89 | + if '$EnunuStyleShift' in ust.setting: |
| 90 | + return 'f0_editor' |
| 91 | + return 'ust_editor' |
| 92 | + |
| 93 | + |
| 94 | +def main(): |
| 95 | + parser = ArgumentParser() |
| 96 | + parser.add_argument('--ust', help='選択部分のノートのUSTファイルのパス') |
| 97 | + parser.add_argument('--f0', help='f0の情報を持ったCSVファイルのパス') |
| 98 | + parser.add_argument('--full_timing', help='タイミング推定済みのフルラベルファイルのパス') |
| 99 | + |
| 100 | + # 使わない引数は無視して、必要な情報だけ取り出す。 |
| 101 | + args, _ = parser.parse_known_args() |
| 102 | + path_ust = args.ust |
| 103 | + |
| 104 | + ust = utaupy.ust.load(path_ust) |
| 105 | + |
| 106 | + # ust_editor として起動されたか、acoustic_editor として起動されたかを取得して動作切り替える |
| 107 | + mode = switch_mode(ust) |
| 108 | + |
| 109 | + # ust編集のステップで実行された場合、ustの音高操作などをする。 |
| 110 | + if mode == 'ust_editor': |
| 111 | + print('USTの音高を加工します。/ Shifting notes in UST.') |
| 112 | + ust = shift_ust_notes(ust) |
| 113 | + ust.write(path_ust) |
| 114 | + print('USTの音高を加工しました。/ Shifted notes in UST.') |
| 115 | + |
| 116 | + # f0加工用に呼び出された場合、f0加工をする。 |
| 117 | + elif mode == 'f0_editor': |
| 118 | + print('f0を加工します。/ Shifting f0.') |
| 119 | + # f0のファイルを読み取る |
| 120 | + path_f0 = args.f0 |
| 121 | + with open(path_f0, 'r', encoding='utf-8') as f: |
| 122 | + f0_list = list(map(float, f.read().splitlines())) |
| 123 | + # フルラベルファイルを読み取る |
| 124 | + full_timing = utaupy.hts.load(args.full_timing) |
| 125 | + # f0を編集する |
| 126 | + new_f0_list = shift_f0(ust, full_timing, f0_list) |
| 127 | + new_f0_list = list(map(str, new_f0_list)) |
| 128 | + s_f0 = '\n'.join(new_f0_list) + '\n' |
| 129 | + with open(path_f0, 'w', encoding='utf-8') as f: |
| 130 | + f.write(s_f0) |
| 131 | + print('f0を加工しました。/ Shifted f0.') |
| 132 | + |
| 133 | + # それ以外 |
| 134 | + else: |
| 135 | + raise Exception('動作モードを判別できませんでした。') |
| 136 | + |
| 137 | + |
| 138 | +if __name__ == "__main__": |
| 139 | + print('style_shifter.py (2022-09-24) -------------------------') |
| 140 | + main() |
| 141 | + print('-------------------------------------------------------') |
0 commit comments