#define NOMINMAX //#include "config.h" #include "MT_SevanaMos.h" #if defined(USE_PVQA_LIBRARY) #if defined(TARGET_SERVER) # include # include using namespace boost::filesystem; #endif #include "../engine/helper/HL_Log.h" #include "../engine/helper/HL_CsvReader.h" #include "../engine/helper/HL_String.h" #include "../engine/audio/Audio_WavFile.h" #include #include #include #include #include #include #if defined(TARGET_SERVER) extern std::string IntervalCacheDir; #endif #define LOG_SUBSYSTEM "Sevana" #define PVQA_ECHO_DETECTOR_NAME "ECHO" //#define PVQA_ECHO_DETECTOR_NAME "EchoM-00" namespace MT { #if !defined(MOS_BEST_COLOR) # define MOS_BEST_COLOR 0x11FF11 # define MOS_BAD_COLOR 0x000000 #endif #if defined(TARGET_WIN) # define popen _popen # define pclose _pclose #endif static std::string execCommand(const std::string& cmd) { std::cout << cmd << "\n"; std::shared_ptr pipe(popen(cmd.c_str(), "r"), pclose); if (!pipe) throw std::runtime_error("Failed to run."); char buffer[1024]; std::string result = ""; while (!feof(pipe.get())) { if (fgets(buffer, 1024, pipe.get()) != nullptr) result += buffer; } return result; } // -------------- SevanaMosUtility -------------- void SevanaMosUtility::run(const std::string& pcmPath, const std::string& intervalPath, std::string& estimation, std::string& intervals) { #if defined(TARGET_SERVER) path sevana = current_path() / "sevana"; #if defined(TARGET_LINUX) || defined(TARGET_OSX) path exec = sevana / "pvqa"; #else path exec = sevana / "pvqa.exe"; #endif path lic = sevana / "pvqa.lic"; path cfg = sevana / "settings.cfg"; estimation.clear(); char cmdbuffer[1024]; sprintf(cmdbuffer, "%s %s analysis %s %s %s 0.799", exec.string().c_str(), lic.string().c_str(), intervalPath.c_str(), cfg.string().c_str(), pcmPath.c_str()); std::string output = execCommand(cmdbuffer); //ICELogDebug(<< "Got PVQA analyzer output: " << output); std::string line; std::istringstream is(output); while (std::getline(is, line)) { std::string::size_type mosPosition = line.find("MOS = "); if ( mosPosition != std::string::npos) { estimation = line.substr(mosPosition + 6); boost::algorithm::trim(estimation); } } if (!estimation.size()) { // Dump utility output if estimation failed ICELogError(<< "PVQA failed with message: " << output); return; } // Read intervals report file if (boost::filesystem::exists(intervalPath) && !estimation.empty()) { std::ifstream t(intervalPath); std::string str((std::istreambuf_iterator(t)), std::istreambuf_iterator()); intervals = str; } #endif } float getSevanaMos(const std::string& audioPath, const std::string& intervalReportPath, std::string& intervalReport) { // Find Sevana MOS estimation ICELogDebug( << "Running MOS utitlity on resulted PCM file " << audioPath ); try { std::string buffer; SevanaMosUtility::run(audioPath, intervalReportPath, buffer, intervalReport); ICELogDebug( << "MOS utility is finished on PCM file " << audioPath ); return (float)atof(buffer.c_str()); } catch(std::exception& e) { ICELogError( << "MOS utility failed on PCM file " << audioPath << ". Error msg: " << e.what() ); return 0.0; } } // ------------------- SevanaPVQA ------------------- void* SevanaPVQA::mLibraryConfiguration = nullptr; int SevanaPVQA::mLibraryErrorCode = 0; std::atomic_int SevanaPVQA::mInstanceCounter; std::atomic_uint_least64_t SevanaPVQA::mAllProcessedMilliseconds; bool SevanaPVQA::mPvqaLoaded = false; std::string SevanaPVQA::getVersion() { return PVQA_GetVersion(); } #if defined(TARGET_ANDROID) void SevanaPVQA::setupAndroidEnvironment(void *environment, void *appcontext) { PVQA_SetupAndroidEnvironment(environment, appcontext); } #endif bool SevanaPVQA::initializeLibrary(const std::string& pathToLicenseFile, const std::string& pathToConfigFile) { mPvqaLoaded = false; ICELogInfo(<< "Sevana PVQA is about to be initialized."); // Initialize PVQA library if (!mLibraryConfiguration) { mInstanceCounter = 0; mLibraryErrorCode = PVQA_InitLib(const_cast(pathToLicenseFile.c_str())); if (mLibraryErrorCode) { ICELogError(<< "Problem when initializing PVQA library. Error code: " << mLibraryErrorCode << ". Path to license file is " << pathToLicenseFile << ". Path to config file is " << pathToConfigFile); return false; } if (pathToConfigFile.size()) { mLibraryConfiguration = PVQA_LoadCFGFile(const_cast(pathToConfigFile.c_str()), &mLibraryErrorCode); if (!mLibraryConfiguration) { PVQA_ReleaseLib(); ICELogError(<< "Problem with PVQA configuration file."); return false; } } mPvqaLoaded = true; } return true; } bool SevanaPVQA::initializeLibraryWithData(const void* license_buffer, size_t license_len, const void* config_buffer, size_t config_len) { mPvqaLoaded = false; ICELogInfo(<< "Sevana PVQA is about to be initialized via byte buffers."); // Initialize PVQA library if (!mLibraryConfiguration) { mInstanceCounter = 0; mLibraryErrorCode = PVQA_InitLibWithLicData(license_buffer, license_len); if (mLibraryErrorCode) { ICELogError(<< "Problem when initializing PVQA library. Error code: " << mLibraryErrorCode); return false; } if (config_buffer && config_len) { mLibraryConfiguration = PVQA_LoadCFGData(config_buffer, config_len, &mLibraryErrorCode); if (!mLibraryConfiguration) { PVQA_ReleaseLib(); ICELogError(<< "Problem with PVQA configuration file."); return false; } } mPvqaLoaded = true; } return true; } bool SevanaPVQA::isInitialized() { return mPvqaLoaded; } int SevanaPVQA::getLibraryError() { return mLibraryErrorCode; } void SevanaPVQA::releaseLibrary() { PVQA_ReleaseLib(); } SevanaPVQA::SevanaPVQA() { } SevanaPVQA::~SevanaPVQA() { close(); } void SevanaPVQA::open(double interval, Model model) { if (!isInitialized()) { ICELogError(<< "PVQA library is not initialized."); return; } if (mOpenFailed) { ICELogError(<< "Open failed already, reject this attempt."); return; } if (mContext) { ICELogError(<< "Already opened (context is not nullptr)."); return; } ICELogDebug(<<"Attempt to create PVQA instance."); mProcessedSamples = 0; mModel = model; mIntervalLength = interval; mAudioLineInitialized = false; mContext = PVQA_CreateAudioQualityAnalyzer(mLibraryConfiguration); if (!mContext) { ICELogError(<< "Failed to create PVQA instance. Instance counter: " << mInstanceCounter); mOpenFailed = true; return; } mInstanceCounter++; int rescode = 0; rescode = PVQA_AudioQualityAnalyzerSetIntervalLength(mContext, interval); if (rescode) { ICELogError(<< "Failed to set interval length on PVQA instance. Result code: " << rescode); close(); mOpenFailed = true; return; } if (mModel == Model::Stream) { rescode = PVQA_OnStartStreamData(mContext); if (rescode) { ICELogError(<< "Failed to start streaming analysis on PVQA instance. Result code: " << rescode); close(); mOpenFailed = true; return; } } ICELogDebug(<<"PVQA instance is created. Instance counter: " << mInstanceCounter); } void SevanaPVQA::close() { if (mContext) { ICELogDebug(<< "Attempt to destroy PVQA instance."); PVQA_ReleaseAudioQualityAnalyzer(mContext); mInstanceCounter--; ICELogDebug(<< "PVQA instance destroyed. Current instance counter: " << mInstanceCounter); mContext = nullptr; mOpenFailed = false; } } bool SevanaPVQA::isOpen() const { return mContext != nullptr; } void SevanaPVQA::update(int samplerate, int channels, const void *pcmBuffer, int pcmLength) { if (!mContext) { ICELogError(<< "No PVQA context."); return; } // Model is assert here as it can be any if context is not created. assert (mModel == Model::Stream); TPVQA_AudioItem item; item.dNChannels = channels; item.dSampleRate = samplerate; item.dNSamples = pcmLength / 2 / channels; item.pSamples = (short*)pcmBuffer; int rescode = PVQA_OnAddStreamAudioData(mContext, &item); if (rescode) { ICELogError(<< "Failed to stream data to PVQA instance. Result code: " << rescode); } int milliseconds = pcmLength / 2 / channels / (samplerate / 1000); mProcessedMilliseconds += milliseconds; mAllProcessedMilliseconds += milliseconds; } SevanaPVQA::DetectorsList SevanaPVQA::getDetectorsNames(const std::string& report) { DetectorsList result; if (!report.empty()) { std::istringstream iss(report); CsvReader reader(iss); reader.readLine(result.mNames); result.mStartIndex = 2; // Remove first columns if (result.mStartIndex < (int)result.mNames.size() - 1) { result.mNames.erase(result.mNames.begin(), result.mNames.begin() + result.mStartIndex); // Remove last column result.mNames.erase(result.mNames.begin() + result.mNames.size() - 1); for (auto& name: result.mNames) name = StringHelper::trim(name); } } return result; } float SevanaPVQA::getResults(std::string& report, EchoData** echo, int samplerate, Codec codec) { if (!mContext) { ICELogError(<< "No PVQA context."); return 0.0; } if (mModel == Model::Stream) { if (mProcessedMilliseconds == 0) { ICELogError(<< "No audio in PVQA."); return -1; } if (PVQA_OnFinalizeStream(mContext, (long)samplerate)) { ICELogError(<< "Failed to finalize results from PVQA."); return -1; } ICELogInfo(<< "Processed " << mProcessedMilliseconds << " milliseconds."); } TPVQA_Results results; if (PVQA_FillQualityResultsStruct(mContext, &results)) { ICELogError(<< "Failed to get results from PVQA."); return -1; } int reportLength = PVQA_GetQualityStringSize(mContext); if (reportLength) { char* buffer = (char*)alloca(reportLength + 1); if (PVQA_FillQualityString(mContext, buffer)) { ICELogError(<< "Failed to fill intervals report."); } else report = buffer; } #if defined(TARGET_LINUX) && defined(PVQA_WITH_ECHO_DATA) if (mModel == SevanaPVQA::Model::Stream && echo) { // Return echo detector counters // Get list of names for echo detector - for debugging only std::vector names; int errCode = 0; const char** iNames = (const char **)PVQA_GetProcessorValuesNamesList(mContext, PVQA_ECHO_DETECTOR_NAME, &errCode); if (!errCode && iNames) { int nameIndex = 0; for(const char * locName = iNames[nameIndex]; locName; locName = iNames[++nameIndex]) names.push_back(locName); // Get values for echo detector PVQA_Array2D* array = PVQA_GetProcessorValuesList(mContext, PVQA_ECHO_DETECTOR_NAME, 0, mProcessedMilliseconds, "values", &errCode); if (array) { *echo = new std::vector>(); for (int r = 0; r < array->rows; r++) { std::vector row; for (int c = 0; c < array->columns; c++) row.push_back(array->data[r * array->columns + c]); (*echo)->push_back(row); } PVQA_ReleaseArray2D(array); array = nullptr; } // For debugging only /*if (*echo) { for (const auto& row: **echo) { std::cout << "<"; for (const auto& v: row) std::cout << v << " "; std::cout << ">" << std::endl; } }*/ // No need to delete maxValues - it will be deleted on PVQA analyzer context freeing. } } #endif // Limit maximal value of MOS depending on codec float result = (float)results.dMOSLike; float mv = 5.0; switch (codec) { case Codec::G711: mv = 4.1f; break; case Codec::G729: mv = 3.92f; break; default: mv = 5.0; } return std::min(result, mv); } void SevanaPVQA::setPathToDumpFile(const std::string& path) { mDumpWavPath = path; } float SevanaPVQA::process(int samplerate, int channels, const void *pcmBuffer, int pcmLength, std::string &report, Codec codec) { //std::cout << "Sent " << pcmLength << " bytes of audio to analyzer." << std::endl; assert (mModel == Model::Interval); if (!mContext) return 0.0; /*if (!mAudioLineInitialized) { mAudioLineInitialized = true; if (PVQA_AudioQualityAnalyzerCreateDelayLine(mContext, samplerate, channels, 20)) ICELogError(<< "Failed to create delay line."); }*/ TPVQA_AudioItem item; item.dNChannels = channels; item.dSampleRate = samplerate; item.dNSamples = pcmLength / 2 / channels; item.pSamples = (short*)pcmBuffer; //std::cout << "Sending chunk of audio with rate = " << samplerate << ", channels = " << channels << ", number of samples " << item.dNSamples << std::endl; /* if (!mDumpWavPath.empty()) { WavFileWriter writer; writer.open(mDumpWavPath, samplerate, channels); writer.write(item.pSamples, item.dNSamples * 2 * channels); writer.close(); ICELogError(<< "Sending chunk of audio with rate = " << samplerate << ", channels = " << channels << ", number of samples " << item.dNSamples); } */ int code = PVQA_OnTestAudioData(mContext, &item); if (code) { ICELogError(<< "Failed to run PVQA on audio buffer with code " << code); return 0.0; } /* if (item.pSamples != pcmBuffer || item.dNSamples != pcmLength / 2 / channels || item.dSampleRate != samplerate || item.dNChannels != channels) { ICELogError(<< "PVQA changed input parameters!!!!"); } */ // Increase counter of processed samples mProcessedSamples += pcmLength / channels / 2; int milliseconds = pcmLength / channels / 2 / (samplerate / 1000); mProcessedMilliseconds += milliseconds; // Overall counter mAllProcessedMilliseconds += milliseconds; // Get results return getResults(report, nullptr, samplerate, codec); } struct RgbColor { uint8_t mRed = 0; uint8_t mGreen = 0; uint8_t mBlue = 0; static RgbColor parse(uint32_t rgb) { RgbColor result; result.mBlue = (uint8_t)(rgb & 0xff); result.mGreen = (uint8_t)((rgb >> 8) & 0xff); result.mRed = (uint8_t)((rgb >> 16) & 0xff); return result; } std::string toHex() const { char result[7]; sprintf(result, "%02x%02x%02x", int(mRed), int(mGreen), int(mBlue)); return std::string(result); } }; int SevanaPVQA::getSize() const { int result = 0; result += sizeof(*this); // TODO: add PVQA analyzer size return result; } std::string SevanaPVQA::mosToColor(float mos) { // Limit MOS value by 5.0 mos = mos > 5.0f ? 5.0f : mos; mos = mos < 1.0f ? 1.0f : mos; // Split to components RgbColor start = RgbColor::parse(MOS_BEST_COLOR), end = RgbColor::parse(MOS_BAD_COLOR); float mosFraction = (mos - 1.0f) / 4.0f; end.mBlue += (uint8_t)((start.mBlue - end.mBlue) * mosFraction); end.mGreen += (uint8_t)((start.mGreen - end.mGreen) * mosFraction); end.mRed += (uint8_t)((start.mRed - end.mRed) * mosFraction); return end.toHex(); } } // end of namespace MT #endif #if defined(USE_AQUA_LIBRARY) #include #include "helper/HL_String.h" #include #include namespace MT { int SevanaAqua::initializeLibrary(const std::string& pathToLicenseFile) { //char buffer[pathToLicenseFile.length() + 1]; //strcpy(buffer, pathToLicenseFile.c_str()); return SSA_InitLib(const_cast(pathToLicenseFile.data())); } int SevanaAqua::initializeLibrary(const void* buffer, size_t len) { return SSA_InitLibWithData(buffer, len); } void SevanaAqua::releaseLibrary() { SSA_ReleaseLib(); } std::string SevanaAqua::FaultsReport::toText() const { std::ostringstream oss; if (mSignalAdvancedInMilliseconds > -4999.0) oss << "Signal advanced in milliseconds: " << mSignalAdvancedInMilliseconds << std::endl; if (mMistimingInPercents > -4999.0) oss << "Mistiming in percents: " << mMistimingInPercents << std::endl; for (ResultMap::const_iterator resultIter = mResultMap.begin(); resultIter != mResultMap.end(); resultIter++) { oss << resultIter->first << ":\t\t\t" << resultIter->second.mSource << " : \t" << resultIter->second.mDegrated << " \t" << resultIter->second.mUnit << std::endl; } return oss.str(); } Json::Value SevanaAqua::FaultsReport::toJson() const { std::ostringstream oss; Json::Value result; result["Mistiming"] = mMistimingInPercents; result["SignalAdvanced"] = mSignalAdvancedInMilliseconds; Json::Value items; for (ResultMap::const_iterator resultIter = mResultMap.begin(); resultIter != mResultMap.end(); resultIter++) { Json::Value item; item["name"] = resultIter->first; item["source"] = resultIter->second.mSource; item["degrated"] = resultIter->second.mDegrated; item["unit"] = resultIter->second.mUnit; items.append(item); } result["items"] = items; return result; } std::string SevanaAqua::getVersion() { TSSA_AQuA_Info* info = SSA_GetPAQuAInfo(); if (info) return info->dVersionString; return ""; } SevanaAqua::SevanaAqua() { open(); } SevanaAqua::~SevanaAqua() { close(); } void SevanaAqua::open() { std::unique_lock l(mMutex); if (mContext) return; mContext = SSA_CreateAudioQualityAnalyzer(); if (!mContext) ; //setParam("OutputFormats", "json"); } void SevanaAqua::close() { std::unique_lock l(mMutex); if (!mContext) return; SSA_ReleaseAudioQualityAnalyzer(mContext); mContext = nullptr; } bool SevanaAqua::isOpen() const { return mContext != nullptr; } void SevanaAqua::setTempPath(const std::string& temp_path) { mTempPath = temp_path; } std::string SevanaAqua::getTempPath() const { return mTempPath; } SevanaAqua::CompareResult SevanaAqua::compare(AudioBuffer& reference, AudioBuffer& test) { // Clear previous temporary file if (!mTempPath.empty()) ::remove(mTempPath.c_str()); // Result value CompareResult r; std::unique_lock l(mMutex); if (!mContext || !reference.isInitialized() || !test.isInitialized()) return r; // Make analysis TSSA_AQuA_AudioData aad; aad.dSrcData.dNChannels = reference.mChannels; aad.dSrcData.dSampleRate = reference.mRate; aad.dSrcData.pSamples = (short*)reference.mData->data(); aad.dSrcData.dNSamples = (long)reference.mData->size() / 2 / reference.mChannels; aad.dTstData.dNChannels = test.mChannels; aad.dTstData.dSampleRate = test.mRate; aad.dTstData.pSamples = (short*)test.mData->data(); aad.dTstData.dNSamples = (long)test.mData->size() / 2 / test.mChannels; int rescode; rescode = SSA_OnTestAudioData(mContext, &aad); if (rescode) return r; // Get results int len = SSA_GetQualityStringSize(mContext); char* qs = (char*)alloca(len + 10); SSA_FillQualityString(mContext, qs); //std::cout << qs << std::endl; std::istringstream iss(qs); while (!iss.eof()) { std::string l; std::getline(iss, l); // Split by : std::vector p; StringHelper::split(l, p, "\t"); if (p.size() == 3) { p[1] = StringHelper::trim(p[1]); p[2] = StringHelper::trim(p[2]); r.mReport[p[1]] = p[2]; } } len = SSA_GetSrcSignalSpecSize(mContext); float* srcSpecs = new float[len]; SSA_FillSrcSignalSpecArray(mContext, srcSpecs); Json::Value src_spec_signal; for(int i=0; i<16 && i 0) { faults_str = new char[faults_str_len + 1]; SSA_FillFaultsAnalysisString(mContext, faults_str); faults_str[faults_str_len] = 0; } } char* pairs_str = nullptr; int pairs_str_len = SSA_GetSpecPairsStringSize(mContext); if (pairs_str_len > 0) { char *pairs_str = new char[pairs_str_len + 1]; SSA_FillSpecPairsString(mContext, pairs_str, pairs_str_len); pairs_str[pairs_str_len] = 0; } TSSA_AQuA_Results iResults; SSA_FillQualityResultsStruct(mContext, &iResults); r.mReport["dPercent"] = iResults.dPercent; r.mReport["dMOSLike"] = iResults.dMOSLike; if (faults_str_len > 0) { std::istringstream iss(faults_str); r.mFaults = loadFaultsReport(iss); } else if (!mTempPath.empty()) { std::ifstream ifs(mTempPath.c_str()); r.mFaults = loadFaultsReport(ifs); } delete[] faults_str; faults_str = nullptr; delete[] pairs_str; pairs_str = nullptr; r.mMos = (float)iResults.dMOSLike; return r; } void SevanaAqua::configureWith(const Config& config) { if (!mContext) return; for (auto& item: config) { const std::string& name = item.first; const std::string& value = item.second; if (!SSA_SetAnyString(mContext, const_cast(name.c_str()), const_cast(value.c_str()))) throw std::runtime_error(std::string("SSA_SetAnyString returned failed for pair ") + name + " " + value); } } SevanaAqua::Config SevanaAqua::parseConfig(const std::string& line) { Config result; // Split command line to parts std::vector pl; StringHelper::split(line, pl, "-"); for (const std::string& s: pl) { std::string::size_type p = s.find(' '); if (p != std::string::npos) { std::string name = StringHelper::trim(s.substr(0, p)); std::string value = StringHelper::trim(s.substr(p + 1)); result[name] = value; } } return result; } SevanaAqua::PFaultsReport SevanaAqua::loadFaultsReport(std::istream& input) { PFaultsReport result = std::make_shared(); std::string line; std::vector parts; // Parse output while (!input.eof()) { std::getline(input, line); if (line.size() < 3) continue; std::string::size_type p = line.find(":"); if (p != std::string::npos) { std::string name = StringHelper::trim(line.substr(0, p)); FaultsReport::Result r; // Split report line to components parts.clear(); StringHelper::split(line.substr(p + 1), parts, " \t"); // Remove empty components parts.erase(std::remove_if(parts.begin(), parts.end(), [](const std::string& item){return item.empty();}), parts.end()); if (parts.size() >= 2) { r.mSource = parts[0]; r.mDegrated = parts[1]; if (parts.size()> 2) r.mUnit = parts[2]; result->mResultMap[name] = r; } } else { p = line.find("ms."); if (p != std::string::npos) { parts.clear(); StringHelper::split(line, parts, " \t"); if (parts.size() >= 3) { if (parts.back() == "ms.") result->mSignalAdvancedInMilliseconds = (float)std::atof(parts[parts.size() - 2].c_str()); } } else { p = line.find("percent."); if (p != std::string::npos) { parts.clear(); StringHelper::split(line, parts, " \t"); if (parts.size() >= 3) { if (parts.back() == "percent.") result->mMistimingInPercents = (float)std::atof(parts[parts.size() - 2].c_str()); } } } } } return result; } } // end of namespace MT // It is to workaround old AQuA NDK build - it has reference to ftime /*#if defined(TARGET_ANDROID) #include // This was removed from POSIX 2008. int ftime(struct timeb* tb) { struct timeval tv; struct timezone tz; if (gettimeofday(&tv, &tz) < 0) return -1; tb->time = tv.tv_sec; tb->millitm = (tv.tv_usec + 500) / 1000; if (tb->millitm == 1000) { ++tb->time; tb->millitm = 0; } tb->timezone = tz.tz_minuteswest; tb->dstflag = tz.tz_dsttime; return 0; } #endif*/ #endif