Skip to content

Commit cb6ebc0

Browse files
committed
[finish] version 1.2.0
1 parent 7a81c18 commit cb6ebc0

File tree

13 files changed

+470
-409
lines changed

13 files changed

+470
-409
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ dmypy.json
133133

134134
# Others
135135
/automation.py
136-
/test.py
136+
test.py
137137
*.wav
138138
*.mp3
139139
*.mid

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ UI 自动化执行的成功与否受到系统流畅度等客观因素影响。
6464
> - 缩短等待时间,提升运行速度
6565
> - Demo - 为同一份工程文件导出不同歌手的演唱音频
6666
67+
#### 1.2.0 (2022.03.06)
68+
69+
> - 修复音轨操作不能向上滚动的问题
70+
> - 切换歌手时将打印日志
71+
> - 重构部分代码,优化使用方式
72+
6773

6874

6975
## 参考资料与相关链接 | References & Links

src/core.py

Lines changed: 0 additions & 394 deletions
This file was deleted.

src/core/engine.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import os
2+
import winreg
3+
4+
import uiautomation as auto
5+
6+
import log
7+
import singers
8+
import verify
9+
10+
logger = log.logger
11+
12+
13+
def find_xstudio() -> str:
14+
"""
15+
根据注册表查找 X Studio 主程序路径。
16+
:return: XStudioSinger.exe 的路径
17+
"""
18+
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, 'SOFTWARE\\Classes\\svipfile\\shell\\open\\command')
19+
value = winreg.QueryValueEx(key, '')
20+
return value[0].split('"')[1]
21+
22+
23+
def start_xstudio(engine: str = None, project: str = None, singer: str = '陈水若'):
24+
"""
25+
启动 X Studio。
26+
:param engine: 手动指定 X Studio 主程序路径
27+
:param project: 启动时需要打开的工程文件路径,默认打开空白工程
28+
:param singer: 若打开空白工程,可指定初始歌手名称
29+
"""
30+
if engine:
31+
engine = os.path.abspath(engine)
32+
if not os.path.exists(engine):
33+
logger.error('指定的主程序路径不存在。')
34+
exit(1)
35+
if not os.path.isfile(engine) or not engine.endswith('.exe'):
36+
logger.error('指定的主程序不是合法的可执行 (.exe) 文件。')
37+
exit(1)
38+
logger.info('指定的主程序:%s。' % engine)
39+
if project:
40+
project = os.path.abspath(project)
41+
if not os.path.exists(project):
42+
logger.error('工程文件不存在。')
43+
exit(1)
44+
if not os.path.isfile(project) or not project.endswith('.svip'):
45+
logger.error('不是合法的 X Studio 工程 (.svip) 文件。')
46+
exit(1)
47+
if engine:
48+
os.popen(f'"{engine}" "{project}"')
49+
else:
50+
os.popen(f'"{project}"')
51+
verify.verify_startup()
52+
verify.verify_opening(auto)
53+
logger.info('启动 X Studio 并打开工程:%s。' % project)
54+
verify.verify_updates()
55+
else:
56+
if engine:
57+
os.popen(f'"{engine}"')
58+
else:
59+
os.popen(f'"{find_xstudio()}"')
60+
verify.verify_startup()
61+
auto.WindowControl(searchDepth=1, Name='X Studio').TextControl(searchDepth=2, Name='开始创作').Click(simulateMove=False)
62+
singers.choose_singer(name=singer)
63+
logger.info('启动 X Studio 并创建空白工程,初始歌手:%s。' % singer)
64+
verify.verify_updates()
65+
66+
67+
def quit_xstudio():
68+
"""
69+
退出 X Studio。
70+
"""
71+
auto.WindowControl(searchDepth=1, RegexName='X Studio .*').ButtonControl(searchDepth=1, AutomationId='btnClose').Click(simulateMove=False)
72+
confirm_window = auto.WindowControl(searchDepth=1, Name='X Studio')
73+
text = confirm_window.TextControl(searchDepth=1, AutomationId='Tbx').Name
74+
if text.startswith('确认'):
75+
confirm_window.ButtonControl(searchDepth=1, AutomationId='OkBtn').Click(simulateMove=False)
76+
else:
77+
confirm_window.ButtonControl(searchDepth=1, AutomationId='NoBtn').Click(simulateMove=False)
78+
logger.info('退出 X Studio。')
79+
80+
81+
if __name__ == '__main__':
82+
start_xstudio(singer='陈水若')

src/core/keybd.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import time
2+
3+
import win32api
4+
import win32con
5+
6+
7+
def key_down(code: int):
8+
win32api.keybd_event(code, win32api.MapVirtualKey(code, 0), 0, 0)
9+
10+
11+
def key_up(code: int):
12+
win32api.keybd_event(code, win32api.MapVirtualKey(code, 0), win32con.KEYEVENTF_KEYUP, 0)
13+
14+
15+
def key_press(code: int):
16+
key_down(code)
17+
time.sleep(0.02)
18+
key_up(code)

src/core/log.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import logging
2+
3+
import colorlog
4+
5+
6+
logger = logging.getLogger()
7+
log_colors_config = {
8+
'DEBUG': 'white',
9+
'INFO': 'green',
10+
'WARNING': 'yellow',
11+
'ERROR': 'red',
12+
'CRITICAL': 'bold_red',
13+
}
14+
console_handler = logging.StreamHandler()
15+
logger.setLevel(logging.INFO)
16+
console_handler.setLevel(logging.INFO)
17+
console_formatter = colorlog.ColoredFormatter(
18+
fmt='%(log_color)s[%(asctime)s.%(msecs)03d] %(filename)s -> %(funcName)s line:%(lineno)d [%(levelname)s] : %(message)s',
19+
datefmt='%Y-%m-%d %H:%M:%S',
20+
log_colors=log_colors_config
21+
)
22+
console_handler.setFormatter(console_formatter)
23+
if not logger.handlers:
24+
logger.addHandler(console_handler)
25+
console_handler.close()

src/core/mouse.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import win32api
2+
import win32con
3+
4+
5+
def move_wheel(distance: int):
6+
win32api.mouse_event(win32con.MOUSEEVENTF_WHEEL, 0, 0, distance)

src/core/projects.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import os
2+
3+
import uiautomation as auto
4+
5+
import keybd
6+
import log
7+
import verify
8+
import singers
9+
10+
logger = log.logger
11+
12+
13+
def new_project(singer: str = None):
14+
"""
15+
新建工程。X Studio 必须已处于启动状态。
16+
:param singer: 可指定新工程的初始歌手
17+
"""
18+
keybd.key_down(17)
19+
keybd.key_press(78)
20+
keybd.key_up(17)
21+
confirm_window = auto.WindowControl(searchDepth=1, Name='X Studio')
22+
if confirm_window.Exists(maxSearchSeconds=1):
23+
confirm_window.ButtonControl(searchDepth=1, AutomationId='NoBtn').Click(simulateMove=False)
24+
if singer:
25+
track_window = auto.WindowControl(searchDepth=1, RegexName='X Studio .*').CustomControl(searchDepth=1, ClassName='TrackWin')
26+
track_window.CustomControl(searchDepth=2, ClassName='TrackChannelControlPanel').ButtonControl(searchDepth=2, AutomationId='switchSingerButton').DoubleClick(simulateMove=False)
27+
singers.choose_singer(singer)
28+
logger.info('创建新工程,初始歌手:%s。' % singer)
29+
else:
30+
logger.info('创建新工程。')
31+
32+
33+
def open_project(filename: str, folder: str = None):
34+
"""
35+
打开工程。X Studio 必须已处于启动状态。
36+
:param filename: 工程文件名
37+
:param folder: 工程所处文件夹路径,默认为 X Studio 上一次打开工程的路径
38+
"""
39+
if folder:
40+
project = os.path.abspath(os.path.join(folder, filename))
41+
if not os.path.exists(project):
42+
logger.error('工程文件不存在。')
43+
exit(1)
44+
else:
45+
project = filename
46+
if not filename.endswith('.svip'):
47+
logger.error('不是一个可打开的 X Studio 工程 (.svip) 文件。')
48+
exit(1)
49+
keybd.key_down(17)
50+
keybd.key_press(79)
51+
keybd.key_up(17)
52+
confirm_window = auto.WindowControl(searchDepth=1, Name='X Studio')
53+
if confirm_window.Exists(maxSearchSeconds=1):
54+
confirm_window.ButtonControl(searchDepth=1, AutomationId='NoBtn').Click(simulateMove=False)
55+
main_window = auto.WindowControl(searchDepth=1, RegexName='X Studio .*')
56+
open_window = main_window.WindowControl(searchDepth=1, Name='打开文件')
57+
open_window.EditControl(searchDepth=3, Name='文件名(N):').GetValuePattern().SetValue(project)
58+
open_window.ButtonControl(searchDepth=1, Name='打开(O)').Click(simulateMove=False)
59+
warning_window = open_window.WindowControl(searchDepth=1, ClassName='#32770')
60+
if warning_window.Exists(maxSearchSeconds=1):
61+
warning = warning_window.TextControl(searchDepth=2).Name
62+
warning_window.ButtonControl(searchDepth=2, Name='确定').Click(simulateMove=False)
63+
open_window.ButtonControl(searchDepth=1, Name='取消').Click(simulateMove=False)
64+
logger.error(warning.replace('\r\n', ' ').replace('。 ', '。'))
65+
exit(1)
66+
verify.verify_opening(main_window)
67+
logger.info('打开工程:%s。' % project)
68+
69+
70+
def export_project(title: str = None, folder: str = None, format: str = 'mp3', samplerate: int = 48000):
71+
"""
72+
导出当前打开的工程。
73+
:param title: 目标文件名,默认与工程同名
74+
:param folder: 目标文件夹路径,默认为工程所在文件夹
75+
:param format: 导出格式 (mp3/wav/midi),默认为 mp3
76+
:param samplerate: 采样率 (48000/44100),默认为 48000
77+
"""
78+
if format not in ['mp3', 'wav', 'midi']:
79+
logger.error('只能保存为 mp3, wav 或 midi 格式。')
80+
exit(1)
81+
if format == 'midi':
82+
samplerate = None
83+
elif samplerate != 48000 and samplerate != 44100:
84+
logger.error('采样率只能为 48000 或 44100。')
85+
exit(1)
86+
if folder and not os.path.exists(folder):
87+
folder = folder.replace('/', '\\')
88+
os.makedirs(folder)
89+
auto.ButtonControl(searchDepth=2, Name='导出').Click(simulateMove=False)
90+
setting_window = auto.WindowControl(searchDepth=2, Name='导出设置')
91+
if title:
92+
setting_window.EditControl(searchDepth=1, AutomationId='FileNameTbx').GetValuePattern().SetValue(title)
93+
else:
94+
title = setting_window.EditControl(searchDepth=1, AutomationId='FileNameTbx').GetValuePattern().Value
95+
if folder:
96+
logger.warning('当前尚不支持指定导出文件夹路径。')
97+
setting_window.EditControl(searchDepth=1, AutomationId='DestTbx').SendKeys(folder, interval=0.05)
98+
if format != 'mp3':
99+
format_box = setting_window.ComboBoxControl(searchDepth=1, AutomationId='FormatComboBox')
100+
format_box.Click(simulateMove=False)
101+
if format == 'wav':
102+
format_box.ListItemControl(searchDepth=1, Name='WAVE文件').Click(simulateMove=False)
103+
else:
104+
format_box.ListItemControl(searchDepth=1, Name='Midi文件').Click(simulateMove=False)
105+
if samplerate == 44100:
106+
samplerate_box = setting_window.ComboBoxControl(searchDepth=1, AutomationId='SampleRateComboBox')
107+
samplerate_box.Click(simulateMove=False)
108+
samplerate_box.ListItemControl(searchDepth=1, Name='44100HZ').Click(simulateMove=False)
109+
setting_window.ButtonControl(searchDepth=1, Name='导出').Click(simulateMove=False)
110+
export_window = auto.WindowControl(searchDepth=2, RegexName='导出.*')
111+
label = export_window.TextControl(searchDepth=1, ClassName='TextBlock', AutomationId='label')
112+
while True:
113+
message_window = auto.WindowControl(searchDepth=2, ClassName='#32770')
114+
if message_window.Exists(maxSearchSeconds=1):
115+
message = message_window.TextControl(searchDepth=1, ClassName='Static').Name
116+
message_window.ButtonControl(searchDepth=1, Name='确定').Click(simulateMove=False)
117+
logger.error(message + '。')
118+
exit(1)
119+
if label.Name == '导出成功':
120+
break
121+
elif label.Name.startswith('导出失败'):
122+
logger.error('导出失败,请稍后再试。')
123+
exit(1)
124+
export_window.ButtonControl(searchDepth=1, AutomationId='okBtn').Click(simulateMove=False)
125+
logger.info('导出工程:%s, 格式 %s, 采样率 %d Hz。' % (title, format, samplerate))
126+
127+
128+
def save_project(filename: str = None, folder: str = None):
129+
"""
130+
保存或另存为当前打开的工程。
131+
:param filename: 另存为的工程文件名
132+
:param folder: 另存为的文件夹路径,默认为工程所在文件夹
133+
"""
134+
if folder:
135+
if not filename:
136+
logger.error('另存为工程时必须指定文件名。')
137+
exit(1)
138+
if not os.path.exists(folder):
139+
os.makedirs(folder)
140+
folder = os.path.abspath(folder.replace('/', '\\'))
141+
if not filename:
142+
keybd.key_down(17)
143+
keybd.key_press(83)
144+
keybd.key_up(17)
145+
logger.info('保存工程。')
146+
else:
147+
if folder:
148+
project = os.path.join(folder, filename)
149+
else:
150+
project = filename
151+
keybd.key_down(17)
152+
keybd.key_down(16)
153+
keybd.key_press(83)
154+
keybd.key_up(16)
155+
keybd.key_up(17)
156+
save_window = auto.WindowControl(searchDepth=1, RegexName='X Studio .*').WindowControl(searchDepth=1, Name='另存为')
157+
save_window.EditControl(searchDepth=6, Name='文件名:').GetValuePattern().SetValue(project)
158+
save_window.ButtonControl(searchDepth=1, Name='保存(S)').Click(simulateMove=False)
159+
confirm_window = save_window.WindowControl(searchDepth=1, ClassName='#32770')
160+
if confirm_window.Exists(maxSearchSeconds=1):
161+
warning = confirm_window.TextControl(searchDepth=2).Name
162+
if warning.endswith('是否替换它?'):
163+
confirm_window.ButtonControl(searchDepth=1, Name='是(Y)').Click(simulateMove=False)
164+
else:
165+
confirm_window.ButtonControl(searchDepth=2, Name='确定').Click(simulateMove=False)
166+
save_window.ButtonControl(searchDepth=1, Name='取消').Click(simulateMove=False)
167+
logger.error(warning.replace('\r\n', ' ').replace('。 ', '。'))
168+
exit(1)
169+
logger.info('另存为工程:%s。' % project)

src/core/singers.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import time
2+
3+
import uiautomation as auto
4+
5+
import log
6+
import mouse
7+
8+
logger = log.logger
9+
10+
11+
def choose_singer(name: str):
12+
"""
13+
选择一名歌手。歌手市场必须处于打开状态。
14+
:param name: 歌手名字
15+
"""
16+
singer_market = auto.WindowControl(searchDepth=2, Name='歌手市场')
17+
singer_market.HyperlinkControl(searchDepth=9, Name='全部歌手').Click(simulateMove=False)
18+
browser_pane = singer_market.PaneControl(searchDepth=3, ClassName='CefBrowserWindow')
19+
bottom = browser_pane.BoundingRectangle.bottom
20+
while True:
21+
singer_text = browser_pane.TextControl(searchDepth=14, Name=name)
22+
bottom_text = browser_pane.TextControl(searchDepth=14, Name='已经到底了')
23+
if singer_text.Exists(maxSearchSeconds=0.5) and 0 < singer_text.BoundingRectangle.bottom < bottom:
24+
singer_text.Click(simulateMove=False)
25+
break
26+
elif bottom_text.Exists(maxSearchSeconds=0.5) and bottom_text.BoundingRectangle.bottom > 0:
27+
singer_market.ButtonControl(searchDepth=1, AutomationId='btnClose').Click(simulateMove=False)
28+
logger.error('指定的歌手“%s”不存在。' % name)
29+
exit(1)
30+
else:
31+
browser_pane.MoveCursorToMyCenter(simulateMove=False)
32+
mouse.move_wheel(-1500)
33+
time.sleep(1)
34+
if singer_market.ButtonControl(searchDepth=17, Name='待解锁').Exists(maxSearchSeconds=0.5):
35+
singer_market.ImageControl(Depth=17).Click(simulateMove=False)
36+
logger.error('指定的歌手“%s”未解锁。' % name)
37+
exit(1)
38+
singer_market.ButtonControl(searchDepth=17, Name='选中').Click(simulateMove=False)

0 commit comments

Comments
 (0)