Skip to content

Commit 8954e3c

Browse files
committed
新增一节:使用条件变量实现后台提示音播放
1 parent d4af5ed commit 8954e3c

File tree

4 files changed

+247
-18
lines changed

4 files changed

+247
-18
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#include <SFML/Audio.hpp>
2+
#include <mutex>
3+
#include <condition_variable>
4+
#include <atomic>
5+
#include <thread>
6+
#include <queue>
7+
#include <array>
8+
#include <iostream>
9+
using namespace std::chrono_literals;
10+
11+
class AudioPlayer{
12+
public:
13+
AudioPlayer() : stop {false}, player_thread{ &AudioPlayer::playMusic, this }
14+
{}
15+
16+
~AudioPlayer(){
17+
while (!audio_queue.empty()){
18+
std::this_thread::sleep_for(50ms);
19+
}
20+
stop = true;
21+
cond.notify_all();
22+
if(player_thread.joinable()){
23+
player_thread.join();
24+
}
25+
}
26+
27+
void addAudioPath(const std::string& path){
28+
std::lock_guard<std::mutex> lc{ m };
29+
audio_queue.push(path);
30+
cond.notify_one();
31+
}
32+
33+
private:
34+
void playMusic(){
35+
while(!stop){
36+
std::string path;
37+
{
38+
std::unique_lock<std::mutex> lock{ m };
39+
// 条件不满足,就解锁 unlock,让其它线程得以运行 如果被唤醒了,就会重新获取锁 lock
40+
cond.wait(lock, [this] {return !audio_queue.empty() || stop; });
41+
42+
if (audio_queue.empty()) return; // 防止对象为空时出问题
43+
44+
path = audio_queue.front(); // 取出
45+
audio_queue.pop(); // 取出后就删除这个元素,表示此元素以及被使用
46+
}
47+
48+
if(!music.openFromFile(path)){
49+
std::cerr << "无法加载音频文件: " << path << std::endl;
50+
continue;
51+
}
52+
53+
music.play(); // 异步 非阻塞
54+
55+
while(music.getStatus() == sf::SoundSource::Playing){
56+
sf::sleep(sf::seconds(0.1f)); // sleep 避免忙等 占用 CPU
57+
}
58+
}
59+
}
60+
61+
std::atomic<bool> stop; // 控制线程的停止与退出
62+
std::thread player_thread; // 后台执行音频播放任务的专用线程
63+
std::mutex m; // 保护共享资源
64+
std::condition_variable cond; // 控制线程的等待和唤醒,当有新的任务的时候通知播放线程
65+
std::queue<std::string> audio_queue; // 音频任务队列,存储待播放的音频文件的路径
66+
sf::Music music; // SFML 音频播放器对象,用来加载播放音频
67+
68+
public:
69+
static constexpr std::array soundResources{
70+
"./sound/01初始化失败.ogg",
71+
"./sound/02初始化成功.ogg",
72+
"./sound/03试剂不足,请添加.ogg",
73+
"./sound/04试剂已失效,请更新.ogg",
74+
"./sound/05清洗液不足,请添加.ogg",
75+
"./sound/06废液桶即将装满,请及时清空.ogg",
76+
"./sound/07废料箱即将装满,请及时清空.ogg",
77+
"./sound/08激发液A液不足,请添加.ogg",
78+
"./sound/09激发液B液不足,请添加.ogg",
79+
"./sound/10反应杯不足,请添加.ogg",
80+
"./sound/11检测全部完成.ogg"
81+
};
82+
};
83+
84+
AudioPlayer audioPlayer;
85+
86+
int main() {
87+
audioPlayer.addAudioPath(AudioPlayer::soundResources[4]);
88+
audioPlayer.addAudioPath(AudioPlayer::soundResources[5]);
89+
audioPlayer.addAudioPath(AudioPlayer::soundResources[6]);
90+
audioPlayer.addAudioPath(AudioPlayer::soundResources[7]);
91+
92+
std::thread t{ [] {
93+
std::this_thread::sleep_for(1s);
94+
audioPlayer.addAudioPath(AudioPlayer::soundResources[1]);
95+
} };
96+
std::thread t2{ [] {
97+
audioPlayer.addAudioPath(AudioPlayer::soundResources[0]);
98+
} };
99+
100+
std::cout << "\n";
101+
102+
t.join();
103+
t2.join();
104+
105+
std::cout << "end\n";
106+
}

Diff for: code/ModernCpp-ConcurrentProgramming-Tutorial/CMakeLists.txt

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "C
1212
add_compile_options("-finput-charset=UTF-8" "-fexec-charset=UTF-8")
1313
endif()
1414

15-
add_executable(${PROJECT_NAME} "25线程安全的队列.cpp")
15+
add_executable(${PROJECT_NAME} "26使用条件变量实现后台提示音播放.cpp")
1616

1717

1818
# 设置 SFML 的 CMake 路径
1919
set(SFML_DIR "D:/lib/SFML-2.6.1-windows-vc17-64-bit/SFML-2.6.1/lib/cmake/SFML")
2020

21-
# 查找 SFML 库并设置链接选项
21+
# 查找 SFML
2222
find_package(SFML 2.6.1 COMPONENTS system window graphics audio network REQUIRED)
2323

24-
# 链接 SFML 库到项目
24+
# 链接 SFML 库到项目 设置链接选项
2525
target_link_libraries(${PROJECT_NAME} sfml-system sfml-window sfml-graphics sfml-audio sfml-network)

Diff for: code/ModernCpp-ConcurrentProgramming-Tutorial/test/AduioPlayer.h

+13-13
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ class AudioPlayer {
3030
}
3131

3232
void addAudioPath(const std::string& path) {
33-
std::lock_guard<std::mutex> lock{ mtx };
34-
audio_queue.push(path);
35-
cond.notify_one(); // 通知线程新的音频
33+
std::lock_guard<std::mutex> lock{ mtx }; // 互斥量确保了同一时间不会有其它地方在操作共享资源(队列)
34+
audio_queue.push(path); // 为队列添加元素 表示有新的提示音需要播放
35+
cond.notify_one(); // 通知线程新的音频
3636
}
3737

3838
private:
@@ -45,8 +45,8 @@ class AudioPlayer {
4545

4646
if (audio_queue.empty()) return; // 防止在对象为空时析构出错
4747

48-
path = audio_queue.front();
49-
audio_queue.pop();
48+
path = audio_queue.front(); // 从队列中取出元素
49+
audio_queue.pop(); // 取出后就删除元素,表示此元素已被使用
5050
}
5151

5252
if (!music.openFromFile(path)) {
@@ -58,18 +58,18 @@ class AudioPlayer {
5858

5959
// 等待音频播放完毕
6060
while (music.getStatus() == sf::SoundSource::Playing) {
61-
sf::sleep(sf::seconds(0.1f)); // sleep 避免忙等占用CPU
61+
sf::sleep(sf::seconds(0.1f)); // sleep 避免忙等占用 CPU
6262
}
6363
}
6464
}
6565

66-
std::atomic<bool> stop;
67-
std::thread player_thread;
68-
std::mutex mtx;
69-
std::condition_variable cond;
70-
std::queue<std::string> audio_queue;
71-
sf::Music music;
72-
66+
std::atomic<bool> stop; // 控制线程的停止与退出,
67+
std::thread player_thread; // 后台执行音频任务的专用线程
68+
std::mutex mtx; // 保护共享资源
69+
std::condition_variable cond; // 控制线程等待和唤醒,当有新任务时通知音频线程
70+
std::queue<std::string> audio_queue; // 音频任务队列,存储待播放的音频文件路径
71+
sf::Music music; // SFML 音频播放器,用于加载和播放音频文件
72+
7373
public:
7474
static constexpr std::array soundResources{
7575
"./sound/01初始化失败.ogg",

Diff for: md/04同步操作.md

+125-2
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)