From 1921fa7f080188943c555a1c53e6ebfca5e4267b Mon Sep 17 00:00:00 2001 From: markshawn2020 Date: Tue, 17 Dec 2024 23:31:04 +0800 Subject: [PATCH] fix: simpleaudio problem --- .windsurfrules | 2 +- docs/development/distribution.md | 5 +- main.spec | 44 --------- pyproject.toml | 4 +- transparent_overlay/ImageMatchThread.py | 35 ++++--- transparent_overlay/sounds.py | 119 ++++++++++++------------ watchcat.spec | 33 +++---- 7 files changed, 102 insertions(+), 140 deletions(-) delete mode 100644 main.spec diff --git a/.windsurfrules b/.windsurfrules index bf6bd84..55495d4 100644 --- a/.windsurfrules +++ b/.windsurfrules @@ -1,3 +1,3 @@ 本项目基于: -- poetry +- poetry,如果要新增包,请直接 `poetry add xx`,否则需要先执行 `poetry lock`,再执行 `poetry install` - pyqt \ No newline at end of file diff --git a/docs/development/distribution.md b/docs/development/distribution.md index c14e85e..b1af97b 100644 --- a/docs/development/distribution.md +++ b/docs/development/distribution.md @@ -14,10 +14,11 @@ WatchCat 使用 PyInstaller 进行打包,通过 GitHub Actions 自动构建各 # 安装项目依赖 poetry install --with dev -# 使用 PyInstaller 打包 +# 使用 PyInstaller 打包(使用配置文件) poetry run pyinstaller watchcat.spec +``` -打包后的文件将在 `dist/WatchCat` 目录下生成。在 macOS 系统上,你可以通过以下两种方式运行打包后的程序: +打包后的文件将在 `dist/WatchCat` 目录下生成。在 macOS 系统上,你可以通过以下方式运行打包后的程序: 1. 命令行方式: ```bash diff --git a/main.spec b/main.spec deleted file mode 100644 index 2ab479a..0000000 --- a/main.spec +++ /dev/null @@ -1,44 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - - -a = Analysis( - ['transparent_overlay/main.py'], - pathex=[], - binaries=[], - datas=[], - hiddenimports=[], - hookspath=[], - hooksconfig={}, - runtime_hooks=[], - excludes=[], - noarchive=False, - optimize=0, -) -pyz = PYZ(a.pure) - -exe = EXE( - pyz, - a.scripts, - a.binaries, - a.datas, - [], - name='main', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=False, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None, -) -app = BUNDLE( - exe, - name='main.app', - icon=None, - bundle_identifier=None, -) diff --git a/pyproject.toml b/pyproject.toml index dc26d86..50e4a02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,9 @@ pyobjc-framework-cocoa = { version = "^10.3.2", platform = "darwin" } pyobjc-framework-quartz = { version = "^10.3.2", platform = "darwin" } pyobjc-framework-applicationservices = { version = "^10.3.2", platform = "darwin" } pyobjc-framework-coretext = { version = "^10.3.2", platform = "darwin" } -simpleaudio = { version = "^1.0.4", platform = "darwin" } +sounddevice = { version = "^0.4.6", platform = "darwin" } +pyobjus = { version = "^1.2.1", platform = "darwin" } +rumps = { version = "^0.4.0", platform = "darwin" } [tool.poetry.group.test.dependencies] pytest = "^7.4.0" diff --git a/transparent_overlay/ImageMatchThread.py b/transparent_overlay/ImageMatchThread.py index 3f02539..fea70a1 100644 --- a/transparent_overlay/ImageMatchThread.py +++ b/transparent_overlay/ImageMatchThread.py @@ -1,14 +1,15 @@ +from math import fabs +import os +import platform +import subprocess import time import cv2 import numpy as np -from PyQt6.QtCore import QThread, pyqtSignal -import platform -import subprocess -import os - from notifypy import Notify - +from PyQt6.QtCore import QThread, pyqtSignal +from PyQt6.QtWidgets import QSystemTrayIcon +from PyQt6.QtGui import QIcon from transparent_overlay.log import logger from transparent_overlay.sounds import SoundPlayer, SoundType @@ -24,6 +25,10 @@ def __init__(self, sct, config): self.running = False self.last_match = None # 存储上次匹配位置 self.last_match_status = False # 跟踪上一次的匹配状态 + # 初始化系统托盘图标用于通知 + self.tray_icon = QSystemTrayIcon() + self.tray_icon.setIcon(QIcon()) # 空图标 + self.tray_icon.show() def set_target(self, image): """设置目标图片""" @@ -36,13 +41,17 @@ def on_match(self, title, message): """跨平台发送系统通知和提示音""" # 检查是否启用通知 if self.config.data.enable_notification: - # 使用 notify-py 发送通知(支持 Windows、Linux、macOS) - logger.info(f"发送通知: {title} - {message}") - notification = Notify() - notification.title = title - notification.message = message - notification.application_name = "Watch Cat" - notification.send(block=False) # 改为非阻塞,避免声音延迟 + try: + # 使用 QSystemTrayIcon 发送通知 + self.tray_icon.showMessage( + title, + message, + QSystemTrayIcon.MessageIcon.Information, + 3000 # 显示3秒 + ) + logger.info(f"发送通知: {title} - {message}") + except Exception as e: + logger.warning(f"通知发送失败: {e}") # 检查是否启用声音 if self.config.data.enable_sound: diff --git a/transparent_overlay/sounds.py b/transparent_overlay/sounds.py index c85d598..8e92c12 100644 --- a/transparent_overlay/sounds.py +++ b/transparent_overlay/sounds.py @@ -1,9 +1,10 @@ """声音管理模块""" import enum import numpy as np -import simpleaudio as sa +import sounddevice as sd from pydub import AudioSegment import os +import time class SoundType(enum.Enum): @@ -33,8 +34,17 @@ def generate_sine_wave(frequency: float, duration: float, sample_rate: int = 441 numpy.ndarray: 音频数据 """ t = np.linspace(0, duration, int(duration * sample_rate), False) - note = np.sin(2 * np.pi * frequency * t) * 32767 - return note.astype(np.int16) + note = np.sin(2 * np.pi * frequency * t) + return note.astype(np.float32) + + @classmethod + def _play_buffer(cls, audio_data, sample_rate=44100): + """安全地播放音频缓冲区""" + try: + sd.play(audio_data, sample_rate, blocking=False) + except Exception as e: + from transparent_overlay.log import logger + logger.warning(f"播放音频失败: {e}") @classmethod def play_sound(cls, sound_type: SoundType, config=None) -> None: @@ -57,6 +67,49 @@ def play_sound(cls, sound_type: SoundType, config=None) -> None: elif sound_type == SoundType.CUSTOM and config: cls.play_custom(config) + @classmethod + def play_beep(cls, frequency: float = 440, duration: float = 0.25) -> None: + """播放蜂鸣声 + + Args: + frequency: 频率 (Hz),默认 440Hz (标准 A 音) + duration: 持续时间 (秒),默认 0.25 秒 + """ + audio = cls.generate_sine_wave(frequency, duration) + cls._play_buffer(audio) + + @classmethod + def play_success(cls) -> None: + """播放成功提示音 (上升音)""" + audio1 = cls.generate_sine_wave(440, 0.1) # A4 + audio2 = cls.generate_sine_wave(523.25, 0.1) # C5 + audio = np.concatenate([audio1, audio2]) + cls._play_buffer(audio) + + @classmethod + def play_error(cls) -> None: + """播放错误提示音 (下降音)""" + audio1 = cls.generate_sine_wave(440, 0.1) # A4 + audio2 = cls.generate_sine_wave(349.23, 0.1) # F4 + audio = np.concatenate([audio1, audio2]) + cls._play_buffer(audio) + + @classmethod + def play_mario(cls) -> None: + """播放马里奥风格的提示音""" + frequencies = [660, 660, 0, 660, 0, 520, 660, 0, 784] + durations = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.15] + audio_parts = [] + + for freq, dur in zip(frequencies, durations): + if freq == 0: # 静音 + audio_parts.append(np.zeros(int(dur * 44100), dtype=np.float32)) + else: + audio_parts.append(cls.generate_sine_wave(freq, dur)) + + audio = np.concatenate(audio_parts) + cls._play_buffer(audio) + @classmethod def play_custom(cls, config) -> None: """播放自定义音乐 @@ -88,70 +141,16 @@ def play_custom(cls, config) -> None: # 导出为临时文件并播放 segment = segment.set_channels(1) # 转换为单声道 - samples = np.array(segment.get_array_of_samples()) + samples = np.array(segment.get_array_of_samples(), dtype=np.float32) # 确保音量适中 max_sample = np.max(np.abs(samples)) if max_sample > 0: - scale = min(32767 / max_sample, 1.0) - samples = (samples * scale).astype(np.int16) + samples = samples / max_sample # 播放音频 - play_obj = sa.play_buffer( - samples, - num_channels=1, - bytes_per_sample=2, - sample_rate=segment.frame_rate - ) - play_obj.stop_on_destroy = True + cls._play_buffer(samples, segment.frame_rate) except Exception as e: from transparent_overlay.log import logger logger.warning(f"播放自定义音乐失败: {e}") - - @classmethod - def play_beep(cls, frequency: float = 440, duration: float = 0.25) -> None: - """播放蜂鸣声 - - Args: - frequency: 频率 (Hz),默认 440Hz (标准 A 音) - duration: 持续时间 (秒),默认 0.25 秒 - """ - audio = cls.generate_sine_wave(frequency, duration) - play_obj = sa.play_buffer(audio, 1, 2, 44100) - play_obj.stop_on_destroy = True # 非阻塞播放 - - @classmethod - def play_success(cls) -> None: - """播放成功提示音 (上升音)""" - audio1 = cls.generate_sine_wave(440, 0.1) # A4 - audio2 = cls.generate_sine_wave(523.25, 0.1) # C5 - audio = np.concatenate([audio1, audio2]) - play_obj = sa.play_buffer(audio, 1, 2, 44100) - play_obj.stop_on_destroy = True - - @classmethod - def play_error(cls) -> None: - """播放错误提示音 (下降音)""" - audio1 = cls.generate_sine_wave(440, 0.1) # A4 - audio2 = cls.generate_sine_wave(349.23, 0.1) # F4 - audio = np.concatenate([audio1, audio2]) - play_obj = sa.play_buffer(audio, 1, 2, 44100) - play_obj.stop_on_destroy = True - - @classmethod - def play_mario(cls) -> None: - """播放马里奥风格的提示音""" - frequencies = [660, 660, 0, 660, 0, 520, 660, 0, 784] - durations = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.15] - audio_parts = [] - - for freq, dur in zip(frequencies, durations): - if freq == 0: # 静音 - audio_parts.append(np.zeros(int(dur * 44100), dtype=np.int16)) - else: - audio_parts.append(cls.generate_sine_wave(freq, dur)) - - audio = np.concatenate(audio_parts) - play_obj = sa.play_buffer(audio, 1, 2, 44100) - play_obj.stop_on_destroy = True diff --git a/watchcat.spec b/watchcat.spec index 17012c8..c4c177f 100644 --- a/watchcat.spec +++ b/watchcat.spec @@ -9,7 +9,7 @@ a = Analysis( datas=[ ('default_config.json', '.'), ('resources', 'resources'), - ('venv/lib/python3.12/site-packages/notifypy/os_notifiers/binaries/Notificator.app', 'notifypy/os_notifiers/binaries'), + ('*/site-packages/notifypy/os_notifiers/binaries/Notificator.app', 'notifypy/os_notifiers/binaries'), ], hiddenimports=[ 'PyQt6.QtCore', @@ -26,11 +26,13 @@ a = Analysis( hookspath=[], hooksconfig={}, runtime_hooks=[], - excludes=[], + excludes=['tkinter', 'matplotlib', 'PIL'], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, - noarchive=False, + noarchive=True, + collect_submodules=['PyQt6.QtCore', 'PyQt6.QtGui'], + collect_data_files=[('PyQt6', '.')], ) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) @@ -38,28 +40,21 @@ pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE( pyz, a.scripts, + a.binaries, + a.zipfiles, + a.datas, [], - exclude_binaries=True, name='WatchCat', debug=False, bootloader_ignore_signals=False, - strip=False, - upx=True, - console=False, + strip=False, + upx=False, + upx_exclude=[], + runtime_tmpdir=None, + console=True, disable_windowed_traceback=False, - argv_emulation=True, + argv_emulation=False, target_arch=None, codesign_identity=None, entitlements_file=None, ) - -coll = COLLECT( - exe, - a.binaries, - a.zipfiles, - a.datas, - strip=False, - upx=True, - upx_exclude=[], - name='WatchCat', -)