当前位置: 首页 > news >正文

PortAudio--Cross-platform Open-Source Audio I/O Library

目录

音频相关概念

PortAudio

核心概念与函数

回调函数

1. 音频缓冲区需要填充 / 读取(实时处理)

2. 流状态变化时的通知

回调函数的核心规范(必须遵守)

返回值规范(决定流的后续状态)

回调函数的调用特性

使用流程

1.编写回调函数

常见使用场景

麦克风录制场景

音频播放场景:

实时音效处理场景(以 “音量调节 + 简单混响” 为例)

全双工(同时录播)场景(麦克风输入→实时变调→播放)

2.初始化 PortAudio 库

3.配置音频流参数(PaStreamParameters)

4.打开音频流(Pa_OpenStream)

5.启动音频流(Pa_StartStream)

6.运行音频逻辑(如等待录制结束、处理数据)

7.释放资源(关闭流 + 终止 PortAudio)

8.完整Demo:


音频相关概念

声音是什么

声音是由物体振动产生的声波。声音作为一种机械波,频率在20 Hz~20 kHz之间的声音是可以被人耳识别的。

音频录制

最简单的音频录制流程为:

设备采集获取模拟信号 --->模数转换 --->存储(播放、传输等).

播放端流程相反:

音频文件 --->数模转换--->播放器播放

模数转换

模拟信号转化为数字信号的流程:

如上图:

  • 模拟数据:最原始的连续信号。
  • 采样:按固定时间间隔 “抓取” 模拟信号的瞬时值,将连续时间的信号转换为离散时间的信号。
  • 量化:把采样得到的连续幅度值,划分到有限个 “量化等级” 中,将连续幅度转换为离散幅度
  • 编码:将量化后的离散整数值,转换为二进制,或其他数字格式,最终生成计算机可存储、处理的数字信号

这些术语是数字信号处理(Digital Signal Processing, DSP) 领域的核心概念。

简单说:模拟信号 → 采样(时间离散)→ 量化(幅度离散)→ 编码(二进制化)→ 数字信号

PortAudio

PortAudio 是一个跨平台的开源音频 I/O 库,旨在为不同操作系统提供统一的音频设备访问接口,方便开发者进行音频采集、播放等操作。

其能够在 Windows、macOS、Linux、iOS、Android 等多种操作系统上使用,支持多种音频设备和多种常见的音频数据格式,并且提供了简洁且一致的 API,使得开发者可以相对轻松地实现音频设备的打开、关闭,音频流的启动、停止,以及音频数据的读写等操作。

核心概念与函数

  • 音频设备(Audio Device):在 PortAudio 中,每个音频输入或输出设备都被抽象为一个音频设备对象。通过Pa_GetDeviceCount()函数可以获取系统中音频设备的数量。使用Pa_GetDeviceInfo()函数可以获取指定设备的详细信息。
  • 音频流(Audio Stream):音频流用于管理音频数据的传输。要使用音频设备进行音频采集或播放,需要先打开一个音频流。通过Pa_OpenStream()函数来打开音频流,该函数需要指定输入设备参数、输出设备参数、采样率、帧大小、音频格式等信息。打开音频流后,使用Pa_StartStream()函数启动音频流,开始进行音频数据的传输;使用Pa_ReadStream()函数可以从输入音频流中读取音频数据(用于音频采集),使用Pa_WriteStream()函数可以向输出音频流中写入音频数据(用于音频播放) ;最后,使用Pa_CloseStream()函数关闭音频流,释放相关资源。
  • 回调函数(Callback Function):在音频流的操作中,回调函数起着重要作用。例如,在进行音频采集时,可以设置一个回调函数,当音频流有新的音频数据可用时,PortAudio 会自动调用该回调函数,开发者可以在回调函数中处理采集到的音频数据。回调函数的使用使得音频数据的处理可以以异步的方式进行,提高了程序的效率和响应性。

回调函数

 PortAudio 的回调函数是其异步音频处理模式的核心,它由 PortAudio 内部的音频驱动线程自动调用,而非用户代码直接触发。其调用时机严格与音频硬件的采样时钟和缓冲区处理逻辑绑定。

1. 音频缓冲区需要填充 / 读取(实时处理)

这是回调函数被调用的最主要、最频繁的原因,是其最核心场景,直接服务于音频的实时输入 / 输出。

PortAudio 采用 “缓冲区” 机制协调软件处理与硬件速度的差异:

  • 对于输出流(播放音频):当音频硬件的输出缓冲区中已有的数据即将播放完毕(或已空)时,PortAudio 会立即调用回调函数,要求用户代码填充新的音频数据到输出缓冲区,避免出现 “断音”(underflow)。
  • 对于输入流(录制音频):当音频硬件的输入缓冲区已收集到足够多的音频数据(达到预设的缓冲区大小)时,PortAudio 会调用回调函数,将缓冲区中录制好的原始数据传递给用户代码,供后续处理,避免数据溢出(overflow)。
  • 对于全双工流(同时输入 + 输出):回调函数会在 “输入缓冲区满” 或 “输出缓冲区空” 的较早触发条件下被调用,此时用户可同时读取输入数据、填充输出数据(例如实时语音处理、音效实时叠加等场景)。
2. 流状态变化时的通知

当音频流的状态发生改变(如启动、停止、出错)时,PortAudio 也会调用回调函数,并通过参数告知用户当前的状态,以便进行对应的处理。常见的状态触发包括:

  • 流启动时:部分系统中,音频流刚通过Pa_StartStream()启动后,会立即调用一次回调函数,用于初始化缓冲区或同步状态。
  • 流停止时:当调用Pa_StopStream()Pa_AbortStream()后,PortAudio 可能会再调用一次回调函数,传递paCompletepaAbort状态,通知用户流已终止,可进行资源清理。
  • 流出错时:若音频硬件发生错误(如设备被占用、采样率不支持、缓冲区溢出 / 下溢),回调函数会被调用,并通过paError状态传递具体错误码,用户可据此进行错误处理。
回调函数的核心规范(必须遵守)

PortAudio 回调函数的函数签名是固定的,用户不能自定义参数或返回值类型,否则会导致程序崩溃。其原型如下(C/C++ 通用):

typedef int PaStreamCallback(const void *inputBuffer,   // 输入音频数据缓冲区(录制场景有效)void *outputBuffer,        // 输出音频数据缓冲区(播放场景有效)unsigned long framesPerBuffer,  // 本次回调需处理的采样点数(帧大小)const PaStreamCallbackTimeInfo *timeInfo,  // 时间信息(可选)PaStreamCallbackFlags statusFlags,  // 状态标志(如缓冲区溢出/下溢)void *userData             // 用户自定义数据(传递上下文)
);

各参数含义详解:

inputBuffer录制场景:指向麦克风等输入设备采集的原始音频数据(格式与流配置一致);无输入时为 NULL
outputBuffer播放场景:指向需要填充的输出音频数据缓冲区;无输出时为 NULL
framesPerBuffer本次回调需处理的采样点数(即 “帧大小”),由 Pa_OpenStream 配置时指定(或系统自动分配)。
timeInfo包含音频处理的时间戳信息(如当前缓冲区的播放 / 采集时间),一般用于高精度同步场景(可选忽略)。
statusFlags状态标志位,如 paInputOverflow(输入缓冲区溢出)、paOutputUnderflow(输出缓冲区下溢),需据此处理错误。
userData用户自定义数据指针(通过 Pa_OpenStream 传入),用于传递上下文(如缓冲区、配置参数等),避免全局变量。
返回值规范(决定流的后续状态)

回调函数的返回值是 PortAudio 控制音频流生命周期的关键,必须返回以下枚举值之一:

  • paContinue:继续运行,PortAudio 会持续调用回调函数;
  • paComplete:正常终止流,PortAudio 后续不再调用回调,需配合 Pa_CloseStream 释放资源;
  • paAbort:立即终止流。
回调函数的调用特性

理解调用时机的同时,需明确其底层特性,调用本质是 “硬件驱动的事件通知”,避免误用:

  1. 线程独立性:回调函数运行在 PortAudio 的内部音频线程中,而非用户的主线程,因此回调函数内需避免阻塞操作(如文件 IO、sleep、锁等待),否则会导致音频卡顿。
  2. 实时性要求:调用时机由硬件时钟决定,具有严格的实时性,用户需确保回调函数内的代码执行速度足够快(耗时 < 缓冲区对应的时间)。
  3. 参数驱动:回调函数的输入参数(如输入数据指针、缓冲区大小)会明确告知当前需要处理的任务,用户无需主动查询流状态。

使用流程

1.编写回调函数

首先引入PA的头文件

#include "portaudio.h"
常见使用场景
场景回调核心逻辑
麦克风录制读取 inputBuffer → (可选预处理)→ 写入文件 / 传递给后续模块(如语音识别)
音频播放从文件 / 内存读取数据 → 写入 outputBuffer → 读完返回 paComplete
实时音效处理读取 inputBuffer → 音效算法处理(如混响、均衡)→ 写入 outputBuffer 播放
全双工(同时录播)读取 inputBuffer 处理,同时填充 outputBuffer 播放,需协调两者的同步性
麦克风录制场景
#include <portaudio.h>
#include <iostream>
#include <fstream>
// 自定义上下文结构体,用于传递录制相关信息
struct RecordContext {std::ofstream outFile;  // 用于存储录制音频的文件流bool isRecording;  // 录制状态标志
};// 麦克风录制回调函数
int RecordCallback(const void *inputBuffer,void *outputBuffer,unsigned long framesPerBuffer,const PaStreamCallbackTimeInfo *timeInfo,PaStreamCallbackFlags statusFlags,void *userData
) {RecordContext* ctx = reinterpret_cast<RecordContext*>(userData);// 检查状态标志,处理可能的错误if (statusFlags & paInputOverflow) {std::cerr << "Input buffer overflow occurred!" << std::endl;}if (statusFlags & paError) {std::cerr << "An error occurred in the audio stream!" << std::endl;return paAbort;}// 如果处于录制状态且有输入音频数据if (ctx->isRecording && inputBuffer != nullptr) {// 假设音频数据是32位浮点数格式,将其写入文件const float* audioData = reinterpret_cast<const float*>(inputBuffer);ctx->outFile.write(reinterpret_cast<const char*>(audioData), framesPerBuffer * sizeof(float));}// 如果停止录制,返回paComplete终止流return ctx->isRecording? paContinue : paComplete;
}
音频播放场景:
#include <portaudio.h>
#include <iostream>
#include <fstream>// 自定义上下文结构体,用于传递播放相关信息
struct PlaybackContext {std::ifstream inFile;  // 要播放的音频文件流bool isPlaying;  // 播放状态标志
};// 音频播放回调函数
int PlaybackCallback(const void *inputBuffer,void *outputBuffer,unsigned long framesPerBuffer,const PaStreamCallbackTimeInfo *timeInfo,PaStreamCallbackFlags statusFlags,void *userData
) {PlaybackContext* ctx = reinterpret_cast<PlaybackContext*>(userData);// 检查状态标志,处理可能的错误if (statusFlags & paOutputUnderflow) {std::cerr << "Output buffer underflow occurred!" << std::endl;}if (statusFlags & paError) {std::cerr << "An error occurred in the audio stream!" << std::endl;return paAbort;}// 如果处于播放状态且有输出缓冲区if (ctx->isPlaying && outputBuffer != nullptr) {float* outData = reinterpret_cast<float*>(outputBuffer);// 从文件读取音频数据到输出缓冲区ctx->inFile.read(reinterpret_cast<char*>(outData), framesPerBuffer * sizeof(float));// 如果读取的帧数少于预期,说明文件已读完,填充剩余部分为0(静音)if (ctx->inFile.gcount() < framesPerBuffer) {std::fill(outData + ctx->inFile.gcount(), outData + framesPerBuffer, 0.0f);ctx->isPlaying = false;return paComplete;}}return paContinue;
}
实时音效处理场景(以 “音量调节 + 简单混响” 为例)
#include <portaudio.h>
#include <iostream>
#include <vector>// 简单混响效果器:使用延迟线实现回声
class ReverbEffect {
private:std::vector<float> delayLine;  // 延迟线缓冲区size_t delayIndex = 0;         // 延迟线当前索引float decayFactor = 0.5f;      // 回声衰减因子public:ReverbEffect(size_t maxDelaySamples) : delayLine(maxDelaySamples, 0.0f) {}// 处理单帧音频,添加混响效果void process(float* audioData, unsigned long framesPerBuffer) {for (unsigned long i = 0; i < framesPerBuffer; ++i) {// 当前输入样本float inputSample = audioData[i];// 延迟线中取出历史样本(回声)float delayedSample = delayLine[delayIndex];// 新样本 = 原始样本 + 衰减后的回声audioData[i] = inputSample + (delayedSample * decayFactor);// 将新样本存入延迟线(覆盖旧样本,实现循环)delayLine[delayIndex] = inputSample;// 更新延迟线索引(循环)delayIndex = (delayIndex + 1) % delayLine.size();}}
};// 自定义上下文结构体:存储音效参数和状态
struct EffectContext {ReverbEffect reverb;  // 混响效果器float volumeGain = 1.2f;  // 音量增益(1.2倍)bool isProcessing = true;  // 处理状态标志
};// 实时音效处理回调函数
int EffectCallback(const void *inputBuffer,void *outputBuffer,unsigned long framesPerBuffer,const PaStreamCallbackTimeInfo *timeInfo,PaStreamCallbackFlags statusFlags,void *userData
) {EffectContext* ctx = reinterpret_cast<EffectContext*>(userData);// 1. 处理状态标志(输入/输出错误)if (statusFlags & (paInputOverflow | paOutputUnderflow)) {std::cerr << "警告:缓冲区溢出/下溢,可能导致音效失真!\n";}if (statusFlags & paError) {std::cerr << "错误:音频流异常,终止处理!\n";return paAbort;}// 2. 核心逻辑:仅在处理状态下执行if (ctx->isProcessing && inputBuffer != nullptr && outputBuffer != nullptr) {// 强转输入/输出数据为 float*(与流配置的 paFloat32 匹配)const float* inData = reinterpret_cast<const float*>(inputBuffer);float* outData = reinterpret_cast<float*>(outputBuffer);// 3. 逐帧处理:音量调节 → 混响效果for (unsigned long i = 0; i < framesPerBuffer; ++i) {// 步骤1:音量调节(原始数据 × 增益)float processedSample = inData[i] * ctx->volumeGain;// 步骤2:限制音量范围(防止溢出)if (processedSample > 1.0f) processedSample = 1.0f;if (processedSample < -1.0f) processedSample = -1.0f;// 步骤3:混响处理(延迟线回声)outData[i] = processedSample;  // 先暂存,混响会直接修改 outData}ctx->reverb.process(outData, framesPerBuffer);} else {// 若未处理,填充静音memset(outputBuffer, 0, framesPerBuffer * sizeof(float));}return ctx->isProcessing ? paContinue : paComplete;
}
全双工(同时录播)场景(麦克风输入→实时变调→播放)
#include <portaudio.h>
#include <iostream>
#include <cmath>// 简单变调效果器:通过插值实现音高调整
class PitchShifter {
private:float pitchRatio = 1.5f;  // 变调比例(1.5倍即升高半音)float readIndex = 0.0f;   // 读取索引(模拟非整数采样)public:void setPitchRatio(float ratio) { pitchRatio = ratio; }// 处理单帧音频,调整音高void process(float* audioData, unsigned long framesPerBuffer) {for (unsigned long i = 0; i < framesPerBuffer; ++i) {// 线性插值:模拟非整数采样点的取值size_t prevIndex = static_cast<size_t>(readIndex);size_t nextIndex = (prevIndex + 1) % framesPerBuffer;float fraction = readIndex - prevIndex;// 插值计算新样本audioData[i] = audioData[prevIndex] * (1.0f - fraction) + audioData[nextIndex] * fraction;// 更新读取索引(按变调比例步进)readIndex = fmod(readIndex + pitchRatio, framesPerBuffer);}}
};// 自定义上下文结构体:存储录制和播放的状态
struct DuplexContext {PitchShifter shifter;  // 变调效果器float* tempBuffer;     // 临时缓冲区(存储原始录制数据)unsigned long bufferSize;  // 缓冲区大小bool isDuplexRunning = true;  // 全双工运行状态
};// 全双工回调函数:同时录制和播放,实时变调
int DuplexCallback(const void *inputBuffer,void *outputBuffer,unsigned long framesPerBuffer,const PaStreamCallbackTimeInfo *timeInfo,PaStreamCallbackFlags statusFlags,void *userData
) {DuplexContext* ctx = reinterpret_cast<DuplexContext*>(userData);// 1. 处理状态标志(输入/输出错误)if (statusFlags & (paInputOverflow | paOutputUnderflow)) {std::cerr << "警告:全双工模式下缓冲区溢出/下溢!\n";}if (statusFlags & paError) {std::cerr << "错误:音频流异常,终止全双工!\n";return paAbort;}// 2. 核心逻辑:仅在运行状态下执行if (ctx->isDuplexRunning && inputBuffer != nullptr && outputBuffer != nullptr) {// 强转输入/输出数据为 float*const float* inData = reinterpret_cast<const float*>(inputBuffer);float* outData = reinterpret_cast<float*>(outputBuffer);// 3. 步骤1:录制原始数据到临时缓冲区memcpy(ctx->tempBuffer, inData, framesPerBuffer * sizeof(float));// 4. 步骤2:对临时缓冲区数据变调处理ctx->shifter.process(ctx->tempBuffer, framesPerBuffer);// 5. 步骤3:将处理后的数据写入输出缓冲区(播放)memcpy(outData, ctx->tempBuffer, framesPerBuffer * sizeof(float));} else {// 若停止,填充静音memset(outputBuffer, 0, framesPerBuffer * sizeof(float));}return ctx->isDuplexRunning ? paContinue : paComplete;
}
2.初始化 PortAudio 库

在使用任何 PortAudio 功能前,必须先初始化库,为后续的音频设备交互和流操作准备环境。

PaError err = Pa_Initialize();
if (err != paNoError) {std::cerr << "PortAudio 初始化失败: " << Pa_GetErrorText(err) << std::endl;return -1;  // 初始化失败,直接退出
}
3.配置音频流参数(PaStreamParameters

根据需求(录制 / 播放 / 全双工),配置输入 / 输出流的参数,包括:

  • 音频设备
  • 通道数
  • 采样格式
  • 延迟

示例:麦克风录制的输入参数配置

PaStreamParameters inputParams;
inputParams.device = Pa_GetDefaultInputDevice();  // 使用默认麦克风
inputParams.channelCount = 1;                    // 单声道
inputParams.sampleFormat = paFloat32;            // 32位浮点数格式
inputParams.suggestedLatency = Pa_GetDeviceInfo(inputParams.device)->defaultLowInputLatency;
inputParams.hostApiSpecificStreamInfo = NULL;    // 无特定宿主API信息
4.打开音频流(Pa_OpenStream

回调函数上下文结构体注册到音频流中,完成 “流 - 回调 - 数据” 的绑定。

PaStream* stream;
err = Pa_OpenStream(&stream,                // 输出:创建的音频流指针&inputParams,           // 输入参数(录制场景,如麦克风)NULL,                   // 输出参数(录制场景为 NULL)SAMPLE_RATE,            // 采样率(如 16000 Hz)FRAMES_PER_BUFFER,      // 每帧采样数(如 512)paClipOff,              // 禁用自动剪辑(避免数据失真)RecordCallback,         // 注册回调函数&recordCtx              // 传递上下文结构体(userData 参数)
);
if (err != paNoError) {std::cerr << "打开音频流失败: " << Pa_GetErrorText(err) << std::endl;Pa_Terminate();         // 初始化失败,终止 PortAudioreturn -1;
}
5.启动音频流(Pa_StartStream

启动后,PortAudio 会自动调用回调函数,开始音频的采集或播放。

err = Pa_StartStream(stream);
if (err != paNoError) {std::cerr << "启动音频流失败: " << Pa_GetErrorText(err) << std::endl;Pa_CloseStream(stream); // 关闭流Pa_Terminate();return -1;
}
6.运行音频逻辑(如等待录制结束、处理数据)

根据业务逻辑,让程序保持运行,直到音频任务完成。

示例:等待麦克风录制 5 秒后停止

std::this_thread::sleep_for(std::chrono::seconds(5));  // 等待 5 秒
recordCtx.isRecording = false;  // 修改上下文状态,让回调返回 paComplete
Pa_StopStream(stream);          // 停止流(等待回调结束)
7.释放资源(关闭流 + 终止 PortAudio)

音频任务结束后,必须释放资源,避免内存泄漏或设备占用。

Pa_CloseStream(stream);  // 关闭音频流
Pa_Terminate();          // 终止 PortAudio 库
8.完整Demo:
#include <portaudio.h>
#include <iostream>
#include <fstream>
#include <thread>// 录制上下文结构体
struct RecordContext {std::ofstream outFile;bool isRecording = true;
};// 回调函数(前文定义的 RecordCallback)
int RecordCallback(const void *inputBuffer,void *outputBuffer,unsigned long framesPerBuffer,const PaStreamCallbackTimeInfo *timeInfo,PaStreamCallbackFlags statusFlags,void *userData
);int main() {// 1. 初始化 PortAudioPaError err = Pa_Initialize();if (err != paNoError) {std::cerr << "Pa_Initialize 失败: " << Pa_GetErrorText(err) << std::endl;return -1;}// 2. 初始化上下文RecordContext recordCtx;recordCtx.outFile.open("output.pcm", std::ios::binary);if (!recordCtx.outFile.is_open()) {std::cerr << "打开输出文件失败" << std::endl;Pa_Terminate();return -1;}// 3. 配置输入参数PaStreamParameters inputParams;inputParams.device = Pa_GetDefaultInputDevice();inputParams.channelCount = 1;inputParams.sampleFormat = paFloat32;inputParams.suggestedLatency = Pa_GetDeviceInfo(inputParams.device)->defaultLowInputLatency;inputParams.hostApiSpecificStreamInfo = NULL;// 4. 打开音频流PaStream* stream;err = Pa_OpenStream(&stream,&inputParams,NULL,16000,         // 采样率 16kHz512,           // 每帧 512 采样paClipOff,RecordCallback,&recordCtx);if (err != paNoError) {std::cerr << "Pa_OpenStream 失败: " << Pa_GetErrorText(err) << std::endl;recordCtx.outFile.close();Pa_Terminate();return -1;}// 5. 启动流err = Pa_StartStream(stream);if (err != paNoError) {std::cerr << "Pa_StartStream 失败: " << Pa_GetErrorText(err) << std::endl;Pa_CloseStream(stream);recordCtx.outFile.close();Pa_Terminate();return -1;}// 6. 等待 5 秒后停止录制std::cout << "录制中... 5 秒后停止" << std::endl;std::this_thread::sleep_for(std::chrono::seconds(5));recordCtx.isRecording = false;Pa_StopStream(stream);// 7. 释放资源Pa_CloseStream(stream);recordCtx.outFile.close();Pa_Terminate();std::cout << "录制完成,文件已保存为 output.pcm" << std::endl;return 0;
}// 回调函数实现(前文定义的 RecordCallback)
int RecordCallback(const void *inputBuffer,void *outputBuffer,unsigned long framesPerBuffer,const PaStreamCallbackTimeInfo *timeInfo,PaStreamCallbackFlags statusFlags,void *userData
) {RecordContext* ctx = reinterpret_cast<RecordContext*>(userData);if (statusFlags & paInputOverflow) {std::cerr << "输入缓冲区溢出!" << std::endl;}if (ctx->isRecording && inputBuffer != nullptr) {const float* audioData = reinterpret_cast<const float*>(inputBuffer);ctx->outFile.write(reinterpret_cast<const char*>(audioData), framesPerBuffer * sizeof(float));}return ctx->isRecording ? paContinue : paComplete;
}

总结:核心流程是 “编写回调函数→ 初始化 → 配置参数 → 打开流 → 启动流 → 运行逻辑 → 释放资源”

👾👾👾...

http://www.dtcms.com/a/392713.html

相关文章:

  • Oracle根据日期进行查询
  • 【C#】C# 中 `ProcessStartInfo` 详解:启动外部进程
  • Python快速入门专业版(三十六):Python列表基础:创建、增删改查与常用方法(15+案例)
  • 微服务项目->在线oj系统(Java-Spring)----5.0
  • 【读书笔记】《鲁迅传》
  • Python 基础:列表、字符串、字典和元组一些简单八股
  • C++ 运算符重载:类内与类外重载详解
  • 【java】jsp被截断问题
  • 在Windows10 Edge浏览器里安装DeepSider大模型插件来免费使用gpt-4o、NanoBanana等AI大模型
  • 车联网网络安全:技术演进与守护智能出行
  • 网络原理-传输层补充1
  • Amber `rism1d` 深度解析与实战教程
  • vscode在断点旁边写expression让条件为true的时候才触发断点提高调试效率
  • 何时使用RESETLOGS
  • 分布式链路追踪关键指标实战:精准定位服务调用 “慢节点” 全指南(一)
  • vaapi硬解码性能评估
  • 第 N 个泰波那契数
  • 面试经典150题[037]:矩阵置零(LeetCode 73)
  • mysql 简单操作
  • Maven:Java项目的自动化构建工具
  • 嵌入式硬件工程师每日提问
  • 2025年AI写小说工具测评:AI写作软件大比拼
  • UL 2808 2020北美能源监测设备安全标准介绍
  • 刷题日记0920
  • 论文复现中的TODO
  • 什么是双向SSL/TLS(mTLS)?深入理解双向认证的守护神
  • app封装是什么意思
  • 什么是机房IP?有什么缺点
  • 【读书笔记】《谣言》
  • golang基础语法(一)变量