本页目录

如何使用C++开发AI模组

1. 屏幕

  • 画图形
    • 绘制基本图形(如直线、圆形、矩形等)
    • 绘制复杂图形(如多边形、贝塞尔曲线等)
  • 显示图片
    • 支持不同格式的图像显示(如JPEG, PNG等)
    • 图片动态更新
  • 显示文字
    • 文本内容显示
    • 文本颜色、大小、对齐方式设置
    • 支持多行文本、文本换行与溢出处理
    • 动态文本更新

2. 摄像头

2.1 代码基础配置

  1. 安装依赖

确保已安装 OpenCV 库。可以使用以下命令安装 OpenCV(假设使用的是 Linux 系统):

sudo apt-get install libopencv-dev
  1. 包含头文件
#include <opencv2/opencv.hpp>
#include <iostream>
#include <string>
#include <thread>
  1. 检查摄像头设备
ls /dev/video*

这会列出所有视频设备。如果你看到 /dev/video0/dev/video1,说明有多个视频设备连接。

根据你的情况,可能需要修改代码或配置文件,确保使用正确的设备索引。例如,尝试使用 /dev/video0 而不是 /dev/video1

cv::VideoCapture cap(0);  // 0 为默认设备,可能需要修改为 1,具体取决于你的设备
  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;
}
  1. 编译与运行

编译此代码时需要链接 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 代码基础配置

  1. 安装依赖
sudo apt-get install pigpio
  1. 包含头文件
#include <iostream>  // 引入标准输入输出流库,用于打印日志信息
#include <functional> // 引入函数对象库,用于存储回调函数
#include <thread>     // 引入多线程库,用于创建和管理线程
#include <atomic>     // 引入原子操作库,用于保证线程间共享变量的原子性
#include <pigpio.h>   // 引入pigpio库,用于Raspberry Pi的GPIO控制
#include <chrono>     // 引入时间库,用于处理时间和计时器
  1. 示例代码
#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;  // 程序结束
}
  1. 编译与运行
g++ key_input.cpp -o key_input -lpigpio -pthread

然后运行程序:

sudo ./key_input

3.2 短按和长按

  1. 按键状态读取

代码通过GPIO读取按钮的状态。我们通过 gpioRead(pin) 来读取按钮的状态。如果按钮被按下,GPIO引脚会是低电平(PI_LOW),如果没有按下,GPIO引脚会是高电平(PI_HIGH)。至于其他模式可选,如上升沿(即从低电平到高电平的变化)、下降沿(即从高电平到低电平的变化)、或推挽输出等设置,可以根据具体需求参考硬件手册进行配置。这些模式常用于实现中断触发或特定信号检测,具体的配置和使用方法可在GPIO库或硬件手册中找到详细说明。

  1. 按键按下时间记录

为了区分短按和长按,我们需要记录按钮按下的持续时间。每当检测到按钮按下时(即从未按下到按下的状态变化),我们记录当前的时间点(last_press_time)。这个时间点用于计算按钮按下的持续时间。

auto last_press_time = std::chrono::steady_clock::now();
  1. 按钮释放时的处理

按钮按下的持续时间是在按钮释放时(即状态从按下变为未按下)计算的。通过获取当前时间和按下时的时间差,得到按钮按下的时间。

auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - last_press_time).count();

duration 就是按钮按下的时间(单位是毫秒)。接下来,我们根据这个时间来判断是短按还是长按。

  1. 短按和长按的判断

短按:如果按钮按下时间小于设定的阈值(例如 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();  // 调用长按的回调函数
}
  1. 检测流程

当按钮按下时,记录按下的时间;当按钮释放时,计算按下的持续时间,并通过与预设的阈值比较,判断是短按还是长按

  1. 按钮持续按下的检测

在按钮保持按下的情况下(button_pressed == true),如果按下时间超过了长按的阈值,会持续触发长按回调函数,并在触发一次后重置状态。

cppif (duration >= LONG_PRESS_DURATION_MS && long_press_callback) {
    long_press_callback();  // 调用长按的回调函数
    button_pressed = false;  // 处理完长按后重置按钮状态
}
  1. 代码中的具体实现:
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 总体顺序预览

  1. 安装必要的库,这里安装的是portaudio。
sudo apt-get update
sudo apt-get install portaudio19-dev
  1. 编写对应程序

接下来是主程序代码,具体功能为:能够录音并保存为 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;
}
  1. 编译函数

在编译时,确保链接了 PortAudio 库,这里使用的g++进行编译。如果您的工程比较复杂,可以使用cmake进行编译操作。

g++ -o recorder recorder.cpp -lportaudio

如无编译错误,则编译成功。

  1. 启动程序

在结束编译操作后,会生成启动程序文件。

./recorder

4.2 代码基础配置

  1. 引入必要的库
#include <portaudio.h> //PortAudio库的头文件,用于处理音频流。
#include <iostream> //用于输出日志信息。
#include <fstream> //用于处理文件的读写操作。
#include <vector>//用于存储录音数据(音频样本)。
  1. 定义回调函数

回调函数是音频流数据的核心,它会在录音时每次处理一块音频数据(每个缓冲区)。

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 录音功能

  1. 初始化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: 每个缓冲区的大小(帧数),决定每次回调的时间段。
  1. 录音持续时间设置
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播放音频

  1. 安装依赖库,这里使用aplay的方式进行播放,所以不需要依赖库
  2. 编写对应程序
#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;
}
  1. 编译程序
g++ play_wav.cpp -o play_wav
  1. 启动程序
./play_wav

5.2 其他方式播放

  1. 安装所需的依赖库

首先,需要确保系统已经安装了 ALSA 音频库。

sudo apt-get update
sudo apt-get install libasound2-dev
  1. 编写对应程序
#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(&params);
    
    // 获取默认的硬件参数
    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;
}
  1. 编译程序

你可以将这个 C++ 文件保存为 audio_playback.cpp。然后,使用 g++ 编译器编译程序。运行以下命令:

g++ -o audio_playback audio_playback.cpp -lasound

这将生成一个名为 audio_playback 的可执行文件,-lasound 参数指定链接 ALSA 库。

  1. 准备音频文件

确保你已经有一个音频文件(如 .wav 文件)。在代码中,音频文件路径是 /home/pi/Cproject/recording.wav,如果你的文件路径不同,请编辑代码并修改路径:

ifstream audio_file("/path/to/your/audio/file.wav", ios::binary);

确保路径正确且文件可读。

  1. 运行程序

运行编译后的程序:

./audio_playback

5.3 注意事项

  • 请确保音频频道没有被其他占用,或者初始化。不然会发生代码没问题,但没有声音的情况
  • 如果当前输出设备为HDMI,则载版是不会发出声音的,连接上电脑等外设可以发出声音。可以更改输出设备设置为外设
  • 缓冲区大小与音频播放流畅性:缓冲区大小对于音频播放的流畅性有一定影响。如果缓冲区设置得过小,可能导致播放时出现断断续续的现象;如果设置得过大,可能增加延迟。可以根据硬件和应用需求进行调节。
  • 确保音频文件完整且无损坏:播放的音频文件如果存在损坏(如文件丢失数据、格式错误等),即使硬件和代码设置正确,也可能导致播放失败。确保音频文件格式完整,并且能够正常读取。
  • 正确释放资源:在程序结束时,确保音频设备和文件流都得到了正确关闭和清理,避免资源泄漏。这不仅有助于程序的稳定性,还能避免占用系统资源,从而影响其他应用程序。
  • 使用 ALSA 播放时出现电流声通常与驱动、缓冲区设置、采样率不匹配、系统负载、硬件问题或配置不当有关,解决方法是优化这些设置并确保设备兼容性。