#include "CreateLongFileThread.h" #include "GlobalInfo.h" #include "spdlog.h" #include #include CreateLongFileThread::CreateLongFileThread(RecordThreadInfo_t& threadInfo) : BaseRecordThread(threadInfo) { m_logger = spdlog::get("RecordAudio"); if(m_logger == nullptr) { fmt::print("RecordThread: RecordAudio Logger not found.\n"); return; } /* 初始化数据 */ initData(); } CreateLongFileThread::~CreateLongFileThread() { } /* 设置数据 */ bool CreateLongFileThread::setData(const AudioSrcData& srcData) { if(srcData.pData == nullptr || srcData.dataSize == 0) { SPDLOG_LOGGER_ERROR(m_logger, "{} 设置数据失败,srcData为空或dataSize为0", m_logBase); return false; } /* 锁定缓冲区 */ std::lock_guard lock(m_mutexBuffer); /* 如果缓冲区没有分配内存,先分配 */ if(m_bufferData.pData == nullptr) { if(!m_bufferData.allocateMemory(m_writeCriticalSize * 3)) { SPDLOG_LOGGER_ERROR(m_logger, "{} 分配缓冲区内存失败", m_logBase); return false; } } /* 添加数据到缓冲区 */ int32_t writtenSize = m_bufferData.appendData(srcData.pData, srcData.dataSize); if(writtenSize == 0) { SPDLOG_LOGGER_ERROR(m_logger, "{} 添加数据到缓冲区失败", m_logBase); return false; } /* 记录日期 */ if(m_bufferData.startTime.isNull() || m_bufferData.startTime.isValid()) { m_bufferData.startTime = srcData.startTime; } m_bufferData.endTime = srcData.endTime; // SPDLOG_LOGGER_DEBUG(m_logger, "{} 设置数据,dataSize: {}, startTime: {}, endTime: {}", // m_logBase, m_bufferData.dataSize, m_bufferData.startTime.toString("yyyy-MM-dd hh:mm:ss").toStdString(), m_bufferData.endTime.toString("yyyy-MM-dd hh:mm:ss").toStdString()); return true; } /* 开启录制 */ bool CreateLongFileThread::startRecordAlarmFile(const AlarmInfo_t& alarmInfo) { std::lock_guard lock(m_mutexBuffer); /* 先检查是否已经在队列中了 */ AlarmKey_t key = {alarmInfo.CompareItemID, alarmInfo.RoadInfo.nCompareRoadNum, alarmInfo.AlarmType, alarmInfo.StartTime}; if(m_mapAlarmFile.find(key) != m_mapAlarmFile.end()) { SPDLOG_LOGGER_WARN(m_logger, "{} 已经在报警录制队列中,无法重复添加", m_logBase); return true; } /* 添加一个新的报警文件路径 */ QString fileName = generateFileName(alarmInfo.StartTime, alarmInfo.EndTime); return true; } /* 停止录制,alarmInfo既是传入参数,也是传出参数,传出文件路径和开始位置 */ bool CreateLongFileThread::stopRecordAlarmFile(AlarmInfo_t& alarmInfo) { return true; } /** * @brief 生成长文件的线程函数,文件生成逻辑如下 * 1、这里有一个缓冲区,存储音频数据,缓冲区大小是2分钟的数据 * 2、每分钟写入一次文件,文件名格式为:ChannelX_yyyyMMdd_hhmmss-yyyyMMdd_hhmmss.wav * 3、文件总长度是1小时的数据,超过1小时则重新开始记录一个新的文件 * */ void CreateLongFileThread::task() { SPDLOG_LOGGER_INFO(m_logger, "➢ {} 开启记录文件线程 ", m_logBase); /* 计算一小时的文件大小 */ while(m_isRunning) { /* 线程休眠1秒 */ std::this_thread::sleep_for(std::chrono::milliseconds(1000)); /* 判断缓存是否达到1分钟数据临界值 */ { std::lock_guard lock(m_mutexBuffer); if(m_bufferData.dataSize < m_writeCriticalSize) { continue; // 缓存数据不足,继续等待 } /* 数据足够了将缓冲区数据拷贝出来 */ memcpy(m_srcData.pData, m_bufferData.pData, m_bufferData.dataSize); m_srcData.dataSize = m_bufferData.dataSize; m_srcData.startTime = m_bufferData.startTime; m_srcData.endTime = m_bufferData.endTime; /* 清空缓冲区数据 */ m_bufferData.clear(); } // SPDLOG_LOGGER_DEBUG(m_logger, "{} 设置数据,dataSize: {}, startTime: {}, endTime: {}", // m_logBase, m_srcData.dataSize, m_srcData.startTime.toString("yyyy-MM-dd hh:mm:ss").toStdString(), m_srcData.endTime.toString("yyyy-MM-dd hh:mm:ss").toStdString()); /*-------------------------------------------------------------- * 打开文件。写入的时候判断是否到达了整点,如果到达了整点,则关闭文件 * 重新创建一个新的文件 *--------------------------------------------------------------*/ bool isNewFile = false; if(m_writtenSize == 0) { /* 如果没有写入过数据,则是新文件 */ isNewFile = true; m_writtenStartTime = m_srcData.startTime; // 记录开始时间 m_writtenNowTime = m_writtenStartTime; // 记录当前时间 } /* 设置今日目录 */ if(!setTodayPath(isNewFile)) { continue; } /* 打开文件 */ QFile wavFile; if(!openFile(wavFile, isNewFile)) { if(m_openFileErrorSize >= 3) { SPDLOG_LOGGER_ERROR(m_logger, "{} 打开文件失败次数过多,重新开始记录", m_logBase); m_writtenSize = 0; m_writtenStartTime = QDateTime::currentDateTime(); // 重新开始时间 m_writtenNowTime = m_writtenStartTime; // 重新开始时间 m_wavFileName.clear(); // 清空文件名 m_srcData.clear(); // 清空缓冲区数据 m_openFileErrorSize = 0; // 重置错误次数 continue; // 重新开始记录 } } /*-------------------------------------------------------------- * 将数据写入文件,并记录其携带的时间和写入的数据大小 *--------------------------------------------------------------*/ int64_t wSize = 0; { std::lock_guard lock(m_mutexBuffer); wSize = wavFile.write(m_srcData.pData, m_srcData.dataSize); /* 更新结束时间 */ m_writtenNowTime = m_srcData.endTime; /* 清空缓冲区 */ m_srcData.clear(); } if(wSize < 0) { SPDLOG_LOGGER_ERROR(m_logger, "{} 写入WAV文件失败: {}", m_logBase, wavFile.errorString().toStdString()); SPDLOG_LOGGER_WARN(m_logger, "文件路径:{}", m_wavFileName.toStdString()); wavFile.close(); continue; } else { SPDLOG_LOGGER_TRACE(m_logger, "{} 写入WAV文件成功: {}, 大小: {} 字节", m_logBase, m_wavFileName.toStdString(), wSize); } wavFile.close(); // SPDLOG_LOGGER_DEBUG(m_logger, "{} 写入WAV文件完成: {}, 大小: {} 字节", // m_logBase, m_wavFileName.toStdString(), wSize); /*-------------------------------------------------------------- * 对该文件进行其他操作,判断是否已经过了一个整点,修改其文件名称 * 现在这里的时间是这一分钟的开始时间,现在需要根据开始时间求出已写入 * 数据大小对应的结束时间 *--------------------------------------------------------------*/ m_writtenSize += wSize; /* 修改文件名称 */ QString newFileName = generateFileName(m_writtenStartTime, m_writtenNowTime); if(modifyFileName(m_wavFileName, newFileName)) { m_wavFileName = newFileName; } /* 判断是否过了整点 */ if(isOneHourPassed()) { /* 修改文件头中记录的数据大小 */ m_wavHeader.setDataSize(m_writtenSize); m_wavHeader.calculateDerivedFields(); modifyWavFileHeader(m_wavFileName, m_wavHeader); SPDLOG_LOGGER_INFO(m_logger, "{} 结束记录一个文件: {}, 已写入大小: {} 字节", m_logBase, m_wavFileName.toStdString(), m_writtenSize); /* 重置已写入大小 */ m_writtenSize = 0; m_writtenStartTime = QDateTime(); // 重新开始时间 m_writtenNowTime = m_writtenStartTime; // 重新开始时间 m_wavFileName.clear(); // 清空文件名 m_openFileErrorSize = 0; // 重置错误次数 } } SPDLOG_LOGGER_WARN(m_logger, "➢ {} 记录长文件线程结束运行", m_logBase); } /* 初始化一些数据 */ bool CreateLongFileThread::initData() { m_logBase = fmt::format("录音通道 {}:{} - 记录长文件线程", m_threadInfo.cardRoadInfo.strSoundCardName.toStdString(), m_threadInfo.cardRoadInfo.roadInfo.nRoadNum); /* 获取全局数据 */ m_sampleRate = GInfo.sampleRate(); /* 采样率 */ m_numChannels = GInfo.numChannels(); /* 声道数 */ m_bitsPerSample = GInfo.bitsPerSample(); /* 每个采样点的位数 */ /* 一秒的数据大小 */ m_oneSecondSize = m_sampleRate * m_numChannels * (m_bitsPerSample / 8); /* 一分钟数据大小 */ m_writeCriticalSize = m_oneSecondSize * 60; /* 一小时数据大小 */ m_oneHourSize = m_writeCriticalSize * 60; /* 给缓存分配空间 */ m_bufferData.allocateMemory(m_writeCriticalSize * 3); m_srcData.allocateMemory(m_writeCriticalSize * 3); return true; } /* 清理数据 */ void CreateLongFileThread::clearData() { /* 清理缓存数据 */ m_bufferData.clear(); } /* 设置今日目录 */ bool CreateLongFileThread::setTodayPath(bool isNewFile) { if(!isNewFile) { return true; } /* 判断现在日期是否还是当天日期 */ QDate today = QDate::currentDate(); if(m_todayDateRecord == today) { return true; } m_todayDateRecord = today; /* 先检查现在的日期文件夹是否存在,因为其他线程可能已经创建了 */ QString todayDirName = QString("%1/%2").arg(GInfo.longWavPath()).arg(today.toString("yyyy-MM-dd")); m_todayDir.setPath(todayDirName); if(!m_todayDir.exists()) { if(!m_todayDir.mkpath(todayDirName)) { SPDLOG_LOGGER_ERROR(m_logger, "{} 创建今日目录失败: {}", m_logBase, todayDirName.toStdString()); return false; // 创建目录失败 } else { SPDLOG_LOGGER_INFO(m_logger, "{} 创建今日目录成功: {}", m_logBase, todayDirName.toStdString()); } } /* 创建这个通道的文件夹,文件夹格式: AudioPCI-0 */ QString roadDirName = QString("%1/%2-%3") .arg(todayDirName) .arg(m_threadInfo.cardRoadInfo.strSoundCardID) .arg(QString::number(m_threadInfo.cardRoadInfo.roadInfo.nRoadNum)); m_todayDir.setPath(roadDirName); if(!m_todayDir.exists()) { if(!m_todayDir.mkpath(todayDirName)) { SPDLOG_LOGGER_ERROR(m_logger, "{} 创建目录失败: {}", m_logBase, roadDirName.toStdString()); return false; } else { SPDLOG_LOGGER_INFO(m_logger, "{} 创建目录成功: {}", m_logBase, roadDirName.toStdString()); } } return true; } /* 打开文件 */ bool CreateLongFileThread::openFile(QFile& wavFile, bool isNewFile) { if(isNewFile) { /* 如果没有写入过数据,则生成一个新的文件名 */ m_wavFileName = generateFileName(m_writtenStartTime, m_writtenNowTime); m_wavHeader.setSampleRate(m_sampleRate); m_wavHeader.setNumChannels(m_numChannels); m_wavHeader.setBitsPerSample(m_bitsPerSample); m_wavHeader.setDataSize(m_writtenSize); m_wavHeader.calculateDerivedFields(); wavFile.setFileName(m_wavFileName); if(!wavFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { m_openFileErrorSize ++; SPDLOG_LOGGER_ERROR(m_logger, "{} 打开WAV文件失败: {}", m_logBase, m_wavFileName.toStdString()); SPDLOG_LOGGER_ERROR(m_logger, "错误原因: {}", wavFile.errorString().toStdString()); return false; } wavFile.write(reinterpret_cast(&m_wavHeader), sizeof(WavHeader)); } else { /* 不是新文件 */ wavFile.setFileName(m_wavFileName); if(!wavFile.open(QIODevice::WriteOnly | QIODevice::Append)) { m_openFileErrorSize ++; SPDLOG_LOGGER_ERROR(m_logger, "{} 打开WAV文件失败: {}", m_logBase, m_wavFileName.toStdString()); SPDLOG_LOGGER_ERROR(m_logger, "错误原因: {}", wavFile.errorString().toStdString()); return false; } } m_openFileErrorSize = 0; return true; } /* 写入音频数据到文件 */ bool CreateLongFileThread::writeAudioDataToFile(const AudioSrcData& audioData, const QString& fileName) { return true; } /* 生成文件名 */ QString CreateLongFileThread::generateFileName(const QDateTime& startTime, const QDateTime& endTime) const { // SPDLOG_LOGGER_DEBUG(m_logger, "{} 生成文件名: 开始时间: {}, 结束时间: {}", // m_logBase, startTime.toString("yyyy-MM-dd hh:mm:ss").toStdString(), // endTime.toString("yyyy-MM-dd hh:mm:ss").toStdString()); QString chnannelStr = QString("%1-%2").arg(m_threadInfo.cardRoadInfo.strSoundCardID) .arg(QString::number(m_threadInfo.cardRoadInfo.roadInfo.nRoadNum)); QString fileName = QString("%1_%2-%3.wav") .arg(chnannelStr) .arg(startTime.toString("yyyyMMdd_hhmmss")) .arg(endTime.toString("yyyyMMdd_hhmmss")); /* 通过目录获取文件的全路径,dir不会检查文件是否存在 */ return m_todayDir.filePath(fileName); } /* 判断是否过了整点 */ bool CreateLongFileThread::isOneHourPassed() { if(m_writtenSize >= m_oneHourSize) { return true; // 已经写入的数据大小超过了一小时的大小 } /* 下面是判断刚启动的时候,到下一个整点不足1小时,也会保存文件 */ int minute = m_writtenNowTime.time().minute(); bool isPassed = false; /* 如果当前时间的分钟数小于等于2,并且已经写入的大小超过2分钟的大小 */ if(minute <= 2 && m_writtenSize >= m_writeCriticalSize * 2) { isPassed = true; } /* 或者已经写入的数据大小超过了一小时的大小,则认为过了整点 */ if(m_writtenSize >= m_oneHourSize) { isPassed = true; } return isPassed; } /* 生成报警文件名 */ QString CreateLongFileThread::generateAlarmFileName(const AlarmInfo_t& alarmInfo, bool isNewFile) { if(isNewFile) { /* 先检查是否已经过了一天了,设置日期文件夹 */ setTodayAlarmPath(); /* 检查这个对比项的报警文件夹是否存在 */ QString itemDirName = QString::fromStdString(alarmInfo.strCompareItemName); QDir itemDir = m_todayDirAlarm; itemDir.cd(itemDirName); if(!itemDir.exists()) { if(!itemDir.mkpath(itemDirName)) { SPDLOG_LOGGER_ERROR(m_logger, "{} 创建报警文件夹失败: {}", m_logBase, itemDirName.toStdString()); return QString(); // 创建目录失败 } else { SPDLOG_LOGGER_INFO(m_logger, "{} 创建报警文件夹成功: {}", m_logBase, itemDirName.toStdString()); } } /* 生成文件名, 格式: Alarm_RoadNum_AlarmType_yyyyMMdd_hhmmss-yyyyMMdd_hhmmss.wav */ QString fileName = QString("Alarm_%1_%2_%3-%4.wav") .arg(alarmInfo.RoadInfo.nCompareRoadNum) .arg(getAlarmTypeString(alarmInfo.AlarmType)) .arg(alarmInfo.StartTime.toString("yyyyMMdd_hhmmss")) .arg(alarmInfo.EndTime.toString("yyyyMMdd_hhmmss")); }else { } return QString(); } /* 设置今日报警文件夹 */ bool CreateLongFileThread::setTodayAlarmPath() { /* 先检查当天日期文件夹是否存在,因为其他线程可能已经创建了 */ QDate today = QDate::currentDate(); if(today == m_todayDateAlarm) { return true; // 今天的目录已经存在 } m_todayDateAlarm = today; /* 创建今日报警目录 */ /* 格式: Application/2025-07-21 */ QString todayDirName = QString("%1/%2").arg(GInfo.alarmWavPath()).arg(today.toString("yyyy-MM-dd")); m_todayDirAlarm.setPath(todayDirName); if(!m_todayDirAlarm.exists()) { if(!m_todayDirAlarm.mkpath(todayDirName)) { SPDLOG_LOGGER_ERROR(m_logger, "{} 创建今日报警目录失败: {}", m_logBase, todayDirName.toStdString()); return false; // 创建目录失败 } else { SPDLOG_LOGGER_INFO(m_logger, "{} 创建今日报警目录成功: {}", m_logBase, todayDirName.toStdString()); } } /* 设置目录 */ m_todayDirAlarm.setPath(todayDirName); QString yesterdayDirName = QString("%1/%2").arg(GInfo.alarmWavPath()).arg(QDate::currentDate().addDays(-1).toString("yyyy-MM-dd")); m_yesterdayDir.setPath(yesterdayDirName); return true; } /* 根据报警类型的枚举获取字符 */ QString CreateLongFileThread::getAlarmTypeString(EAlarmType type) const { switch(type) { case EAlarmType::EAT_Silent: return "Silent"; case EAlarmType::EAT_Overload: return "Overload"; case EAlarmType::EAT_Reversed: return "Reversed"; case EAlarmType::EAR_Consistency: return "Consistency"; case EAlarmType::EAT_Noise: return "Noise"; default: return "Unknown"; } }