#include "CreateRecordFileThread.h" #include "AudioData.h" #include "GlobalInfo.h" #include "spdlog/spdlog.h" #include "ThreadWriteDBManager.h" #include "SystemConfig.h" #include #include #include #include CreateRecordFileThread::CreateRecordFileThread(RecordThreadInfo_t& threadInfo) : BaseRecordThread(threadInfo) { } CreateRecordFileThread::~CreateRecordFileThread() { } /* 设置数据 */ bool CreateRecordFileThread::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_ringQueue.mutex); auto oldData = m_ringQueue.push(new AudioSrcData(srcData)); /* 新的元素数加1 */ m_numNewAlarmSeconds++; m_numNewRecordSeconds++; if(oldData != nullptr) { delete oldData; oldData = nullptr; } } // 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 CreateRecordFileThread::startRecordLongFile(const OneCompareItemRoadInfo_t& compareItemRoadInfo) { if(compareItemRoadInfo.scRoadInfo.strSoundCardName != m_threadInfo.cardRoadInfo.strSoundCardName || compareItemRoadInfo.scRoadInfo.pcmInfo.strPCMName != m_threadInfo.cardRoadInfo.pcmInfo.strPCMName) { SPDLOG_LOGGER_ERROR(m_logger, "{} 开始录制音频文件失败,通道信息不匹配", m_logBase); return false; } std::lock_guard lock(m_mutexCompareItemRoadInfo); if(m_listCompareItemRoadInfo.contains(compareItemRoadInfo)) { return true; } m_listCompareItemRoadInfo.append(compareItemRoadInfo); m_isRequireRecord.store(true); // 设置为需要录音状态 return true; } /* 停止录制长文件 */ bool CreateRecordFileThread::stopRecordLongFile(const OneCompareItemRoadInfo_t& compareItemRoadInfo) { if(compareItemRoadInfo.scRoadInfo.strSoundCardName != m_threadInfo.cardRoadInfo.strSoundCardName || compareItemRoadInfo.scRoadInfo.pcmInfo.strPCMName != m_threadInfo.cardRoadInfo.pcmInfo.strPCMName) { SPDLOG_LOGGER_ERROR(m_logger, "{} 结束录制音频文件失败,通道信息不匹配", m_logBase); return false; } std::lock_guard lock(m_mutexCompareItemRoadInfo); if(!m_listCompareItemRoadInfo.contains(compareItemRoadInfo)) { // SPDLOG_LOGGER_WARN(m_logger, "{} 停止录制音频文件失败,通道信息不在列表中", m_logBase); return true; } /* 从列表中移除 */ m_listCompareItemRoadInfo.removeAll(compareItemRoadInfo); if(m_listCompareItemRoadInfo.isEmpty()) { /* 如果没有对比项信息了,则设置为不需要录音状态 */ m_isRequireRecord.store(false); clearData(); } return true; } /* 开启录制 */ bool CreateRecordFileThread::startRecordAlarmFile(const AlarmInfo_t& alarmInfo) { { std::lock_guard lock(m_mutexAlarmFile); /* 先检查是否已经在队列中了 */ 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, "{} 对比项ID:{},{}, 通道:{}, 报警类型: {}, 已经在报警录制队列中,无法重复添加", m_logBase, alarmInfo.CompareItemID, alarmInfo.strCompareItemName.toStdString(), alarmInfo.RoadInfo.nCompareRoadNum, static_cast(alarmInfo.AlarmType)); return true; } /* 将数据值插入map中 */ AlarmValue_t value; value.alarmType = alarmInfo.AlarmType; value.startTime = alarmInfo.StartTime; value.endTime = alarmInfo.EndTime; value.numConpareItemID = alarmInfo.CompareItemID; value.strCompareItemName = alarmInfo.strCompareItemName; value.itemRoadInfo = alarmInfo.RoadInfo; m_mapAlarmFile.insert({key, value}); } m_cond_var.notify_one(); return true; } /* 停止录制,alarmInfo既是传入参数,也是传出参数,传出文件路径和开始位置 */ bool CreateRecordFileThread::stopRecordAlarmFile(AlarmInfo_t& alarmInfo) { std::lock_guard lock(m_mutexAlarmFile); for(auto& it : m_mapAlarmFile) { if(it.first.compareItemID == alarmInfo.CompareItemID && it.first.roadNum == alarmInfo.RoadInfo.nCompareRoadNum && it.first.alarmType == alarmInfo.AlarmType && it.second.startTime == alarmInfo.StartTime ) { /* 找到对应的报警信息 */ AlarmValue_t& value = it.second; value.endTime = alarmInfo.EndTime; // 设置结束时间 value.state = eRecordState::eRS_RecordCompleted; // 设置为录音完成 value.fileNameEnd = generateAlarmFileName(value, false); // 生成结束文件名 alarmInfo.strAlarmFilePath = value.fileNameEnd; // 返回文件路径 alarmInfo.AlarmStartPos = value.alarmStartPos; // 返回开始位置 SPDLOG_LOGGER_INFO(m_logger, "{} 停止录制报警文件: {}, 开始位置: {} 秒", m_logBase, value.fileNameEnd.toStdString(), value.alarmStartPos); return true; } } return true; } /** * @brief 生成长文件的线程函数,文件生成逻辑如下 * 1、这里有一个缓冲区,存储音频数据,缓冲区大小是2分钟的数据 * 2、每分钟写入一次文件,文件名格式为:ChannelX_yyyyMMdd_hhmmss-yyyyMMdd_hhmmss.wav * 3、文件总长度是1小时的数据,超过1小时则重新开始记录一个新的文件 * */ void CreateRecordFileThread::task() { SPDLOG_LOGGER_INFO(m_logger, "➢ {} 开启记录文件线程 ", m_logBase); /* 计算一小时的文件大小 */ initData(); while(m_isRunning) { /* 使用条件变量阻塞线程,加快开启报警时的响应速度 */ std::unique_lock lock(m_mutexCondVar); m_cond_var.wait_for(lock, std::chrono::milliseconds(500)); /*-------------------------------------------------------------- * 写入报警文件 *--------------------------------------------------------------*/ writeAlarmFile(); /*-------------------------------------------------------------- * 写入长记录文件 *--------------------------------------------------------------*/ writeLongRecordFile(); } clearData(); SPDLOG_LOGGER_WARN(m_logger, "➢ {} 记录长文件线程结束运行", m_logBase); } /* 初始化一些数据 */ bool CreateRecordFileThread::initData() { m_logBase = fmt::format("录音通道 {}:{} - 录音文件线程", m_threadInfo.cardRoadInfo.strSoundCardName, m_threadInfo.cardRoadInfo.pcmInfo.strPCMName); /* 获取全局数据 */ m_sampleRate = GInfo.sampleRate(); /* 采样率 */ m_numChannels = GInfo.numChannels(); /* 声道数 */ m_bitsPerSample = GInfo.bitsPerSample(); /* 每个采样点的位数 */ /* 一秒的数据大小 */ m_oneSecondSize = m_sampleRate * m_numChannels * (m_bitsPerSample / 8); /* 一次写入的大小,10秒钟 */ // m_writeCriticalSize = m_oneSecondSize * 10; /* 一小时数据大小 */ // m_oneHourSize = m_oneSecondSize * 60 * 60; /* 给缓存分配空间 */ // m_bufferData.allocateMemory(m_writeCriticalSize * 3); // m_srcData.allocateMemory(m_writeCriticalSize * 3); /* 设置环形队列大小 */ // m_ringQueue.setQueueCapacity(GInfo.queueElementCount()); m_ringQueue.setQueueCapacity(180); /* 3分钟数据 */ return true; } /* 清理数据 */ void CreateRecordFileThread::clearData() { /* 清理缓存数据 */ // std::lock_guard lock(m_mutexBuffer); // m_bufferData.clear(); // m_srcData.clear(); std::lock_guard lockSrcData(m_ringQueue.mutex); while(!m_ringQueue.isEmpty()) { auto data = m_ringQueue.front_pop(); if(data != nullptr) { delete data; data = nullptr; } } } /* 写入长记录文件 */ bool CreateRecordFileThread::writeLongRecordFile() { /* 判断是否需要录音 */ std::lock_guard lock(m_mutexCompareItemRoadInfo); if(m_isRequireRecord.load() == false) { /* 没有对比项信息,不进行录音 */ /* 判断当前有没有录音,如果有,则结束录音 */ m_numNewRecordSeconds = 0; return true; } /* 取出最新的 m_numNewRecordSeconds 个音频数据 */ std::list newList; { std::lock_guard lock(m_ringQueue.mutex); /* 判断缓存是否达到写入数据的临界值 */ if(m_numNewRecordSeconds < 10) { return true; // 缓存数据不足,继续等待 } int startPos = m_ringQueue.QueueSize() - m_numNewRecordSeconds; if(startPos < 0) { startPos = 0; } for(int i = startPos; i < m_ringQueue.QueueSize(); i++) { newList.push_back(m_ringQueue[i]); } } /*-------------------------------------------------------------- * 打开文件。写入的时候判断是否到达了整点,如果到达了整点,则关闭文件 * 重新创建一个新的文件 *--------------------------------------------------------------*/ bool isPassed = isOneHourPassed(newList); /* 判断 newList 中有没有跨过整点,如果有则分开,分别写入 */ std::list writeList; std::list writeNewFileList; if(isPassed) { /* 跨过了整点 */ for(const auto& it : newList) { if(it == nullptr || it->dataSize == 0) { continue; } if(it->startTime.time().minute() == 59) { writeList.push_back(it); }else { writeNewFileList.push_back(it); } } } else { writeList = newList; } /* 判断是不是刚启动的新文件 */ if(m_writtenSize == 0) { if(isPassed) { /* 刚启动,并且这几秒内也跨过了整点,直接写入并结束这个文件 */ writeRecordFileData(writeList, true, true); /* 写新的文件 */ writeRecordFileData(writeNewFileList, true, false); }else { /* 刚启动,并且没有跨过整点,直接写入 */ writeRecordFileData(writeList, true, false); } }else { if(isPassed) { if(writeList.size() == 0) { /* 跨过了整点,但是没有数据需要写入 */ endRecordFile(); }else { /* 不是新文件,并且跨过了整点,先写完这个文件,然后结束 */ writeRecordFileData(writeList, false, true); } /* 再写新的文件 */ writeRecordFileData(writeNewFileList, true, false); }else { /* 不是新文件,也没有跨过整点,直接写入 */ writeRecordFileData(writeList, false, false); } } m_numNewRecordSeconds = 0; return true; } /* 写入数据 */ bool CreateRecordFileThread::writeRecordFileData(const std::list& dataList, bool isNewFile, bool isRecordCompleted) { if(dataList.empty()) { return true; } if(isNewFile) { /* 如果没有写入过数据,则是新文件 */ m_writtenStartTime = dataList.front()->startTime; m_writtenNowTime = m_writtenStartTime; // SPDLOG_LOGGER_WARN(m_logger, "新录音文件: {} - {}", m_writtenStartTime.toString("yyyy-MM-dd hh:mm:ss").toStdString(), // m_writtenNowTime.toString("yyyy-MM-dd hh:mm:ss").toStdString()); } /* 设置今日目录 */ if(isNewFile) { if(!setTodayPath(isNewFile)) { return false; } } /* 打开文件 */ 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; // 重置错误次数 return false; // 重新开始记录 } } /*-------------------------------------------------------------- * 将数据写入文件,并记录其携带的时间和写入的数据大小 *--------------------------------------------------------------*/ int64_t wSize = 0; bool isWriteSucess = true; { for(auto it : dataList) { if(it == nullptr || it->dataSize == 0) { continue; } int64_t thisWSize = wavFile.write(it->pData, it->dataSize); if(thisWSize > 0) { wSize += thisWSize; m_writtenNowTime = it->endTime; // 记录当前结束时间 }else { isWriteSucess = false; break; } } } if(isWriteSucess == false) { SPDLOG_LOGGER_ERROR(m_logger, "{} 写入WAV文件失败: {}", m_logBase, wavFile.errorString().toStdString()); SPDLOG_LOGGER_WARN(m_logger, "文件路径:{}", m_wavFileName.toStdString()); wavFile.close(); return false; } SPDLOG_LOGGER_TRACE(m_logger, "{} 写入WAV文件成功: {}, 大小: {} 字节", m_logBase, m_wavFileName.toStdString(), wSize); wavFile.close(); // SPDLOG_LOGGER_DEBUG(m_logger, "写入录音文件成功, 写入的时间段: {} - {}", // m_writtenStartTime.toString("yyyy-MM-dd hh:mm:ss").toStdString(), m_writtenNowTime.toString("yyyy-MM-dd hh:mm:ss").toStdString()); /*-------------------------------------------------------------- * 对该文件进行其他操作,判断是否已经过了一个整点,修改其文件名称 * 现在这里的时间是这一分钟的开始时间,现在需要根据开始时间求出已写入 * 数据大小对应的结束时间 *--------------------------------------------------------------*/ m_writtenSize += wSize; /* 修改文件名称 */ QString newFileName = generateFileName(m_writtenStartTime, m_writtenNowTime); if(modifyFileName(m_wavFileName, newFileName)) { m_wavFileName = newFileName; }else { SPDLOG_LOGGER_ERROR(m_logger, "{} 修改文件名失败: {} -> {}", m_logBase, m_wavFileName.toStdString(), newFileName.toStdString()); } /* 判断是否需要结束文件 */ if(isRecordCompleted) { /* 修改文件头中记录的数据大小 */ 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); } /* 更新文件信息到数据库 */ updateRecordFileInfoToDB(isNewFile, isRecordCompleted); /* 清空已经结束的文件信息 */ if(isRecordCompleted) { /* 重置已写入大小 */ m_writtenSize = 0; m_writtenStartTime = QDateTime(); // 重新开始时间 m_writtenNowTime = QDateTime(); // 重新开始时间 m_wavFileName.clear(); // 清空文件名 m_openFileErrorSize = 0; // 重置错误次数 } return true; } /* 结束文件录制 */ bool CreateRecordFileThread::endRecordFile() { /* 修改文件头中记录的数据大小 */ 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); /* 更新文件信息到数据库 */ updateRecordFileInfoToDB(false, true); /* 清空已经结束的文件信息 */ m_writtenSize = 0; m_writtenStartTime = QDateTime(); // 重新开始时间 m_writtenNowTime = QDateTime(); // 重新开始时间 m_wavFileName.clear(); // 清空文件名 m_openFileErrorSize = 0; // 重置错误次数 return true; } /* 设置今日目录 */ bool CreateRecordFileThread::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(m_todayDateRecord.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(QString::fromStdString(m_threadInfo.cardRoadInfo.strSoundCardName)) .arg(QString::fromStdString(m_threadInfo.cardRoadInfo.pcmInfo.strPCMName)); m_todayDir.setPath(roadDirName); if(!m_todayDir.exists()) { SPDLOG_LOGGER_WARN(m_logger, "{} 记录文件目录不存在: {}", m_logBase, roadDirName.toStdString()); if(!m_todayDir.mkpath(roadDirName)) { SPDLOG_LOGGER_ERROR(m_logger, "{} 创建目录失败: {}", m_logBase, roadDirName.toStdString()); return false; } else { SPDLOG_LOGGER_INFO(m_logger, "{} 创建目录成功: {}", m_logBase, roadDirName.toStdString()); } } return true; } /* 打开文件 */ bool CreateRecordFileThread::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 CreateRecordFileThread::writeAudioDataToFile(const AudioSrcData& audioData, const QString& fileName) { return true; } /* 生成文件名 */ QString CreateRecordFileThread::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(QString::fromStdString(m_threadInfo.cardRoadInfo.strSoundCardName)) .arg(QString::fromStdString(m_threadInfo.cardRoadInfo.pcmInfo.strPCMName)); 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 CreateRecordFileThread::isOneHourPassed() // { // if(m_writtenSize >= m_oneHourSize) // { // // 已经写入的数据大小超过了一小时的大小 // return true; // } // /* 下面是判断刚启动的时候,到下一个整点不足1小时,也会保存文件 */ // int minute = m_writtenNowTime.time().minute(); // bool isPassed = false; // /* 如果当前时间的分钟数小于等于2,并且已经写入的大小超过三分钟大小 */ // if( (minute <= 2) && (m_writtenSize > m_oneSecondSize * 60 * 3) ) // { // isPassed = true; // } // return isPassed; // } bool CreateRecordFileThread::isOneHourPassed(const std::list& dataList) { if(dataList.empty()) { return false; } bool has59Minute = false; bool has00Minute = false; /* 计算这些数据中是否跨过了整点 */ for(const auto& it : dataList) { if(it == nullptr || it->dataSize == 0) { continue; } if(it->startTime.time().minute() == 59) { has59Minute = true; } if(it->startTime.time().minute() == 0) { has00Minute = true; } } if(has59Minute && has00Minute) { return true; } return false; } /* 更新文件信息到数据库 */ void CreateRecordFileThread::updateRecordFileInfoToDB(bool isNewFile, bool isRecordCompleted) { std::list listRecordFileInfo; for(const auto& it : m_listCompareItemRoadInfo) { RecordFileInfo_t recordFileInfo; recordFileInfo.ItemID = it.nCompareItemID; recordFileInfo.ItemName = it.strCompareItemName; recordFileInfo.ItemRoadNum = it.nCompareRoadNum; recordFileInfo.ItemRoadName = it.strCompareRoadName; recordFileInfo.scRoadInfo = it.scRoadInfo; recordFileInfo.FileStartTime = m_writtenStartTime; recordFileInfo.FileEndTime = m_writtenNowTime; recordFileInfo.FilePath = m_wavFileName; recordFileInfo.FileDuration = m_writtenStartTime.secsTo(m_writtenNowTime); recordFileInfo.fileState = isRecordCompleted ? eRecordState::eRS_RecordCompleted : eRecordState::eRS_Recording; listRecordFileInfo.push_back(recordFileInfo); } WriteDB.addRecordFileInfo(listRecordFileInfo, isNewFile); } /* 生成报警文件名 */ QString CreateRecordFileThread::generateAlarmFileName(const AlarmValue_t& value, bool isNewFile) { QString retFileName; if(isNewFile) { /* 先检查是否已经过了一天了,设置日期文件夹 */ setTodayAlarmPath(); /* 检查这个对比项的报警文件夹是否存在 */ // QString itemDirName = m_todayDirAlarm.filePath(QString("CompareItemID_%1").arg(QString::number(value.numConpareItemID))); QString itemDirName = QString("CompareItemID_%1").arg(QString::number(value.numConpareItemID)); QDir itemDir(m_todayDirAlarm.filePath(itemDirName)); if(!itemDir.exists()) { SPDLOG_LOGGER_WARN(m_logger, "{} 对比项报警文件夹不存在: {}", m_logBase, itemDir.path().toStdString()); if(!itemDir.mkpath(".")) { SPDLOG_LOGGER_ERROR(m_logger, "{} 创建对比项报警文件夹失败: {}", m_logBase, itemDir.path().toStdString()); return QString(); // 创建目录失败 } else { SPDLOG_LOGGER_INFO(m_logger, "{} 创建对比项报警文件夹成功: {}", m_logBase, itemDir.path().toStdString()); } } /* 生成文件名, 格式: Alarm_RoadNum_AlarmType_yyyyMMdd_hhmmss-yyyyMMdd_hhmmss.wav 这里创建的录音文件只有开始日期,没有结束日期,等报警结束之后才会有结束日期 */ QString fileName = QString("Road%1_%2_%3-.wav") .arg(QString::number(value.itemRoadInfo.nCompareRoadNum)) .arg(getAlarmTypeString(value.alarmType)) .arg(value.startTime.toString("yyyyMMdd_hhmmss")); /* 拼接文件夹路径 */ retFileName = itemDir.filePath(fileName); // SPDLOG_LOGGER_DEBUG(m_logger, "{} 生成新的报警文件名: {}", m_logBase, retFileName.toStdString()); }else { /* 已有的文件,是报警结束的文件名 */ retFileName = QString::fromStdString(value.fileName.toStdString()); /* 这里的文件名格式是:Alarm_CompareItemID_RoadNum_AlarmType_yyyyMMdd_hhmmss-yyyyMMdd_hhmmss.wav, 这里带上结束时间 */ QString endTimeStr = value.endTime.toString("yyyyMMdd_hhmmss"); /* 替换掉原来的结束时间 */ retFileName.replace("-.wav", QString("-%1.wav").arg(endTimeStr)); // SPDLOG_LOGGER_DEBUG(m_logger, "{} 原文件名:{} 生成报警结束文件名: {}", m_logBase, value.fileName.toStdString(), retFileName.toStdString()); } return retFileName; } /* 写入报警文件 */ void CreateRecordFileThread::writeAlarmFile() { /* 取出环形队列中后100秒的数据,前面20秒给环形队列满的时候缓冲使用 */ std::list newDataList; int writeSize = m_numNewAlarmSeconds > 120 ? m_numNewAlarmSeconds : 120; { std::lock_guard lock(m_ringQueue.mutex); /* 取出环形队列中的数据 */ int size = m_ringQueue.QueueSize(); int start = size - writeSize; if(start < 0) { start = 0; // 防止越界 } /* 取出新数据,往前推10秒 */ for(int i = start; i < size; ++i) { AudioSrcData* data = m_ringQueue[i]; if(data != nullptr) { newDataList.push_back(data); } } } /* 写入已经创建的报警文件的数据,需要先写入已经创建的文件,再创建新的文件,防止 创建新的文件后再次写入重复的数据 */ writeExistingAlarmFileData(newDataList); /* 写入新的报警文件 */ writeNewAlarmFileData(newDataList); /* 处理已经结束的报警,修改文件名,并将其移除队列中 */ for(auto it = m_mapAlarmFile.begin(); it != m_mapAlarmFile.end(); ) { AlarmValue_t& value = it->second; if(value.state == eRecordState::eRS_RecordCompleted) { /* 修改wav头文件,修改写入的数据大小 */ value.wavHeader.setDataSize(value.writtenSize); value.wavHeader.calculateDerivedFields(); modifyWavFileHeader(value.fileName, value.wavHeader); /* 已经结束的报警,修改文件名 */ if(!modifyFileName(value.fileName, value.fileNameEnd)) { SPDLOG_LOGGER_ERROR(m_logger, "修改文件名失败: {} -> {}", value.fileName.toStdString(), value.fileNameEnd.toStdString()); } /* 移除这个报警 */ it = m_mapAlarmFile.erase(it); } else { ++it; // 继续下一个 } } /* 清空标志位 */ m_numNewAlarmSeconds = 0; } /* 创建新的文件 */ void CreateRecordFileThread::createNewAlarmFile(AlarmValue_t& value, const std::list& dataList) { /* 新的报警录音,生成文件名 */ value.fileName = generateAlarmFileName(value, true); SPDLOG_LOGGER_INFO(m_logger, "{} 开始写入报警文件: {}", m_logBase, value.fileName.toStdString()); /* 计算出报警开始时间在报警文件中的位置 */ int startPos = 0; for(const auto& audioData : dataList) { if(audioData == nullptr || audioData->pData == nullptr || audioData->dataSize == 0) { continue; // 跳过空数据 } if(audioData->startTime >= value.startTime) { /* 计算报警开始位置 */ break; } startPos++; } /* 打开文件 */ QFile wavFile(value.fileName); if(!wavFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { SPDLOG_LOGGER_ERROR(m_logger, "{} 打开报警文件失败: {}", m_logBase, value.fileName.toStdString()); SPDLOG_LOGGER_ERROR(m_logger, "错误原因: {}", wavFile.errorString().toStdString()); return; } /* 先写入wav头文件 */ value.wavHeader.setSampleRate(m_sampleRate); value.wavHeader.setNumChannels(m_numChannels); value.wavHeader.setBitsPerSample(m_bitsPerSample); value.wavHeader.setDataSize(m_writtenSize); value.wavHeader.calculateDerivedFields(); wavFile.write(reinterpret_cast(&value.wavHeader), sizeof(WavHeader)); /* 写入音频数据到文件 */ int nowPos = 0; for(auto& audioData : dataList) { if(audioData == nullptr || audioData->pData == nullptr || audioData->dataSize == 0) { continue; // 跳过空数据 } if(nowPos < startPos) { nowPos++; continue; // 跳过前面的数据 } auto writeSize = wavFile.write(reinterpret_cast(audioData->pData), audioData->dataSize); if(writeSize < 0) { SPDLOG_LOGGER_ERROR(m_logger, "{} 写入报警文件失败: {}", m_logBase, value.fileName.toStdString()); SPDLOG_LOGGER_ERROR(m_logger, "错误原因: {}", wavFile.errorString().toStdString()); }else { value.writtenSize += writeSize; // 累加写入大小 } } wavFile.close(); } /* 写入新文件数据 */ void CreateRecordFileThread::writeNewAlarmFileData(std::list dataList) { /* 判断是否有新的报警信息 */ bool isNewAlarm = false; std::lock_guard lock(m_mutexAlarmFile); for(const auto& pair : m_mapAlarmFile) { const AlarmValue_t& value = pair.second; if(value.state == eRecordState::eRS_Init) { isNewAlarm = true; break; } } if(!isNewAlarm) { return; // 没有新的报警信息 } /* 创建新文件并写入数据 */ for(auto& alarmInfo : m_mapAlarmFile) { AlarmValue_t& value = alarmInfo.second; /* 判断是新否是新录音的文件 */ if(value.state == eRecordState::eRS_Init) { /* 创建新的文件,并写入wav头文件 */ createNewAlarmFile(value, dataList); value.state = eRecordState::eRS_Recording; // 设置为录音状态 } } } /* 写入已经创建的报警文件数据 */ void CreateRecordFileThread::writeExistingAlarmFileData(std::list newList) { const int listSize = static_cast(newList.size()); if(m_numNewAlarmSeconds > listSize) { SPDLOG_LOGGER_ERROR(m_logger, "{} 音频队列中数据小于需要写入的新的数据大小", m_logBase); return; } /* 已经打开的文件写入数据 */ std::list dataList; if(m_numNewAlarmSeconds > 0) { /* 取出最后 m_numNewAlarmSeconds 个数据 */ int i = newList.size() - m_numNewAlarmSeconds; for(auto it = newList.end(); i < listSize; ++i) { --it; dataList.push_back(*it); } } /* 写入数据 */ std::lock_guard lock(m_mutexAlarmFile); /* 将数据循环写入到文件中 */ for(auto& alarmInfo : m_mapAlarmFile) { /* 获取报警信息 */ // const AlarmKey_t& key = alarmInfo.first; AlarmValue_t& value = alarmInfo.second; /* 判断是新否是新录音的文件 */ if(eRecordState::eRS_Recording == value.state) { /* 已经存在的文件,直接写入新录制的文件 */ /* 打开文件 */ QFile wavFile(value.fileName); if(!wavFile.open(QIODevice::WriteOnly | QIODevice::Append)) { SPDLOG_LOGGER_ERROR(m_logger, "{} 打开报警文件失败: {}", m_logBase, value.fileName.toStdString()); SPDLOG_LOGGER_ERROR(m_logger, "错误原因: {}", wavFile.errorString().toStdString()); continue; } /* 写入音频数据到文件 */ for(auto& audioData : dataList) { if(audioData == nullptr || audioData->pData == nullptr || audioData->dataSize == 0) { continue; // 跳过空数据 } auto writeSize = wavFile.write(reinterpret_cast(audioData->pData), audioData->dataSize); if(writeSize < 0) { SPDLOG_LOGGER_ERROR(m_logger, "{} 写入报警文件失败: {}", m_logBase, value.fileName.toStdString()); SPDLOG_LOGGER_ERROR(m_logger, "错误原因: {}", wavFile.errorString().toStdString()); break; // 写入失败,跳出循环 }else { value.writtenSize += writeSize; // 累加写入大小 } } wavFile.close(); } } } /* 设置今日报警文件夹 */ bool CreateRecordFileThread::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 CreateRecordFileThread::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"; } }