Skip to content

Commit 6dfa40e

Browse files
committed
新增 **音乐设置功能**,用户可以设置抽取有关的音乐相关参数,如音量、渐入时长、渐出时长等
1 parent c1f6e56 commit 6dfa40e

9 files changed

Lines changed: 553 additions & 74 deletions

File tree

CHANGELOG/v1.3.2-alpha.5/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ v2.0 - Koharu(小鸟游星野) Alpha 5
1212
- 新增 **安装更新脚本**,支持**重启应用程序****Linux系统适配**
1313
- 新增 **上课时间禁用功能**,用户可以设置上课时间,方便设置非上课时间段
1414
- 新增 **CSES课程表模板导入功能**,用户可以导入CSES课程表模板,方便设置非上课时间段
15+
- 新增 **音乐设置功能**,用户可以设置抽取有关的音乐相关参数,如音量、渐入时长、渐出时长等
1516

1617
## 💡 功能优化
1718

app/Language/modules/music_settings.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,23 @@
1414
},
1515
"application_position": {"name": "应用位置", "description": "音乐应用的位置"},
1616
"process_music": {"name": "过程音乐", "description": "抽取过程中播放的音乐"},
17-
"fade_in_duration": {"name": "渐入时长", "description": "音乐渐入的时长"},
18-
"fade_out_duration": {"name": "渐出时长", "description`": "音乐渐出的时长"},
17+
"fade_in_duration": {"name": "渐入时长(ms)", "description": "音乐渐入的时长"},
18+
"fade_out_duration": {"name": "渐出时长(ms)", "description": "音乐渐出的时长"},
1919
"result_music": {"name": "结果音乐", "description": "抽取结果时播放的音乐"},
2020
"result_fade_in_duration": {
21-
"name": "渐入时长",
21+
"name": "渐入时长(ms)",
2222
"description": "结果音乐渐入的时长",
2323
},
2424
"result_fade_out_duration": {
25-
"name": "渐出时长",
25+
"name": "渐出时长(ms)",
2626
"description": "结果音乐渐出的时长",
2727
},
28+
"volume": {"name": "音量(%)", "description": "音乐音量"},
2829
"roll_call": {"name": "点名", "description": "点名功能"},
2930
"quick_draw": {"name": "闪抽", "description": "闪抽功能"},
3031
"instant_draw": {"name": "即抽", "description": "即抽功能"},
3132
"lottery": {"name": "抽奖", "description": "抽奖功能"},
33+
"no_music": {"name": "无音乐", "description": "不使用音乐"},
3234
},
3335
"EN_US": {
3436
"title": {
@@ -86,5 +88,6 @@
8688
"description": "Instant draw function",
8789
},
8890
"lottery": {"name": "Lottery", "description": "Lottery function"},
91+
"no_music": {"name": "No Music", "description": "Do not use music"},
8992
},
9093
}

app/common/display/result_display.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import random
55
import colorsys
66
import weakref
7+
from loguru import logger
78

89
from PySide6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QMenu, QApplication
910
from PySide6.QtGui import QMouseEvent, QPalette
@@ -667,8 +668,6 @@ def clear_grid(result_grid):
667668

668669
# 内存优化:只在有组件被删除时记录日志
669670
if count > 0:
670-
from loguru import logger
671-
672671
logger.debug(f"本次销毁了{count}个组件")
673672

674673
# 清理缓存引用
@@ -716,6 +715,4 @@ def cleanup_memory():
716715
# import gc
717716
# gc.collect()
718717

719-
from loguru import logger
720-
721718
logger.debug("ResultDisplayUtils内存清理完成")

app/common/music/music_player.py

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
# ==================================================
2+
# 导入库
3+
# ==================================================
4+
5+
import threading
6+
import time
7+
from typing import Optional
8+
import numpy as np
9+
import sounddevice as sd
10+
import soundfile as sf
11+
from loguru import logger
12+
13+
from app.tools.path_utils import *
14+
from app.tools.settings_default import *
15+
from app.tools.settings_access import *
16+
from app.Language.obtain_language import *
17+
18+
19+
# ==================================================
20+
# 音乐播放器类
21+
# ==================================================
22+
class MusicPlayer:
23+
"""音乐播放器类,用于在点名、闪抽动画和抽奖功能中播放背景音乐"""
24+
25+
def __init__(self):
26+
"""初始化音乐播放器"""
27+
self._current_music: Optional[str] = None
28+
self._is_playing: bool = False
29+
self._stop_flag: threading.Event = threading.Event()
30+
self._play_thread: Optional[threading.Thread] = None
31+
self._volume: float = 1.0 # 默认音量
32+
self._fade_in_duration: float = 0.0 # 渐入时长(秒)
33+
self._fade_out_duration: float = 0.0 # 渐出时长(秒)
34+
self._fade_out_thread: Optional[threading.Thread] = None
35+
36+
def play_music(
37+
self,
38+
music_file: str,
39+
settings_group: str,
40+
loop: bool = True,
41+
fade_in: bool = True,
42+
) -> bool:
43+
"""播放音乐
44+
45+
Args:
46+
music_file: 音乐文件名,空字符串表示不播放音乐
47+
settings_group: 设置组名,如"roll_call_settings"、"quick_draw_settings"等
48+
loop: 是否循环播放
49+
fade_in:是否使用渐入效果
50+
51+
Returns:
52+
bool: 是否成功开始播放
53+
"""
54+
# 如果音乐文件为空或"无音乐",则不播放
55+
if not music_file or music_file == get_content_name_async(
56+
"music_settings", "no_music"
57+
):
58+
logger.debug("音乐文件为空或选择无音乐,不播放")
59+
return False
60+
61+
# 停止当前播放的音乐
62+
self.stop_music()
63+
64+
# 获取音乐文件路径
65+
try:
66+
music_path = get_audio_path(f"music/{music_file}")
67+
if not music_path.exists():
68+
logger.error(f"音乐文件不存在: {music_path}")
69+
return False
70+
except Exception as e:
71+
logger.error(f"获取音乐文件路径失败: {e}")
72+
return False
73+
74+
# 从设置中获取音量和渐入渐出时长
75+
try:
76+
# 获取音量设置,默认为100%
77+
volume = readme_settings_async(
78+
settings_group, "animation_music_volume", 100
79+
)
80+
self._volume = volume / 100.0
81+
82+
# 获取渐入时长设置
83+
if fade_in:
84+
fade_in_ms = readme_settings_async(
85+
settings_group, "animation_music_fade_in", 0
86+
)
87+
self._fade_in_duration = fade_in_ms / 1000.0
88+
else:
89+
self._fade_in_duration = 0.0
90+
91+
# 获取渐出时长设置
92+
fade_out_ms = readme_settings_async(
93+
settings_group, "animation_music_fade_out", 0
94+
)
95+
self._fade_out_duration = fade_out_ms / 1000.0
96+
except Exception as e:
97+
logger.error(f"获取音乐设置失败: {e}")
98+
self._volume = 1.0
99+
self._fade_in_duration = 0.0
100+
self._fade_out_duration = 0.0
101+
102+
# 启动播放线程
103+
self._current_music = music_file
104+
self._stop_flag.clear()
105+
self._is_playing = True
106+
self._play_thread = threading.Thread(
107+
target=self._play_music_worker,
108+
args=(str(music_path), loop),
109+
daemon=True,
110+
)
111+
self._play_thread.start()
112+
113+
logger.info(f"开始播放音乐: {music_file}, 音量: {self._volume}, 循环: {loop}")
114+
return True
115+
116+
def stop_music(self, fade_out: bool = True) -> None:
117+
"""停止播放音乐
118+
119+
Args:
120+
fade_out: 是否使用渐出效果
121+
"""
122+
if not self._is_playing:
123+
return
124+
125+
logger.debug("停止播放音乐")
126+
127+
# 如果需要渐出效果且当前有渐出时长设置
128+
if fade_out and self._fade_out_duration > 0 and self._is_playing:
129+
# 启动渐出线程
130+
self._fade_out_thread = threading.Thread(
131+
target=self._fade_out_worker, daemon=True
132+
)
133+
self._fade_out_thread.start()
134+
# 等待渐出完成
135+
self._fade_out_thread.join(timeout=self._fade_out_duration + 1.0)
136+
137+
# 设置停止标志
138+
self._stop_flag.set()
139+
self._is_playing = False
140+
141+
# 等待播放线程结束
142+
if self._play_thread and self._play_thread.is_alive():
143+
self._play_thread.join(timeout=2.0)
144+
145+
self._current_music = None
146+
logger.debug("音乐已停止")
147+
148+
def is_playing(self) -> bool:
149+
"""检查是否正在播放音乐
150+
151+
Returns:
152+
bool: 是否正在播放音乐
153+
"""
154+
return self._is_playing
155+
156+
def get_current_music(self) -> Optional[str]:
157+
"""获取当前播放的音乐文件名
158+
159+
Returns:
160+
Optional[str]: 当前播放的音乐文件名,如果没有播放则返回None
161+
"""
162+
return self._current_music
163+
164+
def _play_music_worker(self, music_path: str, loop: bool) -> None:
165+
"""音乐播放工作线程
166+
167+
Args:
168+
music_path: 音乐文件路径
169+
loop: 是否循环播放
170+
"""
171+
stream = None
172+
try:
173+
while not self._stop_flag.is_set():
174+
# 读取音乐文件
175+
try:
176+
data, fs = sf.read(music_path)
177+
if len(data.shape) > 1 and data.shape[1] > 1:
178+
# 转换为单声道
179+
data = np.mean(data, axis=1)
180+
except Exception as e:
181+
logger.error(f"读取音乐文件失败: {e}")
182+
break
183+
184+
# 初始化音频流
185+
try:
186+
stream = sd.OutputStream(
187+
samplerate=fs,
188+
channels=1,
189+
dtype="float32",
190+
blocksize=2048,
191+
)
192+
stream.start()
193+
except Exception as e:
194+
logger.error(f"初始化音频流失败: {e}")
195+
break
196+
197+
# 计算渐入步数
198+
fade_in_steps = int(self._fade_in_duration * fs)
199+
fade_in_step = 0
200+
201+
# 分块播放
202+
chunk_size = 4096
203+
for i in range(0, len(data), chunk_size):
204+
if self._stop_flag.is_set():
205+
break
206+
207+
chunk = data[i : i + chunk_size]
208+
chunk = chunk.astype(np.float32)
209+
210+
# 应用渐入效果
211+
if fade_in_steps > 0 and fade_in_step < fade_in_steps:
212+
remaining_steps = min(fade_in_steps - fade_in_step, len(chunk))
213+
fade_in_factor = np.linspace(0, self._volume, remaining_steps)
214+
chunk[:remaining_steps] *= fade_in_factor
215+
if remaining_steps < len(chunk):
216+
chunk[remaining_steps:] *= self._volume
217+
fade_in_step += remaining_steps
218+
else:
219+
# 应用音量
220+
chunk *= self._volume
221+
222+
# 写入音频流
223+
try:
224+
stream.write(chunk)
225+
except Exception as e:
226+
logger.error(f"写入音频流失败: {e}")
227+
break
228+
229+
# 关闭音频流
230+
if stream:
231+
try:
232+
stream.stop()
233+
stream.close()
234+
except Exception as e:
235+
logger.error(f"关闭音频流失败: {e}")
236+
stream = None
237+
238+
# 如果不循环或者收到停止信号,退出循环
239+
if not loop or self._stop_flag.is_set():
240+
break
241+
242+
except Exception as e:
243+
logger.error(f"音乐播放工作线程异常: {e}")
244+
finally:
245+
# 确保音频流关闭
246+
if stream:
247+
try:
248+
stream.stop()
249+
stream.close()
250+
except Exception as e:
251+
logger.error(f"关闭音频流失败: {e}")
252+
self._is_playing = False
253+
logger.debug("音乐播放工作线程结束")
254+
255+
def _fade_out_worker(self) -> None:
256+
"""渐出效果工作线程"""
257+
if not self._is_playing or self._fade_out_duration <= 0:
258+
return
259+
260+
logger.debug(f"开始音乐渐出,时长: {self._fade_out_duration}秒")
261+
262+
# 计算渐出步数
263+
fade_out_steps = int(self._fade_out_duration * 50) # 50Hz更新率
264+
fade_out_step = 0
265+
initial_volume = self._volume
266+
267+
# 渐出效果循环
268+
while fade_out_step < fade_out_steps and not self._stop_flag.is_set():
269+
# 计算当前音量(线性递减)
270+
progress = fade_out_step / fade_out_steps
271+
self._volume = initial_volume * (1.0 - progress)
272+
273+
# 等待下一帧
274+
time.sleep(1.0 / 50) # 50Hz更新率
275+
fade_out_step += 1
276+
277+
# 渐出完成,设置停止标志
278+
self._stop_flag.set()
279+
logger.debug("音乐渐出完成")
280+
281+
282+
# 创建全局音乐播放器实例
283+
music_player = MusicPlayer()
284+
285+
286+
def get_music_files():
287+
"""获取音乐文件列表
288+
289+
Returns:
290+
List[str]: 音乐文件名列表,包含"无音乐"选项
291+
"""
292+
from app.tools.path_utils import get_audio_path
293+
from app.Language.obtain_language import get_content_name_async
294+
295+
# 获取音频文件目录
296+
audio_dir = get_audio_path("music")
297+
# 确保目录存在
298+
from app.tools.path_utils import ensure_dir
299+
300+
ensure_dir(audio_dir)
301+
302+
# 获取音频文件列表
303+
music_files = [
304+
get_content_name_async("music_settings", "no_music")
305+
] # 无音乐选项,表示不使用音乐
306+
if audio_dir.exists():
307+
# 支持的音频格式
308+
supported_formats = [".mp3", ".flac", ".wav", ".ogg"]
309+
# 遍历目录获取所有支持的音频文件
310+
for file in audio_dir.iterdir():
311+
if file.is_file() and file.suffix.lower() in supported_formats:
312+
music_files.append(file.name)
313+
314+
return music_files

0 commit comments

Comments
 (0)