diff --git a/src/Core/Inc/GLCore.h b/src/Core/Inc/GLCore.h index 746b9c7..55a5504 100644 --- a/src/Core/Inc/GLCore.h +++ b/src/Core/Inc/GLCore.h @@ -5,6 +5,9 @@ #if !defined(EMBEDDED_LINUX) #include "menu.h" #endif +#ifdef EMBEDDED_LINUX +#include +#endif #ifdef Q_OS_WIN #include @@ -85,6 +88,9 @@ private: #if !defined(EMBEDDED_LINUX) Menu *contextMenu; /// 使用 Menu 类 #endif +#ifdef EMBEDDED_LINUX + QPushButton *pttButton; /// 嵌入式平台PTT按钮 +#endif bool isLeftPressed; /// 鼠标左键是否按下 bool isRightPressed; /// 鼠标右键是否按下 diff --git a/src/Core/Src/AppCore.cpp b/src/Core/Src/AppCore.cpp index 55f52f7..da6e6ba 100644 --- a/src/Core/Src/AppCore.cpp +++ b/src/Core/Src/AppCore.cpp @@ -8,8 +8,10 @@ #include #include "AudioDataHandle.h" +#if !defined(EMBEDDED_LINUX) #include "AutoAgentHandle.h" #include "ScreenShotReqDataHandle.h" +#endif #include "DeviceDataHandle.h" #include "AudioInput.h" @@ -88,13 +90,31 @@ AppCore::AppCore(QObject *parent) : QObject(parent) // 初始化业务解析单例 AudioDataHandle::getInstance(); +#if !defined(EMBEDDED_LINUX) AutoAgentHandle::getInstance(); ScreenShotReqDataHandle::getInstance(); +#endif // 注入发送接口 NetworkDO::getInstance()->registerSender([](const QString& type, const QJsonObject& data){ WebSocketClient::getInstance()->sendJson(type, data); }); - // TODO Test +#ifdef EMBEDDED_LINUX + // 嵌入式平台无Settings UI,需手动绑定WebSocket接收链路 + connect(WebSocketClient::getInstance(), &WebSocketClient::jsonReceived, + NetworkDO::getInstance(), &NetworkDO::onDataReceived); + // 自动配置并连接服务端(优先读取环境变量 YOSUGA_SERVER_URL) + { + QString serverUrl = qEnvironmentVariable("YOSUGA_SERVER_URL", "ws://192.168.1.18:8765"); + auto *client = WebSocketClient::getInstance(); + client->setConfiguration(QUrl(serverUrl)); + client->setAutoReconnect(true); + client->connectToServer(); + qDebug() << "[AppCore] Embedded: connecting to server:" << serverUrl; + } +#endif +#ifdef EMBEDDED_LINUX + AudioInput::getInstance()->setAudioSettings(16000, 2); +#endif AudioInput::getInstance()->setAudioPath(QDir::currentPath(), "/temp.wav"); // 连接必要的信号 connect(AudioInput::getInstance(), &AudioInput::recordingFinished_Byte, @@ -114,8 +134,10 @@ AppCore::~AppCore() if (m_deviceTcpServer) m_deviceTcpServer->stop(); if (m_deviceWsServer) m_deviceWsServer->stop(); +#if !defined(EMBEDDED_LINUX) ScreenShotReqDataHandle::destroy(); AutoAgentHandle::destroy(); +#endif AudioDataHandle::destroy(); DeviceDataHandle::destroy(); diff --git a/src/Core/Src/GLCore.cpp b/src/Core/Src/GLCore.cpp index 86bbf31..810ba22 100644 --- a/src/Core/Src/GLCore.cpp +++ b/src/Core/Src/GLCore.cpp @@ -14,8 +14,11 @@ #include #include "TextRenderer.h" -// #include "AudioOutput.h" #include "AppContext.h" +#ifdef EMBEDDED_LINUX +#include "AppCore.h" +#include +#endif QMap GLCore::frameRateMap = { {"30", 30.0}, {"60", 60.0}, @@ -87,6 +90,35 @@ GLCore::GLCore(const int width, const int height, QWidget *parent) connect(contextMenu, &Menu::closeMainWindow, this, &GLCore::closeGL); // 关闭窗口信号 #endif +#ifdef EMBEDDED_LINUX + AppCore::getInstance(); + + pttButton = new QPushButton(this); + pttButton->setFixedSize(64, 64); + pttButton->move(width - 80, height - 80); + pttButton->setIcon(QIcon("Resources/Pic/Others/voice.png")); + pttButton->setIconSize(QSize(56, 56)); + pttButton->setStyleSheet( + "QPushButton {" + " background-color: rgba(255, 255, 255, 60);" + " border-radius: 32px;" + " border: none;" + "}" + "QPushButton:pressed {" + " background-color: rgba(250, 80, 80, 150);" + "}" + ); + pttButton->raise(); + pttButton->show(); + + connect(pttButton, &QPushButton::pressed, this, []() { + AppCore::getInstance()->startPttRecording(); + }); + connect(pttButton, &QPushButton::released, this, []() { + AppCore::getInstance()->stopPttRecording(); + }); +#endif + // 注册当前实例到中介类 AppContext::RegisterGLCore(this); } diff --git a/src/Handle/AudioHandle/Inc/AudioInput.h b/src/Handle/AudioHandle/Inc/AudioInput.h index b740328..cffe0a0 100644 --- a/src/Handle/AudioHandle/Inc/AudioInput.h +++ b/src/Handle/AudioHandle/Inc/AudioInput.h @@ -12,6 +12,9 @@ #include #include #include +#ifdef EMBEDDED_LINUX +#include +#endif /** * @brief 录音模块 @@ -150,5 +153,8 @@ private: qreal m_smoothRms = 0.0; /// 平滑RMS值(用于防止低频杂波突然打断静音检测) bool m_hasVoiceDetected = false; /// 是否已检测到人声 +#ifdef EMBEDDED_LINUX + QProcess *m_arecordProcess = nullptr; /// 嵌入式平台使用arecord录音 +#endif }; diff --git a/src/Handle/AudioHandle/Src/AudioInput.cpp b/src/Handle/AudioHandle/Src/AudioInput.cpp index b4ee43e..7e9afed 100644 --- a/src/Handle/AudioHandle/Src/AudioInput.cpp +++ b/src/Handle/AudioHandle/Src/AudioInput.cpp @@ -53,7 +53,6 @@ void AudioInput::setAudioSettings(const int rate, const int channels) { m_format.setSampleRate(rate); m_format.setChannelCount(channels); - // 为了生成标准 WAV 且方便计算 RMS,强制设为 Int16 m_format.setSampleFormat(QAudioFormat::Int16); // 检查设备是否支持该格式,不支持则使用最接近的 @@ -72,6 +71,30 @@ void AudioInput::setAudioPath(const QString &path, const QString &fileName) void AudioInput::startAudio() { +#ifdef EMBEDDED_LINUX + m_rawPCMData.clear(); + if (!m_arecordProcess) { + m_arecordProcess = new QProcess(this); + connect(m_arecordProcess, &QProcess::readyReadStandardOutput, this, [this]() { + m_rawPCMData.append(m_arecordProcess->readAllStandardOutput()); + }); + } else if (m_arecordProcess->state() != QProcess::NotRunning) { + m_arecordProcess->terminate(); + m_arecordProcess->waitForFinished(1000); + } + m_arecordProcess->start("arecord", { + "-D", "hw:0,0", "-f", "S16_LE", + "-r", QString::number(m_format.sampleRate()), + "-c", QString::number(m_format.channelCount()), + "-t", "raw", "-q", "-" + }); + if (m_arecordProcess->waitForStarted(1000)) { + qDebug() << "Started recording via arecord: hw:0,0" + << m_format.sampleRate() << "Hz" << m_format.channelCount() << "ch S16_LE"; + } else { + qCritical() << "Failed to start arecord"; + } +#else // 每次开始前重新创建 QAudioSource,确保状态重置 if (m_audioSource) { delete m_audioSource; @@ -79,8 +102,6 @@ void AudioInput::startAudio() } m_audioSource = new QAudioSource(m_currentDevice, m_format, this); - - // 调大缓冲区以避免溢出 m_audioSource->setBufferSize(128000); // start() 返回一个 QIODevice,可以从中读取数据 @@ -92,14 +113,21 @@ void AudioInput::startAudio() } else { qCritical() << "Failed to start audio recording."; } +#endif } void AudioInput::stopAudio() { +#ifdef EMBEDDED_LINUX + if (m_arecordProcess && m_arecordProcess->state() != QProcess::NotRunning) { + m_arecordProcess->terminate(); + m_arecordProcess->waitForFinished(2000); + } +#else if (m_audioSource) { m_audioSource->stop(); - // 注意:不要立即 delete m_audioSource,某些情况下可能导致 crash,停止即可 } +#endif // 停止所有定时器 m_timer->stop(); diff --git a/src/Handle/AudioHandle/Src/AudioOutput.cpp b/src/Handle/AudioHandle/Src/AudioOutput.cpp index 99b0126..d485781 100644 --- a/src/Handle/AudioHandle/Src/AudioOutput.cpp +++ b/src/Handle/AudioHandle/Src/AudioOutput.cpp @@ -29,27 +29,24 @@ void StreamAudioWorker::start(int sampleRate, int channelCount, int bitDepth) { m_firstChunk = true; - // 配置音频格式 QAudioFormat format; format.setSampleRate(sampleRate); format.setChannelCount(channelCount); if (bitDepth == 8) format.setSampleFormat(QAudioFormat::UInt8); else if (bitDepth == 16) format.setSampleFormat(QAudioFormat::Int16); - else if (bitDepth == 32) format.setSampleFormat(QAudioFormat::Float); // 32位通常为Float - else format.setSampleFormat(QAudioFormat::Int16); // 默认回退 + else if (bitDepth == 32) format.setSampleFormat(QAudioFormat::Float); + else format.setSampleFormat(QAudioFormat::Int16); - // 检查设备是否支持 auto device = QMediaDevices::defaultAudioOutput(); if (!device.isFormatSupported(format)) { qWarning() << "[Worker] Device does not support format, using preferred format."; format = device.preferredFormat(); } - // 创建 Sink (必须在 Worker 线程中创建) m_sink.reset(new QAudioSink(device, format)); + m_sink->setBufferSize(sampleRate * channelCount * (bitDepth / 8)); - // 监听状态 connect(m_sink.data(), &QAudioSink::stateChanged, this, [this](QAudio::State state){ if (state == QAudio::IdleState) { emit playbackFinished(); @@ -61,32 +58,34 @@ void StreamAudioWorker::start(int sampleRate, int channelCount, int bitDepth) { } }); - // 启动,获取 IO 设备 m_ioDevice = m_sink->start(); if (!m_ioDevice) { emit errorOccurred("Failed to start audio device"); } else { - qDebug() << "[Worker] Stream started:" << sampleRate << "Hz"; + qDebug() << "[Worker] Stream started:" << sampleRate << "Hz, buffer:" << m_sink->bufferSize(); } } void StreamAudioWorker::processChunk(const QByteArray& chunk) { if (chunk.isEmpty() || !m_ioDevice || !m_sink) return; QByteArray dataToWrite = chunk; - // 智能处理 WAV 头 if (m_firstChunk) { if (hasWavHeader(chunk)) { - qDebug() << "[Worker] Detected WAV header, stripping 44 bytes."; dataToWrite = chunk.mid(44); } m_firstChunk = false; } - // 写入音频设备 (QAudioSink 内部有缓冲区,这里直接 write 即可) - // 如果数据量过大,write 可能会阻塞,但在独立线程中这是可以接受的 - qint64 written = m_ioDevice->write(dataToWrite); - if (written != dataToWrite.size()) { - qWarning() << "[Worker] Incomplete write:" << written << "/" << dataToWrite.size(); + const char *ptr = dataToWrite.constData(); + qint64 remaining = dataToWrite.size(); + while (remaining > 0) { + qint64 written = m_ioDevice->write(ptr, remaining); + if (written > 0) { + ptr += written; + remaining -= written; + } else { + QThread::msleep(5); + } } }