1. 屏幕
- 画图形
- 绘制基本图形(如直线、圆形、矩形等)
- 绘制复杂图形(如多边形、贝塞尔曲线等)
- 显示图片
- 支持不同格式的图像显示(如JPEG, PNG等)
- 图片动态更新
- 显示文字
- 文本内容显示
- 文本颜色、大小、对齐方式设置
- 支持多行文本、文本换行与溢出处理
- 动态文本更新
2. 摄像头
2.1 代码基础配置
- 安装依赖
确保已安装 OpenCV 库。可以使用以下命令安装 OpenCV(假设使用的是 Linux 系统):
sudo apt-get install libopencv-dev
- 包含头文件
#include <opencv2/opencv.hpp>
#include <iostream>
#include <string>
#include <thread>
- 检查摄像头设备
ls /dev/video*
这会列出所有视频设备。如果你看到 /dev/video0 和 /dev/video1,说明有多个视频设备连接。
根据你的情况,可能需要修改代码或配置文件,确保使用正确的设备索引。例如,尝试使用 /dev/video0 而不是 /dev/video1:
cv::VideoCapture cap(0); // 0 为默认设备,可能需要修改为 1,具体取决于你的设备
- 示例代码
Camera 类封装了相机的基本操作,包括照片拍摄、视频录制和实时预览等功能。构造函数会初始化摄像头,析构函数会停止当前正在进行的录制或预览任务。
#include <opencv2/opencv.hpp>
#include <iostream>
#include <string>
#include <thread>
class Camera {
public:
Camera() {
// 构造函数:初始化摄像头
}
~Camera() {
// 析构函数:停止录制与预览
if (is_recording) StopRecording();
if (is_previewing) StopPreview();
}
// 捕获照片函数
bool CapturePhoto(const std::string& filename, int width = 640, int height = 480, int quality = 85) {
cv::VideoCapture camera(0); // 打开默认相机
//一直找不到摄像机的报错处理
if (!camera.isOpened()) {
std::cerr << "No suitable camera found" << std::endl;
return false;
}
// 设置摄像头分辨率
camera.set(cv::CAP_PROP_FRAME_WIDTH, width);//宽
camera.set(cv::CAP_PROP_FRAME_HEIGHT, height);//高
cv::Mat frame;
camera >> frame; // 捕获一帧图像
if (frame.empty()) {
std::cerr << "Image capture failed" << std::endl;
return false;
}
// 保存图像
bool success = cv::imwrite(filename, frame, {cv::IMWRITE_JPEG_QUALITY, quality});
return success;
}
// 开始录制视频函数
void StartRecording(const std::string& filename, int width = 640, int height = 480, int fps = 30) {
if (is_recording) return;
cv::VideoCapture camera(0); // 打开默认相机
if (!camera.isOpened()) {
std::cerr << "No suitable camera found" << std::endl;
return;
}
// 设置摄像头分辨率
camera.set(cv::CAP_PROP_FRAME_WIDTH, width);
camera.set(cv::CAP_PROP_FRAME_HEIGHT, height);
// 初始化VideoWriter来录制视频
cv::VideoWriter writer(filename, cv::VideoWriter::fourcc('M', 'J', 'P', 'G'), fps, cv::Size(width, height));
if (!writer.isOpened()) {
std::cerr << "Unable to open video file for recording!" << std::endl;
return;
}
is_recording = true;
// 录制视频
cv::Mat frame;
while (is_recording) {
camera >> frame; // 捕获每一帧图像
if (frame.empty()) break;
writer.write(frame); // 写入视频文件
}
}
// 停止录制视频函数
void StopRecording() {
if (!is_recording) return;
is_recording = false;
}
int main() {
Camera camera;
// 捕获照片并保存
if (camera.CapturePhoto("image.jpg", 640, 480, 90)) {
std::cout << "Capture image successfully!" << std::endl;
} else {
std::cout << "Capture image failed!" << std::endl;
}
// 开始录制视频
std::thread record_thread(&Camera::StartRecording, &camera, "video.avi", 640, 480, 30);
std::cout << "Start recording for 5 seconds" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(5)); // 等待 5 秒
// 停止录制
camera.StopRecording();
record_thread.join(); // 等待录制线程完成
std::cout << "stop!" << std::endl;
return 0;
}
- 编译与运行
编译此代码时需要链接 OpenCV 库。例如,如果使用 g++ 编译器,可以执行以下命令:
g++ -o camera_example Camera.cpp `pkg-config --cflags --libs opencv4` -pthread
运行程序:
./camera_example
2.2 拍照
CapturePhoto() 函数允许用户捕捉一张静态照片,并将其保存为 JPEG 格式。该函数会初始化摄像头,捕获一帧图像并保存。
Camera camera;
if (camera.CapturePhoto("image.jpg", 640, 480, 90)) {
std::cout << "capture ok!" << std::endl;
} else {
std::cout << "capture fail!" << std::endl;
}
参数说明:
filename: 保存照片的文件名,例如 "image.jpg"。
width: 拍摄照片的宽度(默认值:640)。
height: 拍摄照片的高度(默认值:480)。
quality: 保存为 JPEG 格式时的压缩质量(默认值:85,范围为 0 到 100)。
2.3 录像
StartRecording() 函数用于启动视频录制。此操作会在一个单独的线程中进行,以保证录制过程不会阻塞主线程。可以在录制过程中进行其他操作。录制完成后,需要调用 StopRecording() 来停止录制。
std::thread record_thread(&Camera::StartRecording, &camera, "video.avi", 640, 480, 30);
std::cout << "start 5 s ..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(5)); // 等待 5 秒
camera.StopRecording();
record_thread.join(); // 等待录制线程完成
std::cout << "stop!" << std::endl;
参数说明:
filename: 保存视频的文件名,例如 "video.avi"。
width: 视频的宽度(默认值:640)。
height: 视频的高度(默认值:480)。
fps: 视频的帧率(默认值:30)。
3. 按键
3.1 代码基础配置
- 安装依赖
sudo apt-get install pigpio
- 包含头文件
#include <iostream> // 引入标准输入输出流库,用于打印日志信息
#include <functional> // 引入函数对象库,用于存储回调函数
#include <thread> // 引入多线程库,用于创建和管理线程
#include <atomic> // 引入原子操作库,用于保证线程间共享变量的原子性
#include <pigpio.h> // 引入pigpio库,用于Raspberry Pi的GPIO控制
#include <chrono> // 引入时间库,用于处理时间和计时器
- 示例代码
#include <iostream> // 引入标准输入输出流库,用于打印日志信息
#include <functional> // 引入函数对象库,用于存储回调函数
#include <thread> // 引入多线程库,用于创建和管理线程
#include <atomic> // 引入原子操作库,用于保证线程间共享变量的原子性
#include <pigpio.h> // 引入pigpio库,用于Raspberry Pi的GPIO控制
#include <chrono> // 引入时间库,用于处理时间和计时器
// 定义一个类KeyInput用于处理GPIO按钮输入
class KeyInput {
public:
// 定义一个类型为函数指针的别名,用于保存按钮按下时的回调函数
using KeyCallback = std::function<void()>;
// 构造函数,传入GPIO引脚号初始化
KeyInput(int pin);
// 析构函数,用于清理资源
~KeyInput();
// 设置短按回调函数
void SetShortPressCallback(KeyCallback callback);
// 设置长按回调函数
void SetLongPressCallback(KeyCallback callback);
// 开始监听按钮按压事件
void StartListening();
// 停止监听按钮按压事件
void StopListening();
private:
// 监听线程的函数
void ListenThread();
int pin; // GPIO引脚号
std::atomic<bool> running{false}; // 用于控制监听线程的状态
std::thread listener_thread; // 按钮监听线程
KeyCallback short_press_callback; // 短按回调函数
KeyCallback long_press_callback; // 长按回调函数
const int LONG_PRESS_DURATION_MS = 1000; // 定义长按的时间阈值,单位为毫秒(1秒)
};
// KeyInput构造函数,初始化GPIO设置
KeyInput::KeyInput(int pin) : pin(pin) {
if (gpioInitialise() < 0) { // 初始化pigpio库,返回值小于0表示初始化失败
std::cerr << "pigpio initialization failed!" << std::endl;
exit(1); // 初始化失败则退出程序
}
gpioSetMode(pin, PI_INPUT); // 设置指定GPIO引脚为输入模式
gpioSetPullUpDown(pin, PI_PUD_UP); // 启用内部上拉电阻,确保引脚不悬空
}
// 析构函数,停止监听并清理资源
KeyInput::~KeyInput() {
StopListening(); // 停止监听按钮按压事件
gpioTerminate(); // 终止pigpio库,释放资源
}
// 设置短按回调函数
void KeyInput::SetShortPressCallback(KeyCallback callback) {
short_press_callback = callback; // 保存短按的回调函数
}
// 设置长按回调函数
void KeyInput::SetLongPressCallback(KeyCallback callback) {
long_press_callback = callback; // 保存长按的回调函数
}
// 启动监听线程,开始监听按钮按压事件
void KeyInput::StartListening() {
if (running) return; // 如果已经在运行,则不再启动新线程
running = true; // 设置监听线程为运行状态
listener_thread = std::thread(&KeyInput::ListenThread, this); // 创建并启动监听线程
}
// 停止监听线程,关闭按钮监听
void KeyInput::StopListening() {
if (!running) return; // 如果线程没有运行,则不做任何操作
running = false; // 设置监听线程为停止状态
if (listener_thread.joinable()) { // 如果线程可以加入
listener_thread.join(); // 等待线程结束
}
}
// 按钮监听线程的函数
void KeyInput::ListenThread() {
auto last_press_time = std::chrono::steady_clock::now(); // 记录按钮按下的时间
bool button_pressed = false; // 用于标记按钮是否被按下
while (running) { // 只要监听线程正在运行,就持续监听按钮状态
bool current_state = gpioRead(pin) == PI_LOW; // 读取GPIO引脚的状态,低电平表示按钮被按下
if (current_state && !button_pressed) { // 按钮刚刚被按下
button_pressed = true; // 设置按钮状态为已按下
last_press_time = std::chrono::steady_clock::now(); // 记录当前时间为按钮按下的时间
} else if (!current_state && button_pressed) { // 按钮被释放
button_pressed = false; // 设置按钮状态为未按下
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - last_press_time).count(); // 计算按钮按下的持续时间
// 如果持续时间小于长按的阈值,则认为是短按
if (duration < LONG_PRESS_DURATION_MS && short_press_callback) {
short_press_callback(); // 调用短按的回调函数
}
} else if (button_pressed) { // 按钮依然被按下
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - last_press_time).count(); // 计算按钮持续按下的时间
// 如果持续时间大于等于长按的阈值,则认为是长按
if (duration >= LONG_PRESS_DURATION_MS && long_press_callback) {
long_press_callback(); // 调用长按的回调函数
button_pressed = false; // 处理完长按后重置按钮状态
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 每10毫秒检查一次按钮状态
}
}
// 短按回调函数的示例处理函数
void shortPressHandler() {
std::cout << "Short press detected!" << std::endl; // 打印短按检测信息
}
// 长按回调函数的示例处理函数
void longPressHandler() {
std::cout << "Long press detected!" << std::endl; // 打印长按检测信息
}
// 主函数,程序的入口
int main() {
int pin = 17; // 使用GPIO 17号引脚
KeyInput keyInput(pin); // 创建KeyInput对象,初始化GPIO 17号引脚
// 设置短按和长按的回调函数
keyInput.SetShortPressCallback(shortPressHandler);
keyInput.SetLongPressCallback(longPressHandler);
keyInput.StartListening(); // 启动按钮监听线程
// 运行10秒后停止监听
std::this_thread::sleep_for(std::chrono::seconds(10));
keyInput.StopListening(); // 停止监听
return 0; // 程序结束
}
- 编译与运行
g++ key_input.cpp -o key_input -lpigpio -pthread
然后运行程序:
sudo ./key_input
3.2 短按和长按
- 按键状态读取
代码通过GPIO读取按钮的状态。我们通过 gpioRead(pin) 来读取按钮的状态。如果按钮被按下,GPIO引脚会是低电平(PI_LOW),如果没有按下,GPIO引脚会是高电平(PI_HIGH)。至于其他模式可选,如上升沿(即从低电平到高电平的变化)、下降沿(即从高电平到低电平的变化)、或推挽输出等设置,可以根据具体需求参考硬件手册进行配置。这些模式常用于实现中断触发或特定信号检测,具体的配置和使用方法可在GPIO库或硬件手册中找到详细说明。
- 按键按下时间记录
为了区分短按和长按,我们需要记录按钮按下的持续时间。每当检测到按钮按下时(即从未按下到按下的状态变化),我们记录当前的时间点(last_press_time)。这个时间点用于计算按钮按下的持续时间。
auto last_press_time = std::chrono::steady_clock::now();
- 按钮释放时的处理
按钮按下的持续时间是在按钮释放时(即状态从按下变为未按下)计算的。通过获取当前时间和按下时的时间差,得到按钮按下的时间。
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - last_press_time).count();
duration 就是按钮按下的时间(单位是毫秒)。接下来,我们根据这个时间来判断是短按还是长按。
- 短按和长按的判断
短按:如果按钮按下时间小于设定的阈值(例如 1000 毫秒,即 1 秒),则认为是短按。此时会调用短按的回调函数。
长按:如果按钮按下时间大于等于设定的阈值(例如 1000 毫秒,即 1 秒),则认为是长按。此时会调用长按的回调函数。
if (duration < LONG_PRESS_DURATION_MS && short_press_callback) {
short_press_callback(); // 调用短按的回调函数
}
if (duration >= LONG_PRESS_DURATION_MS && long_press_callback) {
long_press_callback(); // 调用长按的回调函数
}
- 检测流程
当按钮按下时,记录按下的时间;当按钮释放时,计算按下的持续时间,并通过与预设的阈值比较,判断是短按还是长按
- 按钮持续按下的检测
在按钮保持按下的情况下(button_pressed == true),如果按下时间超过了长按的阈值,会持续触发长按回调函数,并在触发一次后重置状态。
cppif (duration >= LONG_PRESS_DURATION_MS && long_press_callback) {
long_press_callback(); // 调用长按的回调函数
button_pressed = false; // 处理完长按后重置按钮状态
}
- 代码中的具体实现:
void KeyInput::ListenThread() {
auto last_press_time = std::chrono::steady_clock::now(); // 记录按钮按下的时间
bool button_pressed = false; // 用于标记按钮是否被按下
while (running) { // 只要监听线程正在运行,就持续监听按钮状态
bool current_state = gpioRead(pin) == PI_LOW; // 读取GPIO引脚的状态,低电平表示按钮被按下
if (current_state && !button_pressed) { // 按钮刚刚被按下
button_pressed = true; // 设置按钮状态为已按下
last_press_time = std::chrono::steady_clock::now(); // 记录当前时间为按钮按下的时间
} else if (!current_state && button_pressed) { // 按钮被释放
button_pressed = false; // 设置按钮状态为未按下
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - last_press_time).count(); // 计算按钮按下的持续时间
// 如果持续时间小于长按的阈值,则认为是短按
if (duration < LONG_PRESS_DURATION_MS && short_press_callback) {
short_press_callback(); // 调用短按的回调函数
}
} else if (button_pressed) { // 按钮依然被按下
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - last_press_time).count(); // 计算按钮持续按下的时间
// 如果持续时间大于等于长按的阈值,则认为是长按
if (duration >= LONG_PRESS_DURATION_MS && long_press_callback) {
long_press_callback(); // 调用长按的回调函数
button_pressed = false; // 处理完长按后重置按钮状态
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 每10毫秒检查一次按钮状态
}
}
4. 麦克风
4.1 总体顺序预览
- 安装必要的库,这里安装的是portaudio。
sudo apt-get update
sudo apt-get install portaudio19-dev
- 编写对应程序
接下来是主程序代码,具体功能为:能够录音并保存为 WAV 文件
#include <portaudio.h>
#include <iostream>
#include <fstream>
#include <vector>
// 回调函数用于处理音频流数据并保存到文件
static int recordCallback(const void* inputBuffer, void* outputBuffer,
unsigned long framesPerBuffer,
const PaStreamCallbackTimeInfo* timeInfo,
PaStreamCallbackFlags statusFlags,
void* userData) {
std::vector<short>* audioData = reinterpret_cast<std::vector<short>*>(userData);
const short* input = reinterpret_cast<const short*>(inputBuffer);
// 将输入缓冲区数据存入audioData
audioData->insert(audioData->end(), input, input + framesPerBuffer);
return paContinue;
}
int main() {
Pa_Initialize();
PaStream* stream;
std::vector<short> audioData; // 用于保存录音数据
// 打开音频流,设置录音格式
Pa_OpenDefaultStream(&stream, 1, 0, paInt16, 44100, 256, recordCallback, &audioData);
Pa_StartStream(stream);
std::cout << "Recording..." << std::endl;
for (int i = 0; i < 10; i++) {
Pa_Sleep(1000); // 每秒录制一次
}
Pa_StopStream(stream);
Pa_CloseStream(stream);
Pa_Terminate();
// 保存音频数据到WAV文件
std::ofstream outFile("recording.wav", std::ios::binary);
// 写入WAV文件头
outFile.write("RIFF", 4);
int32_t fileSize = 36 + audioData.size() * sizeof(short);
outFile.write(reinterpret_cast<char*>(&fileSize), 4);
outFile.write("WAVE", 4);
// 写入"fmt "块
outFile.write("fmt ", 4);
int32_t fmtSize = 16;
outFile.write(reinterpret_cast<char*>(&fmtSize), 4);
int16_t audioFormat = 1; // PCM
outFile.write(reinterpret_cast<char*>(&audioFormat), 2);
int16_t numChannels = 1; // 单声道
outFile.write(reinterpret_cast<char*>(&numChannels), 2);
int32_t sampleRate = 44100;
outFile.write(reinterpret_cast<char*>(&sampleRate), 4);
int32_t byteRate = sampleRate * numChannels * sizeof(short);
outFile.write(reinterpret_cast<char*>(&byteRate), 4);
int16_t blockAlign = numChannels * sizeof(short);
outFile.write(reinterpret_cast<char*>(&blockAlign), 2);
int16_t bitsPerSample = 16;
outFile.write(reinterpret_cast<char*>(&bitsPerSample), 2);
// 写入"data"块
outFile.write("data", 4);
int32_t dataSize = audioData.size() * sizeof(short);
outFile.write(reinterpret_cast<char*>(&dataSize), 4);
outFile.write(reinterpret_cast<char*>(audioData.data()), dataSize);
outFile.close();
std::cout << "Recording saved as 'recording.wav'" << std::endl;
return 0;
}
- 编译函数
在编译时,确保链接了 PortAudio 库,这里使用的g++进行编译。如果您的工程比较复杂,可以使用cmake进行编译操作。
g++ -o recorder recorder.cpp -lportaudio
如无编译错误,则编译成功。
- 启动程序
在结束编译操作后,会生成启动程序文件。
./recorder
4.2 代码基础配置
- 引入必要的库
#include <portaudio.h> //PortAudio库的头文件,用于处理音频流。
#include <iostream> //用于输出日志信息。
#include <fstream> //用于处理文件的读写操作。
#include <vector>//用于存储录音数据(音频样本)。
- 定义回调函数
回调函数是音频流数据的核心,它会在录音时每次处理一块音频数据(每个缓冲区)。
static int recordCallback(const void* inputBuffer, void* outputBuffer,
unsigned long framesPerBuffer,
const PaStreamCallbackTimeInfo* timeInfo,
PaStreamCallbackFlags statusFlags,
void* userData) {
std::vector<short>* audioData = reinterpret_cast<std::vector<short>*>(userData);
const short* input = reinterpret_cast<const short*>(inputBuffer);
// 将输入缓冲区数据存入audioData
audioData->insert(audioData->end(), input, input + framesPerBuffer);
return paContinue;
}
inputBuffer: 输入音频数据的指针(来自麦克风)。audioData: 用于保存录音数据的容器。framesPerBuffer: 每个缓冲区包含的音频帧数。每次回调都会处理这些数据并将其存入audioData。
4.3 录音功能
- 初始化PortAudio并开始录音
int main() {
Pa_Initialize(); // 初始化PortAudio库
PaStream* stream;
std::vector<short> audioData; // 用于保存录音数据
// 打开音频流,设置录音格式
Pa_OpenDefaultStream(&stream, 1, 0, paInt16, 44100, 256, recordCallback, &audioData);
Pa_StartStream(stream);
Pa_OpenDefaultStream: 打开默认的音频流,设置录音格式。1: 单声道输入(1通道)。44100: 采样率为44.1kHz。256: 每个缓冲区的大小(帧数),决定每次回调的时间段。
- 录音持续时间设置
std::cout << "Recording..." << std::endl;//后台输出日志
for (int i = 0; i < 10; i++) {
Pa_Sleep(1000); // 每秒录制一次,共录制10秒,其中单位为ms
}
Pa_StopStream(stream); // 停止音频流
Pa_CloseStream(stream); // 关闭音频流
Pa_Terminate(); // 结束PortAudio库
Pa_StopStream: 停止录音流。Pa_CloseStream: 关闭音频流。Pa_Terminate: 终止PortAudio。- 注意:如果在此不进行关闭音频流,或者其他非正常退出程序,可能会对后续录音产生影响,建议加入此部分代码。
4.4 保存录音文件
保存音频数据为WAV格式的文件,确保文件符合标准的WAV文件格式。
// 保存音频数据到WAV文件
std::ofstream outFile("recording.wav", std::ios::binary);
// 写入WAV文件头
outFile.write("RIFF", 4); // 文件标识符
int32_t fileSize = 36 + audioData.size() * sizeof(short);
outFile.write(reinterpret_cast<char*>(&fileSize), 4); // 文件大小
outFile.write("WAVE", 4); // WAVE标识符
// 写入"fmt "块
outFile.write("fmt ", 4);
int32_t fmtSize = 16;
outFile.write(reinterpret_cast<char*>(&fmtSize), 4); // fmt块大小
int16_t audioFormat = 1; // PCM格式
outFile.write(reinterpret_cast<char*>(&audioFormat), 2); // 音频格式(1表示PCM)
int16_t numChannels = 1; // 单声道
outFile.write(reinterpret_cast<char*>(&numChannels), 2);
int32_t sampleRate = 44100;
outFile.write(reinterpret_cast<char*>(&sampleRate), 4); // 采样率
int32_t byteRate = sampleRate * numChannels * sizeof(short);
outFile.write(reinterpret_cast<char*>(&byteRate), 4); // 字节率
int16_t blockAlign = numChannels * sizeof(short);
outFile.write(reinterpret_cast<char*>(&blockAlign), 2); // 每块的对齐大小
int16_t bitsPerSample = 16;
outFile.write(reinterpret_cast<char*>(&bitsPerSample), 2); // 每个样本的位数
// 写入"data"块
outFile.write("data", 4); // 数据块标识符
int32_t dataSize = audioData.size() * sizeof(short);
outFile.write(reinterpret_cast<char*>(&dataSize), 4); // 数据块大小
outFile.write(reinterpret_cast<char*>(audioData.data()), dataSize); // 音频数据
outFile.close();
std::cout << "Recording saved as 'recording.wav'" << std::endl;
return 0;
}
以下是 WAV 文件格式中常用数据的表格:
| 字段 | 功能描述 | 位置 | 大小 | 示例值 |
|---|---|---|---|---|
| "RIFF" 标识符 | 用来标识文件是 RIFF 文件。 | 文件开头的 4 个字节 | 4 字节 | "RIFF" |
| 文件大小 | 文件大小从 "RIFF" 开始到文件末尾的总字节数,不包括 "RIFF" 和文件大小本身。 | 紧随 "RIFF" 标识符后 | 4 字节 | 44,100 字节 |
| "fmt " 块标识符 | 标识文件格式信息块。 | 紧随文件大小后,4 字节 | 4 字节 | "fmt " |
| fmt 块大小 | 表示 "fmt " 块的大小,不包括块标识符和块大小。 | "fmt " 标识符后,紧接着 4 字节 | 4 字节 | 16 字节 |
| 音频格式 | 音频编码格式,1 表示 PCM 编码,其他值表示压缩格式。 | fmt 块中 | 2 字节 | 1(表示 PCM 编码) |
| 通道数 | 音频文件的通道数,1 为单声道,2 为立体声。 | fmt 块中 | 2 字节 | 1(单声道) |
| 采样率 | 每秒钟采样的数量,表示音频文件的采样频率。 | fmt 块中 | 4 字节 | 44,100 Hz |
| 字节率 | 每秒传输的字节数,计算公式:采样率 * 通道数 * 位深度 / 8。 | fmt 块中 | 4 字节 | 88,200 字节/秒 |
| 块对齐 | 每个采样帧的字节数,计算公式:通道数 * 位深度 / 8。 | fmt 块中 | 2 字节 | 2 字节 |
| 位深度 | 每个样本的位数,如 16 位、24 位等。 | fmt 块中 | 2 字节 | 16 位 |
| "data" 块标识符 | 表示数据块的标识符。 | 紧随 fmt 块后 | 4 字节 | "data" |
| data 块大小 | 表示数据块的大小,通常是音频数据的字节数。 | 紧随 "data" 标识符后 | 4 字节 | 44,100 字节 |
| 音频数据 | 实际的音频样本数据,根据 fmt 块中的参数组织。 | 紧随 data 块大小后 | 变量 | 具体音频数据 |
要将上述 WAV 保存过程转换为 MP3,你需要先将 PCM 数据编码为 MP3 格式。 可使用libmp3lame 库保存为 MP3 文件。
5. 扬声器
5.1 aplay播放音频
- 安装依赖库,这里使用aplay的方式进行播放,所以不需要依赖库
- 编写对应程序
#include <iostream>
#include <cstdlib> // system() 函数
int main() {
// 设置要播放的 WAV 文件路径
const std::string file_path = " /usr/share/sounds/alsa/Front_Center.wav";
// /usr/share/sounds/alsa/Front_Center.wav这是系统自带的音频文件,可以改成你自己的音频路径
// 构造 aplay 命令
std::string command = "aplay " + file_path;
// 执行命令
int result = system(command.c_str());
// 检查命令执行是否成功
if (result == 0) {
std::cout << "Playback completed successfully.\n";
} else {
std::cout << "An error occurred during playback.\n";
}
return 0;
}
- 编译程序
g++ play_wav.cpp -o play_wav
- 启动程序
./play_wav
5.2 其他方式播放
- 安装所需的依赖库
首先,需要确保系统已经安装了 ALSA 音频库。
sudo apt-get update
sudo apt-get install libasound2-dev
- 编写对应程序
#include <iostream>
#include <fstream>
#include <alsa/asoundlib.h>
using namespace std;
// 定义PCM设备名,"default"是系统默认的音频设备
#define PCM_DEVICE "default"
int main() {
// PCM设备句柄,用于与音频硬件进行交互
snd_pcm_t *pcm_handle;
// PCM硬件参数,用于设置音频设备的硬件属性
snd_pcm_hw_params_t *params;
// 设置音频格式为16位小端格式(S16_LE)
snd_pcm_format_t format = SND_PCM_FORMAT_S16_LE;
// 设置音频采样率为44100Hz(即CD音质)
unsigned int rate = 44100; // Sample rate (44.1 kHz)
// 设置立体声(2个通道)
int channels = 2; // Stereo
// 设置缓冲区大小为1024字节
int buffer_size = 1024;
// 打开PCM设备进行播放(SND_PCM_STREAM_PLAYBACK表示播放流)
if (snd_pcm_open(&pcm_handle, PCM_DEVICE, SND_PCM_STREAM_PLAYBACK, 0) < 0) {
cerr << "Unable to open audio device!" << endl;
return -1;
}
// 分配并初始化硬件参数结构体
snd_pcm_hw_params_alloca(¶ms);
// 获取默认的硬件参数
if (snd_pcm_hw_params_any(pcm_handle, params) < 0) {
cerr << "Unable to get audio hardware parameters!" << endl;
return -1;
}
// 设置音频访问模式为可读写的交错式(interleaved)
if (snd_pcm_hw_params_set_access(pcm_handle, params, SND_PCM_ACCESS_RW_INTERLEAVED) < 0) {
cerr << "Unable to set access mode!" << endl;
return -1;
}
// 设置音频格式为16位小端格式(SND_PCM_FORMAT_S16_LE)
if (snd_pcm_hw_params_set_format(pcm_handle, params, format) < 0) {
cerr << "Unable to set audio format!" << endl;
return -1;
}
// 设置采样率(如果无法精确设置,将接近设置值)
if (snd_pcm_hw_params_set_rate_near(pcm_handle, params, &rate, 0) < 0) {
cerr << "Unable to set sample rate!" << endl;
return -1;
}
// 设置通道数为2(立体声)
if (snd_pcm_hw_params_set_channels(pcm_handle, params, channels) < 0) {
cerr << "Unable to set number of channels!" << endl;
return -1;
}
// 将硬件参数应用到PCM设备中
if (snd_pcm_hw_params(pcm_handle, params) < 0) {
cerr << "Unable to set hardware parameters!" << endl;
return -1;
}
// 打开音频文件,准备读取二进制数据
ifstream audio_file("/home/pi/Cproject/recording.wav", ios::binary);
if (!audio_file) {
cerr << "Unable to open audio file!" << endl;
return -1;
}
// 为读取音频数据分配缓冲区,大小为1024字节
char *buffer = new char[buffer_size];
// 读取音频文件中的数据并写入PCM设备进行播放
while (audio_file.read(buffer, buffer_size)) {
// 将读取的音频数据写入PCM设备
if (snd_pcm_writei(pcm_handle, buffer, buffer_size / 2) < 0) {
cerr << "Failed to write audio data!" << endl;
break;
}
}
// 清理并关闭资源
delete[] buffer; // 释放缓冲区
audio_file.close(); // 关闭音频文件
// 确保所有缓冲区数据被播放完
snd_pcm_drain(pcm_handle);
// 关闭PCM设备
snd_pcm_close(pcm_handle);
// 播放完成后输出信息
cout << "Audio playback completed!" << endl;
return 0;
}
- 编译程序
你可以将这个 C++ 文件保存为 audio_playback.cpp。然后,使用 g++ 编译器编译程序。运行以下命令:
g++ -o audio_playback audio_playback.cpp -lasound
这将生成一个名为 audio_playback 的可执行文件,-lasound 参数指定链接 ALSA 库。
- 准备音频文件
确保你已经有一个音频文件(如 .wav 文件)。在代码中,音频文件路径是 /home/pi/Cproject/recording.wav,如果你的文件路径不同,请编辑代码并修改路径:
ifstream audio_file("/path/to/your/audio/file.wav", ios::binary);
确保路径正确且文件可读。
- 运行程序
运行编译后的程序:
./audio_playback
5.3 注意事项
- 请确保音频频道没有被其他占用,或者初始化。不然会发生代码没问题,但没有声音的情况
- 如果当前输出设备为HDMI,则载版是不会发出声音的,连接上电脑等外设可以发出声音。可以更改输出设备设置为外设
- 缓冲区大小与音频播放流畅性:缓冲区大小对于音频播放的流畅性有一定影响。如果缓冲区设置得过小,可能导致播放时出现断断续续的现象;如果设置得过大,可能增加延迟。可以根据硬件和应用需求进行调节。
- 确保音频文件完整且无损坏:播放的音频文件如果存在损坏(如文件丢失数据、格式错误等),即使硬件和代码设置正确,也可能导致播放失败。确保音频文件格式完整,并且能够正常读取。
- 正确释放资源:在程序结束时,确保音频设备和文件流都得到了正确关闭和清理,避免资源泄漏。这不仅有助于程序的稳定性,还能避免占用系统资源,从而影响其他应用程序。
- 使用 ALSA 播放时出现电流声通常与驱动、缓冲区设置、采样率不匹配、系统负载、硬件问题或配置不当有关,解决方法是优化这些设置并确保设备兼容性。
