| 时间 | 变更内容 |
|---|---|
| 2026-05-14 10:00:50 | 初始创建:完成全量扫描,生成根级 CLAUDE.md 及 .claude/index.json |
| 2026-07-07 | v1.2.0:新增独占广播模式(AudioSessionGuard 持续静音其他所有程序)与设备隔离状态提示 |
| 2026-07-08 | v1.3.0:批量音量、连续测试、批量星期设置与多选删除;播放会话 ID 防止 PlaybackStopped 串扰;审查修复(循环续播不再弹模态框、连续测试跳过缺失文件、切设备恢复失败自动复位、清理 AudioManager 死代码) |
| 2026-07-08 | v1.4.0:新增兼容/标准输出模式(WaveOut / WASAPI 共享)与设备 ID 寻址;标准模式失败关闭且不回落默认设备;音量改在音频流层调节,避免改写系统主音量;定时广播设备缺失不弹模态框,隔离提示识别标准模式设备不可用状态 |
| 2026-07-08 | v1.5.0:新增 WASAPI 独占输出模式,复用设备 ID 寻址;标准/独占模式非播放切换与设备选择执行真实试开,播放中切换先停旧流再重开目标设备,失败保持原模式或失败关闭且不回落默认设备;独占播放按设备支持格式探测并重采样;音量继续只在 AudioFileReader.Volume 流层调节 |
| 2026-07-09 | v1.6.0:新增底部全局播放控制栏,测试播放/连续测试支持暂停、继续、拖动定位和轨道点击定位,定时广播仅只读显示进度并保留停止入口;解析文件夹导入后台读取时长并统一保存,长时长统一显示为 hh:mm:ss;输出工具栏分组、关闭窗口先确认,系统关机/注销时跳过退出确认;暂停中切换输出模式/设备/方案先自动停止;独占重采样 seek 重建 provider |
| 2026-07-09 | v1.7.0:新增无人值守可靠性能力:广播播放日志与只读查看窗口、启动/手动方案文件体检、值守锁定开关与持久化;收紧独占 seek 的 PlaybackStopped 退订时序,新增键盘 seek 去抖和导入/体检互斥门控;继续保持定时广播无模态、日志失败不影响播放、体检不碰输出设备、失败关闭不回落默认设备、音量只在 AudioFileReader.Volume 调节 |
| 2026-07-09 | 测试基建:拆出跨平台 src/SchoolBroadcastSystem.Core(net8.0,链接编译共享源码),调度匹配纯逻辑抽到 BroadcastScheduleMatcher,新增 tests/ 下 xUnit 逻辑测试;BroadcastLogger 写线程改为首次写日志时懒启动;广播项排序改按解析后时间(不可解析排最后);CI 发布前强制先跑测试 |
| 2026-07-09 | v1.7.1:工具栏重构与输出模式提示——新增「工具/帮助」菜单栏承接批量音量/解析文件夹导入/方案体检/播放日志/恢复全部声音,工具栏第一行只保留高频操作(添加/编辑/删除、测试播放/连续测试、值守锁定),第二行输出设置精简为「输出:」前缀 + 模式/设备下拉 + 独占广播勾选(标签缩短)+ 两个「?」按钮,第三行设备隔离警告独立成行;新增只读 OutputModeHelpWindow 展示三种输出模式对比表;OutputModeOption 加 Description,下拉项悬停提示与 ComboBox 自身 ToolTip 跟随当前模式(初始化即赋值);导入/体检进度文本改显示在底部状态栏 ToolProgressTextBlock;不改任何音频播放/调度逻辑 |
一个基于 C# WPF (.NET 8.0) 的桌面校园广播定时播放系统。用户可以配置多个广播方案(Profile),每个方案包含一组定时广播项(BroadcastItem),系统通过 Quartz.NET 调度器每秒检查当前时间并自动播放匹配的音频文件。适用于学校上下课铃声、课间音乐、通知广播等场景。
WPF 主工程位于根目录,扁平结构、无分层模块划分。纯逻辑源码(数据模型、配置、日志、调度匹配)以链接编译方式进入 src/SchoolBroadcastSystem.Core(net8.0 跨平台),主工程对这些文件 Compile Remove 后引用 Core 项目;单元测试位于 tests/。新增共享源码文件时需同时改两处:主工程 csproj 的 Compile Remove 与 Core 工程 csproj 的 Compile Include。
技术栈:
- 框架:.NET 8.0-windows, WPF
- 音频播放:NAudio 2.2.1(兼容模式
WaveOutEvent;标准模式WasapiOut共享输出;独占模式WasapiOut独占输出;音量统一在AudioFileReader流层调节) - 任务调度:Quartz 3.14.0 (每秒轮询检查广播时间)
- 配置持久化:Newtonsoft.Json 13.0.3 (JSON 文件序列化)
- 数据验证:System.ComponentModel.DataAnnotations
- CI/CD:GitHub Actions (PublishSingleFile, win-x64)
核心流程:
- 应用启动 -> 加载
broadcast_config.json-> 初始化 Quartz.NET 调度器 - 调度器每秒触发
CheckBroadcastJob - Job 在 UI 线程上调用
MainWindow.CheckBroadcastSchedule(),比较当前时间与所有 BroadcastItem 的 TimeString - 时间匹配且星期匹配时,调用
AudioManager.PlayFile()播放音频 - 支持循环播放(LoopCount),播放结束事件驱动下一次循环
graph TD
A["SimpleBroadcastingSystem"] --> B["数据模型层"]
A --> C["业务逻辑层"]
A --> D["UI 层"]
A --> E["基础设施层"]
B --> B1["BroadcastItem.cs"]
B --> B2["BroadcastProfile.cs"]
C --> C1["BroadcastScheduler.cs"]
C --> C2["BroadcastJobs.cs"]
C --> C3["AudioManager.cs"]
C --> C4["AudioSessionGuard.cs"]
D --> D1["MainWindow.xaml/.cs"]
D --> D2["AddEditBroadcastWindow.xaml/.cs"]
D --> D3["ProfileManagementWindow.xaml/.cs"]
D --> D4["InputDialog.xaml/.cs"]
D --> D5["BooleanConverters.cs"]
E --> E1["AppConfig.cs / ConfigManager"]
E --> E2["App.xaml/.cs"]
E --> E3["AssemblyInfo.cs"]
| 模块 | 职责 | 入口文件 |
|---|---|---|
| 数据模型层 | 定义 BroadcastItem(广播项)和 BroadcastProfile(广播方案)数据结构 | BroadcastItem.cs, BroadcastProfile.cs |
| 调度器 | Quartz.NET 调度管理,每秒轮询检查广播时间匹配 | BroadcastScheduler.cs |
| 调度匹配 | 纯函数的时间/星期匹配、下次广播计算、排序(跨平台可测,BroadcastScheduler 委托它) | BroadcastScheduleMatcher.cs |
| 定时任务 | Quartz.NET IJob 实现,在 UI 线程触发广播检查 | BroadcastJobs.cs |
| 音频管理 | NAudio 封装,负责兼容/标准/独占输出模式、音频设备管理、播放、停止、流级音量控制 | AudioManager.cs |
| 音频会话守护 | 独占广播模式:静音本进程以外全部程序的声音(已运行 + 新启动),周期兜底重扫,退出保持静音 | AudioSessionGuard.cs |
| 配置管理 | JSON 配置文件的加载、保存、方案同步 | AppConfig.cs |
| 主窗口 | 核心 UI,广播列表管理、方案切换、播放状态显示 | MainWindow.xaml/.cs |
| 添加/编辑窗口 | 广播项的新增与编辑对话框 | AddEditBroadcastWindow.xaml/.cs |
| 方案管理窗口 | 广播方案的新建、重命名、删除、激活 | ProfileManagementWindow.xaml/.cs |
| 输入对话框 | 通用文本输入弹窗 | InputDialog.xaml/.cs |
| 输出模式说明窗口 | 静态只读说明:三种输出模式对比表 + 补充说明,零业务逻辑 | OutputModeHelpWindow.xaml/.cs |
| 值转换器 | BooleanToVisibility / BooleanToFontWeight WPF 转换器 | BooleanConverters.cs |
- .NET 8.0 SDK
- Windows 操作系统(WPF 应用)
# 还原依赖
dotnet restore
# 调试运行
dotnet run
# 发布单文件可执行程序
dotnet publish SchoolBroadcastSystem.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o ./publish- 运行时配置存储在
broadcast_config.json(应用目录下),包含广播项列表、音频设备选择、方案列表 - 首次运行时自动生成默认配置
- 测试项目:
tests/SchoolBroadcastSystem.Tests(xUnit,net8.0,macOS/Linux 可跑),只引用src/SchoolBroadcastSystem.Core,不碰 WPF 运行时 - 覆盖范围:BroadcastScheduleMatcher 调度匹配与排序、ConfigManager 序列化往返与损坏配置恢复、BroadcastLogger tail 读取
- 运行:
dotnet test WpfApp1.sln;CI(release.yml)在发布前强制先跑测试,测试不过不发版 - 测试串行执行(TestAssembly.cs 全局关闭并行),因
ConfigManager.ConfigFilePath是静态可写的测试缝 - 注意:定时触发的生产路径是
MainWindow.CheckBroadcastSchedule里的 TimeString 字符串比较,未走 Matcher;要给触发逻辑加测试,需先把该路径接到 Matcher 并真机验证
- 命名空间:
WpfApp1(与项目名不一致,为历史遗留) - 数据模型实现
INotifyPropertyChanged接口 - 配置使用
Newtonsoft.Json序列化 - 调度使用
Quartz.NET,Job 通过Dispatcher.InvokeAsync回到 UI 线程 - 无依赖注入,无接口抽象层,直接实例化
- 主工程源代码位于根目录;
src/为跨平台 Core 工程(链接编译根目录共享文件),tests/为 xUnit 测试 - 广播调度逻辑核心在
BroadcastScheduler.cs的CheckBroadcastSchedule()方法(通过 MainWindow 调用) - 音频播放核心在
AudioManager.cs的PlayFile()方法 - 配置持久化核心在
AppConfig.cs的ConfigManager类 - 修改广播项属性时需注意
PropertyChanged事件链:BroadcastItem -> MainWindow.BroadcastItem_PropertyChanged -> SaveConfiguration - 方案切换逻辑在
MainWindow.SwitchToProfile(),涉及广播列表重建和事件处理器重绑定
- 统一命名空间 -- 项目名
SchoolBroadcastSystem但命名空间为WpfApp1,建议统一 - 提取接口 -- AudioManager 和 BroadcastScheduler 无接口抽象,不利于测试和替换
- HasTimeConflict 未实现 --
AddEditBroadcastWindow.HasTimeConflict()始终返回 false,为占位实现 - 定时触发路径接入 Matcher --
MainWindow.CheckBroadcastSchedule仍是 TimeString 字符串比较,接入BroadcastScheduleMatcher后触发逻辑才可被测试覆盖(改动播放路径,需真机验证)