diff --git a/README.md b/README.md index f7a37e3..0a98097 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,12 @@ X Studio · 歌手 UI 自动化 | UI Automation for X Studio Singer - 启动和退出 X Studio - 新建、打开、保存、导出工程 - 为某条轨道切换歌手 +- 静音、独奏某条轨道 可能的应用场景: - 批量导出若干份工程 +- 分轨导出一个工程 - 导出一个工程的若干版本 - 批量编辑并另存为工程 - **工程在线试听**(欢迎网站站长合作!) @@ -70,6 +72,12 @@ UI 自动化执行的成功与否受到系统流畅度等客观因素影响。 > - 切换歌手时将打印日志 > - 重构部分代码,优化使用方式 +#### 1.3.0 (2022.03.06) + +> - 支持静音、取消静音、独奏、取消独奏 +> - 调整项目结构,优化部分代码 +> - Demo - 分轨导出一份工程文件 + ## 参考资料与相关链接 | References & Links diff --git a/src/core/mouse.py b/src/core/mouse.py index bb61c32..1e8d351 100644 --- a/src/core/mouse.py +++ b/src/core/mouse.py @@ -1,6 +1,19 @@ +import uiautomation as auto import win32api import win32con def move_wheel(distance: int): win32api.mouse_event(win32con.MOUSEEVENTF_WHEEL, 0, 0, distance) + + +def scroll_inside(target: auto.Control, bound: auto.Control): + while True: + if target.BoundingRectangle.top < bound.BoundingRectangle.top: + bound.MoveCursorToMyCenter(simulateMove=False) + move_wheel(bound.BoundingRectangle.top - target.BoundingRectangle.top) + elif target.BoundingRectangle.bottom > bound.BoundingRectangle.bottom: + bound.MoveCursorToMyCenter(simulateMove=False) + move_wheel(bound.BoundingRectangle.bottom - target.BoundingRectangle.bottom) + else: + break diff --git a/src/core/tracks.py b/src/core/tracks.py index 9968430..48662cc 100644 --- a/src/core/tracks.py +++ b/src/core/tracks.py @@ -7,22 +7,75 @@ logger = log.logger +all_tracks = [] + + +def enum_tracks() -> list: + all_tracks.clear() + index = 1 + track = Track(index) + while track.exists(): + all_tracks.append(track) + index += 1 + track = Track(index) + return all_tracks + + +def count_tracks() -> int: + return len(enum_tracks()) + + class Track: def __init__(self, index: int): if index < 1: logger.error('轨道编号最小为 1。') exit(1) - self.track_window = auto.WindowControl(searchDepth=1, RegexName='X Studio .*').CustomControl(searchDepth=1, ClassName='TrackWin') self.index = index - self.pane = None + self.track_window = auto.WindowControl(searchDepth=1, RegexName='X Studio .*').CustomControl(searchDepth=1, ClassName='TrackWin') + self.scroll_bound = self.track_window.PaneControl(searchDepth=1, ClassName='ScrollViewer') + self.pane = self.track_window.CustomControl(searchDepth=2, foundIndex=self.index, ClassName='TrackChannelControlPanel') + self.mute_button = self.pane.ButtonControl(searchDepth=1, Name='UnMute') + self.unmute_button = self.pane.ButtonControl(searchDepth=1, Name='Mute') + self.solo_button = self.pane.ButtonControl(searchDepth=1, Name='notSolo') + self.notsolo_button = self.pane.ButtonControl(searchDepth=1, Name='Solo') + self.switch_button = self.pane.ButtonControl(searchDepth=2, AutomationId='switchSingerButton') def exists(self) -> bool: - self.pane = self.track_window.CustomControl(searchDepth=2, foundIndex=self.index, ClassName='TrackChannelControlPanel') return self.pane.Exists(maxSearchSeconds=0.5) def is_instrumental(self) -> bool: return self.pane.ComboBoxControl(searchDepth=1, ClassName='ComboBox').IsOffscreen + def is_muted(self) -> bool: + return self.unmute_button.Exists(maxSearchSeconds=0.5) + + def is_solo(self) -> bool: + return self.notsolo_button.Exists(maxSearchSeconds=0.5) + + def set_muted(self, muted: bool): + if muted and not self.is_muted(): + mouse.scroll_inside(target=self.mute_button, bound=self.scroll_bound) + self.mute_button.Click(simulateMove=False) + elif not muted and self.is_muted(): + mouse.scroll_inside(target=self.unmute_button, bound=self.scroll_bound) + self.unmute_button.Click(simulateMove=False) + if muted: + logger.info('静音轨道 %d。' % self.index) + else: + logger.info('取消静音轨道 %d。' % self.index) + + def set_solo(self, solo: bool): + if solo and not self.is_solo(): + mouse.scroll_inside(target=self.solo_button, bound=self.scroll_bound) + self.solo_button.Click(simulateMove=False) + elif not solo and self.is_solo(): + mouse.scroll_inside(target=self.notsolo_button, bound=self.scroll_bound) + self.notsolo_button.Click(simulateMove=False) + if solo: + logger.info('独奏轨道 %d。' % self.index) + else: + logger.info('取消独奏轨道 %d。' % self.index) + def switch_singer(self, singer: str): """ 切换歌手。 @@ -34,18 +87,7 @@ def switch_singer(self, singer: str): if self.is_instrumental(): logger.error('指定的轨道不是演唱轨。') exit(1) - bound = self.track_window.PaneControl(searchDepth=1, ClassName='ScrollViewer').BoundingRectangle - top, bottom = bound.top, bound.bottom - switch_button = self.pane.ButtonControl(searchDepth=2, AutomationId='switchSingerButton') - while True: - if switch_button.BoundingRectangle.top < top: - self.track_window.MoveCursorToMyCenter(simulateMove=False) - mouse.move_wheel(top - switch_button.BoundingRectangle.top) - elif switch_button.BoundingRectangle.bottom > bottom: - self.track_window.MoveCursorToMyCenter(simulateMove=False) - mouse.move_wheel(bottom - switch_button.BoundingRectangle.bottom) - else: - switch_button.DoubleClick(simulateMove=False) - break + mouse.scroll_inside(target=self.switch_button, bound=self.scroll_bound) + self.switch_button.DoubleClick(simulateMove=False) singers.choose_singer(name=singer) logger.info('为轨道 %d 切换歌手:%s。' % (self.index, singer)) diff --git a/src/separate_tracks.py b/src/separate_tracks.py new file mode 100644 index 0000000..05c745f --- /dev/null +++ b/src/separate_tracks.py @@ -0,0 +1,14 @@ +import sys + +sys.path.append('core') + +from core import engine, projects, tracks + +if __name__ == '__main__': + path = r'..\demo\separate_tracks\assets\示例.svip' + prefix = '示例' + engine.start_xstudio(engine=r'E:\YQ数据空间\YQ实验室\实验室:XStudioSinger\内测\XStudioSinger_2.0.0_beta2.exe', project=path) + for track in tracks.enum_tracks(): + track.set_solo(True) + projects.export_project(title=f'{prefix}_轨道{track.index}') + engine.quit_xstudio()