ian@0: // Copyright 2011, Ian Hobson. ian@0: // ian@0: // This file is part of gpsynth. ian@0: // ian@0: // gpsynth is free software: you can redistribute it and/or modify ian@0: // it under the terms of the GNU General Public License as published by ian@0: // the Free Software Foundation, either version 3 of the License, or ian@0: // (at your option) any later version. ian@0: // ian@0: // gpsynth is distributed in the hope that it will be useful, ian@0: // but WITHOUT ANY WARRANTY; without even the implied warranty of ian@0: // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ian@0: // GNU General Public License for more details. ian@0: // ian@0: // You should have received a copy of the GNU General Public License ian@0: // along with gpsynth in the file COPYING. ian@0: // If not, see http://www.gnu.org/licenses/. ian@0: ian@0: #include "file_comparer.hpp" ian@0: ian@0: #include "boost_ex.hpp" ian@0: #include "statistics.hpp" ian@0: #include "std_ex.hpp" ian@0: ian@0: #include "boost/bind.hpp" ian@0: #include "boost/algorithm/string/classification.hpp" ian@0: #include "boost/algorithm/string/split.hpp" ian@0: #include "boost/math/special_functions/fpclassify.hpp" ian@0: ian@0: #include ian@0: #include ian@0: #include ian@0: #include ian@0: #include ian@0: ian@0: // maps the frequency range 20-20480Hz to the 0-1 range on a log scale ian@0: // with 0.1 = 1 octave ian@0: namespace { ian@0: ian@0: dsp::Value ConvertFrequency(dsp::Value frequency) { ian@0: frequency = stdx::Clamp(frequency, 20.0, 20480.0); ian@0: return (::log2(frequency / 440.0) * 10 + 44.5943) / 100.0; ian@0: } ian@0: ian@0: void ConvertFrequencyList(dsp::ValueList& frequencies) { ian@0: std::transform(frequencies.begin(), frequencies.end(), frequencies.begin(), ian@0: &ConvertFrequency); ian@0: } ian@0: ian@0: } // namespace ian@0: ian@0: namespace dsp { ian@0: ian@0: // The FeatureComparer class abstracts common code required by feature set ian@0: // comparisons. ian@0: template ian@0: class FeatureComparer : public FeatureComparerInterface { ian@0: public: ian@0: typedef void (*ConversionFunction)(T&); ian@0: typedef const T& (FeatureExtractor::*FeatureFunction)(); ian@0: typedef typename T::value_type ValueT; ian@0: typedef typename T::const_iterator IteratorT; ian@0: ian@0: private: ian@0: // ian@0: FeatureExtractor* extractor_; ian@0: // A member function of FeatureExtractor that retrieves an audio feature ian@0: FeatureFunction feature_function_; ian@0: // Stores the target feature for comparison ian@0: T target_buffer_; ian@0: // Temporary use stored as member to prevent reallocations ian@0: T comparison_buffer_; ian@0: // Used if a feature data set needs to have a conversion applied to it ian@0: ConversionFunction converter_; ian@0: // the feature's ID ian@0: int id_; ian@0: // true if the feature should be scaled by the frame's energy ian@0: bool scale_by_energy_; ian@0: ian@0: public: ian@0: FeatureComparer(int id, ian@0: FeatureExtractor* extractor, ian@0: FeatureFunction feature_function, ian@0: ConversionFunction converter = NULL, ian@0: bool scale_by_energy = false) ian@0: : id_(id), ian@0: extractor_(extractor), ian@0: feature_function_(feature_function), ian@0: converter_(converter), ian@0: scale_by_energy_(scale_by_energy) ian@0: {} ian@0: ian@0: // copy constructor ian@0: FeatureComparer(const FeatureComparer& other) ian@0: : id_(other.id_), ian@0: extractor_(other.extractor_), ian@0: feature_function_(other.feature_function_), ian@0: converter_(other.converter_), ian@0: scale_by_energy_(other.scale_by_energy_), ian@0: target_buffer_(other.target_buffer_) // copy the target value buffer ian@0: // no need to copy comparison buffer ian@0: {} ian@0: ian@0: virtual void AnalyzeTarget() { ian@0: // retrieve the target feature ian@0: target_buffer_ = (extractor_->*feature_function_)(); ian@0: if (converter_ != NULL) { ian@0: converter_(target_buffer_); ian@0: } ian@0: if (scale_by_energy_) { ian@0: std::transform(target_buffer_.begin(), target_buffer_.end(), ian@0: extractor_->Energy().begin(), ian@0: target_buffer_.begin(), ian@0: std::multiplies()); ian@0: } ian@0: } ian@0: ian@0: virtual Value Compare() { ian@0: // get the values to compare ian@0: const ValueList& feature = (extractor_->*feature_function_)(); ian@0: ValueList* feature_buffer; ian@0: if (converter_ != NULL || scale_by_energy_) { ian@0: comparison_buffer_ = feature; ian@0: if (converter_ != NULL) { ian@0: converter_(comparison_buffer_); ian@0: } ian@0: if (scale_by_energy_) { ian@0: std::transform(comparison_buffer_.begin(), comparison_buffer_.end(), ian@0: extractor_->Energy().begin(), ian@0: comparison_buffer_.begin(), ian@0: std::multiplies()); ian@0: } ian@0: feature_buffer = &comparison_buffer_; ian@0: } else { ian@0: feature_buffer = const_cast(&feature); ian@0: } ian@0: // call conversion function ian@0: ian@0: // find range of buffer to compare against target ian@0: IteratorT buffer_start = feature_buffer->begin(); ian@0: IteratorT buffer_end; ian@0: if (feature_buffer->size() > target_buffer_.size()) { ian@0: buffer_end = buffer_start + target_buffer_.size(); ian@0: } else { ian@0: buffer_end = feature_buffer->end(); ian@0: } ian@0: // return root mean square error of buffer differences ian@0: return std::sqrt(stats::MeanSquaredError(buffer_start, ian@0: buffer_end, ian@0: target_buffer_.begin())); ian@0: } ian@0: ian@0: virtual FeatureComparerInterface* Clone() const { ian@0: return new FeatureComparer(*this); ian@0: } ian@0: ian@0: virtual void SetExtractor(FeatureExtractor* extractor) { ian@0: extractor_ = extractor; ian@0: } ian@0: ian@0: virtual int ID() const { return id_; } ian@0: }; ian@0: ian@0: // specializations for std::vector ian@0: template<> ian@0: void FeatureComparer >::AnalyzeTarget() { ian@0: target_buffer_ = (extractor_->*feature_function_)(); ian@0: if (converter_ != NULL) { ian@0: converter_(target_buffer_); ian@0: } ian@0: // for vector features we can scale by energy on a frame by frame basis ian@0: } ian@0: ian@0: template<> ian@0: Value FeatureComparer >::Compare() { ian@0: // get the values to compare ian@0: const std::vector& feature = (extractor_->*feature_function_)(); ian@0: std::vector* feature_buffer; ian@0: // call conversion function ian@0: if (converter_ != NULL) { ian@0: comparison_buffer_ = feature; ian@0: converter_(comparison_buffer_); ian@0: feature_buffer = &comparison_buffer_; ian@0: } else { ian@0: // this isn't pretty, but it's the only way to allow avoiding a copy ian@0: // when no conversion is taking place.. ian@0: feature_buffer = const_cast*>(&feature); ian@0: } ian@0: // find how many frames to compare ian@0: std::size_t frames_to_compare; ian@0: if (feature_buffer->size() > target_buffer_.size()) { ian@0: frames_to_compare = target_buffer_.size(); ian@0: } else { ian@0: frames_to_compare = feature_buffer->size(); ian@0: } ian@0: // take average of RMSE over all frames ian@0: Value error = 0; ian@0: const ValueList& energy = extractor_->Energy(); ian@0: if (scale_by_energy_) { ian@0: for (std::size_t i = 0; i < frames_to_compare; ++i) { ian@0: Value frame_error; ian@0: frame_error = std::sqrt(stats::MeanSquaredError(feature_buffer->at(i), ian@0: target_buffer_[i])); ian@0: error += frame_error * energy[i]; ian@0: } ian@0: } else { ian@0: for (std::size_t i = 0; i < frames_to_compare; ++i) { ian@0: error += std::sqrt(stats::MeanSquaredError(feature_buffer->at(i), ian@0: target_buffer_[i])); ian@0: } ian@0: } ian@0: return error / frames_to_compare; ian@0: } ian@0: ian@0: ian@0: FileComparer::FileComparer(int window_size /* = 1024 */, ian@0: int hop_size /* = 256 */) ian@0: : extractor_("", window_size, hop_size), ian@0: target_duration_(0) ian@0: { ian@0: EnableFeature(Feature::LogMagnitude); ian@0: EnableFeature(Feature::Pitch); ian@0: } ian@0: ian@0: FileComparer::FileComparer(const std::string& feature_list, ian@0: int window_size /* = 1024 */, ian@0: int hop_size /* = 256 */) ian@0: : extractor_("", window_size, hop_size), ian@0: target_duration_(0) ian@0: { ian@0: feature_names_["pitch"] = Feature::Pitch; ian@0: feature_names_["energy"] = Feature::Energy; ian@0: feature_names_["mfccs"] = Feature::MFCCs; ian@0: feature_names_["dmfccs"] = Feature::DeltaMFCCs; ian@0: feature_names_["ddmfccs"] = Feature::DoubleDeltaMFCCs; ian@0: feature_names_["mag"] = Feature::Magnitude; ian@0: feature_names_["logmag"] = Feature::LogMagnitude; ian@0: feature_names_["centroid"] = Feature::SpectralCentroid; ian@0: feature_names_["spread"] = Feature::SpectralSpread; ian@0: feature_names_["flux"] = Feature::SpectralFlux; ian@0: EnableFeatures(feature_list); ian@0: } ian@0: ian@0: FileComparer::FileComparer(const FileComparer& other) ian@0: : target_file_(other.target_file_), ian@0: target_duration_(other.target_duration_), ian@0: extractor_("", ian@0: other.extractor_.WindowSize(), ian@0: other.extractor_.HopSize()) ian@0: { ian@0: foreach (const FeatureComparerPtr& pointer, other.features_) { ian@0: // clone the feature comparer ian@0: features_.push_back(FeatureComparerPtr(pointer->Clone())); ian@0: features_.back()->SetExtractor(&extractor_); ian@0: } ian@0: } ian@0: ian@0: void FileComparer::SetFeatureExtractorSettings(int window_size, int hop_size) { ian@0: bool reload_target = false; ian@0: if (extractor_.WindowSize() != window_size) { ian@0: extractor_.SetWindowSize(window_size); ian@0: reload_target = true; ian@0: } ian@0: if (extractor_.HopSize() != hop_size) { ian@0: extractor_.SetHopSize(hop_size); ian@0: reload_target = true; ian@0: } ian@0: if (reload_target && !target_file_.empty()) { ian@0: SetTargetFile(target_file_); ian@0: } ian@0: } ian@0: ian@0: void FileComparer::SetTargetFile(const std::string& target_file) { ian@0: if (target_file_ != target_file) { ian@0: target_file_ = target_file; ian@0: extractor_.LoadFile(target_file); ian@0: target_duration_ = extractor_.Duration(); ian@0: std::for_each(features_.begin(), features_.end(), ian@0: boost::bind(&FeatureComparerInterface::AnalyzeTarget, _1)); ian@0: } ian@0: } ian@0: ian@0: ian@0: Value FileComparer::CompareFile(const std::string& file_path) { ian@0: extractor_.LoadFile(file_path); ian@0: Value error; ian@0: foreach (FeatureComparerPtr& feature, features_) { ian@0: error += feature->Compare(); ian@0: } ian@0: return error / features_.size(); ian@0: } ian@0: ian@0: namespace { ian@0: ian@0: // Helper function for creating FeatureComparers ian@0: template ian@0: FeatureComparerPtr MakeFeatureComparer(FileComparer::Feature::ID id, ian@0: FeatureExtractor& extractor, ian@0: const T& (FeatureExtractor::*feature)(), ian@0: void (*converter)(T&) = NULL, ian@0: bool scale_by_energy = false) { ian@0: return FeatureComparerPtr(new FeatureComparer(static_cast(id), ian@0: &extractor, ian@0: feature, ian@0: converter, ian@0: scale_by_energy)); ian@0: } ian@0: ian@0: } // namespace ian@0: ian@0: void FileComparer::EnableFeature(Feature::ID feature_id, bool enable) { ian@0: // check if the requested feature is already enabled ian@0: for (std::vector::iterator feature = features_.begin(), ian@0: end = features_.end(); ian@0: feature != end; ian@0: ++feature) { ian@0: if ((*feature)->ID() == feature_id) { ian@0: // feature enabled, if enable == false then remove the feature ian@0: if (!enable) { ian@0: features_.erase(feature); ian@0: } ian@0: return; ian@0: } ian@0: } ian@0: // make the feature comparer ian@0: FeatureComparerPtr feature; ian@0: switch (feature_id) { ian@0: case Feature::Pitch: ian@0: feature = MakeFeatureComparer(feature_id, ian@0: extractor_, ian@0: &FeatureExtractor::Pitch, ian@0: ConvertFrequencyList, ian@0: true); ian@0: break; ian@0: case Feature::Energy: ian@0: feature = MakeFeatureComparer(feature_id, ian@0: extractor_, ian@0: &FeatureExtractor::Energy); ian@0: break; ian@0: case Feature::MFCCs: ian@0: feature = MakeFeatureComparer(feature_id, ian@0: extractor_, ian@0: &FeatureExtractor::MFCCs); ian@0: break; ian@0: case Feature::DeltaMFCCs: ian@0: feature = MakeFeatureComparer(feature_id, ian@0: extractor_, ian@0: &FeatureExtractor::DeltaMFCCs); ian@0: break; ian@0: case Feature::DoubleDeltaMFCCs: ian@0: feature = MakeFeatureComparer(feature_id, ian@0: extractor_, ian@0: &FeatureExtractor::DoubleDeltaMFCCs); ian@0: break; ian@0: case Feature::Magnitude: ian@0: feature = MakeFeatureComparer(feature_id, ian@0: extractor_, ian@0: &FeatureExtractor::MagnitudeSpectrum); ian@0: case Feature::LogMagnitude: ian@0: feature = MakeFeatureComparer(feature_id, ian@0: extractor_, ian@0: &FeatureExtractor::LogMagnitudeSpectrum); ian@0: break; ian@0: case Feature::SpectralCentroid: ian@0: feature = MakeFeatureComparer(feature_id, ian@0: extractor_, ian@0: &FeatureExtractor::SpectralCentroid, ian@0: ConvertFrequencyList, ian@0: true); ian@0: break; ian@0: case Feature::SpectralSpread: ian@0: feature = MakeFeatureComparer(feature_id, ian@0: extractor_, ian@0: &FeatureExtractor::SpectralSpread, ian@0: ConvertFrequencyList, ian@0: true); ian@0: break; ian@0: case Feature::SpectralFlux: ian@0: feature = MakeFeatureComparer(feature_id, ian@0: extractor_, ian@0: &FeatureExtractor::SpectralFlux); ian@0: break; ian@0: default: ian@0: throw std::runtime_error("FileComparer::EnableFeature - Invalid ID"); ian@0: } ian@0: // analyze the feature if we already have a target file loaded ian@0: if (!target_file_.empty()) { ian@0: feature->AnalyzeTarget(); ian@0: } ian@0: // store the feature ian@0: features_.push_back(feature); ian@0: } ian@0: ian@0: void FileComparer::EnableFeatures(const std::vector& features, ian@0: bool enable /* = true */) { ian@0: foreach (Feature::ID id, features) { ian@0: EnableFeature(id, enable); ian@0: } ian@0: } ian@0: ian@0: void FileComparer::EnableFeatures(std::string feature_name_list, ian@0: bool enable /* = true */) { ian@0: std::vector feature_names; ian@0: // split the comma separated list of names ian@0: boost::algorithm::split(feature_names, ian@0: feature_name_list, ian@0: boost::algorithm::is_any_of(", "), ian@0: boost::algorithm::token_compress_on); ian@0: // enable each feature ian@0: foreach (const std::string& feature_name, feature_names) { ian@0: // check if the requested feature name is valid ian@0: if (feature_names_.find(feature_name) == feature_names_.end()) { ian@0: std::stringstream message; ian@0: message << "FileComparer::EnableFeatures - Unknown feature '" ian@0: << feature_name << "'"; ian@0: throw std::runtime_error(message.str()); ian@0: } ian@0: EnableFeature(feature_names_[feature_name], enable); ian@0: } ian@0: } ian@0: ian@0: void FileComparer::SetFeatures(const std::vector& features) { ian@0: features_.clear(); ian@0: EnableFeatures(features); ian@0: } ian@0: ian@0: } // dsp namespace