@@ -180,7 +180,7 @@ public:
180180 std::shared_ptr<T> pop() {
181181 std::unique_lock<std::mutex> lk{ m };
182182 data_cond.wait(lk, [this] {return !data_queue.empty(); });
183- std::shared_ptr<T>res { std::make_shared<T>(data_queue.front()) };
183+ std::shared_ptr<T> res { std::make_shared<T>(data_queue.front()) };
184184 data_queue.pop();
185185 return res;
186186 }
@@ -286,7 +286,130 @@ Consumer 线程弹出元素 4:
286286
287287到此,也就可以了。
288288
289- ## 使用条件变量实现后台音乐播放
289+ ## 使用条件变量实现后台提示音播放
290+
291+ 一个常见的场景是:当你的软件完成了主要功能后,领导可能突然要求添加一些竞争对手产品的功能。比如领导看到了人家的设备跑起来总是有一些播报,说明当前的情况,执行的过程,或者报错了也会有提示音说明。于是就想让我们的程序也增加“** 语音提示** ”的功能。此时,你需要考虑如何在程序运行到不同状态时添加适当的语音播报,并且** 确保这些提示音的播放不会影响其他功能的正常运行** 。
292+
293+ 为了不影响程序的流畅执行,提示音的播放显然不能占据业务线程的资源。我们需要额外启动一个线程来专门处理这个任务。
294+
295+ 但是,大多数的提示音播放都是短暂且简单。如果每次播放提示音时都新建一个线程,且不说创建线程也需要大量时间,可能影响业务正常的执行任务的流程,就光是其频繁创建线程的开销也是不能接受的。
296+
297+ ---
298+
299+ 因此,更合理的方案是:** 在程序启动时,就启动一个专门用于播放提示音的线程。当没有需要播放的提示时,该线程会一直处于等待状态;一旦有提示音需要播放,线程就被唤醒,完成播放任务** 。
300+
301+ 具体来说,我们可以通过条件变量来实现这一逻辑,核心是监控一个音频队列。我们可以封装一个类型,包含以下功能:
302+
303+ - 一个成员函数在对象构造时就启动,使用条件变量监控队列是否为空,互斥量确保共享资源的同步。如果队列中有任务,就取出并播放提示音;如果队列为空,则线程保持阻塞状态,等待新的任务到来。
304+ - 提供一个外部函数,以供在需要播放提示音的时候调用它,向队列添加新的元素,该函数需要通过互斥量来保护数据一致性,并在成功添加任务后唤醒条件变量,通知播放线程执行任务。
305+
306+ > 这种设计通过合理利用** 条件变量** 和** 互斥量** ,不仅有效减少了 CPU 的无效开销,还能够确保主线程的顺畅运行。它不仅适用于提示音的播放,还能扩展用于其他类似的后台任务场景。
307+
308+ 我们引入 [ SFML] ( https://github.com/SFML/SFML ) 三方库进行声音播放,然后再自己进行上层封装。
309+
310+ ``` CPP
311+ class AudioPlayer {
312+ public:
313+ AudioPlayer() : stop{ false }, player_thread{ &AudioPlayer::playMusic, this }
314+ {}
315+
316+ ~AudioPlayer() {
317+ // 等待队列中所有音乐播放完毕
318+ while (!audio_queue.empty()) {
319+ std::this_thread::sleep_for(50ms);
320+ }
321+ stop = true ;
322+ cond.notify_all();
323+ if (player_thread.joinable()) {
324+ player_thread.join();
325+ }
326+ }
327+
328+ void addAudioPath(const std::string& path) {
329+ std::lock_guard<std::mutex> lock{ mtx }; // 互斥量确保了同一时间不会有其它地方在操作共享资源(队列)
330+ audio_queue.push(path); // 为队列添加元素 表示有新的提示音需要播放
331+ cond.notify_one(); // 通知线程新的音频
332+ }
333+
334+ private:
335+ void playMusic() {
336+ while (!stop) {
337+ std::string path;
338+ {
339+ std::unique_lock< std::mutex > lock{ mtx };
340+ cond.wait(lock, [ this] { return !audio_queue.empty() || stop; });
341+
342+ if (audio_queue.empty()) return; // 防止在对象为空时析构出错
343+
344+ path = audio_queue.front(); // 从队列中取出元素
345+ audio_queue.pop(); // 取出后就删除元素,表示此元素已被使用
346+ }
347+
348+ if (!music.openFromFile(path)) {
349+ std::cerr << "无法加载音频文件: " << path << std::endl;
350+ continue; // 继续播放下一个音频
351+ }
352+
353+ music.play();
354+
355+ // 等待音频播放完毕
356+ while (music.getStatus() == sf::SoundSource::Playing) {
357+ sf::sleep (sf::seconds (0.1f)); // sleep 避免忙等占用 CPU
358+ }
359+ }
360+ }
361+
362+ std::atomic<bool> stop; // 控制线程的停止与退出,
363+ std::thread player_thread; // 后台执行音频任务的专用线程
364+ std::mutex mtx; // 保护共享资源
365+ std::condition_variable cond; // 控制线程等待和唤醒,当有新任务时通知音频线程
366+ std::queue<std::string> audio_queue; // 音频任务队列,存储待播放的音频文件路径
367+ sf::Music music; // SFML 音频播放器,用于加载和播放音频文件
368+ };
369+ ```
370+
371+ 该代码实现了一个简单的** 后台音频播放类型** ,通过** 条件变量** 和** 互斥量** 确保播放线程 ` playMusic ` 只在只在** 有音频任务需要播放时工作** (当外部通过调用 ` addAudioPath() ` 向队列添加播放任务时)。在没有任务时,线程保持等待状态,避免占用 CPU 资源影响主程序的运行。
372+
373+ 此外,关于提示音的播报,为了避免每次都手动添加路径,我们可以创建一个音频资源数组,便于使用:
374+
375+ ``` cpp
376+ static constexpr std::array soundResources{
377+ "./sound/01初始化失败.ogg",
378+ "./sound/02初始化成功.ogg",
379+ "./sound/03试剂不足,请添加.ogg",
380+ "./sound/04试剂已失效,请更新.ogg",
381+ "./sound/05清洗液不足,请添加.ogg",
382+ "./sound/06废液桶即将装满,请及时清空.ogg",
383+ "./sound/07废料箱即将装满,请及时清空.ogg",
384+ "./sound/08激发液A液不足,请添加.ogg",
385+ "./sound/09激发液B液不足,请添加.ogg",
386+ "./sound/10反应杯不足,请添加.ogg",
387+ "./sound/11检测全部完成.ogg"
388+ };
389+ ```
390+
391+ 为了提高代码的可读性,我们还可以使用一个枚举类型来表示音频资源的索引:
392+
393+ ``` cpp
394+ enum SoundIndex {
395+ InitializationFailed,
396+ InitializationSuccessful,
397+ ReagentInsufficient,
398+ ReagentExpired,
399+ CleaningAgentInsufficient,
400+ WasteBinAlmostFull,
401+ WasteContainerAlmostFull,
402+ LiquidAInsufficient,
403+ LiquidBInsufficient,
404+ ReactionCupInsufficient,
405+ DetectionCompleted,
406+ SoundCount // 总音频数量,用于计数
407+ };
408+ ```
409+
410+ 需要注意的是 SFML不支持 ` .mp3 ` 格式的音频文件,大家可以使用 ffmpeg 或者其它软件[ 网站] ( https://www.freeconvert.com/audio-converter ) 将音频转换为支持的格式。
411+
412+ 如果是测试使用,不知道去哪生成这些语音播报,我们推荐 [ ` tts-vue ` ] ( https://github.com/LokerL/tts-vue ) 。
290413
291414## 使用 ` future `
292415
0 commit comments