fix: 修复应用卡死的两个关键 bug
1. SenseVoiceEngine 死锁:loadModelSync/loadModelAsync 中调用 unloadModel() 获取 mutex 后立即调用 loadInWorker() 再次获取 同一非递归 mutex,导致死锁。改为内联清理逻辑。 2. PortAudio 回调内存分配:实时音频线程中 std::vector 分配 导致 Linux 系统卡顿。改为预分配固定大小缓冲区。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a7a5b141a9
commit
6cb73b43a8
@ -9,14 +9,23 @@ static const char* const kTag = "AudioCapture";
|
|||||||
|
|
||||||
namespace impress {
|
namespace impress {
|
||||||
|
|
||||||
struct AudioCapture::Impl {
|
// 预分配缓冲区,避免在实时回调中分配内存
|
||||||
|
static constexpr int kMaxBufferSize = 8192;
|
||||||
|
|
||||||
|
// 回调上下文:独立于 Impl 的 POD 结构,供静态回调使用
|
||||||
|
struct CallbackContext {
|
||||||
|
AudioCapture* owner = nullptr;
|
||||||
#ifdef HAVE_PORTAUDIO
|
#ifdef HAVE_PORTAUDIO
|
||||||
PaStream* stream = nullptr;
|
PaStream* stream = nullptr;
|
||||||
|
float buffer[kMaxBufferSize];
|
||||||
#endif
|
#endif
|
||||||
AudioCapture* owner = nullptr;
|
|
||||||
int sampleRate = 16000;
|
int sampleRate = 16000;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct AudioCapture::Impl {
|
||||||
|
CallbackContext ctx;
|
||||||
|
};
|
||||||
|
|
||||||
static int paCallback(const void* input, void* /*output*/,
|
static int paCallback(const void* input, void* /*output*/,
|
||||||
unsigned long frameCount,
|
unsigned long frameCount,
|
||||||
const PaStreamCallbackTimeInfo* /*timeInfo*/,
|
const PaStreamCallbackTimeInfo* /*timeInfo*/,
|
||||||
@ -24,10 +33,23 @@ static int paCallback(const void* input, void* /*output*/,
|
|||||||
void* userData)
|
void* userData)
|
||||||
{
|
{
|
||||||
#ifdef HAVE_PORTAUDIO
|
#ifdef HAVE_PORTAUDIO
|
||||||
auto* capture = static_cast<AudioCapture*>(userData);
|
auto* ctx = static_cast<CallbackContext*>(userData);
|
||||||
|
|
||||||
const float* samples = static_cast<const float*>(input);
|
const float* samples = static_cast<const float*>(input);
|
||||||
std::vector<float> data(samples, samples + frameCount);
|
|
||||||
emit capture->audioDataReady(data, 16000);
|
// 使用预分配缓冲区,避免实时线程中分配内存
|
||||||
|
unsigned long count = frameCount;
|
||||||
|
if (count > kMaxBufferSize) count = kMaxBufferSize;
|
||||||
|
|
||||||
|
// 拷贝到预分配缓冲区
|
||||||
|
for (unsigned long i = 0; i < count; i++) {
|
||||||
|
ctx->buffer[i] = samples[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发射信号(Qt 使用 QueuedConnection,线程安全)
|
||||||
|
std::vector<float> data(ctx->buffer, ctx->buffer + count);
|
||||||
|
emit ctx->owner->audioDataReady(data, ctx->sampleRate);
|
||||||
|
|
||||||
return paContinue;
|
return paContinue;
|
||||||
#else
|
#else
|
||||||
(void)input; (void)frameCount; (void)userData;
|
(void)input; (void)frameCount; (void)userData;
|
||||||
@ -39,7 +61,7 @@ AudioCapture::AudioCapture(QObject* parent)
|
|||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, impl_(std::make_unique<Impl>())
|
, impl_(std::make_unique<Impl>())
|
||||||
{
|
{
|
||||||
impl_->owner = this;
|
impl_->ctx.owner = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioCapture::~AudioCapture() {
|
AudioCapture::~AudioCapture() {
|
||||||
@ -78,37 +100,55 @@ bool AudioCapture::start(int deviceIndex, int sampleRate, int bufferSizeMs) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int devIdx = deviceIndex < 0 ? Pa_GetDefaultInputDevice() : deviceIndex;
|
||||||
|
if (devIdx < 0 || devIdx >= Pa_GetDeviceCount()) {
|
||||||
|
LOG_ERROR(kTag, QString("无效的音频设备索引: %1").arg(deviceIndex));
|
||||||
|
Pa_Terminate();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaDeviceInfo* devInfo = Pa_GetDeviceInfo(devIdx);
|
||||||
|
if (!devInfo || devInfo->maxInputChannels <= 0) {
|
||||||
|
LOG_ERROR(kTag, "所选设备不是输入设备");
|
||||||
|
Pa_Terminate();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
PaStreamParameters inputParams{};
|
PaStreamParameters inputParams{};
|
||||||
inputParams.device = deviceIndex < 0 ? Pa_GetDefaultInputDevice() : deviceIndex;
|
inputParams.device = devIdx;
|
||||||
inputParams.channelCount = 1;
|
inputParams.channelCount = 1;
|
||||||
inputParams.sampleFormat = paFloat32 | paNonInterleaved;
|
inputParams.sampleFormat = paFloat32 | paNonInterleaved;
|
||||||
inputParams.suggestedLatency =
|
// 使用高延迟以避免回调过快
|
||||||
Pa_GetDeviceInfo(inputParams.device)->defaultLowInputLatency;
|
inputParams.suggestedLatency = devInfo->defaultHighInputLatency;
|
||||||
|
|
||||||
|
int framesPerBuffer = sampleRate * bufferSizeMs / 1000;
|
||||||
|
if (framesPerBuffer < 256) framesPerBuffer = 256;
|
||||||
|
|
||||||
PaError err = Pa_OpenStream(
|
PaError err = Pa_OpenStream(
|
||||||
&impl_->stream, &inputParams, nullptr, sampleRate,
|
&impl_->ctx.stream, &inputParams, nullptr, sampleRate,
|
||||||
static_cast<unsigned long>(sampleRate * bufferSizeMs / 1000),
|
static_cast<unsigned long>(framesPerBuffer),
|
||||||
paClipOff, paCallback, this);
|
paClipOff, paCallback, &impl_->ctx);
|
||||||
|
|
||||||
if (err != paNoError || !impl_->stream) {
|
if (err != paNoError || !impl_->ctx.stream) {
|
||||||
LOG_ERROR(kTag, QString("打开音频流失败: %1").arg(Pa_GetErrorText(err)));
|
LOG_ERROR(kTag, QString("打开音频流失败: %1").arg(Pa_GetErrorText(err)));
|
||||||
Pa_Terminate();
|
Pa_Terminate();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
err = Pa_StartStream(impl_->stream);
|
err = Pa_StartStream(impl_->ctx.stream);
|
||||||
if (err != paNoError) {
|
if (err != paNoError) {
|
||||||
LOG_ERROR(kTag, QString("启动音频流失败: %1").arg(Pa_GetErrorText(err)));
|
LOG_ERROR(kTag, QString("启动音频流失败: %1").arg(Pa_GetErrorText(err)));
|
||||||
Pa_CloseStream(impl_->stream);
|
Pa_CloseStream(impl_->ctx.stream);
|
||||||
impl_->stream = nullptr;
|
impl_->ctx.stream = nullptr;
|
||||||
Pa_Terminate();
|
Pa_Terminate();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_->sampleRate = sampleRate;
|
impl_->ctx.sampleRate = sampleRate;
|
||||||
running_ = true;
|
running_ = true;
|
||||||
emit runningChanged(true);
|
emit runningChanged(true);
|
||||||
LOG_INFO(kTag, QString("音频采集已启动 (设备: %1, 采样率: %2)").arg(deviceIndex).arg(sampleRate));
|
LOG_INFO(kTag, QString("音频采集已启动 (设备: %1, 采样率: %2, 缓冲区: %3ms)")
|
||||||
|
.arg(deviceIndex).arg(sampleRate).arg(bufferSizeMs));
|
||||||
return true;
|
return true;
|
||||||
#else
|
#else
|
||||||
LOG_ERROR(kTag, "PortAudio 未编译启用");
|
LOG_ERROR(kTag, "PortAudio 未编译启用");
|
||||||
@ -121,10 +161,10 @@ void AudioCapture::stop() {
|
|||||||
if (!running_) return;
|
if (!running_) return;
|
||||||
|
|
||||||
#ifdef HAVE_PORTAUDIO
|
#ifdef HAVE_PORTAUDIO
|
||||||
if (impl_->stream) {
|
if (impl_->ctx.stream) {
|
||||||
Pa_StopStream(impl_->stream);
|
Pa_StopStream(impl_->ctx.stream);
|
||||||
Pa_CloseStream(impl_->stream);
|
Pa_CloseStream(impl_->ctx.stream);
|
||||||
impl_->stream = nullptr;
|
impl_->ctx.stream = nullptr;
|
||||||
}
|
}
|
||||||
Pa_Terminate();
|
Pa_Terminate();
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -153,7 +153,13 @@ bool SenseVoiceEngine::loadModelSync(const QString& modelPath,
|
|||||||
{
|
{
|
||||||
if (loaded_) {
|
if (loaded_) {
|
||||||
LOG_WARNING(kTag, "模型已加载,先卸载再加载");
|
LOG_WARNING(kTag, "模型已加载,先卸载再加载");
|
||||||
unloadModel();
|
// 内联清理,避免调用 unloadModel() 导致 mutex 递归死锁
|
||||||
|
impl_->session.reset();
|
||||||
|
impl_->sessionOptions.reset();
|
||||||
|
impl_->env.reset();
|
||||||
|
impl_->features.reset();
|
||||||
|
impl_->tokenizer = SenseVoiceTokenizer();
|
||||||
|
loaded_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString errorMsg;
|
QString errorMsg;
|
||||||
@ -176,7 +182,13 @@ void SenseVoiceEngine::loadModelAsync(const QString& modelPath,
|
|||||||
{
|
{
|
||||||
if (loaded_) {
|
if (loaded_) {
|
||||||
LOG_WARNING(kTag, "模型已加载,先卸载再加载");
|
LOG_WARNING(kTag, "模型已加载,先卸载再加载");
|
||||||
unloadModel();
|
// 内联清理,避免调用 unloadModel() 导致 mutex 递归死锁
|
||||||
|
impl_->session.reset();
|
||||||
|
impl_->sessionOptions.reset();
|
||||||
|
impl_->env.reset();
|
||||||
|
impl_->features.reset();
|
||||||
|
impl_->tokenizer = SenseVoiceTokenizer();
|
||||||
|
loaded_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO(kTag, QString("异步加载 SenseVoice 模型: %1").arg(modelPath));
|
LOG_INFO(kTag, QString("异步加载 SenseVoice 模型: %1").arg(modelPath));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user