Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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
Lines changed: 106 additions & 0 deletions
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+
}

‎code/ModernCpp-ConcurrentProgramming-Tutorial/CMakeLists.txt

Lines changed: 3 additions & 3 deletions
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)

‎code/ModernCpp-ConcurrentProgramming-Tutorial/test/AduioPlayer.h

Lines changed: 13 additions & 13 deletions
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",

‎md/04同步操作.md

Lines changed: 125 additions & 2 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /