diff --git a/app/mediaAnalyser/AvSoundFile.cpp b/app/mediaAnalyser/AvSoundFile.cpp index b720f41..66222fd 100644 --- a/app/mediaAnalyser/AvSoundFile.cpp +++ b/app/mediaAnalyser/AvSoundFile.cpp @@ -5,6 +5,15 @@ #include #include +#include +#include +#include + +const size_t NB_OF_BYTES_24_BITS = 3; +const float MAX_VALUE_24_BITS = 8388607.0; +const std::string CODEC_NAME_24_BITS = "pcm_s24le"; +const std::string SAMPLE_FORMAT_24_BITS = "s32"; + void AvSoundFile::printProgress() { const int p = (float)_cumulOfSamplesAnalysed / _totalNbSamplesToAnalyse * 100; @@ -98,18 +107,48 @@ AvSoundFile::~AvSoundFile() } } +size_t AvSoundFile::getTotalNbSamplesToAnalyse() +{ + size_t newTotalNbSamplesToAnalyse = 0; + for(size_t i = 0; i < _inputSampleRate.size(); i++) + { + newTotalNbSamplesToAnalyse += _forceDurationToAnalyse * _inputSampleRate.at(i) * _inputNbChannels.at(i); + } + return newTotalNbSamplesToAnalyse; +} + +bool AvSoundFile::fillAudioBuffer(float** audioBuffer, size_t& nbSamplesRead, size_t& nbInputChannelAdded) +{ + for(size_t fileIndex = 0; fileIndex < _audioReader.size(); ++fileIndex) + { + avtranscoder::IFrame* dstFrame = _audioReader.at(fileIndex)->readNextFrame(); + + // empty frame: go to the end of process + if(dstFrame == NULL) + { + return false; + } + + size_t inputChannel = 0; + for(size_t channelToAdd = nbInputChannelAdded; + channelToAdd < nbInputChannelAdded + _inputNbChannels.at(fileIndex); ++channelToAdd) + { + audioBuffer[channelToAdd] = (float*)(dstFrame->getData()[inputChannel]); + ++inputChannel; + nbSamplesRead += dstFrame->getAVFrame().nb_samples; + } + nbInputChannelAdded += _inputNbChannels.at(fileIndex); + } + return true; +} + void AvSoundFile::analyse(Loudness::analyser::LoudnessAnalyser& analyser) { // update number of samples to analyse if(_forceDurationToAnalyse) { - size_t newTotalNbSamplesToAnalyse = 0; - for(size_t i = 0; i < _inputSampleRate.size(); i++) - { - newTotalNbSamplesToAnalyse += _forceDurationToAnalyse * _inputSampleRate.at(i) * _inputNbChannels.at(i); - } // set total number of samples to analyse - _totalNbSamplesToAnalyse = newTotalNbSamplesToAnalyse; + _totalNbSamplesToAnalyse = getTotalNbSamplesToAnalyse(); } // open file to print duration @@ -126,34 +165,13 @@ void AvSoundFile::analyse(Loudness::analyser::LoudnessAnalyser& analyser) // Create planar buffer of float data float** audioBuffer = new float*[_nbChannelsToAnalyse]; - bool emptyFrameDecoded = false; - // Decode audio streams + // Analyze audio streams while(!isEndOfAnalysis()) { + // Decode audio streams size_t nbSamplesRead = 0; size_t nbInputChannelAdded = 0; - for(size_t fileIndex = 0; fileIndex < _audioReader.size(); ++fileIndex) - { - avtranscoder::IFrame* dstFrame = _audioReader.at(fileIndex)->readNextFrame(); - - // empty frame: go to the end of process - if(dstFrame == NULL) - { - emptyFrameDecoded = true; - break; - } - - size_t inputChannel = 0; - for(size_t channelToAdd = nbInputChannelAdded; - channelToAdd < nbInputChannelAdded + _inputNbChannels.at(fileIndex); ++channelToAdd) - { - audioBuffer[channelToAdd] = (float*)(dstFrame->getData()[inputChannel]); - ++inputChannel; - nbSamplesRead += dstFrame->getAVFrame().nb_samples; - } - nbInputChannelAdded += _inputNbChannels.at(fileIndex); - } - if(emptyFrameDecoded) + if(!fillAudioBuffer(audioBuffer, nbSamplesRead, nbInputChannelAdded)) break; // Analyse loudness @@ -173,6 +191,129 @@ void AvSoundFile::analyse(Loudness::analyser::LoudnessAnalyser& analyser) delete[] audioBuffer; } +void AvSoundFile::correct(Loudness::analyser::LoudnessAnalyser& analyser, const std::string& outputFilePath, const float gain) +{ + // update number of samples to analyse + if(_forceDurationToAnalyse) + { + // set total number of samples to analyse + _totalNbSamplesToAnalyse = getTotalNbSamplesToAnalyse(); + } + + // open file to print duration + std::ofstream progressOutputFile; + if(! _progressionFileName.empty()) + { + progressOutputFile.open(_progressionFileName.c_str()); + _outputStream = &progressOutputFile; + } + + // init + analyser.initAndStart(_nbChannelsToAnalyse, _inputSampleRate.at(0)); + + // Create planar buffer of float data + float** audioBuffer = new float*[_nbChannelsToAnalyse]; + + // Initialize output encoder + int totalInputNbChannels = 0; + for(size_t i = 0; i < _inputNbChannels.size(); ++i) + totalInputNbChannels += _inputNbChannels.at(i); + + avtranscoder::AudioFrameDesc outputAudioFrameDesc(_audioReader.at(0)->getOutputSampleRate(), totalInputNbChannels, SAMPLE_FORMAT_24_BITS); + avtranscoder::AudioEncoder* encoder = new avtranscoder::AudioEncoder(CODEC_NAME_24_BITS); + encoder->setupAudioEncoder(outputAudioFrameDesc); + + avtranscoder::OutputFile* outputFile = new avtranscoder::OutputFile(outputFilePath); + outputFile->addAudioStream(encoder->getAudioCodec()); + outputFile->beginWrap(); + + // reset counters + _cumulOfSamplesAnalysed = 0; + + while(!isEndOfAnalysis()) + { + // Decode audio streams + size_t nbSamplesRead = 0; + size_t nbInputChannelAdded = 0; + if(!fillAudioBuffer(audioBuffer, nbSamplesRead, nbInputChannelAdded)) + break; + + // Apply gain + const size_t nbSamplesInOneFrame = nbSamplesRead / nbInputChannelAdded; + applyGain(audioBuffer, nbSamplesInOneFrame, gain); + + // Convert corrected frame + const size_t rawDataSize = nbSamplesRead * NB_OF_BYTES_24_BITS; + unsigned char* rawData = new unsigned char[rawDataSize]; + encodePlanarSamplesToInterlacedPcm(audioBuffer, rawData, nbSamplesInOneFrame); + + // Write corrected frame + avtranscoder::CodedData data; + data.copyData(rawData, rawDataSize); + outputFile->wrap(data, 0); + + // Analyse loudness + analyser.processSamples(audioBuffer, nbSamplesInOneFrame); + + // Progress + _cumulOfSamplesAnalysed += nbSamplesRead; + printProgress(); + + delete rawData; + } + + outputFile->endWrap(); + + delete encoder; + delete outputFile; + + // Close progression file + if(! _progressionFileName.empty()) + progressOutputFile.close(); + + // free audio buffer + delete[] audioBuffer; +} + +float AvSoundFile::clipSample(const float value) +{ + if (value > 1.f) { + return 1.f; + } + if (value < -1.f) { + return -1.f; + } + return value; +} + +void AvSoundFile::applyGain(float** audioBuffer, const size_t numberOfSamplesPerChannel, const float gain) +{ + for(size_t channel = 0; channel < _nbChannelsToAnalyse; channel++) + { + for(size_t sample = 0; sample < numberOfSamplesPerChannel; sample++) + { + audioBuffer[channel][sample] = audioBuffer[channel][sample] * gain; + } + } +} + +void AvSoundFile::encodePlanarSamplesToInterlacedPcm(float** planarBuffer, unsigned char* interlacedBuffer, const size_t numberOfSamplesPerChannel) +{ + size_t sampleCounter = 0; + for(size_t sample = 0; sample < numberOfSamplesPerChannel; sample++) + { + for(size_t channel = 0; channel < _nbChannelsToAnalyse; channel++) + { + const size_t offset = sampleCounter++ * NB_OF_BYTES_24_BITS; + const float sampleValueClipped = clipSample(planarBuffer[channel][sample]); + const int sampleValue = (int)(sampleValueClipped * MAX_VALUE_24_BITS); + interlacedBuffer[offset] = sampleValue & 0xff; + interlacedBuffer[offset + 1] = (sampleValue >> 8) & 0xff; + interlacedBuffer[offset + 2] = (sampleValue >> 16) & 0xff; + } + } +} + void AvSoundFile::setDurationToAnalyse(const float durationToAnalyse) { if(durationToAnalyse > 0) diff --git a/app/mediaAnalyser/AvSoundFile.hpp b/app/mediaAnalyser/AvSoundFile.hpp index accf49d..ccd6d51 100644 --- a/app/mediaAnalyser/AvSoundFile.hpp +++ b/app/mediaAnalyser/AvSoundFile.hpp @@ -19,6 +19,7 @@ class AvSoundFile ~AvSoundFile(); void analyse(Loudness::analyser::LoudnessAnalyser& analyser); + void correct(Loudness::analyser::LoudnessAnalyser& analyser, const std::string& outputFilePath, const float gain); /** * @brief Set the output filename of the progress file. @@ -47,6 +48,36 @@ class AvSoundFile */ bool isEndOfAnalysis(); + /** + * @brief Compute the total number of samples to analyse from the several inputs + * sample rate, the number of channels and the expected duration. + * @return the number of samples to analyse + */ + size_t getTotalNbSamplesToAnalyse(); + + /** + * @brief Fill input audio buffer with data from audio readers, and increment the + * number of read channels and the total number of samples read (every read channels). + * @return whether the audiobuffer could be filled + */ + bool fillAudioBuffer(float** audioBuffer, size_t& nbSamplesRead, size_t& nbInputChannelAdded); + + /** + * @brief Apply gain to specified planar audio buffer samples. + */ + void applyGain(float** audioBuffer, const size_t numberOfSamplesPerChannel, const float gain); + + /** + * @brief Clip audio sample value to normalized values [-1.0, 1.0]. + * @return the clipped audio sample value + */ + float clipSample(const float value); + + /** + * @brief Convert planar audio samples buffer into interlaced PCM values buffer. + */ + void encodePlanarSamplesToInterlacedPcm(float** planarBuffer, unsigned char* interlacedBuffer, const size_t numberOfSamplesPerChannel); + private: // for loudness analyser size_t _nbChannelsToAnalyse; diff --git a/app/mediaAnalyser/main.cpp b/app/mediaAnalyser/main.cpp index a68eb75..dbf9218 100644 --- a/app/mediaAnalyser/main.cpp +++ b/app/mediaAnalyser/main.cpp @@ -6,6 +6,7 @@ #include #include #include +#include std::vector parseConfigFile(const std::string& configFilename) { @@ -58,17 +59,18 @@ void printHelp() std::string help; help += "Usage\n"; help += "\tmedia-analyser CONFIG.TXT [--output XMLReportName][--progressionInFile " - "progressionName][--forceDurationToAnalyse durationToAnalyse][--help]\n"; + "progressionName][--forceDurationToAnalyse durationToAnalyse][--correction outputFile][--help]\n"; help += "CONFIG.TXT\n"; help += "\tEach line will be one audio stream analysed by the loudness library.\n"; help += "\tPattern of each line is:\n"; help += "\t[inputFile]=STREAM_INDEX.CHANNEL_INDEX\n"; help += "Command line options\n"; help += "\t--help: display this help\n"; - help += "\t--output: filename of the XML report\n"; help += "\t--progressionInFile: to print the progression in a file instead of in console\n"; + help += "\t--output: filename of the XML report\n"; help += "\t--forceDurationToAnalyse: to force loudness analysis on a specific duration (in seconds). By default this is " "the duration of the input.\n"; + help += "\t--correction: enable loudness correction, and write the corrected streams to the specified output file\n"; std::cout << help << std::endl; } @@ -78,6 +80,9 @@ int main(int argc, char** argv) std::string outputProgressionName; float durationToAnalyse = 0; + bool correction = false; + std::string correctionOutputFile; + // Check required arguments if(argc < 2) { @@ -135,6 +140,19 @@ int main(int argc, char** argv) return 1; } } + else if(arguments.at(argument) == "--correction") + { + correction = true; + try + { + correctionOutputFile = arguments.at(++argument); + } + catch(...) + { + printHelp(); + return 1; + } + } // unknown option continue; } @@ -157,6 +175,18 @@ int main(int argc, char** argv) // Print analyse analyser.printPloudValues(); + const float gain = analyser.getCorrectionGain(); + + if(correction && std::fabs(1.0 - gain) > 0.001) + { + std::cout << "Correction with gain " << gain << std::endl; + AvSoundFile correctedSoundFile(arrayToAnalyse); + correctedSoundFile.setProgressionFile(outputProgressionName); + correctedSoundFile.setDurationToAnalyse(durationToAnalyse); + correctedSoundFile.correct(analyser, correctionOutputFile, gain); + + analyser.printPloudValues(); + } // Write XML std::vector mediaFilenames; @@ -168,7 +198,7 @@ int main(int argc, char** argv) std::stringstream ss; ss << soundFile.getNbChannelsToAnalyse(); ss << " channels"; - writerXml.writeResults(ss.str(), analyser); + writerXml.writeResults(ss.str(), analyser, (correction)? gain : 1.0); } catch(const std::exception& e) { diff --git a/src/loudnessTools/WriteXml.cpp b/src/loudnessTools/WriteXml.cpp index c107e4c..dc0369c 100644 --- a/src/loudnessTools/WriteXml.cpp +++ b/src/loudnessTools/WriteXml.cpp @@ -3,6 +3,7 @@ #include #include #include +#include namespace Loudness { @@ -27,7 +28,7 @@ WriteXml::~WriteXml() xmlFile.close(); } -void WriteXml::writeResults(const std::string& channelType, Loudness::analyser::LoudnessAnalyser& analyser) +void WriteXml::writeResults(const std::string& channelType, Loudness::analyser::LoudnessAnalyser& analyser, const float correctionGain) { xmlFile << "\n"; + << "channelsType=\"" << channelType << "\" "; + if(std::fabs(1.0 - correctionGain) > 0.001) + { + xmlFile << "correctionGain=\"" << getGainAsDb(correctionGain) << "\" "; + } + xmlFile << "date=\"" << getDate() << "\">\n"; xmlFile << "\t" << analyser.getIntegratedLoudness() << "\n"; xmlFile << "\t" << analyser.getIntegratedRange() @@ -155,5 +160,14 @@ std::string WriteXml::getDate() date.assign(buffer); return date; } + +std::string WriteXml::getGainAsDb(const float gain) +{ + std::ostringstream ss; + ss << 20.0 * std::log10(gain); + ss << " dB"; + return ss.str(); +} + } } diff --git a/src/loudnessTools/WriteXml.hpp b/src/loudnessTools/WriteXml.hpp index 7707976..30652bb 100644 --- a/src/loudnessTools/WriteXml.hpp +++ b/src/loudnessTools/WriteXml.hpp @@ -19,7 +19,7 @@ class WriteXml ~WriteXml(); - void writeResults(const std::string& channelType, Loudness::analyser::LoudnessAnalyser& analyser); + void writeResults(const std::string& channelType, Loudness::analyser::LoudnessAnalyser& analyser, const float correctionGain = 1.0); private: void openXMLFile(const std::string& xmlFilename); @@ -28,6 +28,7 @@ class WriteXml std::string writeValues(std::vector datas); std::string replaceXmlSpecialCharacters(std::string& text); std::string getDate(); + std::string getGainAsDb(const float gain); std::ofstream xmlFile; std::vector srcAudioFilenames; diff --git a/tools/travis/install_dependencies.sh b/tools/travis/install_dependencies.sh index b2c70de..0e8e851 100755 --- a/tools/travis/install_dependencies.sh +++ b/tools/travis/install_dependencies.sh @@ -9,6 +9,7 @@ set -x if [[ ! -d "${GTEST_INSTALL}/*" ]]; then git clone https://github.com/google/googletest.git cd googletest + git checkout release-1.10.0 cmake . -DCMAKE_INSTALL_PREFIX=${GTEST_INSTALL} make -k make install