#include "SpeechTestMainWindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include SpeechTestMainWindow::SpeechTestMainWindow(QWidget* parent) : QMainWindow(parent) { // 创建管理器 asrManager = new ASRManager(this); ttsManager = new TTSManager(this); kwsManager = new KWSManager(this); setupUI(); setupMenuBar(); createOutputDirectories(); connectSignals(); // 初始化模型 bool asrOk = asrManager->initialize(); bool ttsOk = ttsManager->initialize(); bool kwsOk = kwsManager->initialize(); // 尝试初始化在线识别器(当前会失败) asrManager->initializeOnlineRecognizer(); setWindowTitle("QSmartAssistant 语音测试工具"); setMinimumSize(1200, 800); // 增加最小尺寸以适应网格布局 resize(1400, 900); // 增加默认尺寸 // 根据在线识别器状态更新麦克风按钮 if (asrManager->isOnlineInitialized()) { micRecordBtn->setEnabled(true); micRecordBtn->setText("开始麦克风识别"); micRecordBtn->setStyleSheet("QPushButton { background-color: #FF5722; color: white; font-weight: bold; }"); } else { micRecordBtn->setEnabled(false); micRecordBtn->setText("麦克风识别(模型未加载)"); micRecordBtn->setStyleSheet("QPushButton { background-color: #9E9E9E; color: white; font-weight: bold; }"); } // 更新状态栏 if (asrOk && ttsOk && kwsOk) { QString modelInfo = ttsManager->isMultilingualModel() ? "(支持中英文混合)" : "(仅支持中文)"; QString micInfo = asrManager->isOnlineInitialized() ? ",支持实时识别" : ",麦克风识别不可用"; QString kwsInfo = kwsOk ? ",支持语音唤醒" : ",语音唤醒不可用"; statusBar()->showMessage("模型初始化成功,就绪 " + modelInfo + micInfo + kwsInfo); } else { statusBar()->showMessage("模型初始化失败"); if (!asrOk) qDebug() << "离线ASR初始化失败"; if (!ttsOk) qDebug() << "TTS初始化失败"; if (!kwsOk) qDebug() << "KWS初始化失败"; } } SpeechTestMainWindow::~SpeechTestMainWindow() { // 停止麦克风识别 if (isRecording) { stopMicRecognition(); } // 停止录音 if (isRecordingWav) { stopRecording(); } // 清理音频输入 if (audioSource) { audioSource->stop(); delete audioSource; audioSource = nullptr; } if (audioTimer) { audioTimer->stop(); delete audioTimer; audioTimer = nullptr; } // 清理录音资源 if (recordAudioSource) { recordAudioSource->stop(); delete recordAudioSource; recordAudioSource = nullptr; } if (recordTimer) { recordTimer->stop(); delete recordTimer; recordTimer = nullptr; } // 清理语音唤醒资源 if (kwsAudioSource) { kwsAudioSource->stop(); delete kwsAudioSource; kwsAudioSource = nullptr; } if (kwsTimer) { kwsTimer->stop(); delete kwsTimer; kwsTimer = nullptr; } } void SpeechTestMainWindow::setupUI() { auto* centralWidget = new QWidget(this); setCentralWidget(centralWidget); auto* mainLayout = new QVBoxLayout(centralWidget); // 创建网格布局(两行两列) auto* gridLayout = new QGridLayout(); gridLayout->setSpacing(10); gridLayout->setContentsMargins(10, 10, 10, 10); // 设置行列拉伸策略,让各模块均匀分配空间 gridLayout->setRowStretch(0, 1); // 第一行拉伸因子为1 gridLayout->setRowStretch(1, 1); // 第二行拉伸因子为1 gridLayout->setColumnStretch(0, 1); // 第一列拉伸因子为1 gridLayout->setColumnStretch(1, 1); // 第二列拉伸因子为1 // 创建一个容器widget来包含网格布局 auto* gridWidget = new QWidget(this); gridWidget->setLayout(gridLayout); mainLayout->addWidget(gridWidget); // ASR部分 auto* asrGroup = new QGroupBox("语音识别 (ASR)", this); auto* asrLayout = new QVBoxLayout(asrGroup); // 文件选择 auto* fileLayout = new QHBoxLayout(); filePathEdit = new QLineEdit(this); filePathEdit->setPlaceholderText("选择WAV音频文件..."); auto* browseBtn = new QPushButton("浏览", this); browseBtn->setObjectName("browseBtn"); auto* recognizeBtn = new QPushButton("开始识别", this); recognizeBtn->setObjectName("recognizeBtn"); recognizeBtn->setStyleSheet("QPushButton { background-color: #4CAF50; color: white; font-weight: bold; }"); fileLayout->addWidget(new QLabel("音频文件:", this)); fileLayout->addWidget(filePathEdit, 1); fileLayout->addWidget(browseBtn); fileLayout->addWidget(recognizeBtn); asrLayout->addLayout(fileLayout); // 麦克风识别控件 auto* micLayout = new QHBoxLayout(); micRecordBtn = new QPushButton("开始麦克风识别", this); micRecordBtn->setStyleSheet("QPushButton { background-color: #FF5722; color: white; font-weight: bold; }"); micStopBtn = new QPushButton("停止识别", this); micStopBtn->setStyleSheet("QPushButton { background-color: #9E9E9E; color: white; font-weight: bold; }"); micStopBtn->setEnabled(false); micLayout->addWidget(new QLabel("实时识别:", this)); micLayout->addStretch(); micLayout->addWidget(micRecordBtn); micLayout->addWidget(micStopBtn); asrLayout->addLayout(micLayout); // 识别结果 asrResultEdit = new QTextEdit(this); asrResultEdit->setPlaceholderText("识别结果将显示在这里..."); asrResultEdit->setMinimumHeight(100); asrResultEdit->setMaximumHeight(200); asrLayout->addWidget(new QLabel("识别结果:", this)); asrLayout->addWidget(asrResultEdit); // 将ASR组件添加到网格布局的第一行第一列 gridLayout->addWidget(asrGroup, 0, 0); // TTS部分 auto* ttsGroup = new QGroupBox("文字转语音 (TTS)", this); auto* ttsLayout = new QVBoxLayout(ttsGroup); // 文本输入 ttsTextEdit = new QTextEdit(this); ttsTextEdit->setPlaceholderText("请输入要合成的文本(支持中英文混合)..."); ttsTextEdit->setMinimumHeight(80); ttsTextEdit->setMaximumHeight(120); ttsLayout->addWidget(new QLabel("输入文本:", this)); ttsLayout->addWidget(ttsTextEdit); // TTS设置 auto* ttsSettingsLayout = new QHBoxLayout(); speakerIdSpinBox = new QSpinBox(this); speakerIdSpinBox->setRange(0, 100); speakerIdSpinBox->setValue(0); auto* synthesizeBtn = new QPushButton("开始合成", this); synthesizeBtn->setObjectName("synthesizeBtn"); synthesizeBtn->setStyleSheet("QPushButton { background-color: #2196F3; color: white; font-weight: bold; }"); ttsSettingsLayout->addWidget(new QLabel("说话人ID:", this)); ttsSettingsLayout->addWidget(speakerIdSpinBox); ttsSettingsLayout->addStretch(); ttsSettingsLayout->addWidget(synthesizeBtn); ttsLayout->addLayout(ttsSettingsLayout); // TTS结果 ttsResultEdit = new QTextEdit(this); ttsResultEdit->setPlaceholderText("合成结果将显示在这里..."); ttsResultEdit->setMinimumHeight(80); ttsResultEdit->setMaximumHeight(120); ttsLayout->addWidget(new QLabel("合成结果:", this)); ttsLayout->addWidget(ttsResultEdit); // 将TTS组件添加到网格布局的第一行第二列 gridLayout->addWidget(ttsGroup, 0, 1); // 录音功能部分 auto* recordGroup = new QGroupBox("麦克风录音", this); auto* recordLayout = new QVBoxLayout(recordGroup); // 录音设置区域(设备参数) auto* recordSettingsGroup = new QGroupBox("录音设置(设备参数)", this); auto* recordSettingsLayout = new QHBoxLayout(recordSettingsGroup); // 录音采样率设置 recordSampleRateComboBox = new QComboBox(this); recordSampleRateComboBox->addItem("自动检测最佳", -1); recordSampleRateComboBox->addItem("48000 Hz (专业)", 48000); recordSampleRateComboBox->addItem("44100 Hz (CD质量)", 44100); recordSampleRateComboBox->addItem("22050 Hz", 22050); recordSampleRateComboBox->addItem("16000 Hz", 16000); recordSampleRateComboBox->setCurrentIndex(0); // 默认自动检测 recordSampleRateComboBox->setToolTip("选择录音时使用的采样率,自动检测会选择设备支持的最佳格式"); // 录音声道设置 recordChannelComboBox = new QComboBox(this); recordChannelComboBox->addItem("自动检测最佳", -1); recordChannelComboBox->addItem("立体声 (Stereo)", 2); recordChannelComboBox->addItem("单声道 (Mono)", 1); recordChannelComboBox->setCurrentIndex(0); // 默认自动检测 recordChannelComboBox->setToolTip("选择录音时使用的声道数,自动检测会选择设备支持的最佳格式"); recordSettingsLayout->addWidget(new QLabel("录音采样率:", this)); recordSettingsLayout->addWidget(recordSampleRateComboBox); recordSettingsLayout->addWidget(new QLabel("录音声道:", this)); recordSettingsLayout->addWidget(recordChannelComboBox); recordSettingsLayout->addStretch(); recordLayout->addWidget(recordSettingsGroup); // 输出设置区域(保存格式) auto* outputSettingsGroup = new QGroupBox("输出设置(保存格式)", this); auto* outputSettingsLayout = new QHBoxLayout(outputSettingsGroup); // 输出采样率设置 outputSampleRateComboBox = new QComboBox(this); outputSampleRateComboBox->addItem("8000 Hz", 8000); outputSampleRateComboBox->addItem("16000 Hz (语音识别)", 16000); outputSampleRateComboBox->addItem("22050 Hz", 22050); outputSampleRateComboBox->addItem("44100 Hz (CD质量)", 44100); outputSampleRateComboBox->addItem("48000 Hz (专业)", 48000); outputSampleRateComboBox->setCurrentIndex(1); // 默认选择16000 Hz outputSampleRateComboBox->setToolTip("选择最终保存文件的采样率"); // 输出声道设置 outputChannelComboBox = new QComboBox(this); outputChannelComboBox->addItem("单声道 (Mono)", 1); outputChannelComboBox->addItem("立体声 (Stereo)", 2); outputChannelComboBox->setCurrentIndex(0); // 默认选择单声道 outputChannelComboBox->setToolTip("选择最终保存文件的声道数"); // 添加预设配置按钮 auto* presetBtn = new QPushButton("预设", this); presetBtn->setToolTip("选择常用输出预设配置"); presetBtn->setMaximumWidth(60); // 连接预设按钮信号 connect(presetBtn, &QPushButton::clicked, this, [this, presetBtn]() { QMenu* presetMenu = new QMenu(this); QAction* voiceAction = presetMenu->addAction("🎤 语音识别 (16kHz 单声道)"); connect(voiceAction, &QAction::triggered, this, [this]() { outputSampleRateComboBox->setCurrentIndex(1); // 16000 Hz outputChannelComboBox->setCurrentIndex(0); // 单声道 }); QAction* musicAction = presetMenu->addAction("🎵 音乐保存 (44.1kHz 立体声)"); connect(musicAction, &QAction::triggered, this, [this]() { outputSampleRateComboBox->setCurrentIndex(3); // 44100 Hz outputChannelComboBox->setCurrentIndex(1); // 立体声 }); QAction* professionalAction = presetMenu->addAction("🎙️ 专业保存 (48kHz 立体声)"); connect(professionalAction, &QAction::triggered, this, [this]() { outputSampleRateComboBox->setCurrentIndex(4); // 48000 Hz outputChannelComboBox->setCurrentIndex(1); // 立体声 }); QAction* compactAction = presetMenu->addAction("📱 紧凑保存 (22kHz 单声道)"); connect(compactAction, &QAction::triggered, this, [this]() { outputSampleRateComboBox->setCurrentIndex(2); // 22050 Hz outputChannelComboBox->setCurrentIndex(0); // 单声道 }); presetMenu->exec(presetBtn->mapToGlobal(QPoint(0, presetBtn->height()))); presetMenu->deleteLater(); }); outputSettingsLayout->addWidget(new QLabel("输出采样率:", this)); outputSettingsLayout->addWidget(outputSampleRateComboBox); outputSettingsLayout->addWidget(new QLabel("输出声道:", this)); outputSettingsLayout->addWidget(outputChannelComboBox); outputSettingsLayout->addWidget(presetBtn); // 添加文件大小预估标签 auto* fileSizeLabel = new QLabel(this); fileSizeLabel->setStyleSheet("QLabel { color: #888; font-size: 10px; }"); fileSizeLabel->setObjectName("fileSizeLabel"); // 连接设置变化信号来更新文件大小预估 auto updateFileSizeEstimate = [this, fileSizeLabel]() { int sampleRate = outputSampleRateComboBox->currentData().toInt(); int channels = outputChannelComboBox->currentData().toInt(); // 计算每秒的字节数 (采样率 × 声道数 × 2字节/样本) int bytesPerSecond = sampleRate * channels * 2; double mbPerMinute = (bytesPerSecond * 60.0) / (1024.0 * 1024.0); QString sizeText = QString("预估输出文件大小: ~%1 MB/分钟").arg(mbPerMinute, 0, 'f', 1); fileSizeLabel->setText(sizeText); }; connect(outputSampleRateComboBox, QOverload::of(&QComboBox::currentIndexChanged), updateFileSizeEstimate); connect(outputChannelComboBox, QOverload::of(&QComboBox::currentIndexChanged), updateFileSizeEstimate); // 初始计算 updateFileSizeEstimate(); outputSettingsLayout->addWidget(fileSizeLabel); outputSettingsLayout->addStretch(); recordLayout->addWidget(outputSettingsGroup); // 录音控制按钮 auto* recordControlLayout = new QHBoxLayout(); recordBtn = new QPushButton("开始录音", this); recordBtn->setStyleSheet("QPushButton { background-color: #E91E63; color: white; font-weight: bold; }"); recordStopBtn = new QPushButton("停止录音", this); recordStopBtn->setStyleSheet("QPushButton { background-color: #9E9E9E; color: white; font-weight: bold; }"); recordStopBtn->setEnabled(false); recordControlLayout->addWidget(new QLabel("WAV录音:", this)); recordControlLayout->addStretch(); recordControlLayout->addWidget(recordBtn); recordControlLayout->addWidget(recordStopBtn); recordLayout->addLayout(recordControlLayout); // 录音结果显示 recordResultEdit = new QTextEdit(this); recordResultEdit->setPlaceholderText("录音文件信息将显示在这里..."); recordResultEdit->setMinimumHeight(80); recordResultEdit->setMaximumHeight(120); recordLayout->addWidget(new QLabel("录音结果:", this)); recordLayout->addWidget(recordResultEdit); // 将录音组件添加到网格布局的第二行第一列 gridLayout->addWidget(recordGroup, 1, 0); // 语音唤醒功能部分 auto* kwsGroup = new QGroupBox("语音唤醒 (KWS)", this); auto* kwsLayout = new QVBoxLayout(kwsGroup); // 语音唤醒控制按钮 auto* kwsControlLayout = new QHBoxLayout(); kwsStartBtn = new QPushButton("开始语音唤醒", this); kwsStartBtn->setStyleSheet("QPushButton { background-color: #9C27B0; color: white; font-weight: bold; }"); kwsStopBtn = new QPushButton("停止唤醒", this); kwsStopBtn->setStyleSheet("QPushButton { background-color: #9E9E9E; color: white; font-weight: bold; }"); kwsStopBtn->setEnabled(false); kwsControlLayout->addWidget(new QLabel("关键词检测:", this)); kwsControlLayout->addStretch(); kwsControlLayout->addWidget(kwsStartBtn); kwsControlLayout->addWidget(kwsStopBtn); kwsLayout->addLayout(kwsControlLayout); // 语音唤醒结果显示 kwsResultEdit = new QTextEdit(this); kwsResultEdit->setPlaceholderText("语音唤醒检测结果将显示在这里..."); kwsResultEdit->setMinimumHeight(80); kwsResultEdit->setMaximumHeight(120); kwsLayout->addWidget(new QLabel("唤醒结果:", this)); kwsLayout->addWidget(kwsResultEdit); // 将语音唤醒组件添加到网格布局的第二行第二列 gridLayout->addWidget(kwsGroup, 1, 1); // 设置一些示例文本(中英文混合) ttsTextEdit->setPlainText("你好,这是语音合成测试。Hello, this is a speech synthesis test. 今天天气很好,适合出门散步。The weather is nice today."); } void SpeechTestMainWindow::setupMenuBar() { // 创建菜单栏 QMenuBar* menuBar = this->menuBar(); // 文件菜单 QMenu* fileMenu = menuBar->addMenu("文件(&F)"); QAction* exitAction = new QAction("退出(&X)", this); exitAction->setShortcut(QKeySequence::Quit); connect(exitAction, &QAction::triggered, this, &QWidget::close); fileMenu->addAction(exitAction); // 设置菜单 QMenu* settingsMenu = menuBar->addMenu("设置(&S)"); QAction* modelSettingsAction = new QAction("模型设置(&M)...", this); modelSettingsAction->setShortcut(QKeySequence("Ctrl+M")); modelSettingsAction->setToolTip("配置ASR和TTS模型"); connect(modelSettingsAction, &QAction::triggered, this, &SpeechTestMainWindow::openModelSettings); settingsMenu->addAction(modelSettingsAction); // 帮助菜单 QMenu* helpMenu = menuBar->addMenu("帮助(&H)"); QAction* aboutAction = new QAction("关于(&A)...", this); connect(aboutAction, &QAction::triggered, [this]() { QMessageBox::about(this, "关于", "QSmartAssistant 语音测试工具 v1.0\n\n" "基于sherpa-onnx的语音识别和合成工具\n" "支持中英文混合语音合成"); }); helpMenu->addAction(aboutAction); } void SpeechTestMainWindow::createOutputDirectories() { // 创建TTS输出目录 QString ttsOutputDir = QDir::currentPath() + "/tts_output"; if (!QDir().exists(ttsOutputDir)) { QDir().mkpath(ttsOutputDir); qDebug() << "创建TTS输出目录:" << ttsOutputDir; } // 创建录音输出目录 QString recordOutputDir = QDir::currentPath() + "/recordings"; if (!QDir().exists(recordOutputDir)) { QDir().mkpath(recordOutputDir); qDebug() << "创建录音输出目录:" << recordOutputDir; } } void SpeechTestMainWindow::connectSignals() { // 通过对象名称查找按钮并连接信号 QPushButton* browseBtn = findChild("browseBtn"); QPushButton* recognizeBtn = findChild("recognizeBtn"); QPushButton* synthesizeBtn = findChild("synthesizeBtn"); if (browseBtn) { connect(browseBtn, &QPushButton::clicked, this, &SpeechTestMainWindow::browseFile); } if (recognizeBtn) { connect(recognizeBtn, &QPushButton::clicked, this, &SpeechTestMainWindow::startRecognition); } if (synthesizeBtn) { connect(synthesizeBtn, &QPushButton::clicked, this, &SpeechTestMainWindow::startSynthesis); } // 连接麦克风按钮信号 connect(micRecordBtn, &QPushButton::clicked, this, &SpeechTestMainWindow::startMicRecognition); connect(micStopBtn, &QPushButton::clicked, this, &SpeechTestMainWindow::stopMicRecognition); // 连接录音按钮信号 connect(recordBtn, &QPushButton::clicked, this, &SpeechTestMainWindow::startRecording); connect(recordStopBtn, &QPushButton::clicked, this, &SpeechTestMainWindow::stopRecording); // 连接语音唤醒按钮信号 connect(kwsStartBtn, &QPushButton::clicked, this, &SpeechTestMainWindow::startKWS); connect(kwsStopBtn, &QPushButton::clicked, this, &SpeechTestMainWindow::stopKWS); } void SpeechTestMainWindow::browseFile() { QString fileName = QFileDialog::getOpenFileName( this, "选择WAV音频文件", "", "WAV Files (*.wav)"); if (!fileName.isEmpty()) { filePathEdit->setText(fileName); } } void SpeechTestMainWindow::startRecognition() { QString filePath = filePathEdit->text().trimmed(); if (filePath.isEmpty()) { QMessageBox::warning(this, "警告", "请先选择音频文件"); return; } if (!QFile::exists(filePath)) { QMessageBox::warning(this, "警告", "文件不存在: " + filePath); return; } if (!asrManager->isInitialized()) { QMessageBox::critical(this, "错误", "ASR模型未初始化"); return; } asrResultEdit->clear(); asrResultEdit->append("正在识别,请稍候..."); statusBar()->showMessage("正在进行语音识别..."); // 使用QTimer延迟执行,避免界面卡顿 QTimer::singleShot(100, this, [this, filePath]() { QString result = asrManager->recognizeWavFile(filePath); asrResultEdit->clear(); asrResultEdit->append("识别结果: " + result); statusBar()->showMessage("语音识别完成"); }); } void SpeechTestMainWindow::startSynthesis() { QString text = ttsTextEdit->toPlainText().trimmed(); if (text.isEmpty()) { QMessageBox::warning(this, "警告", "请输入要合成的文本"); return; } if (!ttsManager->isInitialized()) { QMessageBox::critical(this, "错误", "TTS模型未初始化"); return; } int speakerId = speakerIdSpinBox->value(); // 创建项目目录下的输出文件夹 QString outputDir = QDir::currentPath() + "/tts_output"; QDir().mkpath(outputDir); QString outputPath = outputDir + "/tts_" + QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss") + "_speaker" + QString::number(speakerId) + ".wav"; ttsResultEdit->clear(); ttsResultEdit->append("正在合成,请稍候..."); statusBar()->showMessage("正在进行语音合成..."); // 使用QTimer延迟执行,避免界面卡顿 QTimer::singleShot(100, this, [this, text, speakerId, outputPath]() { bool success = ttsManager->synthesizeText(text, speakerId, outputPath); ttsResultEdit->clear(); if (success) { ttsResultEdit->append("语音合成成功"); // 显示相对路径,更简洁 QString relativePath = QDir::current().relativeFilePath(outputPath); ttsResultEdit->append("输出文件: " + relativePath); ttsResultEdit->append("完整路径: " + outputPath); statusBar()->showMessage("语音合成完成,保存至: " + relativePath); // 询问是否播放 int ret = QMessageBox::question(this, "合成完成", "语音合成完成!是否要播放生成的音频?\n\n文件位置: " + outputPath, QMessageBox::Yes | QMessageBox::No); if (ret == QMessageBox::Yes) { // 在macOS上使用afplay播放音频 QProcess::startDetached("afplay", QStringList() << outputPath); } } else { ttsResultEdit->append("语音合成失败"); statusBar()->showMessage("语音合成失败"); } }); } void SpeechTestMainWindow::startMicRecognition() { if (!asrManager->isOnlineInitialized()) { QMessageBox::information(this, "功能不可用", "在线识别模型未初始化。\n" "请确保sherpa-onnx-streaming-paraformer-bilingual-zh-en模型已正确安装。"); return; } if (isRecording) { return; } // 提示用户检查麦克风权限 qDebug() << "开始麦克风识别,请确保已授予麦克风权限"; // 获取默认音频设备 QAudioDevice defaultDevice = QMediaDevices::defaultAudioInput(); qDebug() << "默认音频设备:" << defaultDevice.description(); qDebug() << "设备ID:" << defaultDevice.id(); // 首先尝试使用设备的首选格式 QAudioFormat preferredFormat = defaultDevice.preferredFormat(); qDebug() << "设备首选格式 - 采样率:" << preferredFormat.sampleRate() << "声道:" << preferredFormat.channelCount() << "格式:" << static_cast(preferredFormat.sampleFormat()); // 使用设备支持的最佳格式进行录制,然后转换为16kHz单声道 QAudioFormat format; // 优先尝试高质量格式 QList preferredSampleRates = {48000, 44100, 22050, 16000}; QList preferredChannels = {2, 1}; // 优先立体声 QList preferredFormats = {QAudioFormat::Int16, QAudioFormat::Float}; bool formatFound = false; // 寻找设备支持的最佳格式 for (int sampleRate : preferredSampleRates) { for (int channels : preferredChannels) { for (QAudioFormat::SampleFormat sampleFormat : preferredFormats) { format.setSampleRate(sampleRate); format.setChannelCount(channels); format.setSampleFormat(sampleFormat); if (defaultDevice.isFormatSupported(format)) { qDebug() << "找到最佳支持格式 - 采样率:" << sampleRate << "声道:" << channels << "格式:" << static_cast(sampleFormat); formatFound = true; break; } } if (formatFound) break; } if (formatFound) break; } if (!formatFound) { // 如果都不支持,使用设备首选格式 format = preferredFormat; qDebug() << "使用设备首选格式"; } qDebug() << "最终使用的音频格式 - 采样率:" << format.sampleRate() << "声道:" << format.channelCount() << "格式:" << static_cast(format.sampleFormat()); // 创建在线流 onlineStream = asrManager->createOnlineStream(); if (!onlineStream) { QMessageBox::critical(this, "错误", "无法创建在线识别流"); return; } qDebug() << "在线识别流创建成功"; // 保存音频格式信息用于后续处理 currentAudioFormat = format; originalSampleRate = format.sampleRate(); originalChannelCount = format.channelCount(); // 创建音频源 - 使用更保守的设置 audioSource = new QAudioSource(defaultDevice, format, this); // 使用较小的缓冲区,有时大缓冲区会导致问题 audioSource->setBufferSize(4096); // 设置音量 audioSource->setVolume(1.0); // 连接状态变化信号 connect(audioSource, &QAudioSource::stateChanged, this, [this](QAudio::State state) { qDebug() << "音频源状态变化:" << state; if (state == QAudio::StoppedState) { qDebug() << "音频源错误:" << audioSource->error(); } else if (state == QAudio::ActiveState) { qDebug() << "音频源已激活!"; } }); qDebug() << "尝试启动音频输入..."; // 启动音频输入 audioDevice = audioSource->start(); if (!audioDevice) { qDebug() << "第一次启动失败,尝试其他方法..."; // 尝试使用pull模式 QByteArray buffer; buffer.resize(4096); audioDevice = audioSource->start(); if (!audioDevice) { QMessageBox::critical(this, "错误", "无法启动音频输入,请检查麦克风权限"); asrManager->destroyOnlineStream(onlineStream); onlineStream = nullptr; delete audioSource; audioSource = nullptr; return; } } qDebug() << "音频输入启动成功"; qDebug() << "初始音频源状态:" << audioSource->state(); qDebug() << "音频源错误:" << audioSource->error(); qDebug() << "缓冲区大小:" << audioSource->bufferSize(); // 等待音频源状态稳定并进行测试 QTimer::singleShot(200, this, [this]() { if (audioSource) { qDebug() << "音频源最终状态:" << audioSource->state(); qDebug() << "音频源错误状态:" << audioSource->error(); // 尝试强制激活音频源 if (audioSource->state() == QAudio::IdleState) { qDebug() << "音频源处于空闲状态,尝试多种激活方法..."; // 方法1:暂停和恢复 audioSource->suspend(); QTimer::singleShot(50, this, [this]() { if (audioSource) { audioSource->resume(); qDebug() << "方法1恢复后状态:" << audioSource->state(); // 方法2:如果仍然是IdleState,尝试重新创建 if (audioSource->state() == QAudio::IdleState) { QTimer::singleShot(100, this, [this]() { if (audioSource) { qDebug() << "尝试重新创建音频源..."; audioSource->stop(); delete audioSource; // 重新创建音频源 QAudioDevice device = QMediaDevices::defaultAudioInput(); audioSource = new QAudioSource(device, currentAudioFormat, this); audioSource->setBufferSize(16384); // 重新连接信号 connect(audioSource, &QAudioSource::stateChanged, this, [this](QAudio::State state) { qDebug() << "重新创建后音频源状态变化:" << state; }); audioDevice = audioSource->start(); qDebug() << "重新创建后音频源状态:" << audioSource->state(); } }); } } }); } // 显示麦克风权限提示 if (audioSource->state() != QAudio::ActiveState) { statusBar()->showMessage("提示:如果没有声音输入,请检查系统设置中的麦克风权限"); asrResultEdit->append("提示:请确保已在系统设置 → 安全性与隐私 → 麦克风中授予权限"); } } }); // 创建定时器读取音频数据 audioTimer = new QTimer(this); connect(audioTimer, &QTimer::timeout, this, &SpeechTestMainWindow::processAudioData); audioTimer->start(100); // 每100ms处理一次音频数据 // 添加一个备用定时器,用于强制检查音频状态 QTimer* statusTimer = new QTimer(this); connect(statusTimer, &QTimer::timeout, this, [this]() { if (audioSource && isRecording) { static int checkCount = 0; checkCount++; if (checkCount % 10 == 0) { // 每秒检查一次 qDebug() << "状态检查 - 音频源状态:" << audioSource->state() << "错误:" << audioSource->error() << "可用字节:" << (audioDevice ? audioDevice->bytesAvailable() : 0); // 如果长时间处于IdleState,尝试重新启动 if (audioSource->state() == QAudio::IdleState && checkCount > 50) { qDebug() << "长时间空闲,尝试重新启动音频源..."; audioSource->stop(); QTimer::singleShot(100, this, [this]() { if (audioSource && isRecording) { audioDevice = audioSource->start(); } }); checkCount = 0; } } } }); statusTimer->start(100); isRecording = true; micRecordBtn->setEnabled(false); micStopBtn->setEnabled(true); micRecordBtn->setText("识别中..."); asrResultEdit->clear(); asrResultEdit->append("开始麦克风识别,请说话..."); statusBar()->showMessage("正在进行麦克风识别..."); qDebug() << "麦克风识别已启动"; } void SpeechTestMainWindow::stopMicRecognition() { if (!isRecording) { return; } isRecording = false; // 停止音频输入 if (audioSource) { audioSource->stop(); delete audioSource; audioSource = nullptr; } // 停止定时器 if (audioTimer) { audioTimer->stop(); delete audioTimer; audioTimer = nullptr; } // 获取最终识别结果 if (onlineStream) { asrManager->inputFinished(onlineStream); // 等待最后的识别结果 QTimer::singleShot(500, this, [this]() { if (onlineStream) { QString finalText = asrManager->getStreamResult(onlineStream); if (!finalText.isEmpty()) { asrResultEdit->append("最终识别结果: " + finalText); } asrManager->destroyOnlineStream(onlineStream); onlineStream = nullptr; } }); } micRecordBtn->setEnabled(true); micStopBtn->setEnabled(false); micRecordBtn->setText("开始麦克风识别"); statusBar()->showMessage("麦克风识别已停止"); qDebug() << "麦克风识别已停止"; } void SpeechTestMainWindow::processAudioData() { if (!audioDevice || !onlineStream || !isRecording) { return; } // 检查音频源状态,但不立即返回 if (audioSource->state() != QAudio::ActiveState) { static int idleCount = 0; idleCount++; if (idleCount % 50 == 0) { // 每50次输出一次警告 qDebug() << "音频源状态异常:" << audioSource->state() << "错误:" << audioSource->error(); } // 尝试重新启动音频源 if (idleCount > 100 && audioSource->state() == QAudio::IdleState) { qDebug() << "尝试重新启动音频源..."; audioSource->stop(); audioDevice = audioSource->start(); idleCount = 0; } // 即使状态异常,也尝试读取数据 } // 强制读取音频数据,即使状态不是Active QByteArray audioData; if (audioDevice) { audioData = audioDevice->readAll(); // 如果没有数据,尝试直接从音频源读取 if (audioData.isEmpty() && audioSource) { qint64 bytesAvailable = audioDevice->bytesAvailable(); if (bytesAvailable > 0) { audioData = audioDevice->read(std::min(bytesAvailable, qint64(4096))); } } } if (audioData.isEmpty()) { return; } static int totalSamples = 0; static int callCount = 0; callCount++; // 每100次调用输出一次调试信息 if (callCount % 100 == 0) { qDebug() << "原始音频数据 - 调用次数:" << callCount << "数据大小:" << audioData.size() << "字节" << "格式:" << currentAudioFormat.sampleRate() << "Hz" << currentAudioFormat.channelCount() << "声道"; } // 定义目标格式(语音识别需要的格式) QAudioFormat targetFormat; targetFormat.setSampleRate(16000); targetFormat.setChannelCount(1); targetFormat.setSampleFormat(QAudioFormat::Float); // 使用音频格式转换方法 QByteArray convertedData = convertAudioFormat(audioData, currentAudioFormat, targetFormat); if (convertedData.isEmpty()) { return; } // 转换后的数据已经是16kHz单声道浮点格式 const float* samples = reinterpret_cast(convertedData.data()); int sampleCount = convertedData.size() / sizeof(float); totalSamples += sampleCount; if (callCount % 100 == 0) { qDebug() << "转换后音频数据 - 样本数:" << sampleCount << "总样本数:" << totalSamples; } // 发送音频数据到识别器 if (sampleCount > 0) { asrManager->acceptWaveform(onlineStream, samples, sampleCount); } // 检查是否有识别结果 int decodeCount = 0; while (asrManager->isStreamReady(onlineStream)) { asrManager->decodeStream(onlineStream); decodeCount++; if (decodeCount > 10) break; // 防止无限循环 } // 获取部分识别结果 QString partialText = asrManager->getStreamResult(onlineStream); if (!partialText.isEmpty()) { qDebug() << "识别到文本:" << partialText; // 更新显示(这里显示实时识别结果) statusBar()->showMessage("识别中: " + partialText); // 检查是否检测到端点 if (asrManager->isEndpoint(onlineStream)) { asrResultEdit->append("识别片段: " + partialText); qDebug() << "检测到端点,重置流"; // 重置流以继续识别 asrManager->destroyOnlineStream(onlineStream); onlineStream = asrManager->createOnlineStream(); } } else { // 即使没有文本,也显示正在处理的状态 if (callCount % 20 == 0) { // 每20次调用更新一次状态 // 计算音频电平 float maxLevel = 0.0f; for (int i = 0; i < sampleCount; i++) { maxLevel = std::max(maxLevel, std::abs(samples[i])); } QString statusMsg = QString("正在监听... (样本: %1, 电平: %2)") .arg(totalSamples) .arg(maxLevel, 0, 'f', 3); statusBar()->showMessage(statusMsg); // 如果检测到音频信号 if (maxLevel > 0.01f) { qDebug() << "检测到音频信号,电平:" << maxLevel; } } } } void SpeechTestMainWindow::openModelSettings() { ModelSettingsDialog dialog(this); // 设置当前配置 ModelConfig offlineAsrConfig; offlineAsrConfig.modelPath = ""; // 从ASRManager获取当前配置 dialog.setCurrentOfflineASRConfig(offlineAsrConfig); ModelConfig onlineAsrConfig; onlineAsrConfig.modelPath = ""; // 从ASRManager获取当前配置 dialog.setCurrentOnlineASRConfig(onlineAsrConfig); ModelConfig kwsConfig; kwsConfig.modelPath = ""; // 从KWS管理器获取当前配置 dialog.setCurrentKWSConfig(kwsConfig); ModelConfig ttsConfig; ttsConfig.modelPath = ""; // 从TTSManager获取当前配置 dialog.setCurrentTTSConfig(ttsConfig); // 连接信号 connect(&dialog, &ModelSettingsDialog::modelsChanged, this, &SpeechTestMainWindow::onModelsChanged); dialog.exec(); } void SpeechTestMainWindow::onModelsChanged() { // 重新初始化模型 reinitializeModels(); // 更新状态栏 bool asrOk = asrManager->isInitialized(); bool ttsOk = ttsManager->isInitialized(); bool kwsOk = kwsManager->isInitialized(); if (asrOk && ttsOk && kwsOk) { QString modelInfo = ttsManager->isMultilingualModel() ? "(支持中英文混合)" : "(仅支持中文)"; QString micInfo = asrManager->isOnlineInitialized() ? "" : ",麦克风识别暂不可用"; QString kwsInfo = kwsOk ? ",语音唤醒可用" : ",语音唤醒不可用"; statusBar()->showMessage("模型重新加载成功 " + modelInfo + micInfo + kwsInfo); } else { statusBar()->showMessage("模型重新加载失败"); } } void SpeechTestMainWindow::reinitializeModels() { // 如果KWS正在运行,先停止它 bool wasKWSActive = isKWSActive; if (isKWSActive) { stopKWS(); } // 重新初始化ASR管理器 bool asrOk = asrManager->initialize(); // 重新初始化TTS管理器 bool ttsOk = ttsManager->initialize(); // 重新初始化KWS管理器 bool kwsOk = kwsManager->initialize(); // 尝试初始化在线识别器 asrManager->initializeOnlineRecognizer(); qDebug() << "模型重新初始化 - ASR:" << (asrOk ? "成功" : "失败") << "TTS:" << (ttsOk ? "成功" : "失败") << "KWS:" << (kwsOk ? "成功" : "失败"); // 如果之前KWS是激活的,重新启动它 if (wasKWSActive && kwsOk) { QTimer::singleShot(1000, this, &SpeechTestMainWindow::startKWS); qDebug() << "将在1秒后重新启动KWS"; } } void SpeechTestMainWindow::startRecording() { if (isRecordingWav) { return; } // 检查是否正在进行语音识别 if (isRecording) { QMessageBox::information(this, "提示", "请先停止语音识别再开始录音"); return; } qDebug() << "开始WAV录音"; // 获取默认音频设备 QAudioDevice defaultDevice = QMediaDevices::defaultAudioInput(); qDebug() << "录音设备:" << defaultDevice.description(); // 获取录音设置(设备参数) int recordSampleRate = recordSampleRateComboBox->currentData().toInt(); int recordChannels = recordChannelComboBox->currentData().toInt(); // 获取输出设置(保存格式) int outputSampleRate = outputSampleRateComboBox->currentData().toInt(); int outputChannels = outputChannelComboBox->currentData().toInt(); qDebug() << "录音设置 - 采样率:" << recordSampleRate << "Hz, 声道:" << recordChannels; qDebug() << "输出设置 - 采样率:" << outputSampleRate << "Hz, 声道:" << outputChannels; // 确定实际录音格式 QAudioFormat deviceOptimalFormat; if (recordSampleRate == -1 || recordChannels == -1) { // 自动检测设备最佳格式 qDebug() << "自动检测设备最佳录音格式..."; QList deviceSampleRates = {48000, 44100, 22050, 16000}; QList deviceChannels = {2, 1}; QList deviceFormats = {QAudioFormat::Int16, QAudioFormat::Float}; bool foundDeviceFormat = false; for (int sampleRate : deviceSampleRates) { for (int channels : deviceChannels) { for (QAudioFormat::SampleFormat format : deviceFormats) { deviceOptimalFormat.setSampleRate(sampleRate); deviceOptimalFormat.setChannelCount(channels); deviceOptimalFormat.setSampleFormat(format); if (defaultDevice.isFormatSupported(deviceOptimalFormat)) { qDebug() << "找到设备最佳格式:" << sampleRate << "Hz," << channels << "声道," << static_cast(format); foundDeviceFormat = true; break; } } if (foundDeviceFormat) break; } if (foundDeviceFormat) break; } if (!foundDeviceFormat) { deviceOptimalFormat = defaultDevice.preferredFormat(); qDebug() << "使用设备首选格式"; } } else { // 使用用户指定的录音格式 deviceOptimalFormat.setSampleRate(recordSampleRate); deviceOptimalFormat.setChannelCount(recordChannels); deviceOptimalFormat.setSampleFormat(QAudioFormat::Int16); // 检查用户指定格式是否被支持 if (!defaultDevice.isFormatSupported(deviceOptimalFormat)) { qDebug() << "用户指定的录音格式不被支持,自动寻找最佳格式..."; // 回退到自动检测 QList deviceSampleRates = {recordSampleRate, 48000, 44100, 22050, 16000}; QList deviceChannels = {recordChannels, 2, 1}; QList deviceFormats = {QAudioFormat::Int16, QAudioFormat::Float}; bool foundDeviceFormat = false; for (int sampleRate : deviceSampleRates) { for (int channels : deviceChannels) { for (QAudioFormat::SampleFormat format : deviceFormats) { deviceOptimalFormat.setSampleRate(sampleRate); deviceOptimalFormat.setChannelCount(channels); deviceOptimalFormat.setSampleFormat(format); if (defaultDevice.isFormatSupported(deviceOptimalFormat)) { qDebug() << "找到兼容格式:" << sampleRate << "Hz," << channels << "声道," << static_cast(format); foundDeviceFormat = true; break; } } if (foundDeviceFormat) break; } if (foundDeviceFormat) break; } if (!foundDeviceFormat) { deviceOptimalFormat = defaultDevice.preferredFormat(); qDebug() << "使用设备首选格式"; } } } // 使用确定的设备格式进行录制 recordAudioFormat = deviceOptimalFormat; // 检查格式支持并智能降级 QString formatInfo = QString("尝试格式: %1 Hz, %2声道") .arg(recordAudioFormat.sampleRate()) .arg(recordAudioFormat.channelCount() == 1 ? "单" : "立体"); qDebug() << formatInfo; if (!defaultDevice.isFormatSupported(recordAudioFormat)) { qDebug() << "设备不支持选择的格式,尝试降级..."; // 如果是立体声,尝试单声道 if (recordAudioFormat.channelCount() == 2) { recordAudioFormat.setChannelCount(1); qDebug() << "尝试单声道格式"; if (!defaultDevice.isFormatSupported(recordAudioFormat)) { // 尝试降低采样率 QList fallbackRates = {44100, 22050, 16000, 8000}; bool foundSupported = false; for (int rate : fallbackRates) { if (rate < recordSampleRate) { recordAudioFormat.setSampleRate(rate); if (defaultDevice.isFormatSupported(recordAudioFormat)) { qDebug() << "降级到采样率:" << rate << "Hz"; foundSupported = true; break; } } } if (!foundSupported) { // 最后使用设备首选格式 recordAudioFormat = defaultDevice.preferredFormat(); qDebug() << "使用设备首选录音格式"; } } } else { // 单声道情况下,尝试降低采样率 QList fallbackRates = {44100, 22050, 16000, 8000}; bool foundSupported = false; for (int rate : fallbackRates) { if (rate < recordSampleRate) { recordAudioFormat.setSampleRate(rate); if (defaultDevice.isFormatSupported(recordAudioFormat)) { qDebug() << "降级到采样率:" << rate << "Hz"; foundSupported = true; break; } } } if (!foundSupported) { recordAudioFormat = defaultDevice.preferredFormat(); qDebug() << "使用设备首选录音格式"; } } // 显示实际使用的格式 QString actualFormat = QString("实际使用格式: %1 Hz, %2声道") .arg(recordAudioFormat.sampleRate()) .arg(recordAudioFormat.channelCount() == 1 ? "单" : "立体"); qDebug() << actualFormat; // 如果格式发生了变化,通知用户 if (recordAudioFormat.sampleRate() != recordSampleRate || recordAudioFormat.channelCount() != recordChannels) { recordResultEdit->append("注意:设备不支持选择的格式,已自动调整"); } } qDebug() << "录音格式 - 采样率:" << recordAudioFormat.sampleRate() << "声道:" << recordAudioFormat.channelCount() << "格式:" << static_cast(recordAudioFormat.sampleFormat()); // 创建输出文件路径 QString outputDir = QDir::currentPath() + "/recordings"; QDir().mkpath(outputDir); currentRecordingPath = outputDir + "/recording_" + QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss") + ".wav"; // 清空录音数据缓冲区 recordedData.clear(); // 创建音频源 recordAudioSource = new QAudioSource(defaultDevice, recordAudioFormat, this); recordAudioSource->setBufferSize(8192); recordAudioSource->setVolume(1.0); // 连接状态变化信号 connect(recordAudioSource, &QAudioSource::stateChanged, this, [this](QAudio::State state) { qDebug() << "录音音频源状态变化:" << state; if (state == QAudio::StoppedState) { qDebug() << "录音音频源错误:" << recordAudioSource->error(); } else if (state == QAudio::ActiveState) { qDebug() << "录音音频源已激活!"; } }); // 启动音频输入 recordAudioDevice = recordAudioSource->start(); if (!recordAudioDevice) { QMessageBox::critical(this, "错误", "无法启动录音,请检查麦克风权限"); delete recordAudioSource; recordAudioSource = nullptr; return; } // 创建定时器读取音频数据 recordTimer = new QTimer(this); connect(recordTimer, &QTimer::timeout, this, &SpeechTestMainWindow::processRecordingData); recordTimer->start(100); // 每100ms处理一次音频数据 isRecordingWav = true; recordBtn->setEnabled(false); recordStopBtn->setEnabled(true); recordBtn->setText("录音中..."); // 录音期间禁用设置选项 recordSampleRateComboBox->setEnabled(false); recordChannelComboBox->setEnabled(false); outputSampleRateComboBox->setEnabled(false); outputChannelComboBox->setEnabled(false); recordResultEdit->clear(); recordResultEdit->append("开始录音,请说话..."); recordResultEdit->append(QString("录音格式: %1 Hz, %2") .arg(recordAudioFormat.sampleRate()) .arg(recordAudioFormat.channelCount() == 1 ? "单声道" : "立体声")); recordResultEdit->append(QString("输出格式: %1 Hz, %2") .arg(outputSampleRate) .arg(outputChannels == 1 ? "单声道" : "立体声")); recordResultEdit->append("输出文件: " + QDir::current().relativeFilePath(currentRecordingPath)); statusBar()->showMessage("正在录音..."); qDebug() << "WAV录音已启动,输出文件:" << currentRecordingPath; } void SpeechTestMainWindow::stopRecording() { if (!isRecordingWav) { return; } isRecordingWav = false; // 停止音频输入 if (recordAudioSource) { recordAudioSource->stop(); delete recordAudioSource; recordAudioSource = nullptr; } // 停止定时器 if (recordTimer) { recordTimer->stop(); delete recordTimer; recordTimer = nullptr; } recordBtn->setEnabled(true); recordStopBtn->setEnabled(false); recordBtn->setText("开始录音"); // 重新启用设置选项 recordSampleRateComboBox->setEnabled(true); recordChannelComboBox->setEnabled(true); outputSampleRateComboBox->setEnabled(true); outputChannelComboBox->setEnabled(true); // 保存WAV文件 if (!recordedData.isEmpty()) { // 获取输出设置 int outputSampleRate = outputSampleRateComboBox->currentData().toInt(); int outputChannels = outputChannelComboBox->currentData().toInt(); QAudioFormat outputFormat; outputFormat.setSampleRate(outputSampleRate); outputFormat.setChannelCount(outputChannels); outputFormat.setSampleFormat(QAudioFormat::Int16); QByteArray finalAudioData = recordedData; QAudioFormat finalFormat = recordAudioFormat; // 如果录制格式与输出格式不同,进行转换 if (recordAudioFormat.sampleRate() != outputSampleRate || recordAudioFormat.channelCount() != outputChannels) { qDebug() << "转换录音格式从" << recordAudioFormat.sampleRate() << "Hz" << recordAudioFormat.channelCount() << "声道到" << outputSampleRate << "Hz" << outputChannels << "声道"; finalAudioData = convertAudioFormat(recordedData, recordAudioFormat, outputFormat); finalFormat = outputFormat; if (finalAudioData.isEmpty()) { recordResultEdit->append("音频格式转换失败!"); statusBar()->showMessage("录音保存失败 - 格式转换错误"); return; } recordResultEdit->append("✅ 音频格式转换完成"); } else { recordResultEdit->append("✅ 录音格式与输出格式一致,无需转换"); } // 保存输出格式的文件 bool success = saveWavFile(currentRecordingPath, finalAudioData, finalFormat); if (success) { QFileInfo fileInfo(currentRecordingPath); double durationSeconds = (double)finalAudioData.size() / (finalFormat.sampleRate() * finalFormat.channelCount() * (finalFormat.sampleFormat() == QAudioFormat::Int16 ? 2 : 4)); recordResultEdit->append(QString("🎉 录音完成!时长: %1 秒").arg(durationSeconds, 0, 'f', 1)); recordResultEdit->append(QString("📊 最终格式: %1 Hz, %2, 16位") .arg(finalFormat.sampleRate()) .arg(finalFormat.channelCount() == 1 ? "单声道" : "立体声")); recordResultEdit->append(QString("📁 文件大小: %1 KB").arg(fileInfo.size() / 1024.0, 0, 'f', 1)); recordResultEdit->append("📂 完整路径: " + currentRecordingPath); statusBar()->showMessage("录音已保存: " + QDir::current().relativeFilePath(currentRecordingPath)); // 询问是否播放录音 int ret = QMessageBox::question(this, "录音完成", QString("录音已保存!\n文件: %1\n时长: %2 秒\n\n是否要播放录音?") .arg(QDir::current().relativeFilePath(currentRecordingPath)) .arg(durationSeconds, 0, 'f', 1), QMessageBox::Yes | QMessageBox::No); if (ret == QMessageBox::Yes) { // 在macOS上使用afplay播放音频 QProcess::startDetached("afplay", QStringList() << currentRecordingPath); } } else { recordResultEdit->append("录音保存失败!"); statusBar()->showMessage("录音保存失败"); } } else { recordResultEdit->append("没有录制到音频数据"); statusBar()->showMessage("录音失败 - 没有数据"); } qDebug() << "WAV录音已停止"; } void SpeechTestMainWindow::processRecordingData() { if (!recordAudioDevice || !isRecordingWav) { return; } // 读取音频数据 QByteArray audioData = recordAudioDevice->readAll(); if (!audioData.isEmpty()) { // 将数据添加到录音缓冲区 recordedData.append(audioData); // 更新录音状态显示 static int updateCount = 0; updateCount++; if (updateCount % 10 == 0) { // 每秒更新一次 double durationSeconds = (double)recordedData.size() / (recordAudioFormat.sampleRate() * recordAudioFormat.channelCount() * (recordAudioFormat.sampleFormat() == QAudioFormat::Int16 ? 2 : 4)); statusBar()->showMessage(QString("录音中... %1 秒").arg(durationSeconds, 0, 'f', 1)); } } } bool SpeechTestMainWindow::saveWavFile(const QString& filePath, const QByteArray& audioData, const QAudioFormat& format) { QFile file(filePath); if (!file.open(QIODevice::WriteOnly)) { qDebug() << "无法创建WAV文件:" << filePath; return false; } // WAV文件头 QDataStream stream(&file); stream.setByteOrder(QDataStream::LittleEndian); // RIFF头 stream.writeRawData("RIFF", 4); quint32 fileSize = 36 + audioData.size(); stream << fileSize; stream.writeRawData("WAVE", 4); // fmt子块 stream.writeRawData("fmt ", 4); quint32 fmtSize = 16; stream << fmtSize; quint16 audioFormat = 1; // PCM stream << audioFormat; quint16 numChannels = format.channelCount(); stream << numChannels; quint32 sampleRate = format.sampleRate(); stream << sampleRate; quint16 bitsPerSample = (format.sampleFormat() == QAudioFormat::Int16) ? 16 : 32; quint32 byteRate = sampleRate * numChannels * (bitsPerSample / 8); stream << byteRate; quint16 blockAlign = numChannels * (bitsPerSample / 8); stream << blockAlign; stream << bitsPerSample; // data子块 stream.writeRawData("data", 4); quint32 dataSize = audioData.size(); stream << dataSize; // 写入音频数据 stream.writeRawData(audioData.constData(), audioData.size()); file.close(); qDebug() << "WAV文件保存成功:" << filePath; qDebug() << "文件大小:" << (fileSize + 8) << "字节"; qDebug() << "音频格式:" << numChannels << "声道," << sampleRate << "Hz," << bitsPerSample << "位"; return true; } QByteArray SpeechTestMainWindow::convertAudioFormat(const QByteArray& inputData, const QAudioFormat& inputFormat, const QAudioFormat& outputFormat) { if (inputData.isEmpty()) { return QByteArray(); } // 如果格式相同,直接返回 if (inputFormat.sampleRate() == outputFormat.sampleRate() && inputFormat.channelCount() == outputFormat.channelCount() && inputFormat.sampleFormat() == outputFormat.sampleFormat()) { return inputData; } // qDebug() << "音频格式转换:" // << inputFormat.sampleRate() << "Hz" << inputFormat.channelCount() << "声道" // << "→" // << outputFormat.sampleRate() << "Hz" << outputFormat.channelCount() << "声道"; // 第一步:转换为浮点格式 std::vector samples; int inputSampleCount = 0; if (inputFormat.sampleFormat() == QAudioFormat::Int16) { const int16_t* intData = reinterpret_cast(inputData.data()); inputSampleCount = inputData.size() / 2; samples.resize(inputSampleCount); for (int i = 0; i < inputSampleCount; i++) { samples[i] = intData[i] / 32768.0f; } } else if (inputFormat.sampleFormat() == QAudioFormat::Float) { const float* floatData = reinterpret_cast(inputData.data()); inputSampleCount = inputData.size() / sizeof(float); samples.assign(floatData, floatData + inputSampleCount); } else { qDebug() << "不支持的输入音频格式:" << static_cast(inputFormat.sampleFormat()); return QByteArray(); } // 第二步:处理多声道转单声道 if (inputFormat.channelCount() > outputFormat.channelCount() && outputFormat.channelCount() == 1) { std::vector monoSamples; int frameCount = inputSampleCount / inputFormat.channelCount(); monoSamples.reserve(frameCount); for (int frame = 0; frame < frameCount; frame++) { float sum = 0.0f; for (int ch = 0; ch < inputFormat.channelCount(); ch++) { int index = frame * inputFormat.channelCount() + ch; if (index < inputSampleCount) { sum += samples[index]; } } monoSamples.push_back(sum / inputFormat.channelCount()); } samples = std::move(monoSamples); inputSampleCount = samples.size(); } // 第三步:重采样 if (inputFormat.sampleRate() != outputFormat.sampleRate()) { std::vector resampledSamples; float ratio = static_cast(outputFormat.sampleRate()) / inputFormat.sampleRate(); int newSampleCount = static_cast(inputSampleCount * ratio); resampledSamples.reserve(newSampleCount); for (int i = 0; i < newSampleCount; i++) { float srcIndex = i / ratio; int index = static_cast(srcIndex); if (index < inputSampleCount - 1) { // 线性插值 float frac = srcIndex - index; float sample = samples[index] * (1.0f - frac) + samples[index + 1] * frac; resampledSamples.push_back(sample); } else if (index < inputSampleCount) { resampledSamples.push_back(samples[index]); } } samples = std::move(resampledSamples); inputSampleCount = samples.size(); } // 第四步:转换为目标格式 QByteArray outputData; if (outputFormat.sampleFormat() == QAudioFormat::Int16) { outputData.resize(inputSampleCount * 2); int16_t* intData = reinterpret_cast(outputData.data()); for (int i = 0; i < inputSampleCount; i++) { // 限制范围并转换为16位整数 float sample = std::max(-1.0f, std::min(1.0f, samples[i])); intData[i] = static_cast(sample * 32767.0f); } } else if (outputFormat.sampleFormat() == QAudioFormat::Float) { outputData.resize(inputSampleCount * sizeof(float)); float* floatData = reinterpret_cast(outputData.data()); for (int i = 0; i < inputSampleCount; i++) { floatData[i] = samples[i]; } } //qDebug() << "音频转换完成,输出大小:" << outputData.size() << "字节"; return outputData; } void SpeechTestMainWindow::startKWS() { if (isKWSActive) { return; } // 检查是否正在进行其他音频操作,如果是则自动停止 if (isRecording) { qDebug() << "KWS启动:自动停止ASR麦克风识别"; stopMicRecognition(); kwsResultEdit->append("🔄 自动停止ASR麦克风识别以启动语音唤醒"); } if (isRecordingWav) { qDebug() << "KWS启动:自动停止录音功能"; stopRecording(); kwsResultEdit->append("🔄 自动停止录音功能以启动语音唤醒"); } qDebug() << "开始语音唤醒检测"; // 获取默认音频设备 QAudioDevice defaultDevice = QMediaDevices::defaultAudioInput(); qDebug() << "语音唤醒设备:" << defaultDevice.description(); // 使用默认配置:设备首选格式 kwsAudioFormat = defaultDevice.preferredFormat(); qDebug() << "KWS使用默认格式 - 采样率:" << kwsAudioFormat.sampleRate() << "声道:" << kwsAudioFormat.channelCount() << "格式:" << static_cast(kwsAudioFormat.sampleFormat()); // 检查KWS管理器是否已初始化 if (!kwsManager->isInitialized()) { QMessageBox::critical(this, "错误", "KWS模型未初始化,请检查模型配置"); return; } // 创建KWS检测器 kwsSpotter = kwsManager->createKeywordSpotter(); if (!kwsSpotter) { QMessageBox::critical(this, "错误", "无法创建KWS关键词检测器"); return; } // 创建KWS流 kwsStream = kwsManager->createKeywordStream(kwsSpotter); if (!kwsStream) { QMessageBox::critical(this, "错误", "无法创建KWS关键词流"); kwsManager->destroyKeywordSpotter(kwsSpotter); kwsSpotter = nullptr; return; } qDebug() << "KWS检测器和流创建成功"; // 创建音频源 - 优化缓冲区设置 kwsAudioSource = new QAudioSource(defaultDevice, kwsAudioFormat, this); kwsAudioSource->setBufferSize(16384); // 增大缓冲区,减少音频丢失 kwsAudioSource->setVolume(1.0); // 启动音频输入 kwsAudioDevice = kwsAudioSource->start(); if (!kwsAudioDevice) { QMessageBox::critical(this, "错误", "无法启动语音唤醒音频输入"); delete kwsAudioSource; kwsAudioSource = nullptr; return; } // 创建定时器处理音频数据 - 优化处理频率 kwsTimer = new QTimer(this); connect(kwsTimer, &QTimer::timeout, this, &SpeechTestMainWindow::processKWSData); kwsTimer->start(30); // 30ms处理一次,更频繁的处理提高识别率 isKWSActive = true; kwsStartBtn->setEnabled(false); kwsStopBtn->setEnabled(true); kwsStartBtn->setText("唤醒检测中..."); kwsResultEdit->clear(); kwsResultEdit->append("🎤 语音唤醒检测已启动"); kwsResultEdit->append("⚙️ 音频配置:默认格式 → 16kHz单声道"); // 尝试读取关键词文件 QString keywordsPath = QDir::homePath() + "/.config/QSmartAssistant/Data/sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01/keywords.txt"; QFile keywordsFile(keywordsPath); kwsResultEdit->append("📋 支持的关键词:"); if (keywordsFile.open(QIODevice::ReadOnly | QIODevice::Text)) { QTextStream in(&keywordsFile); QString line; int lineCount = 0; while (!in.atEnd() && lineCount < 8) { // 显示前8个关键词 line = in.readLine().trimmed(); if (!line.isEmpty() && !line.startsWith("#")) { kwsResultEdit->append(QString(" • %1").arg(line)); lineCount++; } } keywordsFile.close(); if (lineCount == 0) { kwsResultEdit->append(" • 小米小米"); kwsResultEdit->append(" • 小爱同学"); kwsResultEdit->append(" • 你好问问"); } } else { kwsResultEdit->append(" • 小米小米"); kwsResultEdit->append(" • 小爱同学"); kwsResultEdit->append(" • 你好问问"); } kwsResultEdit->append("🎯 等待关键词检测..."); kwsResultEdit->append("⚙️ 优化配置:阈值=0.25 (提高识别率)"); kwsResultEdit->append("💡 提示:发音要清晰标准,现在更容易检测"); statusBar()->showMessage("语音唤醒检测运行中"); qDebug() << "KWS启动完成"; } void SpeechTestMainWindow::stopKWS() { if (!isKWSActive) { return; } isKWSActive = false; // 停止音频输入 if (kwsAudioSource) { kwsAudioSource->stop(); delete kwsAudioSource; kwsAudioSource = nullptr; } // 停止定时器 if (kwsTimer) { kwsTimer->stop(); delete kwsTimer; kwsTimer = nullptr; } // 清理KWS资源 if (kwsStream) { kwsManager->destroyKeywordStream(kwsStream); kwsStream = nullptr; qDebug() << "KWS关键词流已销毁"; } if (kwsSpotter) { kwsManager->destroyKeywordSpotter(kwsSpotter); kwsSpotter = nullptr; qDebug() << "KWS关键词检测器已销毁"; } kwsStartBtn->setEnabled(true); kwsStopBtn->setEnabled(false); kwsStartBtn->setText("开始语音唤醒"); kwsResultEdit->append("🛑 语音唤醒检测已停止"); kwsResultEdit->append("📊 KWS资源已清理完成"); statusBar()->showMessage("语音唤醒检测已停止"); qDebug() << "语音唤醒检测已停止,资源已清理"; } void SpeechTestMainWindow::processKWSData() { if (!kwsAudioDevice || !isKWSActive || !kwsStream || !kwsSpotter) { return; } // 读取音频数据 QByteArray audioData = kwsAudioDevice->readAll(); if (audioData.isEmpty()) { return; } // 定义目标格式:16kHz单声道 QAudioFormat targetFormat; targetFormat.setSampleRate(16000); targetFormat.setChannelCount(1); targetFormat.setSampleFormat(QAudioFormat::Float); // 转换音频格式为16kHz单声道 QByteArray convertedData = convertAudioFormat(audioData, kwsAudioFormat, targetFormat); if (convertedData.isEmpty()) { return; } // 转换后的数据是16kHz单声道浮点格式 const float* samples = reinterpret_cast(convertedData.data()); int sampleCount = convertedData.size() / sizeof(float); // 分块发送音频数据,提高处理效果 const int chunkSize = 1600; // 100ms的音频数据 (16000 * 0.1) for (int i = 0; i < sampleCount; i += chunkSize) { int currentChunkSize = std::min(chunkSize, sampleCount - i); kwsManager->acceptWaveform(kwsStream, samples + i, currentChunkSize); // 每个块都检查是否准备好解码 while (kwsManager->isReady(kwsStream, kwsSpotter)) { kwsManager->decode(kwsStream, kwsSpotter); // 立即检查结果 QString detectedKeyword = kwsManager->getResult(kwsStream, kwsSpotter); if (!detectedKeyword.isEmpty()) { static int successCount = 0; successCount++; qDebug() << "🎯 KWS检测到关键词:" << detectedKeyword << "(第" << successCount << "次)"; kwsResultEdit->append(QString("🎯 检测到关键词: %1 (第%2次)") .arg(detectedKeyword).arg(successCount)); statusBar()->showMessage(QString("🎯 检测到关键词: %1 (总计%2次)") .arg(detectedKeyword).arg(successCount)); // 重置流以继续检测 kwsManager->reset(kwsStream, kwsSpotter); return; // 检测到关键词后立即返回 } } } // 简化的调试信息 static int callCount = 0; callCount++; if (callCount % 100 == 0) { // 减少调试输出频率 // 计算音频电平 float maxLevel = 0.0f; for (int i = 0; i < std::min(sampleCount, 1000); i++) { maxLevel = std::max(maxLevel, std::abs(samples[i])); } qDebug() << "KWS处理:" << callCount << "次,样本数:" << sampleCount << "电平:" << maxLevel << "阈值:0.25"; if (maxLevel > 0.02f) { statusBar()->showMessage(QString("检测中... (电平: %1)") .arg(maxLevel, 0, 'f', 3)); } } }