#include "audio_capture.h" #include "utils/logger.h" #ifdef HAVE_PORTAUDIO #include #include #endif static const char* const kTag = "AudioCapture"; namespace impress { // 预分配缓冲区,避免在实时回调中分配内存 static constexpr int kMaxBufferSize = 8192; #ifdef HAVE_PORTAUDIO // 全局 PortAudio 初始化状态 static bool gPaInitialized = false; /** 安全初始化 PortAudio(多次调用不报错) */ static bool ensurePaInitialized() { if (gPaInitialized) return true; if (Pa_Initialize() == paNoError) { gPaInitialized = true; return true; } return false; } /** 安全终止 PortAudio */ static void safePaTerminate() { if (gPaInitialized) { Pa_Terminate(); gPaInitialized = false; } } #endif // 回调上下文:独立于 Impl 的 POD 结构,供静态回调使用 struct CallbackContext { AudioCapture* owner = nullptr; #ifdef HAVE_PORTAUDIO PaStream* stream = nullptr; float buffer[kMaxBufferSize]; #endif int sampleRate = 16000; // 音频电平诊断 float peakLevel = 0.0f; double sumSquares = 0.0; int sampleCount = 0; }; struct AudioCapture::Impl { CallbackContext ctx; }; #ifdef HAVE_PORTAUDIO static int paCallback(const void* input, void* /*output*/, unsigned long frameCount, const PaStreamCallbackTimeInfo* /*timeInfo*/, PaStreamCallbackFlags /*statusFlags*/, void* userData) { auto* ctx = static_cast(userData); const float* samples = static_cast(input); // 使用预分配缓冲区,避免实时线程中分配内存 unsigned long count = frameCount; if (count > kMaxBufferSize) count = kMaxBufferSize; // 拷贝到预分配缓冲区 + 计算音频电平 float peak = 0.0f; double ss = 0.0; for (unsigned long i = 0; i < count; i++) { float s = samples[i]; ctx->buffer[i] = s; float absS = std::fabs(s); if (absS > peak) peak = absS; ss += s * s; } // 更新诊断数据 if (peak > ctx->peakLevel) ctx->peakLevel = peak; ctx->sumSquares += ss; ctx->sampleCount += count; // 发射信号(Qt 使用 QueuedConnection,线程安全) std::vector data(ctx->buffer, ctx->buffer + count); emit ctx->owner->audioDataReady(data, ctx->sampleRate); return paContinue; } #else // 占位回调(无 PortAudio 时不使用) static int paCallbackStub(const void*, void*, unsigned long, const int*, int, void*) { return 0; } #endif AudioCapture::AudioCapture(QObject* parent) : QObject(parent) , impl_(std::make_unique()) { impl_->ctx.owner = this; } AudioCapture::~AudioCapture() { stopAndClose(); } QStringList AudioCapture::getDeviceList() { QStringList devices; #ifdef HAVE_PORTAUDIO devices << "默认设备"; if (!ensurePaInitialized()) { LOG_ERROR(kTag, "PortAudio 初始化失败"); return devices; } int count = Pa_GetDeviceCount(); for (int i = 0; i < count; ++i) { const PaDeviceInfo* info = Pa_GetDeviceInfo(i); if (info && info->maxInputChannels > 0) { const PaHostApiInfo* hostApi = Pa_GetHostApiInfo(info->hostApi); QString hostApiName = hostApi ? hostApi->name : "未知"; devices << QString("[%1] %2 (CH:%3, SR:%4, %5)") .arg(i).arg(info->name).arg(info->maxInputChannels) .arg(info->defaultSampleRate).arg(hostApiName); } } #else devices << "PortAudio 未启用(占位设备)"; LOG_WARNING(kTag, "PortAudio 未编译启用,设备列表为占位"); #endif return devices; } int AudioCapture::getDefaultDeviceIndex() { #ifdef HAVE_PORTAUDIO if (!ensurePaInitialized()) return -1; return Pa_GetDefaultInputDevice(); #else return -1; #endif } bool AudioCapture::start(int deviceIndex, int sampleRate, int bufferSizeMs) { if (running_) { LOG_WARNING(kTag, "已在运行中"); return false; } #ifdef HAVE_PORTAUDIO if (!ensurePaInitialized()) { LOG_ERROR(kTag, "PortAudio 初始化失败"); return false; } // 如果流已打开且参数匹配,直接启动(跳过耗时的 OpenStream) if (streamOpen_ && impl_->ctx.stream && deviceIndex == lastDeviceIndex_ && sampleRate == lastSampleRate_ && bufferSizeMs == lastBufferSizeMs_) { LOG_DEBUG(kTag, "复用已打开的音频流,跳过 OpenStream"); } else { // 关闭旧流(如果存在) if (impl_->ctx.stream) { Pa_CloseStream(impl_->ctx.stream); impl_->ctx.stream = nullptr; streamOpen_ = false; } // 枚举所有 Host API 用于诊断 LOG_DEBUG(kTag, QString("Host API 数量: %1").arg(Pa_GetHostApiCount())); for (int i = 0; i < Pa_GetHostApiCount(); i++) { const PaHostApiInfo* api = Pa_GetHostApiInfo(i); if (api) { LOG_DEBUG(kTag, QString(" Host API #%1: %2 (设备数: %3)") .arg(i).arg(api->name).arg(api->deviceCount)); } } // 选择设备 int devIdx = deviceIndex < 0 ? Pa_GetDefaultInputDevice() : deviceIndex; if (devIdx < 0 || devIdx >= Pa_GetDeviceCount()) { LOG_ERROR(kTag, QString("无效的音频设备索引: %1 (默认设备: %2)") .arg(deviceIndex).arg(Pa_GetDefaultInputDevice())); return false; } const PaDeviceInfo* devInfo = Pa_GetDeviceInfo(devIdx); if (!devInfo || devInfo->maxInputChannels <= 0) { LOG_ERROR(kTag, "所选设备不是输入设备"); return false; } const PaHostApiInfo* hostApi = Pa_GetHostApiInfo(devInfo->hostApi); LOG_INFO(kTag, QString("=== 音频设备诊断 ===")); LOG_INFO(kTag, QString(" 设备 #%1: %2").arg(devIdx).arg(devInfo->name)); LOG_INFO(kTag, QString(" Host API: %1").arg(hostApi ? hostApi->name : "未知")); LOG_INFO(kTag, QString(" 最大输入通道: %1").arg(devInfo->maxInputChannels)); LOG_INFO(kTag, QString(" 设备默认采样率: %1 Hz").arg(devInfo->defaultSampleRate, 0, 'f', 0)); LOG_INFO(kTag, QString(" 请求采样率: %1 Hz").arg(sampleRate)); LOG_INFO(kTag, QString(" 采样格式: paFloat32 | paNonInterleaved")); LOG_INFO(kTag, QString(" 请求通道数: 1 (mono)")); LOG_INFO(kTag, QString(" 缓冲区: %1ms (%2 帧)").arg(bufferSizeMs) .arg(sampleRate * bufferSizeMs / 1000)); // 检查是否可能选错设备(名称包含 monitor 的通常是回环设备) QString devName = QString(devInfo->name).toLower(); if (devName.contains("monitor") || devName.contains("output")) { LOG_WARNING(kTag, "⚠️ 当前设备名称包含 'monitor' 或 'output'," "这可能是扬声器回环设备而非麦克风!如果录制的是噪音,请在设置中选择正确的麦克风设备。"); } PaStreamParameters inputParams{}; inputParams.device = devIdx; inputParams.channelCount = 1; inputParams.sampleFormat = paFloat32; inputParams.suggestedLatency = devInfo->defaultHighInputLatency; int framesPerBuffer = sampleRate * bufferSizeMs / 1000; if (framesPerBuffer < 256) framesPerBuffer = 256; PaError err = Pa_OpenStream( &impl_->ctx.stream, &inputParams, nullptr, sampleRate, static_cast(framesPerBuffer), paClipOff, paCallback, &impl_->ctx); if (err != paNoError || !impl_->ctx.stream) { LOG_ERROR(kTag, QString("打开音频流失败: %1").arg(Pa_GetErrorText(err))); return false; } streamOpen_ = true; lastDeviceIndex_ = deviceIndex; lastSampleRate_ = sampleRate; lastBufferSizeMs_ = bufferSizeMs; } PaError err = Pa_StartStream(impl_->ctx.stream); if (err != paNoError) { LOG_ERROR(kTag, QString("启动音频流失败: %1").arg(Pa_GetErrorText(err))); return false; } // 重置诊断计数器 impl_->ctx.peakLevel = 0.0f; impl_->ctx.sumSquares = 0.0; impl_->ctx.sampleCount = 0; impl_->ctx.sampleRate = sampleRate; running_ = true; emit runningChanged(true); LOG_INFO(kTag, QString("音频采集已启动 (设备: %1, 采样率: %2, 缓冲区: %3ms)") .arg(deviceIndex).arg(sampleRate).arg(bufferSizeMs)); return true; #else (void)deviceIndex; (void)sampleRate; (void)bufferSizeMs; LOG_ERROR(kTag, "PortAudio 未编译启用,无法启动采集"); emit error("PortAudio 未编译启用,请在 third_party/portaudio/ 中部署后重新编译"); return false; #endif } void AudioCapture::stop() { if (!running_) return; #ifdef HAVE_PORTAUDIO // 输出音频电平诊断信息 if (impl_->ctx.sampleCount > 0) { double rms = std::sqrt(impl_->ctx.sumSquares / impl_->ctx.sampleCount); LOG_INFO(kTag, QString("=== 音频电平诊断 ===")); LOG_INFO(kTag, QString(" 总样本数: %1").arg(impl_->ctx.sampleCount)); LOG_INFO(kTag, QString(" RMS 电平: %1").arg(rms, 0, 'f', 6)); LOG_INFO(kTag, QString(" 峰值: %1").arg(impl_->ctx.peakLevel, 0, 'f', 4)); if (rms < 0.001) { LOG_WARNING(kTag, "⚠️ 音频信号过弱,可能是静音或设备未正确采集"); } else if (rms > 0.5) { LOG_WARNING(kTag, "⚠️ 音频信号过强,可能存在削波"); } else if (impl_->ctx.peakLevel > 0.9f) { LOG_INFO(kTag, " 信号幅度正常"); } else { LOG_WARNING(kTag, "⚠️ 信号幅度偏低,请检查设备选择"); } } // 只停止流,不关闭 — 下次 start() 可快速复用 if (impl_->ctx.stream) { Pa_StopStream(impl_->ctx.stream); // 不调用 Pa_CloseStream,保留流以便下次快速启动 } #endif running_ = false; emit runningChanged(false); LOG_INFO(kTag, "音频采集已停止(流保留,下次启动更快)"); } void AudioCapture::stopAndClose() { #ifdef HAVE_PORTAUDIO if (running_ && impl_->ctx.stream) { Pa_StopStream(impl_->ctx.stream); } if (impl_->ctx.stream) { Pa_CloseStream(impl_->ctx.stream); impl_->ctx.stream = nullptr; } safePaTerminate(); streamOpen_ = false; #endif running_ = false; emit runningChanged(false); LOG_INFO(kTag, "音频采集已停止,流已关闭"); } } // namespace impress