Repository: windhide/SkyMusicPlay-for-Windows Branch: main Commit: 8446e7c36849 Files: 103 Total size: 684.1 KB Directory structure: gitextract_xsmzwekj/ ├── .gitattributes ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── .version ├── README.md ├── buildProject.ps1 ├── draw-follow-window/ │ ├── README.md │ ├── draw_server.py │ └── draw_socket_demo.py ├── sky-music-server/ │ ├── README.md │ ├── music_compare_repeat.py │ ├── music_file_process.py │ ├── music_translate.py │ ├── requirements.txt │ ├── sky_music_apis.py │ ├── sky_music_server.py │ ├── version.txt │ └── windhide/ │ ├── auto/ │ │ └── auto_thread.py │ ├── musicToSheet/ │ │ ├── aigc_handler_sheet.py │ │ ├── music2html.py │ │ ├── process_audio.py │ │ ├── transfer_MID.py │ │ └── vocals_split.py │ ├── playRobot/ │ │ ├── amd_robot.py │ │ └── intel_robot.py │ ├── static/ │ │ └── global_variable.py │ ├── thread/ │ │ ├── amd_play_thread.py │ │ ├── follow_process_thread.py │ │ ├── follow_thread.py │ │ ├── frame_alive_thread.py │ │ ├── hwnd_check_thread.py │ │ ├── intel_play_thread.py │ │ ├── queue_thread.py │ │ └── shortcut_thread.py │ └── utils/ │ ├── auto_util.py │ ├── command_util.py │ ├── config_util.py │ ├── hook_util.py │ ├── hwnd_utils.py │ ├── ocr_follow_util.py │ ├── ocr_heart_utils.py │ ├── ocr_normal_utils.py │ ├── path_util.py │ ├── play_util.py │ └── sheet_decrypt_util.py ├── sky-music-web/ │ ├── .editorconfig │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .npmrc │ ├── .prettierignore │ ├── .prettierrc.yaml │ ├── README.md │ ├── build/ │ │ └── entitlements.mac.plist │ ├── dev-app-update.yml │ ├── electron-builder.yml │ ├── electron.vite.config.ts │ ├── package.json │ ├── src/ │ │ ├── main/ │ │ │ └── index.ts │ │ ├── preload/ │ │ │ ├── index.d.ts │ │ │ └── index.ts │ │ └── renderer/ │ │ ├── index.html │ │ └── src/ │ │ ├── App.vue │ │ ├── component/ │ │ │ └── svg/ │ │ │ ├── cr.vue │ │ │ ├── dm.vue │ │ │ └── dmcr.vue │ │ ├── env.d.ts │ │ ├── i18n/ │ │ │ ├── index.ts │ │ │ └── locales/ │ │ │ ├── en.json │ │ │ ├── jp.json │ │ │ ├── ko.json │ │ │ ├── zh-classical.json │ │ │ ├── zh-cn.json │ │ │ └── zh-tw.json │ │ ├── main.ts │ │ ├── router/ │ │ │ └── index.ts │ │ ├── store/ │ │ │ └── index.ts │ │ ├── utils/ │ │ │ ├── configStore.ts │ │ │ └── fetchUtils.ts │ │ └── views/ │ │ ├── ai_setting.vue │ │ ├── home.vue │ │ ├── home_loader.vue │ │ ├── hwndHandle.vue │ │ ├── kube.vue │ │ ├── magicTools.vue │ │ ├── music.vue │ │ ├── music_edit.vue │ │ ├── setting.vue │ │ ├── shortcutKeys.vue │ │ └── tutorial.vue │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── tsconfig.web.json └── template-resources/ ├── myFavorite/ │ └── .keep ├── myImport/ │ └── .keep ├── myTranslate/ │ └── .keep ├── systemTools/ │ ├── drawTool/ │ │ └── .keep │ ├── modelData/ │ │ ├── check_key_model.pt │ │ ├── demoScheenshot/ │ │ │ └── .keep │ │ └── friend_model.pt │ └── scriptTemplate/ │ └── .keep ├── translateMID/ │ └── .keep └── translateOriginalMusic/ └── .keep ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ *.pth filter=lfs diff=lfs merge=lfs -text *.exe filter=lfs diff=lfs merge=lfs -text ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry polar: # Replace with a single Polar username buy_me_a_coffee: # Replace with a single Buy Me a Coffee username thanks_dev: # Replace with a single thanks.dev username custom: ['https://afdian.com/a/WindHdie'] ================================================ FILE: .gitignore ================================================ .DS_Store node_modules /dist # local env files .env.local .env.*.local # Log files npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw? #Electron-builder output /dist_electron *.pyc sky-music-server/.idea/ sky-music-server/node_modules/ sky-music-web/node_modules/ sky-music-web/.idea/ sky-music-server/build/ sky-music-web/web2/ sky-music-web/dist_electron/ sky-music-web/backend_dist sky-music-web/out draw-follow-window/build draw-follow-window/dist web2/ **/__pycache__/** resources/ *.spec ffmpeg.exe template-resources/systemMusic/** ================================================ FILE: .version ================================================ { "version": "2.6.5", "title": "更新啦~新年快乐🔔", "content": "🍉新版本v2.6.6\\n🍊更新群-1007672060\\n💡版本更新协助\\n🥝 更新日志\\n\\t⭐️ 演奏、转谱界面,已转换歌曲进行细分分类\\n\\t⭐️ 转谱添加人声分离功能\\n\\t🔧 修复同步歌单的时候带来的问题 \\n\\t (如果有需要 群里也有单独解密的软件进行下载)", "downloadUrl": "https://github.com/windhide/SkyMusicPlay-for-Windows/releases/download/v2.6.6/v2.6.6_x64_windows.exe", "positiveText": "新年快乐🎇", "negativeText": "朕知道了,退下吧。🥲" } ================================================ FILE: README.md ================================================ # SkyMusicPlay-for-Windows

SkyMusicPlay-for-Windows

星星弹琴软件

教程文档   ·   点我下载
## 丨安装提醒 > > **安装请`关闭杀毒软件`进行** > > 支持PC端,也支持模拟器端 > > 🚧目前还在施工中,功能快速迭代中...🚧 > > > ✨如果需要相关创意功能欢迎在issues中提出✨ > ## 丨项目开发环境 - **Python**:3.11 - **Node**:16.20.2 ## 丨技术栈 - **Electron** - **TypeScript** - **Vite** - **Naive-UI** - **Python** - **YOLO** - **WebSocket** ## 丨其他 - 项目引用或代码引用请注明原作者出处,禁止商业用途 - [Creative Commons Attribution-NonCommercial (CC BY-NC) 许可协议](https://creativecommons.org/licenses/by-nc/4.0/deed.zh-hans) ## 丨联系方式 - 如有任何问题或建议,欢迎通过以下方式与我联系: - [Email](mailto:WindHide520@gmail.com) - [GitHub](https://github.com/windhide) ## 致谢 - [pianotrans](https://github.com/azuwis/pianotrans) 音乐转钢琴谱 - [basic-pitch](https://github.com/spotify/) 音乐转MIDI ================================================ FILE: buildProject.ps1 ================================================ # ========================= # 自动管理员提权 # ========================= $principal = New-Object Security.Principal.WindowsPrincipal( [Security.Principal.WindowsIdentity]::GetCurrent() ) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Write-Host "This script requires administrator privileges." Write-Host "Attempting to restart with administrator rights..." Start-Process powershell ` -Verb RunAs ` -ArgumentList "-NoExit -ExecutionPolicy Bypass -File `"$PSCommandPath`"" exit } # ========================= # 脚本所在目录 # ========================= $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition Set-Location $ScriptDir Write-Host "Current script directory: $ScriptDir" # ========================= # 删除旧构建目录 # ========================= $pathsToRemove = @( "$ScriptDir\sky-music-web\dist", "$ScriptDir\sky-music-server\build", "$ScriptDir\sky-music-web\backend_dist" ) foreach ($path in $pathsToRemove) { if (Test-Path $path) { Write-Host "Removing: $path" Remove-Item -Recurse -Force -Path $path } } # ========================= # 构建 Python 服务器 # ========================= Write-Host "`n=== Building Python Server ===" Set-Location "$ScriptDir\sky-music-server" & ".\.venv\Scripts\python.exe" -m PyInstaller ` -i icon.ico ` sky_music_server.py ` --distpath "$ScriptDir\sky-music-web\backend_dist" ` --version-file "$ScriptDir\sky-music-server\version.txt" ` --hidden-import main ` --collect-all sklearn ` --collect-all basic_pitch ` --collect-all plyer ` --collect-all torch ` --collect-all torchvision ` --hidden-import=torch ` --hidden-import demucs ` --collect-all demucs ` --hidden-import=torchvision ` --uac-admin if ($LASTEXITCODE -ne 0) { Write-Error "PyInstaller build failed." exit 1 } # ========================= # 复制 ffmpeg.exe # ========================= Write-Host "`nCopying ffmpeg.exe" $ffmpegSrc = "$ScriptDir\ffmpeg.exe" $ffmpegDst = "$ScriptDir\sky-music-web\backend_dist\sky_music_server\ffmpeg.exe" if (Test-Path $ffmpegSrc) { Copy-Item $ffmpegSrc $ffmpegDst -Force } else { Write-Warning "ffmpeg.exe not found: $ffmpegSrc" } # ========================= # 构建 Electron 应用 # ========================= Write-Host "`n=== Building Electron App ===" Set-Location "$ScriptDir\sky-music-web" & npm run build:win if ($LASTEXITCODE -ne 0) { Write-Error "Electron build failed." exit 1 } # ========================= # 完成提示 # ========================= Write-Host "`nAll tasks completed successfully!" Pause ================================================ FILE: draw-follow-window/README.md ================================================ ```shell pyinstaller --onefile --noconsole --clean --strip --name draw_server draw_server.py ``` ```shell draw_server.exe --width 1024 --height 768 --x 200 --y 300 ``` ================================================ FILE: draw-follow-window/draw_server.py ================================================ import tkinter as tk import socket import threading import sys import argparse import os # 针对 Windows 系统设置 DPI Awareness(适用于 Windows 8.1 及以上) if os.name == "nt": try: from ctypes import windll windll.shcore.SetProcessDpiAwareness(1) except Exception as e: print("无法设置 DPI Awareness,可能影响窗口几何尺寸的准确性。") # 绘制和删除方框的 API def draw_box_api(canvas, width, height, position_x, position_y, box_id=None): # 在 Canvas 上绘制一个指定大小和位置的方框 if box_id is not None: # 重用已有的方框对象 canvas.coords(box_id, position_x, position_y, position_x + width, position_y + height) return box_id else: # 创建新的方框对象 return canvas.create_rectangle( position_x, position_y, position_x + width, position_y + height, outline="#00ffff", width=3 ) def delete_box_api(canvas, box_id, box_pool=None): # 从 Canvas 上删除指定 ID 的方框 if box_pool is not None and len(box_pool) < 100: # 将方框对象添加到对象池中 canvas.itemconfig(box_id, state='hidden') box_pool.append(box_id) else: # 对象池已满或未使用对象池,直接删除 canvas.delete(box_id) # 主窗口类 class TransparentBoxWindow: def __init__(self, root, width, height, position_x, position_y): self.root = root self.port = 12345 # 固定端口 # 设置 Canvas self.canvas = tk.Canvas(root, width=width, height=height, bg="white", highlightthickness=0) self.canvas.pack() # 绘制红色边框 # self.draw_red_border(width, height) # 存储用户自定义 ID 与 Tkinter 方框 ID 的映射 self.boxes = {} # 对象池 - 存储可重用的方框对象 self.box_pool = [] self.max_pool_size = 100 # 对象池最大容量 # 启动 Socket 服务器 threading.Thread(target=self.start_server, daemon=True).start() def draw_red_border(self, width, height, border_thickness=10): """在 Canvas 上绘制一个红色的边框""" self.canvas.create_rectangle( border_thickness // 2, border_thickness // 2, width - border_thickness // 2, height - border_thickness // 2, outline="red", width=border_thickness, tags="red_border" ) def start_server(self): # 创建 Socket 服务器 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(("localhost", self.port)) server.listen(5) print(f"服务器已启动,监听端口:{self.port},等待连接...") while True: client, addr = server.accept() print(f"收到连接来自 {addr}") threading.Thread(target=self.handle_client, args=(client,), daemon=True).start() def handle_client(self, client): try: buffer = "" command_batch = [] while True: data = client.recv(1024).decode("utf-8") if not data: break buffer += data # 按换行符分割命令 commands = buffer.split("\n") # 收集完整命令到批处理列表 for command in commands[:-1]: print(f"收到命令: {command}") command_batch.append(command) # 当积累了足够的命令或遇到特殊命令时执行批处理 if len(command_batch) >= 30 or any(cmd.startswith(("resize", "exit", "update")) for cmd in command_batch): self.process_command_batch(command_batch) command_batch = [] # 将最后一个不完整的命令保留到下一次处理 buffer = commands[-1] # 处理剩余的命令 if command_batch: self.process_command_batch(command_batch) except ConnectionResetError: print("客户端断开连接") finally: client.close() def process_command_batch(self, commands): # 创建命令分类字典 command_groups = { "draw": [], "delete": [], "update": None, "resize": None, "exit": False } # 对命令进行分类 for command in commands: parts = command.split() cmd_type = parts[0] if cmd_type == "draw": command_groups["draw"].append({ "id": parts[1], "width": int(parts[2]), "height": int(parts[3]), "x": int(parts[4]), "y": int(parts[5]) }) elif cmd_type == "delete": command_groups["delete"].append(parts[1]) elif cmd_type == "update": # 将update命令作为触发批处理的信号 command_groups["update"] = True elif cmd_type == "resize": command_groups["resize"] = { "width": int(parts[1]), "height": int(parts[2]), "x": int(parts[3]), "y": int(parts[4]) } elif cmd_type == "exit": command_groups["exit"] = True # 批量处理删除命令 for box_id in command_groups["delete"]: if box_id in self.boxes: tkinter_id = self.boxes[box_id] delete_box_api(self.canvas, tkinter_id, self.box_pool) del self.boxes[box_id] # 批量处理绘制命令 for box in command_groups["draw"]: if box["id"] not in self.boxes: # 尝试从对象池中获取方框对象 reused_box_id = None if self.box_pool: reused_box_id = self.box_pool.pop() self.canvas.itemconfig(reused_box_id, state='normal') tkinter_id = draw_box_api( self.canvas, box["width"], box["height"], box["x"], box["y"], reused_box_id ) self.boxes[box["id"]] = tkinter_id # 处理调整窗口大小命令 if command_groups["resize"]: resize = command_groups["resize"] self.change_window_geometry( resize["width"], resize["height"], resize["x"], resize["y"] ) # 处理退出命令 if command_groups["exit"]: print("接收到退出指令,正在退出程序...") self.exit_program() # 更新Canvas self.root.update_idletasks() def change_window_geometry(self, width, height, position_x, position_y): print(f"更改窗口尺寸和位置为:{width}x{height}, 坐标: ({position_x}, {position_y})") # 隐藏所有方框而不是删除它们 for tkinter_id in self.boxes.values(): self.canvas.itemconfig(tkinter_id, state='hidden') if len(self.box_pool) < self.max_pool_size: self.box_pool.append(tkinter_id) self.boxes.clear() # 修改窗口和Canvas大小 self.root.geometry(f"{width}x{height}+{position_x}+{position_y}") self.canvas.config(width=width, height=height) # 一次性更新UI self.root.update_idletasks() # 输出调整后的实际窗口大小 actual_width = self.root.winfo_width() actual_height = self.root.winfo_height() print(f"调整后实际窗口尺寸: {actual_width}x{actual_height}") # 输出调整后的实际窗口大小 actual_width = self.root.winfo_width() actual_height = self.root.winfo_height() print(f"调整后实际窗口尺寸: {actual_width}x{actual_height}") def exit_program(self): self.root.destroy() sys.exit(0) if __name__ == "__main__": # 解析命令行参数 parser = argparse.ArgumentParser(description="Transparent Box GUI") parser.add_argument("--width", type=int, default=800, help="Window width") # 窗口宽度 parser.add_argument("--height", type=int, default=600, help="Window height") # 窗口高度 parser.add_argument("--x", type=int, default=100, help="Window position X") # 窗口 X 坐标 parser.add_argument("--y", type=int, default=100, help="Window position Y") # 窗口 Y 坐标 args = parser.parse_args() width = args.width height = args.height x = args.x y = args.y root = tk.Tk() # 尽管已在 Windows 下尝试设置 DPI Awareness,仍保持 scaling 为 1.0 root.tk.call('tk', 'scaling', 1.0) # 先设置 geometry,再调用 update_idletasks 确保尺寸生效 root.geometry(f"{width}x{height}+{x}+{y}") root.update_idletasks() root.overrideredirect(True) root.attributes("-transparentcolor", "white") root.attributes("-topmost", True) app = TransparentBoxWindow(root, width, height, x, y) root.mainloop() ================================================ FILE: draw-follow-window/draw_socket_demo.py ================================================ import socket import time client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(("localhost", 12345)) # 连接到服务器 def send_command(command): try: client.sendall(command.encode("utf-8")) # 发送命令 print(f"发送命令: {command}") except ConnectionRefusedError: print("无法连接到服务器,请确保服务端已启动。") # 示例指令序列 send_command("draw box1 100 50 200 200\n") # 绘制第一个方框 time.sleep(1) # 等待 1 秒 send_command("draw box2 150 100 300 300\n") # 绘制第二个方框 time.sleep(1) # 等待 1 秒 # 调整窗口尺寸和位置,同时清空所有已绘制的方框 send_command("resize 2560 1080 50 10\n") # 更改窗口为 1000x800 大小,位置 (50, 50) # time.sleep(2) # 等待 2 秒 send_command("draw box2 150 100 1000 1000\n") # 绘制第二个方框 time.sleep(1) # 等待 1 秒 # 再次绘制新的方框 send_command("draw box3 120 80 400 100\n") # 绘制第三个方框 time.sleep(1) # 等待 1 秒 send_command("draw box4 200 150 500 400\n") # 绘制第四个方框 time.sleep(2) # 等待 2 秒 # 删除某些方框 send_command("delete box3\n") # 删除第三个方框 time.sleep(2) # 等待 2 秒 send_command("delete box4\n") # 删除第四个方框 time.sleep(2) # 等待 2 秒 send_command("update\n") # 删除第四个方框 time.sleep(2) # 等待 2 秒 # 添加退出指令 send_command("exit \n") # 发送退出指令到服务器 print("发送退出指令,客户端结束运行。") ================================================ FILE: sky-music-server/README.md ================================================ 打包指令 ```shell # 有命令调试打包 pyinstaller --uac-admin sky_music_server.py -i icon.ico --upx-dir D:\Desktop\upx-4.2.2-win64\ --distpath D:\Desktop\SkyMusicPlay-for-Windows\sky-music-web\dist\win-unpacked\backend_dist --hidden-import=main --collect-all=sklearn --collect-all=basic_pitch # 无命令调试打包 pyinstaller --uac-admin -w sky_music_server.py -i icon.ico --upx-dir D:\Desktop\upx-4.2.2-win64\ --distpath D:\Desktop\SkyMusicPlay-for-Windows\sky-music-web\dist\win-unpacked\backend_dist --hidden-import=main --collect-all=sklearn --collect-all=basic_pitch ``` > ffmpeg.exe 放在和 sky_windows_music.exe平级 ================================================ FILE: sky-music-server/music_compare_repeat.py ================================================ import difflib import hashlib import os import shutil from collections import defaultdict def get_file_hash(file_path, block_size=65536): """计算文件的 SHA-256 哈希值""" hasher = hashlib.sha256() with open(file_path, 'rb') as f: while chunk := f.read(block_size): hasher.update(chunk) return hasher.hexdigest() def get_file_similarity(file1, file2): """计算两个文件的内容相似度""" with open(file1, 'r', errors='ignore') as f1, open(file2, 'r', errors='ignore') as f2: text1 = f1.readlines() text2 = f2.readlines() return difflib.SequenceMatcher(None, text1, text2).ratio() def find_similar_files(input_folder, threshold=0.8): """查找相似文件并标记需要删除的文件""" files = [os.path.join(input_folder, f) for f in os.listdir(input_folder) if os.path.isfile(os.path.join(input_folder, f))] # 用哈希分组 hash_groups = defaultdict(list) for file in files: file_hash = get_file_hash(file) hash_groups[file_hash].append(file) to_delete = set() checked_pairs = set() for group in hash_groups.values(): # 如果 MD5 相同的文件超过 1 个,则这些都属于重复文件 if len(group) > 1: # 保留最大(或最老)的一个,其余删除 group_sorted = sorted(group, key=lambda f: os.path.getsize(f), reverse=True) keep = group_sorted[0] duplicates = group_sorted[1:] to_delete.update(duplicates) continue # 其它情况下才继续相似度比对(不同 hash) for i, file1 in enumerate(group): for file2 in group[i + 1:]: if (file1, file2) in checked_pairs: continue checked_pairs.add((file1, file2)) similarity = get_file_similarity(file1, file2) if similarity >= threshold: smaller_file = min(file1, file2, key=lambda f: os.path.getsize(f)) to_delete.add(smaller_file) return to_delete def process_files(input_folder, output_folder, threshold=0.8): """处理文件夹,删除重复文件并移动剩余文件""" if not os.path.exists(output_folder): os.makedirs(output_folder) to_delete = find_similar_files(input_folder, threshold) # 删除重复文件 deleted_files = list(to_delete) for file in to_delete: os.remove(file) # 移动剩余文件 for file in os.listdir(input_folder): full_path = os.path.join(input_folder, file) if os.path.isfile(full_path): shutil.move(full_path, os.path.join(output_folder, file)) print(f"删除了 {len(deleted_files)} 个文件:") for file in deleted_files: print(file) if __name__ == "__main__": input_folder = r"D:\Desktop\处理好的" # 输入文件夹路径 output_folder = r"D:\Desktop\二次处理的" # 处理后正常输出路径 process_files(input_folder, output_folder) ================================================ FILE: sky-music-server/music_file_process.py ================================================ import json import os import re import shutil # 用于移动文件 from concurrent.futures import ThreadPoolExecutor import chardet # 用于检测文件编码 def sanitize_filename(name): """清理文件名中的非法字符""" return re.sub(r'[<>:"/\\\\|?*]', '_', name) def process_file(file_path, normal_output_folder, encrypted_folder, keyword="1Key"): try: # 自动检测文件编码 with open(file_path, 'rb') as file: raw_data = file.read() detected = chardet.detect(raw_data) encoding = detected.get('encoding', 'utf-8') or 'utf-8' # 避免 None # 用检测到的编码读取文件 with open(file_path, 'r', encoding=encoding) as file: content = file.read() # 解析 JSON try: json_data = json.loads(content) except json.JSONDecodeError as e: print(f"❌ 解析 JSON 失败: {file_path}, 错误: {e}") return # 确保 JSON 是列表并且不为空 if not isinstance(json_data, list) or len(json_data) == 0: print(f"❌ 无效的 JSON 结构: {file_path}") return # **检查是否包含关键词 "1Key"** if keyword in content: first_name = json_data[0].get('name', '未命名文件') sanitized_name = sanitize_filename(first_name) new_file_name = f"{sanitized_name}.txt" new_file_path = os.path.join(normal_output_folder, new_file_name) # 避免文件名冲突 counter = 1 while os.path.exists(new_file_path): new_file_name = f"{sanitized_name}_{counter}.txt" new_file_path = os.path.join(normal_output_folder, new_file_name) counter += 1 # **将内容转换为 UTF-8 并保存** with open(new_file_path, 'w', encoding='utf-8') as new_file: new_file.write(content) print(f"✅ 文件 {os.path.basename(file_path)} 已重命名并保存到 {normal_output_folder}") return # **检查是否包含 isEncrypted: true** for item in json_data: if isinstance(item, dict) and item.get("isEncrypted") is True: # **直接移动文件到加密文件夹** shutil.move(file_path, os.path.join(encrypted_folder, os.path.basename(file_path))) print(f"✅ 文件 {os.path.basename(file_path)} 已移动到 {encrypted_folder}") return # **如果既不是加密文件,也不包含关键词 "1Key",则跳过** print(f"⚠️ 文件 {os.path.basename(file_path)} 不包含 'isEncrypted': true 或 '{keyword}',跳过处理") except (KeyError, IOError, UnicodeDecodeError) as e: print(f"❌ 处理文件 {os.path.basename(file_path)} 时出错: {e}") def process_files(input_folder, normal_output_folder, encrypted_folder, keyword="1Key"): # **确保目标文件夹存在** os.makedirs(normal_output_folder, exist_ok=True) os.makedirs(encrypted_folder, exist_ok=True) # **使用线程池并行处理文件** with ThreadPoolExecutor() as executor: for root, _, files in os.walk(input_folder): for file_name in files: file_path = os.path.join(root, file_name) executor.submit(process_file, file_path, normal_output_folder, encrypted_folder, keyword) print("✅ 所有文件处理完成。") if __name__ == '__main__': # **文件夹路径** input_folder = r"D:\SoftWareStorage\Tencent Download\额外的谱子_倒卖死妈_一辈子都是垃圾" # 输入文件夹路径 normal_output_folder = r"D:\Desktop\处理好的" # 处理后正常输出路径 encrypted_folder = r"D:\Desktop\加密的" # 加密文件存放路径 process_files(input_folder, normal_output_folder, encrypted_folder) ================================================ FILE: sky-music-server/music_translate.py ================================================ import os from basic_pitch import ICASSP_2022_MODEL_PATH from basic_pitch.inference import predict_and_save if __name__ == '__main__': # 定义输入文件和输出路径 desktop_path = os.path.join(os.path.expanduser("~"), "Desktop") # 获取桌面路径 output_midi_path = "D:\\Desktop" try: predict_and_save( [ r"D:\Desktop\孙燕姿 - 第一天.mp3" ], output_midi_path, True, False, False, False, ICASSP_2022_MODEL_PATH ) except Exception as e: print(f"处理失败:{e}") ================================================ FILE: sky-music-server/requirements.txt ================================================ plyer==2.1.0 pynput==1.7.7 charset-normalizer==3.4.1 chardet==5.2.0 ultralytics==8.3.61 opencv-python==4.10.0.84 numpy==1.23.5 PyAutoGUI==0.9.54 scikit-learn==1.5.1 psutil==6.1.1 keyboard==0.13.5 Jinja2==3.1.6 platformdirs==4.3.6 requests==2.32.3 uvicorn==0.34.0 fastapi==0.115.6 torch==2.5.1 matplotlib==3.10.0 torchlibrosa==0.1.0 librosa==0.10.2.post1 audioread==3.0.1 mido==1.3.3 pretty_midi pywin32 python-multipart pyinstaller websocket_server basic_pitch coremltools scikit-learn==1.5.1 protobuf==4.25.6 openai==1.85.0 demucs==4.0.1 ================================================ FILE: sky-music-server/sky_music_apis.py ================================================ import json import os import time import webbrowser import psutil import requests from fastapi import FastAPI, UploadFile from fastapi.middleware.cors import CORSMiddleware from windhide.musicToSheet.aigc_handler_sheet import general_ai from windhide.playRobot import amd_robot, intel_robot from windhide.static.global_variable import GlobalVariable from windhide.utils.auto_util import auto_click_fire, shutdown from windhide.utils.config_util import set_config, get_config, favorite_music, convert_sheet, drop_file from windhide.utils.hwnd_utils import get_running_apps, get_running_apps_by_struct from windhide.utils.ocr_follow_util import set_next_sheet, get_key_position, test_key_model_position, \ open_follow from windhide.utils.ocr_heart_utils import get_friend_model_position from windhide.utils.path_util import getTypeMusicList, getResourcesPath, process_sheet_rename_time from windhide.utils.play_util import start, pause, resume, stop from windhide.utils.sheet_decrypt_util import decrypt_sheet # 避开与光遇相同核心运行 process = psutil.Process(os.getpid()) all_cores = list(range(psutil.cpu_count())) cores_to_use = [core for core in all_cores if core != 0] process.cpu_affinity(cores_to_use) app = FastAPI() app.add_middleware( CORSMiddleware, allow_origins=["*"], # 允许的源,可根据需求设置特定地址或使用 ["*"] 允许所有 allow_credentials=True, # 允许携带认证信息(如 Cookies) allow_methods=["*"], # 允许的 HTTP 方法(如 GET、POST) allow_headers=["*"], # 允许的 HTTP 请求头 ) async def get_list(listName: str, searchStr: str): return getTypeMusicList(listName, searchStr) def play_operate(request: dict): match request["operate"]: case 'start': start(request) case 'pause': pause() case 'resume': resume() case 'stop': stop() def get_progress(): return { "overall_progress": f"{GlobalVariable.overall_progress:.1f}", "now_progress": f"{GlobalVariable.now_progress:.1f}", "now_translate_text": GlobalVariable.now_translate_text, "now_play_music": GlobalVariable.now_play_music, "now_total_time": GlobalVariable.now_total_time, "now_current_time": GlobalVariable.now_current_time } async def create_upload_files(file: UploadFile): path = os.path.join(getResourcesPath("translateOriginalMusic"), f'{file.filename}') with open(path, 'wb') as f: for chunk in iter(lambda: file.file.read(1024), b''): f.write(chunk) return "ok" async def create_upload_files_user(file: UploadFile): # 读取文件内容 file_content = await file.read() import chardet detected = chardet.detect(file_content) encoding = detected.get('encoding', 'utf-8') text_content = file_content.decode(encoding) data = json.loads(text_content) is_encrypted = data[0].get("isEncrypted", False) if is_encrypted: print("解密触发") data = decrypt_sheet(data) # 提取 songNotes 并计算时间戳 song_notes = data[0].get("songNotes", []) if not song_notes: raise ValueError("No songNotes found in the file") sum_time = int(song_notes[-1]["time"]) + int(song_notes[-1].get("duration", 0)) # 生成新的文件名 name, ext = os.path.splitext(file.filename) new_filename = f"{name}-#{sum_time}{ext}" path = os.path.join(getResourcesPath("myImport"), new_filename) # 保存文件 with open(path, 'wb') as f: f.write(json.dumps(data).encode()) return "ok" def translate(request: dict): from windhide.musicToSheet.process_audio import process_directory_with_progress match request["operate"]: case 'translate': process_directory_with_progress(request["value"]) process_sheet_rename_time(isImportOrTranslate=True) return "ok" def config_operate(request: dict): match request["operate"]: case 'set': set_config(request) case 'get': return get_config(request) case 'favorite_music': favorite_music(request) case 'convert_sheet': return convert_sheet(request) case 'drop_file': drop_file(request) case 'get_key_position': return get_key_position(float(request["conf"])) case 'cpu_type': return True if GlobalVariable.cpu_type == "AMD" else False case 'hwnd_get': return get_running_apps() case 'hwnd_get_now': if GlobalVariable.window["hWnd"] is None: return "Nothing" else: return GlobalVariable.hwnd_title case 'hwnd_set': if request["value"] == 'reset': GlobalVariable.is_custom_hwnd = False else: return get_running_apps_by_struct(request["value"]) return "ok" def follow(request: dict): match request["operate"]: case 'setSheet': set_next_sheet(request) case 'openFollow': open_follow() def check(): return 'True' def open_browser(url: str): webbrowser.open(url) return 'ok' def open_files(request: dict): match request["operate"]: case 'images': appdata_path = os.getenv('APPDATA') os.startfile(os.path.join(appdata_path, 'ThatGameCompany', 'com.netease.sky', 'images')) case 'files': os.startfile(os.path.join(getResourcesPath(None), request["type"])) def get_update(): response = requests.get('https://gitee.com/WindHide/SkyMusicPlay-for-Windows/raw/main/.version') if response.status_code == 200: return json.loads(response.text) return "404" # 下面放识别相关的调用 def auto(request: dict): match request["operate"]: case 'click_fire': auto_click_fire() case 'shutdown': shutdown() def get_path(request: dict): return getResourcesPath(request["type"]) def test(request: dict): match request["operate"]: case 'image': return get_friend_model_position(float(request["conf"]), isTest=True) case 'key': test_key_model_position(float(request["conf"])) return None case 'press': match GlobalVariable.cpu_type: case 'Intel': intel_robot.key_down(request["key"]) time.sleep(0.01) intel_robot.key_up(request["key"]) return None case 'AMD': amd_robot.key_down(request["key"]) time.sleep(0.01) amd_robot.key_up(request["key"]) return None return None return None def aigc(request: dict): print(f"okay now is running aigc {request['ai']}") # if request["ai"] == "Gemini": # return gemini_ai(model=request["model"],filename=request["filename"],type_=request["type"]) # else: return general_ai(model=request["model"],filename=request["filename"],type_=request["type"], platform=request["ai"]) def register_routes(app: FastAPI): app.get("/")(get_list) app.get("/check")(check) app.get("/update")(get_update) app.get("/getProgress")(get_progress) app.get("/openBrowser")(open_browser) app.get("/syncSheetName")(process_sheet_rename_time) app.post("/play_operate")(play_operate) app.post("/userMusicUpload")(create_upload_files_user) app.post("/config_operate")(config_operate) app.post("/fileUpload")(create_upload_files) app.post("/openFiles")(open_files) app.post("/translate")(translate) app.post("/follow")(follow) app.post("/auto")(auto) app.post("/aigc")(aigc) app.post("/path")(get_path) app.post("/test")(test) ================================================ FILE: sky-music-server/sky_music_server.py ================================================ import logging import os import queue from contextlib import asynccontextmanager import sys import threading import psutil import uvicorn from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from sky_music_apis import register_routes from windhide.static.global_variable import GlobalVariable from windhide.thread.frame_alive_thread import monitor_process from windhide.thread.hwnd_check_thread import start_thread as hwnd_check_thread from windhide.thread.queue_thread import music_start_tasks from windhide.thread.shortcut_thread import startThread as shortcut_thread os.environ["TORCHAUDIO_USE_TORCHCODEC"] = "0" # 设置 CPU 亲和性,避开与光遇相同核心运行 process = psutil.Process(os.getpid()) all_cores = list(range(psutil.cpu_count())) # 避开光遇核心(假设是 0)和系统核心(可选避开 1) available_cores = [core for core in all_cores if core not in [0, 1]] # 选择前两个空闲核心 cores_to_use = available_cores[:2] process.cpu_affinity(cores_to_use) @asynccontextmanager async def lifespan(app: FastAPI): # 检查是否为生产模式 if "--prod" in sys.argv: GlobalVariable.isProd = True else: GlobalVariable.isProd = False print("当前为开发模式") # 启动快捷键监听线程 shortcut_websocket_thread = threading.Thread(target=shortcut_thread, daemon=True) shortcut_websocket_thread.start() # 启动目标进程监控线程(仅在生产模式下) if GlobalVariable.isProd: target_process = "Sky_Music.exe" process_monitor_thread = threading.Thread(target=monitor_process, args=(target_process,), daemon=True) process_monitor_thread.start() # 启动窗口监听线程 hwnd_thread = threading.Thread(target=hwnd_check_thread, daemon=True) hwnd_thread.start() # 初始化播放任务队列 GlobalVariable.task_queue = queue.Queue() task_thread = threading.Thread(target=music_start_tasks, daemon=True) task_thread.start() yield # 👈 此处之后 FastAPI 正式启动 # 关闭时的清理逻辑(可选) print("应用正在关闭...") app = FastAPI(lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) register_routes(app) if __name__ == '__main__': try: uvicorn.run(app, host="localhost", port=9899, log_config=None) logging.info("服务启动完成") except Exception as e: logging.error(e) ================================================ FILE: sky-music-server/version.txt ================================================ # version.txt VSVersionInfo( ffi=FixedFileInfo( filevers=(1, 0, 0, 0), prodvers=(1, 0, 0, 0), mask=0x3f, flags=0x0, OS=0x40004, fileType=0x1, subtype=0x0, date=(0, 0) ), kids=[ StringFileInfo([ StringTable( '040904B0', [ StringStruct('CompanyName', 'WindHide'), StringStruct('FileDescription', '小星弹琴软件'), StringStruct('FileVersion', '1.0.0.0'), StringStruct('InternalName', 'sky-music-server'), StringStruct('LegalCopyright', '© 2025 WindHide'), StringStruct('OriginalFilename', 'sky-music-server.exe'), StringStruct('ProductName', '小星弹琴软件'), StringStruct('ProductVersion', '1.0.0.0') ] ) ]), VarFileInfo([VarStruct('Translation', [1033, 1200])]) ] ) ================================================ FILE: sky-music-server/windhide/auto/auto_thread.py ================================================ import threading from os import path import time import plyer from pynput.keyboard import Controller, Key from windhide.static.global_variable import GlobalVariable from windhide.utils.ocr_heart_utils import get_friend_model_position from windhide.utils.ocr_normal_utils import resetGameFrame from windhide.utils.path_util import getResourcesPath if GlobalVariable.cpu_type == 'Intel': from windhide.playRobot.intel_robot import mouse_move_to, key_press else: from windhide.playRobot.amd_robot import mouse_move_to, key_press keyboard = Controller() class HeartFireThread(threading.Thread): def __init__(self): super().__init__() self._running = False # 控制线程运行的标志位 self._lock = threading.Lock() # 保证线程安全 def run(self): """线程启动后执行的主逻辑""" self._running = True resetGameFrame() # mouse_wheel_scroll("down") # time.sleep(2) # key_press("g") time.sleep(2) # 先判断是不是第一页 while self._running: friend_button = get_friend_model_position(GlobalVariable.sheld)["button"] if len(friend_button) >= 2: break else: key_press("z") self.check_running() time.sleep(3) key_press("c") time.sleep(2) # 来到第一页 while True: if not self._running: break results = get_friend_model_position(GlobalVariable.sheld) button = results["button"] friend = results["friend"] if len(friend) != 0: for position in friend: if not self._running: break time.sleep(1) mouse_move_to(position[0], position[1]) key_press("space") time.sleep(0.1) key_press("space") time.sleep(1.5) key_press("f") time.sleep(1) key_press("ESC") # 下一页 time.sleep(1.5) key_press("c") time.sleep(3) else: # 如果没有,显示别是不是到第一页去了,否则直接下一页 if len(button) < 2: key_press("c") time.sleep(3) else: plyer.notification.notify( app_name='小星弹琴软件', app_icon=path.join(getResourcesPath("systemTools"), "icon.ico"), title='🔥🔥🔥🔥🔥🔥🔥🔥', message='点火结束🔥🔥🔥🔥🔥', timeout=1 ) return "点火结束" def stop(self): """安全停止线程""" with self._lock: self._running = False GlobalVariable.auto_thread = None def check_running(self): """检查线程是否正在运行,若未运行则安全退出""" with self._lock: if not self._running: raise StopIteration("线程已停止") def press_left(): keyboard.press(Key.left) time.sleep(0.1) keyboard.release(Key.left) ================================================ FILE: sky-music-server/windhide/musicToSheet/aigc_handler_sheet.py ================================================ import json import os import re # from google import genai from openai import OpenAI from windhide.static.global_variable import GlobalVariable from windhide.utils.path_util import getResourcesPath from windhide.utils.play_util import detect_encoding # def gemini_ai(model,filename, type_): # client = genai.Client(api_key=GlobalVariable.ai_token["Gemini"]) # song_data = loadSheetFile(type_,filename) # contents = GlobalVariable.duration_prompt if model == 'duration' else GlobalVariable.translate_prompt # contents = contents.replace("{input}",json.dumps(song_data["songNotes"])) # print(contents) # response = client.models.generate_content( # model="gemini-2.5-flash-preview-05-20", contents=contents # ) # return match_to_sheet(response.text, filename+"Gemini", song_data["bpm"], model) # noinspection PyTypeChecker def general_ai(model, filename, type_, platform): try: client = OpenAI( api_key=GlobalVariable.general_ai[platform]["key"], base_url=GlobalVariable.general_ai[platform]["url"] ) # 加载原始乐谱 song_data = loadSheetFile(type_, filename) target_length = len(song_data["songNotes"]) # 准备初始提示词 contents = GlobalVariable.duration_prompt if model == 'duration' else GlobalVariable.translate_prompt # 初始化消息上下文 messages = [ {"role": "system", "content": contents}, {"role": "user", "content": json.dumps(song_data["songNotes"])}, ] # 初始化状态变量 full_objects = [] max_retry = 20 retry_count = 0 total_segments = None current_segment = 1 print("🎼 开始生成 JSON 音符数据...") while retry_count < max_retry: # 向模型发起请求 response = client.chat.completions.create( model=GlobalVariable.general_ai[platform]["model"], messages=messages, stream=True, ) # 逐步拼接响应内容 partial = "" for chunk in response: if chunk.choices: delta = chunk.choices[0].delta if hasattr(delta, "content") and delta.content: partial += delta.content print(delta.content, end="", flush=True) # 提取段落信息 seg_cur, seg_tot = parse_segment_info(partial) if seg_cur and seg_tot: current_segment = seg_cur total_segments = seg_tot allow_extra_retry = 5 max_retry = total_segments + allow_extra_retry retry_count = current_segment print(f"\n🔍 当前第 {current_segment} 段 / 共 {total_segments} 段") # 提取 JSON 对象 new_objs = re.findall(r'\{[^{}]*?"time"\s*:\s*\d+[^{}]*?\}', partial) full_objects.extend(new_objs) print(f"\n📦 已收集 JSON 元素数:{len(full_objects)} / 目标 {target_length}") # ✅ 判断是否完成(段数或数量) if (total_segments and current_segment >= total_segments) or len(full_objects) >= target_length: json_text = f"[{','.join(full_objects)}]" saveSheetFile(json_text, filename + platform, song_data["bpm"], model) print("✅ JSON 输出已完成,已保存") return "ok" # 🚫 若未完成,续问 —— 精简上下文避免 token 超限 retry_count += 1 print(f"⚠️ 第 {current_segment} 段未完成,开始第 {retry_count} 次续问...") messages = [ {"role": "system", "content": contents}, {"role": "user", "content": json.dumps(song_data["songNotes"])}, {"role": "assistant", "content": partial}, {"role": "user", "content": "请继续上次未完成的 JSON 输出。"} ] print("❌ 达到最大重试次数,输出失败") return "output incomplete" except Exception as e: print("❗ 异常:", e) return "Nothing to do" def parse_segment_info(text): """ 解析格式如“第 N 段(共预计 M 段)”,返回 (N, M) """ pattern = r'第\s*(\d+)\s*段(共预计\s*(\d+)\s*段)' match = re.search(pattern, text) if match: current_segment = int(match.group(1)) total_segments = int(match.group(2)) return current_segment, total_segments return None, None def match_to_sheet(text, filename, bpm, model): start_idx = text.find('[') end_idx = text.rfind(']') if start_idx == -1 or end_idx == -1 or end_idx <= start_idx: return "error" json_str = text[start_idx:end_idx + 1] try: parsed = json.loads(json_str) if isinstance(parsed, list) and len(parsed) >= 20: print("✅ 成功解析 JSON 数组,长度:", len(parsed)) saveSheetFile(json.dumps(parsed), filename, bpm, model) return "ok" else: return "error" except json.JSONDecodeError as e: print("⚠️ JSON 解码失败:", e) return "error" def loadSheetFile(type, fileName): # 优化了文件路径构建 file_path = os.path.join(getResourcesPath(type), fileName + ".txt") with open(file_path, 'r', encoding=detect_encoding(file_path)) as file: data = json.load(file) song_notes = data[0].get("songNotes", []) bpm = data[0].get("bpm", []) if not song_notes: return [] return { "songNotes": song_notes, "bpm": bpm } def saveSheetFile(song_notes, fileName, bpm, model): if isinstance(song_notes, str): song_notes = json.loads(song_notes) output_file_name = fileName + "_AIGC_Handler_"+ ("延音" if model == "duration" else "间隔优化") output = [{ "name": output_file_name, "author": "skyMusic-WindHide", "transcribedBy": "WindHide's Software", "bpm": bpm, "bitsPerPage": 15, "pitchLevel": 0, "isComposed": True, "songNotes": song_notes, "isEncrypted": False, }] file_output_path =os.path.join(getResourcesPath("myTranslate"), f"{output_file_name}.txt") with open(file_output_path, 'w') as f: json.dump(output, f, indent=4, ensure_ascii=False) ================================================ FILE: sky-music-server/windhide/musicToSheet/music2html.py ================================================ import os from jinja2 import Template from platformdirs import user_desktop_dir html_template = """ {{ sheet_name }}

{{ sheet_name }}

文件由:WindHide's Sky Music弹琴软件创建 https://github.com/windhide/SkyMusicPlay-for-Windows

{% for line in content %}
{% for instr in line.instruments %}
{% for note in instr.notes %} <{{ note.type }} class="{{ note.class }}"> {% endfor %}
{% endfor %}
{% endfor %}
""" def generatorSheetHtml(sheet_name, convert_sheet): context = [] instr_index = 0 line_index = 0 line_data = {"id": f"line-{line_index}", "instruments": []} max_instruments_per_line = 13 switch_limit = {13: 11, 11: 13} # 交替限制 # 映射按键到音符类型 note_mapping = { "y": ("crdm", "r1"), "u": ("dmn", "r1"), "i": ("crc", "r1"), "o": ("dmn", "r1"), "p": ("crc", "r1"), "h": ("crc", "r2"), "j": ("dmn", "r2"), "k": ("crdm", "r2"), "l": ("dmn", "r2"), ";": ("crc", "r2"), "n": ("crc", "r3"), "m": ("dmn", "r3"), ",": ("crc", "r3"), ".": ("dmn", "r3"), "/": ("crdm", "r3"), } for index, key in enumerate(convert_sheet): notes = [] for char in "yuiophjkl;nm,./": if char in key: note_type, note_class = note_mapping[char] else: note_type, note_class = "d1", "" # 默认值 notes.append({"type": note_type, "class": note_class}) instrument = {"id": f"instr-{instr_index}", "notes": notes} line_data["instruments"].append(instrument) instr_index += 1 # 判断是否换行 if instr_index == max_instruments_per_line: context.append(line_data) line_index += 1 line_data = {"id": f"line-{line_index}", "instruments": []} instr_index = 0 max_instruments_per_line = switch_limit[max_instruments_per_line] # 处理最后一行 if line_data["instruments"]: context.append(line_data) # 渲染模板 template = Template(html_template) final_html = template.render(sheet_name=sheet_name, content=context) desktop_path = os.path.join(user_desktop_dir(), f"{sheet_name}.html") with open(desktop_path, "w", encoding="utf-8") as file: file.write(final_html) return "ok" ================================================ FILE: sky-music-server/windhide/musicToSheet/process_audio.py ================================================ import json import os import pretty_midi from windhide.musicToSheet.transfer_MID import inference from windhide.musicToSheet.vocals_split import split_vocals from windhide.static.global_variable import GlobalVariable from windhide.utils.path_util import getResourcesPath # 15个音符与键盘按键的映射 note_to_key = {"_C_c¹":{36:'1Key0',38:'1Key1',40:'1Key2',41:'1Key3',43:'1Key4',45:'1Key5',47:'1Key6',48:'1Key7',50:'1Key8',52:'1Key9',53:'1Key10',55:'1Key11',57:'1Key12',59:'1Key13',60:'1Key14'},"c_c²":{48:'1Key0',50:'1Key1',52:'1Key2',53:'1Key3',55:'1Key4',57:'1Key5',59:'1Key6',60:'1Key7',62:'1Key8',64:'1Key9',65:'1Key10',67:'1Key11',69:'1Key12',71:'1Key13',72:'1Key14'},'c¹_c³':{60:'1Key0',62:'1Key1',64:'1Key2',65:'1Key3',67:'1Key4',69:'1Key5',71:'1Key6',72:'1Key7',74:'1Key8',76:'1Key9',77:'1Key10',79:'1Key11',81:'1Key12',83:'1Key13',84:'1Key14'},"c²_c⁴":{72:'1Key0',74:'1Key1',76:'1Key2',77:'1Key3',79:'1Key4',81:'1Key5',83:'1Key6',84:'1Key7',86:'1Key8',88:'1Key9',89:'1Key10',91:'1Key11',93:'1Key12',95:'1Key13',96:'1Key14'},"c³_c⁵":{84:'1Key0',86:'1Key1',88:'1Key2',89:'1Key3',91:'1Key4',93:'1Key5',95:'1Key6',96:'1Key7',98:'1Key8',100:'1Key9',101:'1Key10',103:'1Key11',105:'1Key12',107:'1Key13',108:'1Key14'},"_C_c²":{36:'1Key-7',38:'1Key-6',40:'1Key-5',41:'1Key-4',43:'1Key-3',45:'1Key-2',47:'1Key-1',48:'1Key0',50:'1Key1',52:'1Key2',53:'1Key3',55:'1Key4',57:'1Key5',59:'1Key6',60:'1Key7',62:'1Key8',64:'1Key9',65:'1Key10',67:'1Key11',69:'1Key12',71:'1Key13',72:'1Key14'},"c_c³":{48:'1Key-7',50:'1Key-6',52:'1Key-5',53:'1Key-4',55:'1Key-3',57:'1Key-2',59:'1Key-1',60:'1Key0',62:'1Key1',64:'1Key2',65:'1Key3',67:'1Key4',69:'1Key5',71:'1Key6',72:'1Key7',74:'1Key8',76:'1Key9',77:'1Key10',79:'1Key11',81:'1Key12',83:'1Key13',84:'1Key14'},"c¹_c⁴":{60:'1Key-7',62:'1Key-6',64:'1Key-5',65:'1Key-4',67:'1Key-3',69:'1Key-2',71:'1Key-1',72:'1Key0',74:'1Key1',76:'1Key2',77:'1Key3',79:'1Key4',81:'1Key5',83:'1Key6',84:'1Key7',86:'1Key8',88:'1Key9',89:'1Key10',91:'1Key11',93:'1Key12',95:'1Key13',96:'1Key14'},"c²_c⁵":{72:'1Key-7',74:'1Key-6',76:'1Key-5',77:'1Key-4',79:'1Key-3',81:'1Key-2',83:'1Key-1',84:'1Key0',86:'1Key1',88:'1Key2',89:'1Key3',91:'1Key4',93:'1Key5',95:'1Key6',96:'1Key7',98:'1Key8',100:'1Key9',101:'1Key10',103:'1Key11',105:'1Key12',107:'1Key13',108:'1Key14'},"_C_c³":{36:'1Key-14',38:'1Key-13',40:'1Key-12',41:'1Key-11',43:'1Key-10',45:'1Key-9',47:'1Key-8',48:'1Key-7',50:'1Key-6',52:'1Key-5',53:'1Key-4',55:'1Key-3',57:'1Key-2',59:'1Key-1',60:'1Key0',62:'1Key1',64:'1Key2',65:'1Key3',67:'1Key4',69:'1Key5',71:'1Key6',72:'1Key7',74:'1Key8',76:'1Key9',77:'1Key10',79:'1Key11',81:'1Key12',83:'1Key13',84:'1Key14'},"c_c⁴":{48:'1Key-14',50:'1Key-13',52:'1Key-12',53:'1Key-11',55:'1Key-10',57:'1Key-9',59:'1Key-8',60:'1Key-7',62:'1Key-6',64:'1Key-5',65:'1Key-4',67:'1Key-3',69:'1Key-2',71:'1Key-1',72:'1Key0',74:'1Key1',76:'1Key2',77:'1Key3',79:'1Key4',81:'1Key5',83:'1Key6',84:'1Key7',86:'1Key8',88:'1Key9',89:'1Key10',91:'1Key11',93:'1Key12',95:'1Key13',96:'1Key14'},"c¹_c⁵":{60:'1Key-14',62:'1Key-13',64:'1Key-12',65:'1Key-11',67:'1Key-10',69:'1Key-9',71:'1Key-8',72:'1Key-7',74:'1Key-6',76:'1Key-5',77:'1Key-4',79:'1Key-3',81:'1Key-2',83:'1Key-1',84:'1Key0',86:'1Key1',88:'1Key2',89:'1Key3',91:'1Key4',93:'1Key5',95:'1Key6',96:'1Key7',98:'1Key8',100:'1Key9',101:'1Key10',103:'1Key11',105:'1Key12',107:'1Key13',108:'1Key14'},"_C_c⁴":{36:'1Key-14',38:'1Key-13',40:'1Key-12',41:'1Key-11',43:'1Key-10',45:'1Key-9',47:'1Key-8',48:'1Key-7',50:'1Key-6',52:'1Key-5',53:'1Key-4',55:'1Key-3',57:'1Key-2',59:'1Key-1',60:'1Key0',62:'1Key1',64:'1Key2',65:'1Key3',67:'1Key4',69:'1Key5',71:'1Key6',72:'1Key7',74:'1Key8',76:'1Key9',77:'1Key10',79:'1Key11',81:'1Key12',83:'1Key13',84:'1Key14',86:'1Key15',88:'1Key16',89:'1Key17',91:'1Key18',93:'1Key19',95:'1Key20',96:'1Key21',},"c_c⁵":{48:'1Key-14',50:'1Key-13',52:'1Key-12',53:'1Key-11',55:'1Key-10',57:'1Key-9',59:'1Key-8',60:'1Key-7',62:'1Key-6',64:'1Key-5',65:'1Key-4',67:'1Key-3',69:'1Key-2',71:'1Key-1',72:'1Key0',74:'1Key1',76:'1Key2',77:'1Key3',79:'1Key4',81:'1Key5',83:'1Key6',84:'1Key7',86:'1Key8',88:'1Key9',89:'1Key10',91:'1Key11',93:'1Key12',95:'1Key13',96:'1Key14',98:'1Key15',100:'1Key16',101:'1Key17',103:'1Key18',105:'1Key19',107:'1Key20',108:'1Key21',},"_C_c⁵":{36:'1Key-14',38:'1Key-13',40:'1Key-12',41:'1Key-11',43:'1Key-10',45:'1Key-9',47:'1Key-8',48:'1Key-7',50:'1Key-6',52:'1Key-5',53:'1Key-4',55:'1Key-3',57:'1Key-2',59:'1Key-1',60:"1Key0",62:"1Key1",64:"1Key2",65:"1Key3",67:"1Key4",69:"1Key5",71:"1Key6",72:"1Key7",74:"1Key8",76:"1Key9",77:"1Key10",79:"1Key11",81:"1Key12",83:"1Key13",84:"1Key14",86:"1Key15",88:"1Key16",89:"1Key17",91:"1Key18",93:"1Key19",95:"1Key20",96:"1Key21",98:"1Key22",100:"1Key23",101:"1Key24",103:"1Key25",105:"1Key26",107:"1Key27",108:"1Key28"},"_C_b":{36:'1Key0',38:'1Key1',40:'1Key2',41:'1Key3',43:'1Key4',45:'1Key5',47:'1Key6',48:'1Key7',50:'1Key8',52:'1Key9',53:'1Key10',55:'1Key11',57:'1Key12',59:'1Key13'},"c_b¹":{48:'1Key0',50:'1Key1',52:'1Key2',53:'1Key3',55:'1Key4',57:'1Key5',59:'1Key6',60:'1Key7',62:'1Key8',64:'1Key9',65:'1Key10',67:'1Key11',69:'1Key12',71:'1Key13'},'c¹_b²':{60:'1Key0',62:'1Key1',64:'1Key2',65:'1Key3',67:'1Key4',69:'1Key5',71:'1Key6',72:'1Key7',74:'1Key8',76:'1Key9',77:'1Key10',79:'1Key11',81:'1Key12',83:'1Key13'},"c²_b³":{72:'1Key0',74:'1Key1',76:'1Key2',77:'1Key3',79:'1Key4',81:'1Key5',83:'1Key6',84:'1Key7',86:'1Key8',88:'1Key9',89:'1Key10',91:'1Key11',93:'1Key12',95:'1Key13'},"c³_b⁴":{84:'1Key0',86:'1Key1',88:'1Key2',89:'1Key3',91:'1Key4',93:'1Key5',95:'1Key6',96:'1Key7',98:'1Key8',100:'1Key9',101:'1Key10',103:'1Key11',105:'1Key12',107:'1Key13'},"_C_b¹":{36:'1Key-7',38:'1Key-6',40:'1Key-5',41:'1Key-4',43:'1Key-3',45:'1Key-2',47:'1Key-1',48:'1Key0',50:'1Key1',52:'1Key2',53:'1Key3',55:'1Key4',57:'1Key5',59:'1Key6',60:'1Key7',62:'1Key8',64:'1Key9',65:'1Key10',67:'1Key11',69:'1Key12',71:'1Key13'},"c_b²":{48:'1Key-7',50:'1Key-6',52:'1Key-5',53:'1Key-4',55:'1Key-3',57:'1Key-2',59:'1Key-1',60:'1Key0',62:'1Key1',64:'1Key2',65:'1Key3',67:'1Key4',69:'1Key5',71:'1Key6',72:'1Key7',74:'1Key8',76:'1Key9',77:'1Key10',79:'1Key11',81:'1Key12',83:'1Key13'},"c¹_b³":{60:'1Key-7',62:'1Key-6',64:'1Key-5',65:'1Key-4',67:'1Key-3',69:'1Key-2',71:'1Key-1',72:'1Key0',74:'1Key1',76:'1Key2',77:'1Key3',79:'1Key4',81:'1Key5',83:'1Key6',84:'1Key7',86:'1Key8',88:'1Key9',89:'1Key10',91:'1Key11',93:'1Key12',95:'1Key13'},"c²_b⁴":{72:'1Key-7',74:'1Key-6',76:'1Key-5',77:'1Key-4',79:'1Key-3',81:'1Key-2',83:'1Key-1',84:'1Key0',86:'1Key1',88:'1Key2',89:'1Key3',91:'1Key4',93:'1Key5',95:'1Key6',96:'1Key7',98:'1Key8',100:'1Key9',101:'1Key10',103:'1Key11',105:'1Key12',107:'1Key13'},"_C_b²":{36:'1Key-14',38:'1Key-13',40:'1Key-12',41:'1Key-11',43:'1Key-10',45:'1Key-9',47:'1Key-8',48:'1Key-7',50:'1Key-6',52:'1Key-5',53:'1Key-4',55:'1Key-3',57:'1Key-2',59:'1Key-1',60:'1Key0',62:'1Key1',64:'1Key2',65:'1Key3',67:'1Key4',69:'1Key5',71:'1Key6',72:'1Key7',74:'1Key8',76:'1Key9',77:'1Key10',79:'1Key11',81:'1Key12',83:'1Key13'},"c_b³":{48:'1Key-14',50:'1Key-13',52:'1Key-12',53:'1Key-11',55:'1Key-10',57:'1Key-9',59:'1Key-8',60:'1Key-7',62:'1Key-6',64:'1Key-5',65:'1Key-4',67:'1Key-3',69:'1Key-2',71:'1Key-1',72:'1Key0',74:'1Key1',76:'1Key2',77:'1Key3',79:'1Key4',81:'1Key5',83:'1Key6',84:'1Key7',86:'1Key8',88:'1Key9',89:'1Key10',91:'1Key11',93:'1Key12',95:'1Key13'},"c¹_b⁴":{60:'1Key-14',62:'1Key-13',64:'1Key-12',65:'1Key-11',67:'1Key-10',69:'1Key-9',71:'1Key-8',72:'1Key-7',74:'1Key-6',76:'1Key-5',77:'1Key-4',79:'1Key-3',81:'1Key-2',83:'1Key-1',84:'1Key0',86:'1Key1',88:'1Key2',89:'1Key3',91:'1Key4',93:'1Key5',95:'1Key6',96:'1Key7',98:'1Key8',100:'1Key9',101:'1Key10',103:'1Key11',105:'1Key12',107:'1Key13'},"_C_b³":{36:'1Key-14',38:'1Key-13',40:'1Key-12',41:'1Key-11',43:'1Key-10',45:'1Key-9',47:'1Key-8',48:'1Key-7',50:'1Key-6',52:'1Key-5',53:'1Key-4',55:'1Key-3',57:'1Key-2',59:'1Key-1',60:'1Key0',62:'1Key1',64:'1Key2',65:'1Key3',67:'1Key4',69:'1Key5',71:'1Key6',72:'1Key7',74:'1Key8',76:'1Key9',77:'1Key10',79:'1Key11',81:'1Key12',83:'1Key13',84:'1Key14',86:'1Key15',88:'1Key16',89:'1Key17',91:'1Key18',93:'1Key19',95:'1Key20'},"c_b⁴":{48:'1Key-14',50:'1Key-13',52:'1Key-12',53:'1Key-11',55:'1Key-10',57:'1Key-9',59:'1Key-8',60:'1Key-7',62:'1Key-6',64:'1Key-5',65:'1Key-4',67:'1Key-3',69:'1Key-2',71:'1Key-1',72:'1Key0',74:'1Key1',76:'1Key2',77:'1Key3',79:'1Key4',81:'1Key5',83:'1Key6',84:'1Key7',86:'1Key8',88:'1Key9',89:'1Key10',91:'1Key11',93:'1Key12',95:'1Key13',96:'1Key14',98:'1Key15',100:'1Key16',101:'1Key17',103:'1Key18',105:'1Key19',107:'1Key20'},"_C_b⁴":{36:'1Key-14',38:'1Key-13',40:'1Key-12',41:'1Key-11',43:'1Key-10',45:'1Key-9',47:'1Key-8',48:'1Key-7',50:'1Key-6',52:'1Key-5',53:'1Key-4',55:'1Key-3',57:'1Key-2',59:'1Key-1',60:"1Key0",62:"1Key1",64:"1Key2",65:"1Key3",67:"1Key4",69:"1Key5",71:"1Key6",72:"1Key7",74:"1Key8",76:"1Key9",77:"1Key10",79:"1Key11",81:"1Key12",83:"1Key13",84:"1Key14",86:"1Key15",88:"1Key16",89:"1Key17",91:"1Key18",93:"1Key19",95:"1Key20",96:"1Key21",98:"1Key22",100:"1Key23",101:"1Key24",103:"1Key25",105:"1Key26",107:"1Key27"}} extra_note_to_key = {"_C_c¹":{37:['1Key0','1Key1'],39:['1Key1','1Key2'],42:['1Key3','1Key4'],44:['1Key4','1Key5'],46:['1Key5','1Key6'],49:['1Key6','1Key7'],51:['1Key8','1Key9'],54:['1Key10','1Key11'],56:['1Key11','1Key12'],58:['1Key12','1Key13'],},"c_c²":{49:['1Key0','1Key1'],51:['1Key1','1Key2'],54:['1Key3','1Key4'],56:['1Key4','1Key5'],58:['1Key5','1Key6'],61:['1Key6','1Key7'],63:['1Key8','1Key9'],66:['1Key10','1Key11'],68:['1Key11','1Key12'],70:['1Key12','1Key13'],},'c¹_c³':{61:['1Key0','1Key1'],63:['1Key1','1Key2'],66:['1Key3','1Key4'],68:['1Key4','1Key5'],70:['1Key5','1Key6'],73:['1Key6','1Key7'],75:['1Key8','1Key9'],78:['1Key10','1Key11'],80:['1Key11','1Key12'],82:['1Key12','1Key13'],},"c²_c⁴":{73:['1Key0','1Key1'],75:['1Key1','1Key2'],78:['1Key3','1Key4'],80:['1Key4','1Key5'],82:['1Key5','1Key6'],85:['1Key6','1Key7'],87:['1Key8','1Key9'],90:['1Key10','1Key11'],92:['1Key11','1Key12'],94:['1Key12','1Key13'],},"c³_c⁵":{85:['1Key0','1Key1'],87:['1Key1','1Key2'],90:['1Key3','1Key4'],92:['1Key4','1Key5'],94:['1Key5','1Key6'],97:['1Key6','1Key7'],99:['1Key8','1Key9'],102:['1Key10','1Key11'],104:['1Key11','1Key12'],106:['1Key12','1Key13'],},"_C_c²":{37:['1Key-7','1Key-6'],39:['1Key-6','1Key-5'],42:['1Key-4','1Key-3'],44:['1Key-3','1Key-2'],46:['1Key-2','1Key-1'],49:['1Key0','1Key1'],51:['1Key1','1Key2'],54:['1Key3','1Key4'],56:['1Key4','1Key5'],58:['1Key5','1Key6'],61:['1Key6','1Key7'],63:['1Key8','1Key9'],66:['1Key10','1Key11'],68:['1Key11','1Key12'],70:['1Key12','1Key13'],},"c_c³":{49:['1Key-7','1Key-6'],51:['1Key-6','1Key-5'],54:['1Key-4','1Key-3'],56:['1Key-3','1Key-2'],58:['1Key-2','1Key-1'],61:['1Key0','1Key1'],63:['1Key1','1Key2'],66:['1Key3','1Key4'],68:['1Key4','1Key5'],70:['1Key5','1Key6'],73:['1Key6','1Key7'],75:['1Key8','1Key9'],78:['1Key10','1Key11'],80:['1Key11','1Key12'],82:['1Key12','1Key13'],},"c¹_c⁴":{61:['1Key-7','1Key-6'],63:['1Key-6','1Key-5'],66:['1Key-4','1Key-3'],68:['1Key-3','1Key-2'],70:['1Key-2','1Key-1'],73:['1Key0','1Key1'],75:['1Key1','1Key2'],78:['1Key3','1Key4'],80:['1Key4','1Key5'],82:['1Key5','1Key6'],85:['1Key6','1Key7'],87:['1Key8','1Key9'],90:['1Key10','1Key11'],92:['1Key11','1Key12'],94:['1Key12','1Key13'],},"c²_c⁵":{73:['1Key-7','1Key-6'],75:['1Key-6','1Key-5'],78:['1Key-4','1Key-3'],80:['1Key-3','1Key-2'],82:['1Key-2','1Key-1'],85:['1Key0','1Key1'],87:['1Key1','1Key2'],90:['1Key3','1Key4'],92:['1Key4','1Key5'],94:['1Key5','1Key6'],97:['1Key6','1Key7'],99:['1Key8','1Key9'],102:['1Key10','1Key11'],104:['1Key11','1Key12'],106:['1Key12','1Key13'],},"_C_c³":{37:['1Key-14','1Key-13'],39:['1Key-13','1Key-12'],42:['1Key-11','1Key-10'],44:['1Key-10','1Key-9'],46:['1Key-9','1Key-8'],49:['1Key-7','1Key-6'],51:['1Key-6','1Key-5'],54:['1Key-4','1Key-3'],56:['1Key-3','1Key-2'],58:['1Key-2','1Key-1'],61:['1Key0','1Key1'],63:['1Key1','1Key2'],66:['1Key3','1Key4'],68:['1Key4','1Key5'],70:['1Key5','1Key6'],73:['1Key6','1Key7'],75:['1Key8','1Key9'],78:['1Key10','1Key11'],80:['1Key11','1Key12'],82:['1Key12','1Key13'],},"c_c⁴":{49:['1Key-14','1Key-13'],51:['1Key-13','1Key-12'],54:['1Key-11','1Key-10'],56:['1Key-10','1Key-9'],58:['1Key-9','1Key-8'],61:['1Key-7','1Key-6'],63:['1Key-6','1Key-5'],66:['1Key-4','1Key-3'],68:['1Key-3','1Key-2'],70:['1Key-2','1Key-1'],73:['1Key0','1Key1'],75:['1Key1','1Key2'],78:['1Key3','1Key4'],80:['1Key4','1Key5'],82:['1Key5','1Key6'],85:['1Key6','1Key7'],87:['1Key8','1Key9'],90:['1Key10','1Key11'],92:['1Key11','1Key12'],94:['1Key12','1Key13'],},"c¹_c⁵":{61:['1Key-14','1Key-13'],63:['1Key-13','1Key-12'],66:['1Key-11','1Key-10'],68:['1Key-10','1Key-9'],70:['1Key-9','1Key-8'],73:['1Key-7','1Key-6'],75:['1Key-6','1Key-5'],78:['1Key-4','1Key-3'],80:['1Key-3','1Key-2'],82:['1Key-2','1Key-1'],85:['1Key0','1Key1'],87:['1Key1','1Key2'],90:['1Key3','1Key4'],92:['1Key4','1Key5'],94:['1Key5','1Key6'],97:['1Key6','1Key7'],99:['1Key8','1Key9'],102:['1Key10','1Key11'],104:['1Key11','1Key12'],106:['1Key12','1Key13'],},"_C_c⁴":{37:['1Key-14','1Key-13'],39:['1Key-13','1Key-12'],42:['1Key-11','1Key-10'],44:['1Key-10','1Key-9'],46:['1Key-9','1Key-8'],49:['1Key-7','1Key-6'],51:['1Key-6','1Key-5'],54:['1Key-4','1Key-3'],56:['1Key-3','1Key-2'],58:['1Key-2','1Key-1'],61:['1Key0','1Key1'],63:['1Key1','1Key2'],66:['1Key3','1Key4'],68:['1Key4','1Key5'],70:['1Key5','1Key6'],73:['1Key6','1Key7'],75:['1Key8','1Key9'],78:['1Key10','1Key11'],80:['1Key11','1Key12'],82:['1Key12','1Key13'],85:['1Key14','1Key15'],87:['1Key15','1Key16'],90:['1Key17','1Key18'],92:['1Key18','1Key19'],94:['1Key19','1Key20'],},"c_c⁵":{49:['1Key-14','1Key-13'],51:['1Key-13','1Key-12'],54:['1Key-11','1Key-10'],56:['1Key-10','1Key-9'],58:['1Key-9','1Key-8'],61:['1Key-7','1Key-6'],63:['1Key-6','1Key-5'],66:['1Key-4','1Key-3'],68:['1Key-3','1Key-2'],70:['1Key-2','1Key-1'],73:['1Key0','1Key1'],75:['1Key1','1Key2'],78:['1Key3','1Key4'],80:['1Key4','1Key5'],82:['1Key5','1Key6'],85:['1Key6','1Key7'],87:['1Key8','1Key9'],90:['1Key10','1Key11'],92:['1Key11','1Key12'],94:['1Key12','1Key13'],97:['1Key14','1Key15'],99:['1Key15','1Key16'],102:['1Key17','1Key18'],104:['1Key18','1Key19'],106:['1Key19','1Key20'],},"_C_c⁵":{37:['1Key-14','1Key-13'],39:['1Key-13','1Key-12'],42:['1Key-11','1Key-10'],44:['1Key-10','1Key-9'],46:['1Key-9','1Key-8'],49:['1Key-7','1Key-6'],51:['1Key-6','1Key-5'],54:['1Key-4','1Key-3'],56:['1Key-3','1Key-2'],58:['1Key-2','1Key-1'],61:['1Key0','1Key1'],63:['1Key1','1Key2'],66:['1Key3','1Key4'],68:['1Key4','1Key5'],70:['1Key5','1Key6'],73:['1Key6','1Key7'],75:['1Key8','1Key9'],78:['1Key10','1Key11'],80:['1Key11','1Key12'],82:['1Key12','1Key13'],85:['1Key14','1Key15'],87:['1Key15','1Key16'],90:['1Key17','1Key18'],92:['1Key18','1Key19'],94:['1Key19','1Key20'],97:['1Key21','1Key22'],99:['1Key22','1Key23'],102:['1Key24','1Key25'],104:['1Key25','1Key26'],106:['1Key26','1Key27'],},"_C_b":{37:['1Key0','1Key1'],39:['1Key1','1Key2'],42:['1Key3','1Key4'],44:['1Key4','1Key5'],46:['1Key5','1Key6'],49:['1Key6','1Key7'],51:['1Key8','1Key9'],54:['1Key10','1Key11'],56:['1Key11','1Key12'],58:['1Key12','1Key13'],},"c_b¹":{49:['1Key0','1Key1'],51:['1Key1','1Key2'],54:['1Key3','1Key4'],56:['1Key4','1Key5'],58:['1Key5','1Key6'],61:['1Key6','1Key7'],63:['1Key8','1Key9'],66:['1Key10','1Key11'],68:['1Key11','1Key12'],70:['1Key12','1Key13'],},'c¹_b²':{61:['1Key0','1Key1'],63:['1Key1','1Key2'],66:['1Key3','1Key4'],68:['1Key4','1Key5'],70:['1Key5','1Key6'],73:['1Key6','1Key7'],75:['1Key8','1Key9'],78:['1Key10','1Key11'],80:['1Key11','1Key12'],82:['1Key12','1Key13'],},"c²_b³":{73:['1Key0','1Key1'],75:['1Key1','1Key2'],78:['1Key3','1Key4'],80:['1Key4','1Key5'],82:['1Key5','1Key6'],85:['1Key6','1Key7'],87:['1Key8','1Key9'],90:['1Key10','1Key11'],92:['1Key11','1Key12'],94:['1Key12','1Key13'],},"c³_b⁴":{85:['1Key0','1Key1'],87:['1Key1','1Key2'],90:['1Key3','1Key4'],92:['1Key4','1Key5'],94:['1Key5','1Key6'],97:['1Key6','1Key7'],99:['1Key8','1Key9'],102:['1Key10','1Key11'],104:['1Key11','1Key12'],106:['1Key12','1Key13'],},"_C_b¹":{37:['1Key-7','1Key-6'],39:['1Key-6','1Key-5'],42:['1Key-4','1Key-3'],44:['1Key-3','1Key-2'],46:['1Key-2','1Key-1'],49:['1Key0','1Key1'],51:['1Key1','1Key2'],54:['1Key3','1Key4'],56:['1Key4','1Key5'],58:['1Key5','1Key6'],61:['1Key6','1Key7'],63:['1Key8','1Key9'],66:['1Key10','1Key11'],68:['1Key11','1Key12'],70:['1Key12','1Key13'],},"c_b²":{49:['1Key-7','1Key-6'],51:['1Key-6','1Key-5'],54:['1Key-4','1Key-3'],56:['1Key-3','1Key-2'],58:['1Key-2','1Key-1'],61:['1Key0','1Key1'],63:['1Key1','1Key2'],66:['1Key3','1Key4'],68:['1Key4','1Key5'],70:['1Key5','1Key6'],73:['1Key6','1Key7'],75:['1Key8','1Key9'],78:['1Key10','1Key11'],80:['1Key11','1Key12'],82:['1Key12','1Key13'],},"c¹_b³":{61:['1Key-7','1Key-6'],63:['1Key-6','1Key-5'],66:['1Key-4','1Key-3'],68:['1Key-3','1Key-2'],70:['1Key-2','1Key-1'],73:['1Key0','1Key1'],75:['1Key1','1Key2'],78:['1Key3','1Key4'],80:['1Key4','1Key5'],82:['1Key5','1Key6'],85:['1Key6','1Key7'],87:['1Key8','1Key9'],90:['1Key10','1Key11'],92:['1Key11','1Key12'],94:['1Key12','1Key13'],},"c²_b⁴":{73:['1Key-7','1Key-6'],75:['1Key-6','1Key-5'],78:['1Key-4','1Key-3'],80:['1Key-3','1Key-2'],82:['1Key-2','1Key-1'],85:['1Key0','1Key1'],87:['1Key1','1Key2'],90:['1Key3','1Key4'],92:['1Key4','1Key5'],94:['1Key5','1Key6'],97:['1Key6','1Key7'],99:['1Key8','1Key9'],102:['1Key10','1Key11'],104:['1Key11','1Key12'],106:['1Key12','1Key13'],},"_C_b²":{37:['1Key-14','1Key-13'],39:['1Key-13','1Key-12'],42:['1Key-11','1Key-10'],44:['1Key-10','1Key-9'],46:['1Key-9','1Key-8'],49:['1Key-7','1Key-6'],51:['1Key-6','1Key-5'],54:['1Key-4','1Key-3'],56:['1Key-3','1Key-2'],58:['1Key-2','1Key-1'],61:['1Key0','1Key1'],63:['1Key1','1Key2'],66:['1Key3','1Key4'],68:['1Key4','1Key5'],70:['1Key5','1Key6'],73:['1Key6','1Key7'],75:['1Key8','1Key9'],78:['1Key10','1Key11'],80:['1Key11','1Key12'],82:['1Key12','1Key13'],},"c_b³":{49:['1Key-14','1Key-13'],51:['1Key-13','1Key-12'],54:['1Key-11','1Key-10'],56:['1Key-10','1Key-9'],58:['1Key-9','1Key-8'],61:['1Key-7','1Key-6'],63:['1Key-6','1Key-5'],66:['1Key-4','1Key-3'],68:['1Key-3','1Key-2'],70:['1Key-2','1Key-1'],73:['1Key0','1Key1'],75:['1Key1','1Key2'],78:['1Key3','1Key4'],80:['1Key4','1Key5'],82:['1Key5','1Key6'],85:['1Key6','1Key7'],87:['1Key8','1Key9'],90:['1Key10','1Key11'],92:['1Key11','1Key12'],94:['1Key12','1Key13'],},"c¹_b⁴":{61:['1Key-14','1Key-13'],63:['1Key-13','1Key-12'],66:['1Key-11','1Key-10'],68:['1Key-10','1Key-9'],70:['1Key-9','1Key-8'],73:['1Key-7','1Key-6'],75:['1Key-6','1Key-5'],78:['1Key-4','1Key-3'],80:['1Key-3','1Key-2'],82:['1Key-2','1Key-1'],85:['1Key0','1Key1'],87:['1Key1','1Key2'],90:['1Key3','1Key4'],92:['1Key4','1Key5'],94:['1Key5','1Key6'],97:['1Key6','1Key7'],99:['1Key8','1Key9'],102:['1Key10','1Key11'],104:['1Key11','1Key12'],106:['1Key12','1Key13'],},"_C_b³":{37:['1Key-14','1Key-13'],39:['1Key-13','1Key-12'],42:['1Key-11','1Key-10'],44:['1Key-10','1Key-9'],46:['1Key-9','1Key-8'],49:['1Key-7','1Key-6'],51:['1Key-6','1Key-5'],54:['1Key-4','1Key-3'],56:['1Key-3','1Key-2'],58:['1Key-2','1Key-1'],61:['1Key0','1Key1'],63:['1Key1','1Key2'],66:['1Key3','1Key4'],68:['1Key4','1Key5'],70:['1Key5','1Key6'],73:['1Key6','1Key7'],75:['1Key8','1Key9'],78:['1Key10','1Key11'],80:['1Key11','1Key12'],82:['1Key12','1Key13'],85:['1Key14','1Key15'],87:['1Key15','1Key16'],90:['1Key17','1Key18'],92:['1Key18','1Key19'],94:['1Key19','1Key20'],},"c_b⁴":{49:['1Key-14','1Key-13'],51:['1Key-13','1Key-12'],54:['1Key-11','1Key-10'],56:['1Key-10','1Key-9'],58:['1Key-9','1Key-8'],61:['1Key-7','1Key-6'],63:['1Key-6','1Key-5'],66:['1Key-4','1Key-3'],68:['1Key-3','1Key-2'],70:['1Key-2','1Key-1'],73:['1Key0','1Key1'],75:['1Key1','1Key2'],78:['1Key3','1Key4'],80:['1Key4','1Key5'],82:['1Key5','1Key6'],85:['1Key6','1Key7'],87:['1Key8','1Key9'],90:['1Key10','1Key11'],92:['1Key11','1Key12'],94:['1Key12','1Key13'],97:['1Key14','1Key15'],99:['1Key15','1Key16'],102:['1Key17','1Key18'],104:['1Key18','1Key19'],106:['1Key19','1Key20'],},"_C_b⁴":{37:['1Key-14','1Key-13'],39:['1Key-13','1Key-12'],42:['1Key-11','1Key-10'],44:['1Key-10','1Key-9'],46:['1Key-9','1Key-8'],49:['1Key-7','1Key-6'],51:['1Key-6','1Key-5'],54:['1Key-4','1Key-3'],56:['1Key-3','1Key-2'],58:['1Key-2','1Key-1'],61:['1Key0','1Key1'],63:['1Key1','1Key2'],66:['1Key3','1Key4'],68:['1Key4','1Key5'],70:['1Key5','1Key6'],73:['1Key6','1Key7'],75:['1Key8','1Key9'],78:['1Key10','1Key11'],80:['1Key11','1Key12'],82:['1Key12','1Key13'],85:['1Key14','1Key15'],87:['1Key15','1Key16'],90:['1Key17','1Key18'],92:['1Key18','1Key19'],94:['1Key19','1Key20'],97:['1Key21','1Key22'],99:['1Key22','1Key23'],102:['1Key24','1Key25'],104:['1Key25','1Key26'],106:['1Key26','1Key27'],}} # 特殊音符的映射规则 special_note_mapping = {'_C_c¹':{62:57,64:59,65:60,},'c_c²':{74:69,76:71,77:72,},'c¹_c³':{86:81,88:83,89:84,},'c²_c⁴':{98:93,100:95,101:96,},'c³_c⁵':{110:105,112:107,113:108,},"_C_c²":{74:69,76:71,77:72,},"c_c³":{86:81,88:83,89:84,},"c¹_c⁴":{98:93,100:95,101:96,},"c²_c⁵":{110:105,112:107,113:108,},"_C_c³":{86:81,88:83,89:84,},"c_c⁴":{98:93,100:95,101:96,},"c¹_c⁵":{110:105,112:107,113:108,},"_C_c⁴":{98:93,100:95,101:96,},"c_c⁵":{110:105,112:107,113:108,},"_C_c⁵":{110:105,112:107,113:108,},"_C_b":{62:57,64:59,},"c_b¹":{74:69,76:71,},"c¹_b²":{86:81,88:83,},"c²_b³":{98:93,100:95,},"c³_b⁴":{110:105,112:107,},"_C_b¹":{74:69,76:71,},"c_b²":{86:81,88:83,},"c¹_b³":{98:93,100:95,},"c²_b⁴":{110:105,112:107,},"_C_b²":{86:81,88:83,},"c_b³":{98:93,100:95,},"c¹_b⁴":{110:105,112:107,},"_C_b³":{98:93,100:95,},"c_b⁴":{110:105,112:107,},"_C_b⁴":{110:105,112:107,}} # 根据 BPM 动态调整时间合并阈值 def get_dynamic_time_merge_threshold(bpm): return max(GlobalVariable.merge_min, min(GlobalVariable.merge_max, int(60000 / bpm / 4))) # 限制阈值在 范围区间 # 15 个音符与键盘按键的映射 def get_bpm_from_midi(midi_file_path): midi = pretty_midi.PrettyMIDI(midi_file_path) tempos = midi.get_tempo_changes() return tempos[1][0] if len(tempos[1]) > 0 else 120 def merge_keys(keys): key_count = len(keys) return [f"{key_count}Key{key.replace('1Key', '')}" for key in keys] def process_midi_to_txt(input_path, output_path, version): midi = pretty_midi.PrettyMIDI(input_path) bpm = get_bpm_from_midi(input_path) time_merge_threshold = get_dynamic_time_merge_threshold(bpm) notes = [] for instrument in midi.instruments: if not instrument.is_drum: for note in instrument.notes: pitch, time, velocity= note.pitch, int(note.start * 1000), note.velocity if velocity < GlobalVariable.velocity_filter: continue if pitch in note_to_key[version]: notes.append({'time': time, 'key': note_to_key[version][pitch]}) elif GlobalVariable.semitone_switch is True and pitch in extra_note_to_key[version]: for extra_key in extra_note_to_key[version][pitch]: notes.append({'time': time, 'key': extra_key}) elif GlobalVariable.detail_switch is True and pitch in special_note_mapping[version]: notes.append({'time': time, 'key': note_to_key[version][special_note_mapping[version][pitch]]}) notes.sort(key=lambda x: x['time']) merged_notes, last_time, temp_keys = [], None, [] for note in notes: if last_time is None or note['time'] - last_time <= time_merge_threshold: temp_keys.append(note['key']) else: merged_notes.extend({'time': last_time, 'key': k} for k in merge_keys(temp_keys)) temp_keys = [note['key']] last_time = note['time'] merged_notes.extend({'time': last_time, 'key': k} for k in merge_keys(temp_keys)) output = [{ "name": os.path.basename(input_path).replace("_basic_pitch.mid","_Range") + version, "author": "skyMusic-WindHide", "transcribedBy": "WindHide's Software", "bpm": bpm, "bitsPerPage": 15, "pitchLevel": 0, "isComposed": True, "songNotes": merged_notes, "isEncrypted": False, }] with open(output_path, 'w') as f: json.dump(output, f, indent=4) return 100 def process_directory_with_progress(typeStr, output_dir=getResourcesPath("myTranslate")): GlobalVariable.overall_progress = 0 os.makedirs(output_dir, exist_ok=True) files_to_process = [f for f in os.listdir(getResourcesPath("translateOriginalMusic")) if f.endswith(('.mp3', '.ogg', '.wav', '.flac', '.mid', '.m4a'))] total_files = len(files_to_process) tranMap = [] if GlobalVariable.is_singular: mapping = { "2": ["_C_c¹", "c_c²", "c¹_c³", "c²_c⁴", "c³_c⁵"], "3": ["_C_c²", "c_c³", "c¹_c⁴", "c²_c⁵"], "4": ["_C_c³", "c_c⁴", "c¹_c⁵"], "5": ["_C_c⁴", "c_c⁵"], "6": ["_C_c⁵"] } else: mapping = { "2": ["_C_b", "c_b¹", "c¹_b²", "c²_b³", "c³_b⁴"], "3": ["_C_b¹", "c_b²", "c¹_b³", "c²_b⁴"], "4": ["_C_b²", "c_b³", "c¹_b⁴"], "5": ["_C_b³", "c_b⁴"], "6": ["_C_b⁴"] } for key in mapping: if key in typeStr: tranMap.extend(mapping[key]) if not total_files: print("没有找到需要处理的文件") return if GlobalVariable.split_switch: for idx, file in enumerate(files_to_process): if "_ok" in file or "_vocals.flac" in file or "_beat.flac" in file: continue musicFilePath = os.path.join(getResourcesPath("translateOriginalMusic"), file) split_vocals(musicFilePath) # 处理人声分离 files_to_process = [f for f in os.listdir(getResourcesPath("translateOriginalMusic")) if f.endswith(('.mp3', '.ogg', '.wav', '.flac', '.mid', '.m4a'))] for idx, file in enumerate(files_to_process): if "_ok" in file: continue GlobalVariable.now_translate_text = [f"{idx + 1}/{total_files}", file] fileNameNoEnd = file.rsplit('.', 1)[0] midFilePath = os.path.join(getResourcesPath("translateMID"), f"{fileNameNoEnd}_basic_pitch") musicFilePath = os.path.join(getResourcesPath("translateOriginalMusic"), file) if not file.endswith(".mid"): inference(input_path=musicFilePath) else: midFilePath = os.path.join(getResourcesPath("translateOriginalMusic"), f"{fileNameNoEnd}") for version in tranMap: process_midi_to_txt(midFilePath + ".mid", os.path.join(output_dir, f"{fileNameNoEnd}_Range_{version}.txt"), version) new_file_path = os.path.join(getResourcesPath("translateOriginalMusic"), f"{fileNameNoEnd}_ok.{file.split('.')[-1]}") os.rename(os.path.join(getResourcesPath("translateOriginalMusic"), file), new_file_path) print(f"已将文件 {file} 重命名为 {new_file_path}") GlobalVariable.overall_progress = ((idx + 1) / total_files) * 100 GlobalVariable.overall_progress = 100 ================================================ FILE: sky-music-server/windhide/musicToSheet/transfer_MID.py ================================================ from windhide.utils.path_util import getResourcesPath def inference(input_path): from basic_pitch import ICASSP_2022_MODEL_PATH from basic_pitch.inference import predict_and_save output_midi_path = getResourcesPath("translateMID") try: predict_and_save([ input_path ], output_midi_path, True, False, False, False, ICASSP_2022_MODEL_PATH ) except Exception as e: print(e) ================================================ FILE: sky-music-server/windhide/musicToSheet/vocals_split.py ================================================ import os import shutil import demucs.separate from windhide.utils.path_util import getResourcesPath def split_vocals(musicFilePath): ffmpeg_dir = os.path.join(getResourcesPath("systemTools"), "ffmpeg") if not os.path.exists(os.path.join(ffmpeg_dir, "ffmpeg.exe")): raise RuntimeError("FFmpeg 未找到,请确认路径正确") # 注入 PATH os.environ["PATH"] = ffmpeg_dir + os.pathsep + os.environ.get("PATH", "") os.environ['TORCH_HOME'] = os.path.join(getResourcesPath("systemTools"), "modelData") demucs.separate.main([ "--flac", "--two-stems", "vocals", "-o", getResourcesPath("splitMusic"), "-n", "mdx_extra", musicFilePath ]) # 转换完成后 # 文件移动到translateOriginalMusic filename = musicFilePath.split("\\")[-1].split(".")[0] os.path.join(getResourcesPath("splitMusic"), "mdx_extra", filename) no_vocals_path = os.path.join(getResourcesPath("splitMusic"), "mdx_extra", filename, "no_vocals.flac") vocals_path = os.path.join(getResourcesPath("splitMusic"), "mdx_extra", filename, "vocals.flac") # 移动文件 targetFolder = getResourcesPath("translateOriginalMusic") shutil.move(no_vocals_path, f"{targetFolder}/{filename + '_beat.flac'}") shutil.move(vocals_path, f"{targetFolder}/{filename + '_vocals.flac'}") shutil.rmtree(os.path.join(getResourcesPath("splitMusic"), "mdx_extra", filename)) # 删除子文件夹 ================================================ FILE: sky-music-server/windhide/playRobot/amd_robot.py ================================================ import ctypes import threading import time from ctypes import windll import keyboard import pyautogui import win32con import win32gui from windhide.static.global_variable import GlobalVariable from windhide.thread.amd_play_thread import ControlledThread from windhide.utils.path_util import convert_notes_to_delayed_format, convert_json_to_play PostMessageW = windll.user32.PostMessageW # ok 消息队列 SendMessageW = windll.user32.SendMessageW # ok 立即处理 MapVirtualKeyW = windll.user32.MapVirtualKeyW VkKeyScanW = windll.user32.VkKeyScanW user32 = ctypes.windll.user32 WM_KEYDOWN = 0x100 WM_KEYUP = 0x101 pyautogui.FAILSAFE = False def send_single_key_to_window_task(key, duration): """发送单个按键,减少延迟""" key_down(key) time.sleep(duration/1000 + GlobalVariable.duration) key_up(key) def send_multiple_key_to_window_task(keys, duration): """发送组合按键,减少延迟""" for key in keys: key_down(key) time.sleep(duration/1000 + GlobalVariable.duration) for key in keys: key_up(key) # 这里是跟弹独立出来的 def send_multiple_key_press(keys): for key in keys: keyboard.press(key) def send_multiple_key_release(keys): for key in keys: keyboard.release(key) # 现在给模拟器和跟弹在用了 def send_single_key_to_window_follow(key, duration): """发送单个按键,减少延迟""" keyboard.press(key) time.sleep(duration/1000 + GlobalVariable.duration) keyboard.release(key) # 现在给模拟器和跟弹在用了 def send_multiple_key_to_window_follow(keys, duration): """发送组合按键,减少延迟""" for key in keys: keyboard.press(key) time.sleep(duration/1000 + GlobalVariable.duration) for key in keys: keyboard.release(key) def execute_in_thread(target, *args, **kwargs): """通用线程执行器,采用线程池管理""" thread = threading.Thread(target=target, args=args, kwargs=kwargs) thread.daemon = True # 将线程设置为守护线程,程序退出时自动结束线程 thread.start() return thread def send_single_key_to_window(key, duration): """发送单个按键(新线程中执行)""" if GlobalVariable.compatibility_mode: execute_in_thread(send_single_key_to_window_follow, key,duration) else: execute_in_thread(send_single_key_to_window_task, key,duration) def send_multiple_key_to_window(keys, duration): """发送组合按键(新线程中执行)""" if GlobalVariable.compatibility_mode: execute_in_thread(send_multiple_key_to_window_follow, keys,duration) else: execute_in_thread(send_multiple_key_to_window_task, keys,duration) def playMusic(fileName, type): """优化音乐播放逻辑,只加载乐谱数据一次""" convert_notes_to_delayed_format(fileName, type) if GlobalVariable.thread is not None: stop() GlobalVariable.thread = ControlledThread() GlobalVariable.thread.start() def playMusic_edit(text): """优化音乐播放逻辑,只加载乐谱数据一次""" convert_json_to_play(text) if GlobalVariable.thread is not None: stop() GlobalVariable.thread = ControlledThread() GlobalVariable.thread.start() def resume(): """恢复播放""" if GlobalVariable.thread: GlobalVariable.thread.resume() def pause(): """暂停播放""" if GlobalVariable.thread: GlobalVariable.thread.pause() def stop(): """停止播放""" if GlobalVariable.thread: GlobalVariable.thread.stop() GlobalVariable.set_progress = 0 GlobalVariable.thread = None # 点击,按下 def mouse_move_to(x: int, y: int): # 获取窗口的屏幕位置 window_rect = win32gui.GetWindowRect(GlobalVariable.window["hWnd"]) # 返回 (left, top, right, bottom) window_x, window_y = window_rect[0], window_rect[1] client_x = window_x + x client_y = window_y + y win32gui.SendMessage(GlobalVariable.window["hWnd"], win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) pyautogui.moveTo(client_x, client_y, duration=0) # 核心 def key_press(key: str): key = key.lower() if key in special_keys: vk_code, scan_code = special_keys[key] else: # 普通按键的处理 vk_code = VkKeyScanW(ctypes.c_wchar(key)) scan_code = keyboard.key_to_scan_codes(key)[0] if key != '/' else keyboard.key_to_scan_codes(key)[1] lparam = (scan_code << 16) | 1 if GlobalVariable.is_post_w: PostMessageW(GlobalVariable.window["hWnd"], win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) PostMessageW(GlobalVariable.window["hWnd"], WM_KEYDOWN, vk_code, lparam) time.sleep(0.01) PostMessageW(GlobalVariable.window["hWnd"], WM_KEYUP, vk_code, lparam) else: SendMessageW(GlobalVariable.window["hWnd"], win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) SendMessageW(GlobalVariable.window["hWnd"], WM_KEYDOWN, vk_code, lparam) time.sleep(0.01) SendMessageW(GlobalVariable.window["hWnd"], WM_KEYUP, vk_code, lparam) def key_down(key: str): set_us_keyboard_layout() key = key.lower() if key in special_keys: vk_code, scan_code = special_keys[key] else: # 普通按键的处理 vk_code = VkKeyScanW(ctypes.c_wchar(key)) scan_code = keyboard.key_to_scan_codes(key)[0] if key != '/' else keyboard.key_to_scan_codes(key)[1] lparam = (scan_code << 16) | 1 if GlobalVariable.is_post_w: PostMessageW(GlobalVariable.window["hWnd"], win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) PostMessageW(GlobalVariable.window["hWnd"], WM_KEYDOWN, vk_code, lparam) else: SendMessageW(GlobalVariable.window["hWnd"], win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) SendMessageW(GlobalVariable.window["hWnd"], WM_KEYDOWN, vk_code, lparam) def key_up(key: str): set_us_keyboard_layout() key = key.lower() if key in special_keys: vk_code, scan_code = special_keys[key] else: # 普通按键的处理 vk_code = VkKeyScanW(ctypes.c_wchar(key)) scan_code = keyboard.key_to_scan_codes(key)[0] if key != '/' else keyboard.key_to_scan_codes(key)[1] lparam = (scan_code << 16) | 0XC0000001 if GlobalVariable.is_post_w: PostMessageW(GlobalVariable.window["hWnd"], win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) PostMessageW(GlobalVariable.window["hWnd"], WM_KEYUP, vk_code, lparam) else: SendMessageW(GlobalVariable.window["hWnd"], win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) SendMessageW(GlobalVariable.window["hWnd"], WM_KEYUP, vk_code, lparam) def mouse_wheel_scroll(operator): match operator: case 'up': delta = 3000 case 'down': delta = -3000 window_rect = win32gui.GetWindowRect(GlobalVariable.window["hWnd"]) # 返回 (left, top, right, bottom) # 窗口中心 window_x, window_y = window_rect[0] + window_rect[0] / 2, window_rect[1] + window_rect[1] / 2 # 激活窗口 if GlobalVariable.is_post_w: PostMessageW(GlobalVariable.window["hWnd"], win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) else: SendMessageW(GlobalVariable.window["hWnd"], win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) pyautogui.moveTo(window_x, window_y) pyautogui.scroll(delta) def set_us_keyboard_layout(): # LoadKeyboardLayoutW 函数的定义 user32.LoadKeyboardLayoutW.argtypes = [ctypes.c_wchar_p, ctypes.c_uint] user32.LoadKeyboardLayoutW.restype = ctypes.c_void_p user32.LoadKeyboardLayoutW("00000409", 1) # 0409 是美国键盘布局标识符,1 表示激活 special_keys = { 'space': (0x20, 0x39), # 空格键 'tab': (0x09, 0x0F), # Tab 键 'esc': (0x1B, 0x01), # Escape 键 'shift': (0x10, 0x2A), # 左 Shift 键 'right': (0x27, 0x4D), # 方向键右 'left': (0x25, 0x4B), # 方向键左 'up': (0x26, 0x48), # 方向键上 'down': (0x28, 0x50) # 方向键下 } ================================================ FILE: sky-music-server/windhide/playRobot/intel_robot.py ================================================ import ctypes import time import keyboard import pyautogui import win32con import win32gui from windhide.static.global_variable import GlobalVariable from windhide.thread.intel_play_thread import ControlledThread from windhide.utils.path_util import convert_notes_to_delayed_format, convert_json_to_play PostMessageW = ctypes.windll.user32.PostMessageW # 消息队列 SendMessageW = ctypes.windll.user32.SendMessageW # 立即处理 MapVirtualKeyW = ctypes.windll.user32.MapVirtualKeyW VkKeyScanW = ctypes.windll.user32.VkKeyScanW user32 = ctypes.windll.user32 WM_KEYDOWN = 0x100 WM_KEYUP = 0x101 pyautogui.FAILSAFE = False def send_single_key_to_window_task(key, duration): """发送单个按键,减少延迟""" key_down(key) time.sleep(duration/1000 + GlobalVariable.duration) key_up(key) def send_multiple_key_to_window_task(keys, duration): """发送组合按键,减少延迟""" for key in keys: key_down(key) time.sleep(duration/1000 + GlobalVariable.duration) for key in keys: key_up(key) def send_single_key_to_window(key, duration): """发送单个按键(单线程)""" if GlobalVariable.compatibility_mode: send_single_key_to_window_follow(key, duration) else: send_single_key_to_window_task(key, duration) def send_multiple_key_to_window(keys, duration): """发送组合按键(单线程)""" if GlobalVariable.compatibility_mode: send_multiple_key_to_window_follow(keys, duration) else: send_multiple_key_to_window_task(keys, duration) def send_single_key_to_window_follow(key, duration): """发送单个按键,减少延迟(单线程)""" keyboard.press(key) time.sleep(duration/1000 + GlobalVariable.duration) keyboard.release(key) def send_multiple_key_to_window_follow(keys, duration): """发送组合按键,减少延迟(单线程)""" for key in keys: keyboard.press(key) time.sleep(duration/1000 + GlobalVariable.duration) for key in keys: keyboard.release(key) def playMusic(fileName, type): """优化音乐播放逻辑,只加载乐谱数据一次""" convert_notes_to_delayed_format(fileName, type) if GlobalVariable.thread is not None: stop() GlobalVariable.thread = ControlledThread() GlobalVariable.thread.start() def playMusic_edit(text): """优化音乐播放逻辑,只加载乐谱数据一次""" convert_json_to_play(text) if GlobalVariable.thread is not None: stop() GlobalVariable.thread = ControlledThread() GlobalVariable.thread.start() def resume(): """恢复播放""" if GlobalVariable.thread: GlobalVariable.thread.resume() def pause(): """暂停播放""" if GlobalVariable.thread: GlobalVariable.thread.pause() def stop(): """停止播放""" if GlobalVariable.thread: GlobalVariable.thread.stop() GlobalVariable.set_progress = 0 GlobalVariable.thread = None # 点击,按下 def mouse_move_to(x: int, y: int): # 获取窗口的屏幕位置 window_rect = win32gui.GetWindowRect(GlobalVariable.window["hWnd"]) # 返回 (left, top, right, bottom) window_x, window_y = window_rect[0], window_rect[1] client_x = window_x + x client_y = window_y + y if GlobalVariable.is_post_w: PostMessageW(GlobalVariable.window["hWnd"], win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) else: SendMessageW(GlobalVariable.window["hWnd"], win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) pyautogui.moveTo(client_x, client_y, duration=0) # 核心 def key_press(key: str): key = key.lower() if key in special_keys: vk_code, scan_code = special_keys[key] else: # 普通按键的处理 vk_code = VkKeyScanW(ctypes.c_wchar(key)) scan_code = keyboard.key_to_scan_codes(key)[0] if key != '/' else keyboard.key_to_scan_codes(key)[1] lparam = (scan_code << 16) | 1 if GlobalVariable.is_post_w: PostMessageW(GlobalVariable.window["hWnd"], win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) PostMessageW(GlobalVariable.window["hWnd"], WM_KEYDOWN, vk_code, lparam) time.sleep(0.01) PostMessageW(GlobalVariable.window["hWnd"], WM_KEYUP, vk_code, lparam) else: SendMessageW(GlobalVariable.window["hWnd"], win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) SendMessageW(GlobalVariable.window["hWnd"], WM_KEYDOWN, vk_code, lparam) time.sleep(0.01) SendMessageW(GlobalVariable.window["hWnd"], WM_KEYUP, vk_code, lparam) def key_down(key: str): set_us_keyboard_layout() key = key.lower() if key in special_keys: vk_code, scan_code = special_keys[key] else: # 普通按键的处理 vk_code = VkKeyScanW(ctypes.c_wchar(key)) scan_code = keyboard.key_to_scan_codes(key)[0] if key != '/' else keyboard.key_to_scan_codes(key)[1] lparam = (scan_code << 16) | 1 if GlobalVariable.is_post_w: PostMessageW(GlobalVariable.window["hWnd"], win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) PostMessageW(GlobalVariable.window["hWnd"], WM_KEYDOWN, vk_code, lparam) else: SendMessageW(GlobalVariable.window["hWnd"], win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) SendMessageW(GlobalVariable.window["hWnd"], WM_KEYDOWN, vk_code, lparam) def key_up(key: str): key = key.lower() if key in special_keys: vk_code, scan_code = special_keys[key] else: # 普通按键的处理 vk_code = VkKeyScanW(ctypes.c_wchar(key)) scan_code = keyboard.key_to_scan_codes(key)[0] if key != '/' else keyboard.key_to_scan_codes(key)[1] lparam = (scan_code << 16) | 0XC0000001 if GlobalVariable.is_post_w: PostMessageW(GlobalVariable.window["hWnd"], win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) PostMessageW(GlobalVariable.window["hWnd"], WM_KEYUP, vk_code, lparam) else: SendMessageW(GlobalVariable.window["hWnd"], win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) SendMessageW(GlobalVariable.window["hWnd"], WM_KEYUP, vk_code, lparam) def mouse_wheel_scroll(operator): match operator: case 'up': delta = 3000 case 'down': delta = -3000 window_rect = win32gui.GetWindowRect(GlobalVariable.window["hWnd"]) # 返回 (left, top, right, bottom) # 窗口中心 window_x, window_y = window_rect[0] + window_rect[0] / 2, window_rect[1] + window_rect[1] / 2 # 激活窗口 if GlobalVariable.is_post_w: PostMessageW(GlobalVariable.window["hWnd"], win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) else: SendMessageW(GlobalVariable.window["hWnd"], win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) pyautogui.moveTo(window_x, window_y) pyautogui.scroll(delta) def set_us_keyboard_layout(): # LoadKeyboardLayoutW 函数的定义 user32.LoadKeyboardLayoutW.argtypes = [ctypes.c_wchar_p, ctypes.c_uint] user32.LoadKeyboardLayoutW.restype = ctypes.c_void_p user32.LoadKeyboardLayoutW("00000409", 1) # 0409 是美国键盘布局标识符,1 表示激活 special_keys = { 'space': (0x20, 0x39), # 空格键 'tab': (0x09, 0x0F), # Tab 键 'esc': (0x1B, 0x01), # Escape 键 'shift': (0x10, 0x2A), # 左 Shift 键 'right': (0x27, 0x4D), # 方向键右 'left': (0x25, 0x4B), # 方向键左 'up': (0x26, 0x48), # 方向键上 'down': (0x28, 0x50) # 方向键下 } ================================================ FILE: sky-music-server/windhide/static/global_variable.py ================================================ class GlobalVariable: # 运行环境配置 isProd = False cpu_type = None compatibility_mode = False # 是否虚拟机 is_post_w = False # 是否插队模式 duration_prompt = """你现在是一个音乐数据生成器,不是聊天助手。 🎼 输入是一首歌曲的音符数据,格式为一个 JSON 数组,每个元素如下: {"time":0,"key":"1Key10","duration":400} 解释: - `time` 表示时间,单位为毫秒。 - `key` 表示音符键名,格式为 `{轨道号}Key{音高编号}`。 - `轨道号` 是数字,如 `1` 或 `2`,表示旋律或和声的分轨。 - `duration` 表示这个音要持续的时长(单位毫秒,最少 300 毫秒)。 - `音高编号` 必须从下表中选择: | 编号 | keyN | 对应音高 | |------|--------|-----------------| | 0 | Key0 | C4 (MIDI 60) | | 1 | Key1 | D4 (62) | | 2 | Key2 | E4 (64) | | 3 | Key3 | F4 (65) | | 4 | Key4 | G4 (67) | | 5 | Key5 | A4 (69) | | 6 | Key6 | B4 (71) | | 7 | Key7 | C5 (72) | | 8 | Key8 | D5 (74) | | 9 | Key9 | E5 (76) | | 10 | Key10 | F5 (77) | | 11 | Key11 | G5 (79) | | 12 | Key12 | A5 (81) | | 13 | Key13 | B5 (83) | | 14 | Key14 | C6 (84) | 🧠 你的任务: - 对原始旋律进行改编,使旋律更自然流畅。 - 添加或修改 `duration` 字段。 - **允许自由发挥,增加音符数量不得少于原始音符总数,鼓励适度创新和变化。** - 避免长时间重复相同旋律,保证整体音乐连贯性。 - 生成的 `time` 应合理反映节奏,避免毫秒级零散断裂,建议保持符合歌曲节拍风格。 - 轨道号代表不同声部,1号轨道为主旋律,2号轨道为和声伴奏,可适当调整和声层次。 - 同一时间点最多同时按下3个不同音符,保持和声自然。 - 每次输出最多 40 个完整 JSON 元素,不允许跨元素截断。 - 每个元素结构严格为:`{"time":x,"key":"xKeyx","duration":x},` - 所有 `key` 必须严格来自上方表格(含轨道前缀,如 `"1Key4"`)。 - 如果多个音符同时按下,使用相同的 `time` 和不同的 `key`。 - 允许对不合理音节进行改造,使其更符合音乐逻辑。 🧩 输出格式要求: - 每次输出仅包含中间段 JSON 元素,不输出 `[` 或 `]`。 - 每次输出第一行必须注明:`第 N 段(共预计 M 段)` - 输出不得包含除 JSON 结构外的任何注释、说明或总结语。 - 不得说“已完成”、“以下是输出”、“JSON 完整”等总结语。 🪛 响应规范: - 保证每个 JSON 对象都闭合(不能残缺或中断)。 - 禁止输出非 `{"time":,"key":}` 格式的字段。 - 当我回复“继续”,你只输出**上一次后的 JSON 段落**,禁止重复。 - 你不能停止任务,除非我显式让你“停止”。 🎵 准备开始时,我会提供原始 JSON 数据,你必须根据以上规范对其改编并分段输出。 示例输出片段: 第 1 段(共预计 10 段) {"time":0,"key":"1Key10","duration":400},{"time":250,"key":"1Key12","duration":120},{"time":250,"key":"2Key4","duration":200},{"time":500,"key":"1Key14","duration":300}, {"time":750,"key":"1Key13","duration":312},{"time":1000,"key":"1Key11","duration":313},{"time":1250,"key":"1Key9","duration":421},{"time":1250,"key":"2Key7","duration":436}, {"time":1500,"key":"1Key10","duration":332},{"time":1750,"key":"1Key12","duration":315}""" translate_prompt = """你现在是一个音乐数据生成器,不是聊天助手。 🎼 输入是一首歌曲的音符数据,格式为一个 JSON 数组,每个元素如下: {"time":0,"key":"1Key10"} 解释: - `time` 表示时间,单位为毫秒。 - `key` 表示音符键名,格式为 `{轨道号}Key{音高编号}`。 - `轨道号` 是数字,如 `1` 或 `2`,表示旋律或和声的分轨。 - `音高编号` 必须从下表中选择: | 编号 | keyN | 对应音高 | |------|--------|-----------------| | 0 | Key0 | C4 (MIDI 60) | | 1 | Key1 | D4 (62) | | 2 | Key2 | E4 (64) | | 3 | Key3 | F4 (65) | | 4 | Key4 | G4 (67) | | 5 | Key5 | A4 (69) | | 6 | Key6 | B4 (71) | | 7 | Key7 | C5 (72) | | 8 | Key8 | D5 (74) | | 9 | Key9 | E5 (76) | | 10 | Key10 | F5 (77) | | 11 | Key11 | G5 (79) | | 12 | Key12 | A5 (81) | | 13 | Key13 | B5 (83) | | 14 | Key14 | C6 (84) | 🧠 你的任务: - 对原始旋律进行改编,使旋律更自然流畅。 - **允许自由发挥,增加音符数量不得少于原始音符总数,鼓励适度创新和变化。** - 避免长时间重复相同旋律,保证整体音乐连贯性。 - 生成的 `time` 应合理反映节奏,避免毫秒级零散断裂,建议保持符合歌曲节拍风格。 - 轨道号代表不同声部,1号轨道为主旋律,2号轨道为和声伴奏,可适当调整和声层次。 - 同一时间点最多同时按下3个不同音符,保持和声自然。 - 每次输出最多 40 个完整 JSON 元素,不允许跨元素截断。 - 每个元素结构严格为:`{"time":1234,"key":"xKeyN"},` - 所有 `key` 必须严格来自上方表格(含轨道前缀,如 `"1Key4"`)。 - 如果多个音符同时按下,使用相同的 `time` 和不同的 `key`。 - 允许对不合理音节进行改造,使其更符合音乐逻辑。 🧩 输出格式要求: - 每次输出仅包含中间段 JSON 元素,不输出 `[` 或 `]`。 - 每次输出第一行必须注明:`第 N 段(共预计 M 段)` - 输出不得包含除 JSON 结构外的任何注释、说明或总结语。 - 不得说“已完成”、“以下是输出”、“JSON 完整”等总结语。 🪛 响应规范: - 保证每个 JSON 对象都闭合(不能残缺或中断)。 - 禁止输出非 `{"time":,"key":}` 格式的字段。 - 当我回复“继续”,你只输出**上一次后的 JSON 段落**,禁止重复。 - 你不能停止任务,除非我显式让你“停止”。 🎵 准备开始时,我会提供原始 JSON 数据,你必须根据以上规范对其改编并分段输出。 示例输出片段: 第 1 段(共预计 10 段) {"time":0,"key":"1Key10"},{"time":250,"key":"1Key12"},{"time":250,"key":"2Key4"},{"time":500,"key":"1Key14"}, {"time":750,"key":"1Key13"},{"time":1000,"key":"1Key11"},{"time":1250,"key":"1Key9"},{"time":1250,"key":"2Key7"}, {"time":1500,"key":"1Key10"},{"time":1750,"key":"1Key12"}""" general_ai = { # "Kimi":{ # "key": "sk-sfpR8uS6S0puwi3d758viFNLrPmXTwDyhoecDEEIorX49bbr", # "url": "https://api.moonshot.cn/v1", # "model": "moonshot-v1-128k", # } , # "Qwen":{ # "key": "sk-99dae6996fb24d62a85a4e5b68c5619e", # "url": "https://dashscope.aliyuncs.com/compatible-mode/v1", # "model": "qwen-turbo-latest", # } } # 窗口相关 is_custom_hwnd = False hwnd_title = "Sky.exe" hwnd_select_struct = {} window = { "hWnd": None, "width": 0, "height": 0, "position_x": 0, "position_y": 0, "is_change": False, "key_position": None, "wait": False } # 快捷键相关 shortcutStruct = { "follow_key":{ "tap_key": "yuiophjkl;nm,./", "string": "-=q", "repeat": "-", "repeat_next": '=', "resize": "q", "exit": "esc" }, "music_key":{ "string": "f2f5f6f7f8updownleftrightpage_uppage_down", "next": "f2", "start": "f5", "resume": "f6", "pause": "f7", "stop": "f8", "add_duration": "up", "reduce_duration": "down", "add_delay": "right", "reduce_delay": "left", "add_speed": "page_up", "reduce_speed": "page_down", } } # 线程相关 thread = None auto_thread = None task_queue = None # 乐谱相关 music_sheet = [] # 乐谱 # 进度条相关 now_progress = 0 # 进度条 set_progress = -0.01 # 进度条 now_play_music = "没有正在播放的歌曲哦" now_total_time = "" now_current_time = "" # 音乐转换进度条 overall_progress = 0 # 总体进度 now_translate_text = [] merge_min = 20 merge_max = 30 velocity_filter = 10 is_singular = True semitone_switch = True # 半音转换开关 detail_switch = True # 超3音转换开关 split_switch = False # 人声分离开关 # 演奏相关 play_speed = 1 # 倍速 delay_interval = 0 # 0 duration = 0 # 20毫秒 # 跟弹相关 follow_music = "" follow_sheet = [] nowClientKey = "" nowRobotKey = "" follow_client = None isNowAutoPlaying = False follow_thread = None # 跟弹按键处理相关线程 exit_flag = False draw_process = None window_offset_x = 0 window_offset_y = 0 follow_process = None sheld = 0.7 # 乐谱映射 keyMap = { 'Key-14': '', 'Key-13': '', 'Key-12': '', 'Key-11': '', 'Key-10': '', 'Key-9': '', 'Key-8': '', 'Key-7': '', 'Key-6': '', 'Key-5': '', 'Key-4': '', 'Key-3': '', 'Key-2': '', 'Key-1': '', 'Key0': 'y', 'Key1': 'u', 'Key2': 'i', 'Key3': 'o', 'Key4': 'p', 'Key5': 'h', 'Key6': 'j', 'Key7': 'k', 'Key8': 'l', 'Key9': ';', 'Key10': 'n', 'Key11': 'm', 'Key12': ',', 'Key13': '.', 'Key14': '/', 'Key15': '', 'Key16': '', 'Key17': '', 'Key18': '', 'Key19': '', 'Key20': '', 'Key21': '', 'Key22': '', 'Key23': '', 'Key24': '', 'Key25': '', 'Key26': '', 'Key27': '', 'Key28': '' } ================================================ FILE: sky-music-server/windhide/thread/amd_play_thread.py ================================================ import math import threading import time from windhide.playRobot import amd_robot from windhide.static.global_variable import GlobalVariable class ControlledThread: def __init__(self): self._pause_event = threading.Event() self._pause_event.set() # 初始状态允许运行 self._thread = None self._running = False def _run(self): if not GlobalVariable.music_sheet: self.stop() return local_music_sheet = GlobalVariable.music_sheet[:] total_length = 1 if len(local_music_sheet) == 1 else len(local_music_sheet) - 1 index = 0 # 在播放开始时计算总时长(单位为毫秒) total_time = sum(sheet["delay"] for sheet in local_music_sheet) GlobalVariable.now_total_time = self.format_time(total_time) current_time = 0 # 当前时间(单位为毫秒) while self._running and index < len(local_music_sheet): self._pause_event.wait() # 等待恢复运行 if GlobalVariable.set_progress != -0.01: index = math.floor(total_length * GlobalVariable.set_progress) GlobalVariable.set_progress = -0.01 current_time = total_time * (index / total_length) # 调整当前时间 continue sheet = local_music_sheet[index] keys = sheet["key"] delay = sheet["delay"] / GlobalVariable.play_speed if total_length == 0: GlobalVariable.now_progress = 100 else: GlobalVariable.now_progress = (index / total_length) * 100 if keys: (amd_robot.send_single_key_to_window if len(keys) == 1 else amd_robot.send_multiple_key_to_window)(keys, sheet["duration"] if "duration" in sheet else 0) time.sleep(delay / 1000 + GlobalVariable.delay_interval) # 格式化当前时间/总时间 # formatted_time = self.format_time(current_time) + " / " + self.format_time(total_time) # print(f"播放时间:{formatted_time}", end="\r") GlobalVariable.now_current_time = self.format_time(current_time) current_time += delay # 更新当前时间 index += 1 def format_time(self, milliseconds): """ 将毫秒数格式化为时分秒格式(HH:MM:SS 或 MM:SS) """ seconds = milliseconds / 1000 hours, remainder = divmod(seconds, 3600) minutes, seconds = divmod(remainder, 60) if hours > 0: return f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}" else: return f"{int(minutes):02}:{int(seconds):02}" def start(self): if self._running: return self._running = True if self._thread and self._thread.is_alive(): self._thread.join() self._thread = threading.Thread(target=self._run, daemon=True) self._thread.start() def pause(self): self._pause_event.clear() def resume(self): self._pause_event.set() def stop(self): self._running = False self.resume() # 避免暂停状态导致的死锁 if self._thread and self._thread.is_alive(): self._thread.join() self._thread = None ================================================ FILE: sky-music-server/windhide/thread/follow_process_thread.py ================================================ import os import subprocess import psutil from windhide.static.global_variable import GlobalVariable from windhide.utils.ocr_normal_utils import get_game_position from windhide.utils.path_util import getResourcesPath GlobalVariable.draw_process = None # 进程对象 def run_follow_process(): try: x1, y1, x2, y2 = get_game_position() width, height = x2 - x1, y2 - y1 GlobalVariable.draw_process = subprocess.Popen( [ os.path.join(getResourcesPath("systemTools"), "drawTool", "draw_server.exe"), f"--width={width}", f"--height={height}", f"--x={x1}", f"--y={y1}", ], creationflags=subprocess.CREATE_NEW_PROCESS_GROUP, # 独立进程组,便于管理 ) except (FileNotFoundError, subprocess.SubprocessError): pass # 直接忽略异常,避免冗余输出 def stop_follow_process(): """彻底终止 draw_server.exe 及其所有子进程""" process = GlobalVariable.draw_process if process and process.poll() is None: try: parent = psutil.Process(process.pid) children = parent.children(recursive=True) for child in children: child.terminate() _, still_alive = psutil.wait_procs(children, timeout=5) for child in still_alive: child.kill() parent.terminate() parent.wait(5) GlobalVariable.draw_process = None # 清理进程记录 except psutil.NoSuchProcess: pass except Exception: pass ================================================ FILE: sky-music-server/windhide/thread/follow_thread.py ================================================ import time from os import path import plyer from pynput import keyboard from windhide.playRobot.amd_robot import send_multiple_key_press, send_multiple_key_release from windhide.static.global_variable import GlobalVariable from windhide.utils import hook_util from windhide.utils.command_util import resize_and_reload_key, clear_window_key, add_window_key, \ quit_window, update_key from windhide.utils.path_util import getResourcesPath hook_util.sout_null() GlobalVariable.exit_flag = False # 添加全局退出标志 originalKeys = "None" pressedKeys = set() def get_key_string(key): if hasattr(key, "char") and key.char is not None: # 处理普通字符按键 return key.char elif hasattr(key, "name"): # 处理特殊按键 return key.name else: return str(key) # 兜底方案,转换为字符串 def on_press(key): global pressedKeys, originalKeys if GlobalVariable.exit_flag: return False # 终止监听 if GlobalVariable.follow_music != "": try: key = get_key_string(key) if key in GlobalVariable.shortcutStruct["follow_key"]["string"] or key in GlobalVariable.shortcutStruct["follow_key"]["tap_key"]: if GlobalVariable.isNowAutoPlaying: if key not in GlobalVariable.shortcutStruct["follow_key"]["repeat"]: GlobalVariable.nowRobotKey += key if len(GlobalVariable.nowRobotKey) == len(GlobalVariable.nowClientKey): GlobalVariable.isNowAutoPlaying = False GlobalVariable.nowRobotKey = '' else: if key in GlobalVariable.shortcutStruct["follow_key"]["repeat"]: GlobalVariable.isNowAutoPlaying = True send_multiple_key_press(GlobalVariable.nowClientKey) if key in GlobalVariable.shortcutStruct["follow_key"]["repeat_next"]: send_multiple_key_press(GlobalVariable.nowClientKey) except Exception as e: print(f"发生错误: {e.__doc__} , 开始重新加载") # 处理 Esc 退出监听 if key == GlobalVariable.shortcutStruct["follow_key"]["exit"] or key == "alt_l": print("检测到 Esc 键,退出监听...") GlobalVariable.exit_flag = True # 设置全局标志 try: quit_window() except Exception as e: return False return False # 停止监听 def key_release(key): global pressedKeys, originalKeys if GlobalVariable.exit_flag: return False # 终止监听 if GlobalVariable.follow_music != "": try: key = get_key_string(key) if key in GlobalVariable.shortcutStruct["follow_key"]["string"] or key in GlobalVariable.shortcutStruct["follow_key"]["tap_key"]: if GlobalVariable.isNowAutoPlaying: if key not in GlobalVariable.shortcutStruct["follow_key"]["repeat"]: GlobalVariable.nowRobotKey += key if len(GlobalVariable.nowRobotKey) == len(GlobalVariable.nowClientKey): GlobalVariable.isNowAutoPlaying = False GlobalVariable.nowRobotKey = '' else: if key in GlobalVariable.shortcutStruct["follow_key"]["repeat"]: GlobalVariable.isNowAutoPlaying = True send_multiple_key_release(GlobalVariable.nowClientKey) if key in GlobalVariable.shortcutStruct["follow_key"]["repeat_next"]: send_multiple_key_release(GlobalVariable.nowClientKey) if key in GlobalVariable.shortcutStruct["follow_key"]["resize"]: resize_and_reload_key() else: if key in originalKeys: pressedKeys.add(key) if len(pressedKeys) == len(originalKeys): pressedKeys.clear() maybe_next_key = get_next_sheet_demo("ok") if not maybe_next_key: return else: originalKeys = set(maybe_next_key) except Exception as e: print(f"发生错误: {e.__doc__} , 开始重新加载") # 处理 Esc 退出监听 if key == GlobalVariable.shortcutStruct["follow_key"]["exit"] or key == "alt_l": print("检测到 Esc 键,退出监听...") GlobalVariable.exit_flag = True # 设置全局标志 try: quit_window() except Exception as e: return False return False # 停止监听 def get_next_sheet_demo(operator): if len(GlobalVariable.follow_sheet) == 0: return "" try: if operator != "load": global originalKeys clear_window_key(originalKeys) if operator == "ok" or operator == "load": sheet = GlobalVariable.follow_sheet[0] GlobalVariable.nowClientKey = sheet GlobalVariable.follow_sheet = GlobalVariable.follow_sheet[1:] if len(GlobalVariable.follow_sheet) == 0: GlobalVariable.exit_flag = True # 设置全局标志 quit_window() plyer.notification.notify( app_name='小星弹琴软件', app_icon=path.join(getResourcesPath("systemTools"), "icon.ico"), title='已经结束啦', message='这首歌已经弹完了哦', timeout=1 ) send_multiple_key_press("z") send_multiple_key_release("z") return False for key in sheet: add_window_key(key) update_key() return sheet else: GlobalVariable.nowClientKey = GlobalVariable.follow_sheet[0] return GlobalVariable.follow_sheet[0] except IndexError: print("空数组") return "" def startThread(): global originalKeys, pressedKeys time.sleep(2) originalKeys = set(get_next_sheet_demo("load")) pressedKeys = set() GlobalVariable.exit_flag = False # 确保每次启动时标志位重置 with keyboard.Listener(on_press=on_press, on_release=key_release) as listener: listener.join() ================================================ FILE: sky-music-server/windhide/thread/frame_alive_thread.py ================================================ import os import time import psutil def is_process_running(process_name): """检查目标进程是否运行""" return any(proc.info["name"] == process_name for proc in psutil.process_iter(["name"])) def monitor_process(process_name): """监听目标进程的状态,如果退出则结束主程序""" print(f"🔍 监听进程: {process_name}") while True: if not is_process_running(process_name): print(f"⚠️ {process_name} 已退出,关闭主程序。") os._exit(0) # 强制退出主进程 time.sleep(1) # 每秒检查一次 ================================================ FILE: sky-music-server/windhide/thread/hwnd_check_thread.py ================================================ import os import time import traceback import psutil import pywintypes import win32gui import win32process from windhide.static.global_variable import GlobalVariable from windhide.utils import hook_util from windhide.utils.ocr_follow_util import get_key_position hook_util.sout_null() def safe_enum_windows(callback): """封装 EnumWindows 调用,捕获特定异常,防止异常中断调用链""" try: win32gui.EnumWindows(callback, None) except pywintypes.error as e: error_code = e.args[0] # 获取错误码 error_message = e.args[1] # 获取错误消息 error_details = e.args[2] # 获取错误详细信息 if error_code == 2: # 系统找不到指定文件,忽略 print(f"[DEBUG] EnumWindows 错误码 2:{error_message},详细信息:{error_details}") else: # 打印错误码、消息和详细信息,同时也捕获堆栈跟踪 print(f"[DEBUG] EnumWindows 异常:") print(f" 错误码: {error_code}") print(f" 错误消息: {error_message}") print(f" 错误详细信息: {error_details}") print("[DEBUG] 错误堆栈跟踪:") traceback.print_exc() # 打印堆栈跟踪 def get_exe_name_from_hwnd(hwnd): # 获取窗口的 PID _, pid = win32process.GetWindowThreadProcessId(hwnd) try: # 使用 psutil 获取进程的 exe 路径 process = psutil.Process(pid) exe_path = process.exe() # 获取 exe 路径 exe_name = os.path.basename(exe_path) # 只获取文件名 return exe_name except (psutil.NoSuchProcess, psutil.AccessDenied): return None def find_window_by_class(class_name): """根据窗口类名查找窗口句柄""" hwnd = win32gui.FindWindow(class_name, None) return hwnd def update_window_handle(): """查找窗口句柄,并更新全局变量""" if GlobalVariable.is_custom_hwnd is False: class_name = "TgcMainWindow" hwnd = find_window_by_class(class_name) if hwnd: left, top, right, bottom = win32gui.GetWindowRect(hwnd) width, height = right - left, bottom - top window = GlobalVariable.window window["hWnd"], window["width"], window["height"] = hwnd, width, height window["position_x"], window["position_y"] = left, top # 仅在窗口信息发生变化时设置 is_change if any([ window["width"] != width, window["height"] != height, window["position_x"] != left, window["position_y"] != top, ]): window["is_change"] = True GlobalVariable.hwnd_title = get_exe_name_from_hwnd(hwnd) get_key_position(0.88) else: GlobalVariable.window["hWnd"] = None else: GlobalVariable.hwnd_title = get_exe_name_from_hwnd(GlobalVariable.window["hWnd"]) def start_thread(): """后台线程循环更新窗口句柄""" while True: update_window_handle() time.sleep(2) ================================================ FILE: sky-music-server/windhide/thread/intel_play_thread.py ================================================ import math import threading import time from windhide.playRobot import intel_robot from windhide.static.global_variable import GlobalVariable class ControlledThread: def __init__(self): self._pause_event = threading.Event() self._pause_event.set() # 初始状态为运行 self._thread = None self._running = False def _run(self): if not GlobalVariable.music_sheet: self.stop() return local_music_sheet = GlobalVariable.music_sheet[:] total_length = 1 if len(local_music_sheet) == 1 else len(local_music_sheet) - 1 index = 0 # 在播放开始时计算总时长(单位为毫秒) total_time = sum(sheet["delay"] for sheet in local_music_sheet) GlobalVariable.now_total_time = self.format_time(total_time) current_time = 0 # 当前时间(单位为毫秒) while self._running and index < len(local_music_sheet): self._pause_event.wait() # 等待恢复 if GlobalVariable.set_progress != -0.01: index = math.floor(total_length * GlobalVariable.set_progress) GlobalVariable.set_progress = -0.01 current_time = total_time * (index / total_length) # 调整当前时间 continue sheet = local_music_sheet[index] keys = sheet["key"] delay = sheet["delay"] / GlobalVariable.play_speed if total_length == 0: GlobalVariable.now_progress = 100 else: GlobalVariable.now_progress = (index / total_length) * 100 if keys: (intel_robot.send_single_key_to_window if len(keys) == 1 else intel_robot.send_multiple_key_to_window)(keys, sheet["duration"] if "duration" in sheet else 0) time.sleep(delay / 1000 + GlobalVariable.delay_interval) # 格式化当前时间/总时间 # formatted_time = self.format_time(current_time) + " / " + self.format_time(total_time) # print(f"播放时间:{formatted_time}", end="\r") GlobalVariable.now_current_time = self.format_time(current_time) current_time += delay # 更新当前时间 index += 1 def format_time(self, milliseconds): """ 将毫秒数格式化为时分秒格式(HH:MM:SS 或 MM:SS) """ seconds = milliseconds / 1000 hours, remainder = divmod(seconds, 3600) minutes, seconds = divmod(remainder, 60) if hours > 0: return f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}" else: return f"{int(minutes):02}:{int(seconds):02}" def start(self): if self._running: return self._running = True if self._thread and self._thread.is_alive(): self._thread.join() self._thread = threading.Thread(target=self._run, daemon=True) self._thread.start() def pause(self): self._pause_event.clear() def resume(self): self._pause_event.set() def stop(self): self._running = False self.resume() # 防止死锁 if self._thread and self._thread.is_alive(): self._thread.join() self._thread = None ================================================ FILE: sky-music-server/windhide/thread/queue_thread.py ================================================ import queue import re from os import path import plyer from windhide.playRobot import intel_robot, amd_robot from windhide.static.global_variable import GlobalVariable from windhide.utils.path_util import getResourcesPath def music_start_tasks(): while True: try: request = GlobalVariable.task_queue.get(block=True, timeout=0.5) # 最多等待1秒 except queue.Empty: continue try: if GlobalVariable.window["hWnd"] is None and GlobalVariable.compatibility_mode is False: plyer.notification.notify( app_name='小星弹琴软件', app_icon=path.join(getResourcesPath("systemTools"),"icon.ico"), title='🔥🔥🔥🔥🔥', message='未检测到游戏窗口,请打开游戏或者去句柄页面进行指定!本次播放操作释放', timeout=1 ) GlobalVariable.now_progress = 100 GlobalVariable.task_queue.task_done() continue GlobalVariable.now_play_music = ("" if 'sheet' in request else re.sub(r"-#(\d+)(?=\.\w+)?", "", request["fileName"].replace(".txt", ""))) match GlobalVariable.cpu_type: case "Intel": intel_robot.playMusic_edit(request["sheet"]) if 'sheet' in request else intel_robot.playMusic(request["fileName"], request["type"]) case "AMD": amd_robot.playMusic_edit(request["sheet"]) if 'sheet' in request else amd_robot.playMusic(request["fileName"], request["type"]) except Exception as e: print(f"Error in processing task: {str(e)}") GlobalVariable.task_queue.task_done() # 标记任务完成 ================================================ FILE: sky-music-server/windhide/thread/shortcut_thread.py ================================================ import urllib from pynput import keyboard from websocket_server import WebsocketServer from windhide.static.global_variable import GlobalVariable from windhide.utils import hook_util hook_util.sout_null() server = WebsocketServer("127.0.0.1", 11452) def get_key_string(key): if hasattr(key, "char") and key.char is not None: # 处理普通字符按键 return key.char elif hasattr(key, "name"): # 处理特殊按键 return key.name else: return str(key) # 兜底方案,转换为字符串 # 键盘按键事件处理 def on_press(key): key = get_key_string(key) if key in GlobalVariable.shortcutStruct["music_key"]["string"]: try: server.send_message_to_all(urllib.parse.quote(key)) except Exception as e: print(f"发送消息时发生错误: {e}") # 客户端事件处理 def on_client_connect(client, server): print(f"客户端 {client['id']} 已连接") def on_client_disconnect(client, server): print(f"客户端 {client['id']} 已断开连接") # 启动 WebSocket 服务 def startThread(): try: server.set_fn_new_client(on_client_connect) server.set_fn_client_left(on_client_disconnect) listener = keyboard.Listener(on_press=on_press) listener.start() print("WebSocket 服务正在启动,监听 127.0.0.1:11452...") server.run_forever() except Exception as e: print(f"WebSocket 服务器启动失败: {e}") ================================================ FILE: sky-music-server/windhide/utils/auto_util.py ================================================ from windhide.auto.auto_thread import HeartFireThread from windhide.static.global_variable import GlobalVariable def auto_click_fire(): if GlobalVariable.auto_thread is not None: GlobalVariable.auto_thread.stop() GlobalVariable.auto_thread = HeartFireThread() GlobalVariable.auto_thread.daemon = True GlobalVariable.auto_thread.start() def shutdown(): GlobalVariable.auto_thread.stop() ================================================ FILE: sky-music-server/windhide/utils/command_util.py ================================================ import socket import threading import time from os import path from time import sleep import plyer from windhide.static.global_variable import GlobalVariable from windhide.thread.follow_process_thread import run_follow_process, stop_follow_process from windhide.utils.ocr_normal_utils import get_game_position from windhide.utils.path_util import getResourcesPath # 全局变量,存储客户端连接对象 GlobalVariable.follow_client = None def start_process(): # 创建并启动线程,传递参数 thread = threading.Thread(target=run_follow_process) thread.daemon = True # 设置为守护线程,主线程退出时自动退出 thread.start() def add_window_key(key): try: width = GlobalVariable.window['key_position'][key]['width'] height = GlobalVariable.window['key_position'][key]['height'] position_x = GlobalVariable.window['key_position'][key]['position_x'] - GlobalVariable.window_offset_x position_y = GlobalVariable.window['key_position'][key]['position_y'] - GlobalVariable.window_offset_y send_command(f"draw {key} {width} {height} {position_x} {position_y} \n") # 绘制 except KeyError as e: print("按键识别不完全,重新调用 => ", e, e.__doc__) GlobalVariable.window["is_change"] = True time.sleep(2) resize_and_reload_key() def del_window_key(key): send_command(f"delete {key} \n") # 绘制 def clear_window_key(keys): for key in keys: send_command(f"delete {key} \n") def update_key(): send_command(f"update \n") def resize_and_reload_key(): if GlobalVariable.window["wait"] is not True: GlobalVariable.window["is_change"] = True GlobalVariable.window["wait"] = True plyer.notification.notify( app_name='小星弹琴软件', app_icon=path.join(getResourcesPath("systemTools"), "icon.ico"), title='已经在很努力的检测了', message='如果长时间未检测完成请换个位置打开琴键', timeout=0.3 ) while True: sleep(1) if len(GlobalVariable.window['key_position']) == 15 and GlobalVariable.window["wait"] == False: GlobalVariable.window["wait"] = False position = get_game_position() position_x, position_y = position[0], position[1] width, height = position[2] - position[0], position[3] - position[1] # 检查窗口大小是否发生变化 if width != GlobalVariable.window.get('width', 0) or height != GlobalVariable.window.get('height', 0): GlobalVariable.window['width'] = width GlobalVariable.window['height'] = height # 重新计算按键位置 from windhide.utils.ocr_follow_util import get_key_position get_key_position(None) # 重新获取按键位置 send_command(f"resize {width} {height} {position_x} {position_y} \n") clear_window_key(GlobalVariable.nowClientKey) # 重新获取新的15个按键 if len(GlobalVariable.follow_sheet) > 0: GlobalVariable.nowClientKey = GlobalVariable.follow_sheet[0] for key in GlobalVariable.nowClientKey: add_window_key(key) update_key() break plyer.notification.notify( app_name='小星弹琴软件', app_icon=path.join(getResourcesPath("systemTools"), "icon.ico"), title='检测完毕', message='OK', timeout=1 ) def quit_window(): send_command(f"exit\n") GlobalVariable.follow_client.shutdown(socket.SHUT_RDWR) # 关闭套接字的读写 GlobalVariable.follow_client.close() GlobalVariable.follow_client = None stop_follow_process() def wait_for_server(host, port, max_retries=30, delay=2): """等待服务器启动并可连接""" retries = 0 while retries < max_retries: try: with socket.create_connection((host, port), timeout=1) as sock: return True # 服务器已启动 except (ConnectionRefusedError, OSError): retries += 1 time.sleep(delay) return False # 超时 def send_command(command): try: if GlobalVariable.follow_client is None: if not wait_for_server("localhost", 12345): print("无法连接到 draw_server.exe,超时退出。") return GlobalVariable.follow_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) GlobalVariable.follow_client.connect(("localhost", 12345)) # 连接到服务器 GlobalVariable.follow_client.send(command.encode("utf-8")) print(f"发送命令: {command}") except WindowsError as e: GlobalVariable.exit_flag = True print("send Command错误", e.__doc__) except Exception as e: print("send Command错误", e.__doc__) GlobalVariable.exit_flag = True # GlobalVariable.follow_client.shutdown(socket.SHUT_RDWR) # 关闭套接字的读写 # GlobalVariable.follow_client.close() # GlobalVariable.follow_client = None ================================================ FILE: sky-music-server/windhide/utils/config_util.py ================================================ import os import shutil from windhide.musicToSheet.music2html import generatorSheetHtml from windhide.static.global_variable import GlobalVariable from windhide.utils.path_util import getResourcesPath, convert_notes_to_delayed_format def set_config(request: dict): match request["name"]: case 'delay_interval': GlobalVariable.delay_interval = float(request["value"]) case 'duration': GlobalVariable.duration = float(request["value"]) case 'set_progress': GlobalVariable.set_progress = float(request["value"]) case 'play_speed': GlobalVariable.play_speed = float(request["value"]) case 'compatibility_mode': GlobalVariable.compatibility_mode = request["value"] case 'is_post_w': GlobalVariable.is_post_w = request["value"] case 'cpu_type': GlobalVariable.cpu_type = "AMD" if request["value"] else "Intel" case 'shortcutStruct': GlobalVariable.shortcutStruct = request["value"] case 'keyMap': GlobalVariable.keyMap = request["value"] case 'merge_min': GlobalVariable.merge_min = int(request["value"]) case 'merge_max': GlobalVariable.merge_max = int(request["value"]) case 'velocity_filter': GlobalVariable.velocity_filter = int(request["value"]) case 'is_singular': GlobalVariable.is_singular = request["value"] case 'semitone_switch': GlobalVariable.semitone_switch = request["value"] case 'detail_switch': GlobalVariable.detail_switch = request["value"] case 'split_switch': GlobalVariable.split_switch = request["value"] case 'ai_token': GlobalVariable.ai_token[request["value"]["type"]] = request["value"]["token"] case 'translate_prompt': GlobalVariable.translate_prompt = request["value"] case 'duration_prompt': GlobalVariable.duration_prompt = request["value"] case 'sheld': GlobalVariable.sheld = float(request["value"]) def get_config(request: dict): configValue = eval("GlobalVariable." + request["name"]) return configValue def favorite_music(request: dict): src = os.path.join(getResourcesPath(request['type']), request['fileName'] + ".txt") dst = os.path.join(getResourcesPath('myFavorite'), request['fileName'] + ".txt") shutil.copy(src, dst, follow_symlinks=False) def convert_sheet(request: dict): convert_notes_to_delayed_format(request["fileName"], request["type"]) generatorSheetHtml(request["fileName"], list(map(lambda item: item['key'], GlobalVariable.music_sheet))) GlobalVariable.music_sheet = [] return "ok" def drop_file(request: dict): file_name = request["fileName"] if file_name is None: return '不ok' if request.get('suffix', None) is None: drop_path = os.path.join(getResourcesPath(request['type']), file_name + '.txt') else: drop_path = os.path.join(getResourcesPath(request['type']), file_name + request['suffix']) os.remove(drop_path) return 'ok' ================================================ FILE: sky-music-server/windhide/utils/hook_util.py ================================================ import builtins import platform from windhide.static.global_variable import GlobalVariable # 重定向 print 到空函数 def sout_null(): if GlobalVariable.isProd: builtins.print = lambda *args, **kwargs: None cpu_check() return None def cpu_check(): cpu_info = platform.processor() if "Intel" in cpu_info: GlobalVariable.cpu_type = 'Intel' elif "AMD" in cpu_info: GlobalVariable.cpu_type = 'AMD' ================================================ FILE: sky-music-server/windhide/utils/hwnd_utils.py ================================================ import os import psutil import win32gui import win32process from windhide.static.global_variable import GlobalVariable def get_running_apps(): apps = [] # 获取当前系统运行的所有进程 for proc in psutil.process_iter(['pid', 'name']): try: pid = proc.info['pid'] name = proc.info['name'] exe_path = proc.exe() if 'System' in name or 'svchost' in name: continue def enum_windows(hwnd, lParam): if win32gui.IsWindowVisible(hwnd): window_title = win32gui.GetWindowText(hwnd) if window_title: _, window_pid = win32process.GetWindowThreadProcessId(hwnd) if window_pid == pid: apps.append({ 'title': window_title, 'exe_name': os.path.basename(exe_path), 'pid': pid, 'hwnd': hwnd }) win32gui.EnumWindows(enum_windows, None) except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): pass return apps def get_running_apps_by_struct(check_struct): if any(item == check_struct for item in get_running_apps()) is True: GlobalVariable.hwnd_select_struct = check_struct GlobalVariable.window["hWnd"] = check_struct["hwnd"] GlobalVariable.is_custom_hwnd = True else: GlobalVariable.is_custom_hwnd = False ================================================ FILE: sky-music-server/windhide/utils/ocr_follow_util.py ================================================ import logging import os import threading import keyboard import time from ultralytics import YOLO from windhide.static.global_variable import GlobalVariable from windhide.thread.follow_thread import startThread as follow_thread_demo from windhide.utils.command_util import start_process from windhide.utils.ocr_normal_utils import get_window_screenshot from windhide.utils.path_util import getResourcesPath, convert_notes_to_delayed_format if GlobalVariable.cpu_type == 'Intel': from windhide.playRobot.intel_robot import mouse_move_to, key_press else: from windhide.playRobot.amd_robot import mouse_move_to, key_press global_button_model = None logging.getLogger("ultralytics").setLevel(logging.WARNING) def set_next_sheet(request: dict): try: print(f"Setting follow sheet for file: {request['fileName']}") convert_notes_to_delayed_format(request["fileName"], request["type"]) GlobalVariable.follow_sheet = list(map(lambda item: item['key'], GlobalVariable.music_sheet)) GlobalVariable.music_sheet = [] GlobalVariable.follow_music = request["fileName"] except Exception as e: print(f"Error in /followSheet: {str(e)}") def get_next_sheet(request: dict): if len(GlobalVariable.follow_sheet) == 0: return "" try: if request["type"] == "ok": sheet = GlobalVariable.follow_sheet[0] GlobalVariable.nowClientKey = sheet GlobalVariable.follow_sheet = GlobalVariable.follow_sheet[1:] return sheet else: GlobalVariable.nowClientKey = GlobalVariable.follow_sheet[0] return GlobalVariable.follow_sheet[0] except IndexError: print("空数组") return "" def load_key_model(): """加载模型并保存在全局变量中""" global global_button_model if global_button_model is None: button_model_path = os.path.join(getResourcesPath("systemTools"), "modelData", "check_key_model.pt") global_button_model = YOLO(button_model_path) # 加载模型并保存在 global_friend_model 中 print("模型加载完成") return global_button_model def get_key_position(conf, threshold=10): if GlobalVariable.window["key_position"] is not None: if len(GlobalVariable.window["key_position"]) == 15 and not GlobalVariable.window["is_change"]: return GlobalVariable.window["key_position"] print("开始检测按键布局") GlobalVariable.window["key_position"] = None image = None try: bgr_image = get_window_screenshot() height, width = bgr_image.shape[:2] crop_width = int(width * 0.15) # 右边10%的宽度 crop_height = int(height * 0.1) # 底部 10% 的高度 image = bgr_image[: -crop_height, : -crop_width] except Exception as e: print(e) model = load_key_model() results = model(image, conf=conf) # 替换为你的图片路径 boxes = results[0].boxes # 检测到的所有框 xyxy = boxes.xyxy.cpu().numpy() # 获取每个框的绝对坐标 (x1, y1, x2, y2) all_boxes = [] for box in xyxy: x1, y1, x2, y2 = box width = int(x2 - x1) height = int(y2 - y1) all_boxes.append({ "left": int(x1), "top": int(y1), "right": int(x2), "bottom": int(y2), "width": width, "height": height, "position_x": int(x1), # 添加 position_x "position_y": int(y1) # 添加 position_y }) result_dict = {} for box in all_boxes: y_key = box["top"] # 使用上边距作为分组依据 added = False for key in result_dict: if abs(key - y_key) <= threshold: result_dict[key].append(box) added = True break if not added: result_dict[y_key] = [box] sorted_result = {} sorted_keys = sorted(result_dict) for key in sorted_keys: group = result_dict[key] sorted_group = sorted(group, key=lambda b: b["position_x"]) sorted_result[key] = [{ "width": box["width"], "height": box["height"], "position_x": box["position_x"], "position_y": box["position_y"] } for box in sorted_group] key_mapping = { 0: ["y", "u", "i", "o", "p"], # 第一组 1: ["h", "j", "k", "l", ";"], # 第二组 2: ["n", "m", ",", ".", "/"] # 第三组 } final_result = {} for idx, (group_key, group_boxes) in enumerate(zip(sorted_keys, sorted_result.values())): if idx == 3: break # 避免越界 keys = key_mapping[idx] # 获取当前分组的键名列表 for key_name, box in zip(keys, group_boxes): final_result[key_name] = box # 使用键名作为最终结果的 key GlobalVariable.window["key_position"] = final_result if len(final_result) == 15: GlobalVariable.window["is_change"] = False GlobalVariable.window["wait"] = False print("final_result",final_result) print("final_result len =>",len(final_result)) return final_result def test_key_model_position(conf): image = None time.sleep(1) try: bgr_image = get_window_screenshot() height, width = bgr_image.shape[:2] crop_width = int(width * 0.15) # 右边10%的宽度 crop_height = int(height * 0.1) # 底部 10% 的高度 image = bgr_image[: -crop_height, : -crop_width] except Exception as e: print(e) model = load_key_model() results = model(image, conf=conf) # 替换为你的图片路径 results[0].show() def open_follow(): if GlobalVariable.exit_flag == False: keyboard.press('left alt') time.sleep(0.05) keyboard.press('left alt') time.sleep(1.3) start_process() GlobalVariable.follow_thread = threading.Thread(target=follow_thread_demo) GlobalVariable.follow_thread.daemon = True # 设置为守护线程,主线程退出时自动退出 GlobalVariable.follow_thread.start() ================================================ FILE: sky-music-server/windhide/utils/ocr_heart_utils.py ================================================ import os import cv2 import numpy as np import pyautogui import win32con import win32gui from sklearn.cluster import DBSCAN from ultralytics import YOLO from windhide.static.global_variable import GlobalVariable from windhide.utils.path_util import getResourcesPath if GlobalVariable.cpu_type == 'AMD': from windhide.playRobot.amd_robot import PostMessageW else: from windhide.playRobot.intel_robot import PostMessageW # 全局变量,保存模型 global_friend_model = None def load_model(): """加载模型并保存在全局变量中""" global global_friend_model if global_friend_model is None: model_path = os.path.join(getResourcesPath("systemTools"), "modelData", "friend_model.pt") global_friend_model = YOLO(model_path) # 加载模型并保存在 global_friend_model 中 print("模型加载完成") return global_friend_model def merge_boxes(boxes, confs, max_distance=50): """ 合并距离相近的识别框,合并后的框为置信度高的框。 :param boxes: 识别框的坐标 [x1, y1, x2, y2] (形状为 [num_boxes, 4]) :param confs: 每个框的置信度 (形状为 [num_boxes, ]) :param max_distance: 合并框时的最大距离,单位为像素 :return: 合并后的框的坐标和置信度 """ # 使用 DBSCAN 聚类算法来找出距离相近的框 clustering = DBSCAN(eps=max_distance, min_samples=1, metric='euclidean').fit(boxes[:, :2]) # 只考虑框的中心点进行聚类 # 聚类标签 labels = clustering.labels_ # 合并结果 merged_boxes = [] merged_confs = [] for label in np.unique(labels): # 获取同一类框的索引 indices = np.where(labels == label)[0] # 获取该组框的坐标和置信度 group_boxes = boxes[indices] group_confs = confs[indices] # 选择置信度最高的框作为合并后的框 max_conf_index = np.argmax(group_confs) # best_box = group_boxes[max_conf_index] # 计算合并后的框,使用最小外接矩形 x1 = np.min(group_boxes[:, 0]) y1 = np.min(group_boxes[:, 1]) x2 = np.max(group_boxes[:, 2]) y2 = np.max(group_boxes[:, 3]) # 将合并后的框添加到结果列表 merged_boxes.append([x1, y1, x2, y2]) merged_confs.append(group_confs[max_conf_index]) # 置信度为该类中的最大置信度 return np.array(merged_boxes), np.array(merged_confs) def get_friend_model_position(conf, isTest=False, max_distance=50): # 加载已经训练好的模型 image = get_window_screenshot_friend() model = load_model() results = model(image, conf=conf) # 替换为你的图片路径 boxes = results[0].boxes # 这是检测到的所有框 xyxy = boxes.xyxy.cpu().numpy() # 获取每个框的绝对坐标 (x1, y1, x2, y2) classes = boxes.cls.cpu().numpy() # 获取每个框对应的分类号 confs = boxes.conf.cpu().numpy() # 获取每个框的置信度 class_names = ["button", "friend"] # 获取所有类别的名称 merged_boxes, merged_confs = merge_boxes(xyxy, confs, max_distance) # 获取截图的偏移量 screenshot_offset_x = 150 screenshot_offset_y = 110 # 创建结果字典,专门存储合并框的中心点坐标 result_dict = { "button": [], "friend": [], } # 填充结果字典:仅保存修正后的中心点坐标 for i, box in enumerate(merged_boxes): class_id = int(classes[i]) # 获取当前合并框的类别ID label = class_names[class_id] # 获取类别名称 x1, y1, x2, y2 = box # 修正坐标:加上截图的偏移量 x1 += screenshot_offset_x y1 += screenshot_offset_y x2 += screenshot_offset_x y2 += screenshot_offset_y # 计算修正后的中心点坐标 center_x = (x1 + x2) / 2 center_y = (y1 + y2) / 2 result_dict[label].append((center_x, center_y)) # 如果需要可视化 image_with_boxes = image.copy() for i, box in enumerate(merged_boxes): x1, y1, x2, y2 = box # 同样修正坐标 x1 += screenshot_offset_x y1 += screenshot_offset_y x2 += screenshot_offset_x y2 += screenshot_offset_y cv2.rectangle(image_with_boxes, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(image_with_boxes, f"{merged_confs[i]:.2f}", (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2) if isTest: results[0].show() return result_dict def get_window_screenshot_friend(): png_path = os.path.join(getResourcesPath("systemTools"), 'modelData', 'demoScheenshot', 'demo.png') saturation_scale = 2.4 # >1增加饱和度,<1降低饱和度 PostMessageW(GlobalVariable.window["hWnd"], win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) client_rect = win32gui.GetClientRect(GlobalVariable.window["hWnd"]) # 将客户区矩形转换为屏幕坐标 client_x1, client_y1 = win32gui.ClientToScreen(GlobalVariable.window["hWnd"], (client_rect[0], client_rect[1])) client_x2, client_y2 = win32gui.ClientToScreen(GlobalVariable.window["hWnd"], (client_rect[2], client_rect[3])) # 截取窗口客户区范围内的图像 screenshot = pyautogui.screenshot( region=(client_x1 + 150, client_y1 + 80, client_x2 - client_x1 - 240, client_y2 - client_y1 - 150)) _, s_channel, _ = cv2.split(cv2.cvtColor(np.array(screenshot), cv2.COLOR_BGR2HSV)) d = np.clip(s_channel * saturation_scale, 0, 220).astype(np.uint8) cv2.imwrite(png_path, d) image = cv2.imread(png_path) return image ================================================ FILE: sky-music-server/windhide/utils/ocr_normal_utils.py ================================================ import ctypes import cv2 import numpy as np import pyautogui import win32con import win32gui from windhide.static.global_variable import GlobalVariable def resetGameFrame(): win32gui.ShowWindow(GlobalVariable.window["hWnd"], win32con.SW_RESTORE) # 先恢复窗口,以防最小化 rect = win32gui.GetWindowRect(GlobalVariable.window["hWnd"]) x, y = rect[0], rect[1] # 保持窗口的左上角位置不变 win32gui.MoveWindow(GlobalVariable.window["hWnd"], x, y, 1280, 720, True) def get_window_screenshot(): """获取指定窗口的截图""" # 获取窗口位置和大小 # PostMessageW(GlobalVariable.window["hWnd"], win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) rect = win32gui.GetWindowRect(GlobalVariable.window["hWnd"]) x1, y1, x2, y2 = rect # 截取窗口范围内的图像 screenshot = pyautogui.screenshot(region=(x1, y1, x2 - x1, y2 - y1)) return cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2BGR) def get_system_dpi(): hdc = ctypes.windll.user32.GetDC(0) # 获取屏幕设备上下文 dpi = ctypes.windll.gdi32.GetDeviceCaps(hdc, 88) # LOGPIXELSX = 88 ctypes.windll.user32.ReleaseDC(0, hdc) # 释放设备上下文 return dpi def get_game_position(): hwnd = GlobalVariable.window["hWnd"] # 获取窗口物理坐标 rect = win32gui.GetWindowRect(hwnd) client_rect = win32gui.GetClientRect(hwnd) # 获取窗口 DPI 缩放比例 border_x = (rect[2] - rect[0] - client_rect[2]) // 2 border_y = (rect[3] - rect[1] - client_rect[3] - border_x) GlobalVariable.window_offset_x = border_x GlobalVariable.window_offset_y = border_y # 转换物理坐标为逻辑坐标,去掉边框和标题栏 x1 = int((rect[0] + border_x)) y1 = int((rect[1] + border_y)) x2 = int((rect[2] - border_x)) y2 = int((rect[3] - border_y)) return x1, y1, x2, y2 ================================================ FILE: sky-music-server/windhide/utils/path_util.py ================================================ import json import os import re import chardet import plyer from windhide.static.global_variable import GlobalVariable def matchKey(key): match = re.search(r'(Key-?\d+)', key) return match.group(1) def convert_notes_to_delayed_format(fileName, type): # 优化了文件路径构建 file_path = os.path.join(getResourcesPath(type), fileName + ".txt") with open(file_path, 'r', encoding=detect_encoding(file_path)) as file: data = json.load(file) song_notes = data[0].get("songNotes", []) if not song_notes: return [] result = [] combined_keys = "" # 按键累积 combined_time = None # 当前时间 for i, note in enumerate(song_notes): current_time = note["time"] key = note["key"] # 处理新时间点 if current_time != combined_time: if combined_keys: next_time = song_notes[i]["time"] if i < len(song_notes) else current_time delay = next_time - combined_time result.append({"key": combined_keys, "delay": delay, "duration": note["duration"] if "duration" in note else 0}) combined_time = current_time combined_keys = GlobalVariable.keyMap.get(matchKey(key), '') # 获取按键,默认空字符串 else: combined_keys += GlobalVariable.keyMap.get(matchKey(key), '') # 如果时间相同,合并按键 # 处理最后的累积按键 if combined_keys: result.append({"key": combined_keys, "delay": 0, 'duration':song_notes[len(song_notes) - 1]['duration'] if "duration" in note else 0}) # 最后条目的延迟为0 GlobalVariable.music_sheet = result def convert_json_to_play(song_notes): result = [] combined_keys = "" # 按键累积 combined_time = None # 当前时间 for i, note in enumerate(song_notes): current_time = note["time"] key = note["key"] # 处理新时间点 if current_time != combined_time: if combined_keys: next_time = song_notes[i]["time"] if i < len(song_notes) else current_time delay = next_time - combined_time result.append({"key": combined_keys, "delay": delay, "duration": note["duration"]}) combined_time = current_time combined_keys = GlobalVariable.keyMap.get(matchKey(key), '') # 获取按键,默认空字符串 else: combined_keys += GlobalVariable.keyMap.get(matchKey(key), '') # 如果时间相同,合并按键 if combined_keys: result.append({"key": combined_keys, "delay": 0, "duration": song_notes[len(song_notes) - 1]['duration']}) # 最后条目的延迟为0 GlobalVariable.music_sheet = result def getResourcesPath(file): nowPath = os.path.dirname(os.path.abspath(__file__)) resources_path = os.path.dirname(os.path.dirname(nowPath)) target_subpath = os.path.join("backend_dist", "sky_music_server") resources_path = os.path.abspath( os.path.join(resources_path, "..", "..", "..")) if target_subpath in resources_path else os.path.abspath( os.path.join(resources_path, "..")) if file is None: return os.path.join(resources_path, 'resources') else: return os.path.join(resources_path, 'resources', file) def getTypeMusicList(type, searchStr=None): # 获取资源目录路径 resources_dir = os.path.join(getResourcesPath(None), type) # 获取目录下所有文件名 file_names = [ file for file in os.listdir(resources_dir) if os.path.isfile(os.path.join(resources_dir, file)) and file != ".keep" and not re.search(r"-#\d+(?=\.\w+)?$", file) # 排除匹配 -#数字 的文件 ] # 如果 searchStr 不为空,过滤包含 searchStr 的文件名,忽略大小写 if searchStr and searchStr.strip(): file_names = [file for file in file_names if searchStr.lower() in file.lower()] # 构建返回的音乐列表,包含文件名和总时长 music_list = [] for file in file_names: music_list.append({ "name": re.sub(r"-#(\d+)(?=\.\w+)?", "", file.replace(".txt", "")), "total_duration": format_time(int(re.search(r"-#(\d+)(?=\.\w+)?", file).group(1) if re.search(r"-#(\d+)(?=\.\w+)?", file) else "0")), "truthName": f"{file.replace('.txt', '')}" }) return music_list def process_sheet_rename_time(isImportOrTranslate = False): plyer.notification.notify( app_name='小星弹琴软件', app_icon=os.path.join(getResourcesPath("systemTools"), "icon.ico"), title='开始同步乐谱时长操作', message='时间可能会比较长,耐心等待,执行过程中可能会影响正常演奏。', timeout=1 ) if isImportOrTranslate: resource_dirs = [ os.path.join(getResourcesPath(None), "myImport"), os.path.join(getResourcesPath(None), "myTranslate"), ] else: resource_dirs = [ os.path.join(getResourcesPath(None), "systemMusic"), os.path.join(getResourcesPath(None), "myTranslate"), os.path.join(getResourcesPath(None), "myImport"), os.path.join(getResourcesPath(None), "myFavorite") ] all_file_paths = [] for resources_dir in resource_dirs: if not os.path.exists(resources_dir): continue file_paths = [ os.path.abspath(os.path.join(resources_dir, file)) for file in os.listdir(resources_dir) if os.path.isfile(os.path.join(resources_dir, file)) and file != ".keep" ] all_file_paths.extend(file_paths) for file_path in all_file_paths: if re.search(r"-#(\d+)(?=\.\w+)?", file_path): continue try: with open(file_path, 'r', encoding=detect_encoding(file_path)) as file: data = json.load(file) song_notes = data[0].get("songNotes", []) if not song_notes: continue sumTime = int(song_notes[-1]["time"]) + int(song_notes[-1].get("duration", 0)) directory, filename = os.path.split(file_path) name, ext = os.path.splitext(filename) new_filename = f"{name}-#{sumTime}{ext}" new_file_path = os.path.join(directory, new_filename) # 重命名文件 os.rename(file_path, new_file_path) except Exception as e: continue plyer.notification.notify( app_name='小星弹琴软件', app_icon=os.path.join(getResourcesPath("systemTools"), "icon.ico"), title='开始同步乐谱时长操作', message='操作完成', timeout=1 ) return "ok" def format_time(milliseconds): """ 将毫秒数格式化为时分秒格式(HH:MM:SS 或 MM:SS) """ seconds = milliseconds / 1000 hours, remainder = divmod(seconds, 3600) minutes, seconds = divmod(remainder, 60) if hours > 0: return f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}" else: return f"{int(minutes):02}:{int(seconds):02}" def detect_encoding(file_path): with open(file_path, 'rb') as file: raw_data = file.read(32000) # 读取文件的前32KB进行编码检测 return chardet.detect(raw_data)['encoding'] # 直接返回编码 ================================================ FILE: sky-music-server/windhide/utils/play_util.py ================================================ import chardet from windhide.playRobot import intel_robot, amd_robot from windhide.static.global_variable import GlobalVariable def detect_encoding(file_path): with open(file_path, 'rb') as file: raw_data = file.read(32000) # 读取文件的前32KB进行编码检测 return chardet.detect(raw_data)['encoding'] # 直接返回编码 def start(request: dict): GlobalVariable.task_queue.put(request) # 将请求放入队列 def pause(): try: print("Pausing music") match GlobalVariable.cpu_type: case "Intel": intel_robot.pause() case "AMD": amd_robot.pause() except Exception as e: print(f"Error in /pause: {str(e)}") def stop(): try: print("Stopping music") GlobalVariable.now_play_music = "没有正在播放的歌曲哦" match GlobalVariable.cpu_type: case "Intel": intel_robot.stop() case "AMD": amd_robot.stop() except Exception as e: print(f"Error in /stop: {str(e)}") def resume(): try: print("Resuming music") match GlobalVariable.cpu_type: case "Intel": intel_robot.resume() case "AMD": amd_robot.resume() except Exception as e: print(f"Error in /resume: {str(e)}") ================================================ FILE: sky-music-server/windhide/utils/sheet_decrypt_util.py ================================================ import json def decrypt_sheet(data): # 加密密钥和签名(来自 CEN.cs) KEY = "TB,R&Q}-ULFXF7={nU7v?fy#Khr9Mhuu" SIGNATURE = "ztB_kaFeQe/wa8Kq{r_jz!r=P])hQL(f" sheet_data = data[0] # 获取加密的 songNotes encrypted_notes = sheet_data.get('songNotes', []) if not encrypted_notes: print("错误:songNotes 为空") return print(f"找到 {len(encrypted_notes)} 个加密字符") print("开始解密...") # 解密算法(基于 CEN.ToString) decrypted_chars = [] for i, short_val in enumerate(encrypted_notes): # decrypted = (short - key[i % keyLength] + 100) key_char = KEY[i % len(KEY)] decrypted_char = chr(short_val - ord(key_char) + 100) decrypted_chars.append(decrypted_char) # 组合解密后的字符串 decrypted_str = ''.join(decrypted_chars) # 移除尾部签名 decrypted_str = decrypted_str.replace(SIGNATURE, '') print(f"解密后字符串长度: {len(decrypted_str)}") data[0]['songNotes'] = json.loads(decrypted_str) return data ================================================ FILE: sky-music-web/.editorconfig ================================================ root = true [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true ================================================ FILE: sky-music-web/.eslintignore ================================================ node_modules dist out .gitignore ================================================ FILE: sky-music-web/.eslintrc.cjs ================================================ /* eslint-env node */ require('@rushstack/eslint-patch/modern-module-resolution') module.exports = { extends: [ 'eslint:recommended', 'plugin:vue/vue3-recommended', '@electron-toolkit', '@electron-toolkit/eslint-config-ts/eslint-recommended', '@vue/eslint-config-typescript/recommended', '@vue/eslint-config-prettier' ], rules: { 'vue/require-default-prop': 'off', 'vue/multi-word-component-names': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': 'off', 'no-debugger': 'off', } } ================================================ FILE: sky-music-web/.gitignore ================================================ node_modules dist out *.txt .DS_Store *.log* ================================================ FILE: sky-music-web/.npmrc ================================================ electron_mirror=https://npmmirror.com/mirrors/electron/ electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/ ================================================ FILE: sky-music-web/.prettierignore ================================================ out dist pnpm-lock.yaml LICENSE.md tsconfig.json tsconfig.*.json ================================================ FILE: sky-music-web/.prettierrc.yaml ================================================ singleQuote: true semi: false printWidth: 100 trailingComma: none ================================================ FILE: sky-music-web/README.md ================================================ # electron-app An Electron application with Vue and TypeScript ## Recommended IDE Setup - [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) ## Project Setup ### Install ```bash $ yarn ``` ### Development ```bash $ yarn dev ``` ### Build ```bash # For windows $ yarn build:win # For macOS $ yarn build:mac # For Linux $ yarn build:linux ``` ================================================ FILE: sky-music-web/build/entitlements.mac.plist ================================================ com.apple.security.cs.allow-jit com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.cs.allow-dyld-environment-variables ================================================ FILE: sky-music-web/dev-app-update.yml ================================================ provider: generic url: https://example.com/auto-updates updaterCacheDirName: electron-app-updater ================================================ FILE: sky-music-web/electron-builder.yml ================================================ appId: 星星弹琴 productName: Sky_Music directories: buildResources: build files: - '!**/.vscode/*' - '!src/*' - '!electron.vite.config.{js,ts,mjs,cjs}' - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' - '!**/backend_dist/**' - '!**/template-resources/**' - '!**/ffmpeg.exe' - '!*.keep' win: executableName: Sky_Music icon: build/icon.ico requestedExecutionLevel: requireAdministrator nsis: artifactName: 小星弹琴软件v${version}_x64_windows.${ext} shortcutName: "小星弹琴软件" uninstallDisplayName: '卸载 Uninstaller' perMachine: true installerIcon: build/icon.ico uninstallerIcon: build/icon.ico installerHeaderIcon: build/icon.ico oneClick: false # 允许自定义安装目录和其他设置 allowToChangeInstallationDirectory: true # 允许用户选择安装目录 createDesktopShortcut: true # 允许创建桌面快捷方式 createStartMenuShortcut: true # 允许创建开始菜单快捷方式 deleteAppDataOnUninstall: true # 卸载时删除软件缓存数据 extraFiles: - from: ".\\backend_dist" to: ".\\backend_dist" - from: "..\\template-resources" to: ".\\resources" npmRebuild: false publish: provider: generic url: https://example.com/auto-updates electronDownload: mirror: https://npmmirror.com/mirrors/electron/ ================================================ FILE: sky-music-web/electron.vite.config.ts ================================================ import { resolve } from 'path' import { defineConfig, externalizeDepsPlugin } from 'electron-vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ main: { plugins: [externalizeDepsPlugin()] }, preload: { plugins: [externalizeDepsPlugin()] }, renderer: { resolve: { alias: { '@renderer': resolve('src/renderer/src') } }, plugins: [vue()] } }) ================================================ FILE: sky-music-web/package.json ================================================ { "name": "sky-music-web", "version": "v2.6.6", "description": "WindHide", "main": "./out/main/index.js", "author": "WindHide", "homepage": "https://github.com/windhide", "scripts": { "format": "prettier --write .", "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix", "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", "typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false", "typecheck": "npm run typecheck:node && npm run typecheck:web", "start": "electron-vite preview", "dev": "electron-vite dev", "build": "npm run typecheck && electron-vite build", "build:win": "npm run build && electron-builder --win" }, "dependencies": { "@electron-toolkit/preload": "^3.0.0", "@electron-toolkit/utils": "^3.0.0", "@vicons/fluent": "^0.13.0", "@vicons/ionicons5": "^0.12.0", "electron-store": "^8.0.0", "electron-updater": "^6.1.7", "hotkeys-js": "^3.13.9", "html2canvas": "^1.4.1", "iconv-lite": "^0.6.3", "lodash-es": "^4.17.21", "naive-ui": "^2.41.0", "node-cmd": "^5.0.0", "p-limit": "^4.0.0", "vfonts": "^0.0.3", "vue-i18n": "^11.1.3", "vue-router": "^4.0.3", "vuex": "^4.0.0" }, "devDependencies": { "@electron-toolkit/eslint-config": "^1.0.2", "@electron-toolkit/eslint-config-ts": "^2.0.0", "@electron-toolkit/tsconfig": "^1.0.1", "@rushstack/eslint-patch": "^1.10.3", "@types/node": "^20.14.8", "@vitejs/plugin-vue": "^5.0.5", "@vue/eslint-config-prettier": "^9.0.0", "@vue/eslint-config-typescript": "^13.0.0", "electron": "^31.0.2", "electron-builder": "^24.13.3", "electron-vite": "^2.3.0", "eslint": "^8.57.0", "eslint-plugin-vue": "^9.26.0", "prettier": "^3.3.2", "typescript": "^5.5.2", "vite": "^5.3.1", "vue": "^3.4.30", "vue-tsc": "^2.0.22" } } ================================================ FILE: sky-music-web/src/main/index.ts ================================================ import { app, shell, BrowserWindow, ipcMain, screen, Notification, powerSaveBlocker } from 'electron' import { join } from 'path' import { electronApp, is } from '@electron-toolkit/utils' import icon from '../../build/icon.png?asset' import { spawn, execSync } from 'child_process' import Store from 'electron-store'; Store.initRenderer() const path = require('path') const fs = require('fs'); const iconv = require('iconv-lite'); // 用于支持多种编码格式 const elStore = new Store() const MAX_CONCURRENT_COPIES = 20; // 限制最大并行任务数 let limit; (async () => { const { default: pLimit } = await import('p-limit'); limit = pLimit(MAX_CONCURRENT_COPIES); })(); let mainWindow: BrowserWindow | null = null; app.commandLine.appendSwitch('no-sandbox'); // 启动电源保护 powerSaveBlocker.start('prevent-display-sleep'); powerSaveBlocker.start('prevent-app-suspension'); app.commandLine.appendSwitch('enable-gpu-rasterization'); // 强制 GPU 光栅化 app.commandLine.appendSwitch('ignore-gpu-blacklist'); // 忽略 Electron GPU 黑名单 app.commandLine.appendSwitch('force-color-profile', 'srgb'); // 统一颜色配置 app.commandLine.appendSwitch('disable-background-timer-throttling'); // 关闭后台定时器节流 // 允许 GPU 但在必要时自动回退到软件渲染 app.commandLine.appendSwitch('disable-software-rasterizer'); const syncFolders = ["myImport", "myTranslate", "myFavorite"] function createWindow(): void { // Create the browser window. mainWindow = new BrowserWindow({ width: 800, height: 774, resizable: false, autoHideMenuBar: true, frame: false, transparent: true, alwaysOnTop: false, icon, webPreferences: { preload: join(__dirname, '../preload/index.js'), sandbox: false, nodeIntegration: false, contextIsolation: true } }) mainWindow.on('ready-to-show', () => { mainWindow?.show() }) mainWindow.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url) return { action: 'deny' } }) if (is.dev && process.env['ELECTRON_RENDERER_URL']) { mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) } else { // Menu.setApplicationMenu(null) mainWindow.loadFile(join(__dirname, '../renderer/index.html')) } // 窗口拖动逻辑 let isMousePressed = false let offsetX = 0 let offsetY = 0 let nowHeight = 774 let nowWidth = 800 ipcMain.on('mousedown', (_event, { }) => { console.log(_event) const cursorPoint = screen.getCursorScreenPoint() let bounds:any = null bounds = mainWindow?.getBounds() if (bounds) { isMousePressed = true offsetX = cursorPoint.x - bounds.x // 精确鼠标偏移量 offsetY = cursorPoint.y - bounds.y } }) ipcMain.on('mousemove', (event) => { console.log(event) if (isMousePressed && mainWindow) { const cursorPoint = screen.getCursorScreenPoint() // 实时更新窗口位置 mainWindow.setBounds({ x: cursorPoint.x - offsetX, y: cursorPoint.y - offsetY, width: nowWidth, height: nowHeight }) } }) ipcMain.on('mouseup', () => { isMousePressed = false }) ipcMain.on('window-min', (event) => { console.log(event) mainWindow?.minimize() }) ipcMain.on('window-close', (event) => { console.log(event) ipcMain.removeAllListeners('mousedown') ipcMain.removeAllListeners('mousemove') ipcMain.removeAllListeners('mouseup') mainWindow?.close() app.quit() }) ipcMain.on('set-always-on-top', (event) => { console.log(event) mainWindow?.setAlwaysOnTop(!mainWindow?.isAlwaysOnTop()) }) ipcMain.on('send_system_notification', (_event, title:string, body: string) => { const notification = new Notification({title,body}); notification.show(); // 显示通知 setTimeout(()=>{ notification.close() },1500) }) ipcMain.on('window_size', (_event, height:number, width: number) => { console.log(_event) let bounds:any = null bounds = mainWindow?.getBounds() nowHeight = height === 0 ? 774 : height nowWidth = width === 0 ? 800 : width mainWindow?.setBounds({ x: bounds.x, y: bounds.y, width:nowWidth, height:nowHeight }) }) ipcMain.handle('read-file', async (_event, filePath:string, needData: boolean) => { try { let fileContent = await fs.promises.readFile(filePath, 'utf8'); const encodingList = ['utf8', 'utf16le', 'gbk']; // 支持的编码 for (const encoding of encodingList) { try { const buffer = await fs.promises.readFile(filePath); fileContent = iconv.decode(buffer, encoding); // 测试解析(假设文件是 JSON 格式) JSON.parse(fileContent); // 尝试解析 break; // 成功解析则退出循环 } catch (err) { // 如果解析失败,继续尝试下一个编码 fileContent = undefined; } } if (needData){ return JSON.parse(fileContent) } return fileContent.includes("songNotes") } catch (err) { console.error('Error loading JSON:', err); return false; } }); ipcMain.handle('getVersion', (_event) => { return app.getVersion() }); // console.log('当前缓存存储位置',app.getPath('userData')) ipcMain.on('setElStore', (_event, key, value) => { elStore.set(key, value) }) ipcMain.on('getElStore', (event, key) => { const result = elStore.get(key) event.returnValue = result }) ipcMain.on('sync_sheet_2_el', async (_event,) =>{ let cachePath = elStore.path.replaceAll("\\config.json", ""); for (const folder of syncFolders) { fetch("http://127.0.0.1:9899/path", { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ "type": folder }), }).then(response => response.json()) .then(data =>{ copyFolderIncremental(data, path.join(cachePath, folder)); }) .catch(error => console.error(error)) } }) ipcMain.on('sync_el_2_sheet', async (_event,) =>{ let cachePath = elStore.path.replaceAll("\\config.json", ""); for (const folder of syncFolders) { fetch("http://127.0.0.1:9899/path", { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ "type": folder }), }).then(response => response.json()).then(data =>{ copyFolderIncremental(path.join(cachePath, folder), data); }).catch(error => console.error(error)) } }) async function copyFolderIncremental(source, target) { try { target = path.resolve(target); source = path.resolve(source); console.log("target=>", target); console.log("source=>", source); // 检查源文件夹是否存在 let sourceExists = true; try { await fs.promises.access(source, fs.constants.F_OK); } catch (err) { sourceExists = false; await fs.promises.mkdir(source, { recursive: true }); } // 检查目标文件夹是否存在 let targetExists = true; try { await fs.promises.access(target, fs.constants.F_OK); } catch (err) { targetExists = false; } // 如果源文件夹不存在且目标文件夹存在,删除目标文件夹 if (!sourceExists && targetExists) { await fs.promises.rm(target, { recursive: true, force: true }); } // 确保目标文件夹存在 await fs.promises.mkdir(target, { recursive: true }); // 获取源文件夹中的文件列表 const sourceFiles = await fs.promises.readdir(source); const sourceFilesSet = new Set(sourceFiles); // 如果目标文件夹存在,检查需要删除的文件 if (targetExists) { const targetFiles = await fs.promises.readdir(target); const deletePromises = targetFiles .filter(file => !sourceFilesSet.has(file)) .map(file => { const targetPath = path.join(target, file); return fs.promises.rm(targetPath, { recursive: true, force: true }); }); await Promise.all(deletePromises); } // 复制或更新文件 const copyTasks = sourceFiles.map(file => limit(async () => { try { const sourcePath = path.join(source, file); const targetPath = path.join(target, file); const sourceStats = await fs.promises.stat(sourcePath); if (sourceStats.isFile()) { let shouldCopy = true; try { const targetStats = await fs.promises.stat(targetPath); shouldCopy = targetStats.mtimeMs < sourceStats.mtimeMs; } catch (err) { // 目标文件不存在,需要复制 } if (shouldCopy) { await fs.promises.copyFile(sourcePath, targetPath); } } else if (sourceStats.isDirectory()) { await copyFolderIncremental(sourcePath, targetPath); } } catch (err) { console.error(`Error processing file ${file}:`, err); } })); await Promise.all(copyTasks); } catch (err) { console.error("Error copying folder:", err); console.log("Continuing with other operations..."); } } } app.on('window-all-closed', () => { app.quit(); }) function launchBackend() { const args = ['--prod'] const exeName = 'sky_music_server.exe' let runPath = __dirname.replace("resources\\app.asar\\out\\main","") const exePath = path.join(runPath, 'backend_dist/sky_music_server/sky_music_server.exe') if (!fs.existsSync(exePath)) { console.error('[server] server File not found:', exePath) return } if (isBackendRunning(exeName)) { console.log('[server] is running, do Nothiong') return } console.log('[server] starting:', exePath) const subprocess = spawn(exePath, args, { detached: true, stdio: 'ignore', windowsHide: true }) subprocess.unref() // 让它在主进程退出后仍能运行 } function isBackendRunning(processName: string): boolean { try { const stdout = execSync('tasklist', { encoding: 'utf-8' }) return stdout.toLowerCase().includes(processName.toLowerCase()) } catch (err) { console.error('[check server] faild:', err) return false } } app.whenReady().then(() => { electronApp.setAppUserModelId('com.windhide') createWindow() launchBackend() }) ================================================ FILE: sky-music-web/src/preload/index.d.ts ================================================ import { ElectronAPI } from '@electron-toolkit/preload' declare global { interface Window { api: { setAlwaysOnTop: () => void; close: () => void; mini: () => void; async readFile: (filePath:string, needData: boolean) => any; system_notification: (title,body) => void; getVersion: () => any; window_size: (height:number, width: number) => void; syncElToSheetFiles sync_el_2_sheet:() => void; sync_sheet_2_el:() => void; }; electron:{ onMouseDown: (x, y) => void; onMouseMove: (x, y) => void; onMouseUp: ()=> void; } } } ================================================ FILE: sky-music-web/src/preload/index.ts ================================================ import { contextBridge, ipcRenderer } from 'electron' import { electronAPI } from '@electron-toolkit/preload' // Custom APIs for renderer const api = {} // @ts-ignore (define in dts) const elStore = {} // Use `contextBridge` APIs to expose Electron APIs to // renderer only if context isolation is enabled, otherwise // just add to the DOM global. if (process.contextIsolated) { try { contextBridge.exposeInMainWorld('api', { setAlwaysOnTop: () => { ipcRenderer.send('set-always-on-top'); }, close: () => { ipcRenderer.send('window-close'); }, mini: () => { ipcRenderer.send('window-min'); }, readFile: async (filePath: string, needData: boolean) => { return await ipcRenderer.invoke('read-file', filePath, needData); }, getVersion: () => { return ipcRenderer.invoke('getVersion'); }, system_notification: async (title,body) => { ipcRenderer.send('send_system_notification', title, body); }, window_size: (height:number, width: number) => { ipcRenderer.send('window_size', height, width); }, sync_el_2_sheet:() => { ipcRenderer.send('sync_el_2_sheet'); }, sync_sheet_2_el:() => { ipcRenderer.send('sync_sheet_2_el'); } });// 用于向主进程发送拖动事件 contextBridge.exposeInMainWorld('electron', { onMouseDown: (x, y) => ipcRenderer.send('mousedown', { x, y }), onMouseMove: (x, y) => ipcRenderer.send('mousemove', { x, y }), onMouseUp: () => ipcRenderer.send('mouseup') }) // 用于数据持久化 contextBridge.exposeInMainWorld('elStore', { setElStore:(key, value)=>{ ipcRenderer.send('setElStore', key, value); }, getElStore:(key)=>{ return ipcRenderer.sendSync('getElStore', key); } }) } catch (error) { console.error(error) } } else { // @ts-ignore (define in dts) window.electron = electronAPI // @ts-ignore (define in dts) window.api = api // @ts-ignore (define in dts) window.elStore = elStore } ================================================ FILE: sky-music-web/src/renderer/index.html ================================================ Sky_Music
================================================ FILE: sky-music-web/src/renderer/src/App.vue ================================================ ================================================ FILE: sky-music-web/src/renderer/src/component/svg/cr.vue ================================================ ================================================ FILE: sky-music-web/src/renderer/src/views/home.vue ================================================ ================================================ FILE: sky-music-web/src/renderer/src/views/home_loader.vue ================================================ ================================================ FILE: sky-music-web/src/renderer/src/views/hwndHandle.vue ================================================ ================================================ FILE: sky-music-web/src/renderer/src/views/kube.vue ================================================ ================================================ FILE: sky-music-web/src/renderer/src/views/magicTools.vue ================================================ ================================================ FILE: sky-music-web/src/renderer/src/views/music.vue ================================================ ================================================ FILE: sky-music-web/src/renderer/src/views/music_edit.vue ================================================ ================================================ FILE: sky-music-web/src/renderer/src/views/setting.vue ================================================ ================================================ FILE: sky-music-web/src/renderer/src/views/shortcutKeys.vue ================================================ ================================================ FILE: sky-music-web/src/renderer/src/views/tutorial.vue ================================================ ================================================ FILE: sky-music-web/tsconfig.json ================================================ { "files": [], "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }] } ================================================ FILE: sky-music-web/tsconfig.node.json ================================================ { "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"], "compilerOptions": { "composite": true, "types": ["electron-vite/node"] } } ================================================ FILE: sky-music-web/tsconfig.web.json ================================================ { "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", "include": [ "src/renderer/src/env.d.ts", "src/renderer/src/**/*", "src/renderer/src/**/*.vue", "src/preload/*.d.ts" ], "compilerOptions": { "composite": true, "baseUrl": ".", "paths": { "@renderer/*": [ "src/renderer/src/*" ] }, "noUnusedLocals":false } } ================================================ FILE: template-resources/myFavorite/.keep ================================================ ================================================ FILE: template-resources/myImport/.keep ================================================ ================================================ FILE: template-resources/myTranslate/.keep ================================================ ================================================ FILE: template-resources/systemTools/drawTool/.keep ================================================ ================================================ FILE: template-resources/systemTools/modelData/demoScheenshot/.keep ================================================ ================================================ FILE: template-resources/systemTools/scriptTemplate/.keep ================================================ ================================================ FILE: template-resources/translateMID/.keep ================================================ ================================================ FILE: template-resources/translateOriginalMusic/.keep ================================================