ian@0
|
1 // Copyright 2011, Ian Hobson.
|
ian@0
|
2 //
|
ian@0
|
3 // This file is part of gpsynth.
|
ian@0
|
4 //
|
ian@0
|
5 // gpsynth is free software: you can redistribute it and/or modify
|
ian@0
|
6 // it under the terms of the GNU General Public License as published by
|
ian@0
|
7 // the Free Software Foundation, either version 3 of the License, or
|
ian@0
|
8 // (at your option) any later version.
|
ian@0
|
9 //
|
ian@0
|
10 // gpsynth is distributed in the hope that it will be useful,
|
ian@0
|
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
|
ian@0
|
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
ian@0
|
13 // GNU General Public License for more details.
|
ian@0
|
14 //
|
ian@0
|
15 // You should have received a copy of the GNU General Public License
|
ian@0
|
16 // along with gpsynth in the file COPYING.
|
ian@0
|
17 // If not, see http://www.gnu.org/licenses/.
|
ian@0
|
18
|
ian@0
|
19 #include "file_comparer.hpp"
|
ian@0
|
20
|
ian@0
|
21 #include "boost_ex.hpp"
|
ian@0
|
22 #include "statistics.hpp"
|
ian@0
|
23 #include "std_ex.hpp"
|
ian@0
|
24
|
ian@0
|
25 #include "boost/bind.hpp"
|
ian@0
|
26 #include "boost/algorithm/string/classification.hpp"
|
ian@0
|
27 #include "boost/algorithm/string/split.hpp"
|
ian@0
|
28 #include "boost/math/special_functions/fpclassify.hpp"
|
ian@0
|
29
|
ian@0
|
30 #include <algorithm>
|
ian@0
|
31 #include <cmath>
|
ian@0
|
32 #include <cstddef>
|
ian@0
|
33 #include <iostream>
|
ian@0
|
34 #include <iterator>
|
ian@0
|
35
|
ian@0
|
36 // maps the frequency range 20-20480Hz to the 0-1 range on a log scale
|
ian@0
|
37 // with 0.1 = 1 octave
|
ian@0
|
38 namespace {
|
ian@0
|
39
|
ian@0
|
40 dsp::Value ConvertFrequency(dsp::Value frequency) {
|
ian@0
|
41 frequency = stdx::Clamp(frequency, 20.0, 20480.0);
|
ian@0
|
42 return (::log2(frequency / 440.0) * 10 + 44.5943) / 100.0;
|
ian@0
|
43 }
|
ian@0
|
44
|
ian@0
|
45 void ConvertFrequencyList(dsp::ValueList& frequencies) {
|
ian@0
|
46 std::transform(frequencies.begin(), frequencies.end(), frequencies.begin(),
|
ian@0
|
47 &ConvertFrequency);
|
ian@0
|
48 }
|
ian@0
|
49
|
ian@0
|
50 } // namespace
|
ian@0
|
51
|
ian@0
|
52 namespace dsp {
|
ian@0
|
53
|
ian@0
|
54 // The FeatureComparer class abstracts common code required by feature set
|
ian@0
|
55 // comparisons.
|
ian@0
|
56 template<typename T>
|
ian@0
|
57 class FeatureComparer : public FeatureComparerInterface {
|
ian@0
|
58 public:
|
ian@0
|
59 typedef void (*ConversionFunction)(T&);
|
ian@0
|
60 typedef const T& (FeatureExtractor::*FeatureFunction)();
|
ian@0
|
61 typedef typename T::value_type ValueT;
|
ian@0
|
62 typedef typename T::const_iterator IteratorT;
|
ian@0
|
63
|
ian@0
|
64 private:
|
ian@0
|
65 //
|
ian@0
|
66 FeatureExtractor* extractor_;
|
ian@0
|
67 // A member function of FeatureExtractor that retrieves an audio feature
|
ian@0
|
68 FeatureFunction feature_function_;
|
ian@0
|
69 // Stores the target feature for comparison
|
ian@0
|
70 T target_buffer_;
|
ian@0
|
71 // Temporary use stored as member to prevent reallocations
|
ian@0
|
72 T comparison_buffer_;
|
ian@0
|
73 // Used if a feature data set needs to have a conversion applied to it
|
ian@0
|
74 ConversionFunction converter_;
|
ian@0
|
75 // the feature's ID
|
ian@0
|
76 int id_;
|
ian@0
|
77 // true if the feature should be scaled by the frame's energy
|
ian@0
|
78 bool scale_by_energy_;
|
ian@0
|
79
|
ian@0
|
80 public:
|
ian@0
|
81 FeatureComparer(int id,
|
ian@0
|
82 FeatureExtractor* extractor,
|
ian@0
|
83 FeatureFunction feature_function,
|
ian@0
|
84 ConversionFunction converter = NULL,
|
ian@0
|
85 bool scale_by_energy = false)
|
ian@0
|
86 : id_(id),
|
ian@0
|
87 extractor_(extractor),
|
ian@0
|
88 feature_function_(feature_function),
|
ian@0
|
89 converter_(converter),
|
ian@0
|
90 scale_by_energy_(scale_by_energy)
|
ian@0
|
91 {}
|
ian@0
|
92
|
ian@0
|
93 // copy constructor
|
ian@0
|
94 FeatureComparer(const FeatureComparer& other)
|
ian@0
|
95 : id_(other.id_),
|
ian@0
|
96 extractor_(other.extractor_),
|
ian@0
|
97 feature_function_(other.feature_function_),
|
ian@0
|
98 converter_(other.converter_),
|
ian@0
|
99 scale_by_energy_(other.scale_by_energy_),
|
ian@0
|
100 target_buffer_(other.target_buffer_) // copy the target value buffer
|
ian@0
|
101 // no need to copy comparison buffer
|
ian@0
|
102 {}
|
ian@0
|
103
|
ian@0
|
104 virtual void AnalyzeTarget() {
|
ian@0
|
105 // retrieve the target feature
|
ian@0
|
106 target_buffer_ = (extractor_->*feature_function_)();
|
ian@0
|
107 if (converter_ != NULL) {
|
ian@0
|
108 converter_(target_buffer_);
|
ian@0
|
109 }
|
ian@0
|
110 if (scale_by_energy_) {
|
ian@0
|
111 std::transform(target_buffer_.begin(), target_buffer_.end(),
|
ian@0
|
112 extractor_->Energy().begin(),
|
ian@0
|
113 target_buffer_.begin(),
|
ian@0
|
114 std::multiplies<ValueT>());
|
ian@0
|
115 }
|
ian@0
|
116 }
|
ian@0
|
117
|
ian@0
|
118 virtual Value Compare() {
|
ian@0
|
119 // get the values to compare
|
ian@0
|
120 const ValueList& feature = (extractor_->*feature_function_)();
|
ian@0
|
121 ValueList* feature_buffer;
|
ian@0
|
122 if (converter_ != NULL || scale_by_energy_) {
|
ian@0
|
123 comparison_buffer_ = feature;
|
ian@0
|
124 if (converter_ != NULL) {
|
ian@0
|
125 converter_(comparison_buffer_);
|
ian@0
|
126 }
|
ian@0
|
127 if (scale_by_energy_) {
|
ian@0
|
128 std::transform(comparison_buffer_.begin(), comparison_buffer_.end(),
|
ian@0
|
129 extractor_->Energy().begin(),
|
ian@0
|
130 comparison_buffer_.begin(),
|
ian@0
|
131 std::multiplies<ValueT>());
|
ian@0
|
132 }
|
ian@0
|
133 feature_buffer = &comparison_buffer_;
|
ian@0
|
134 } else {
|
ian@0
|
135 feature_buffer = const_cast<ValueList*>(&feature);
|
ian@0
|
136 }
|
ian@0
|
137 // call conversion function
|
ian@0
|
138
|
ian@0
|
139 // find range of buffer to compare against target
|
ian@0
|
140 IteratorT buffer_start = feature_buffer->begin();
|
ian@0
|
141 IteratorT buffer_end;
|
ian@0
|
142 if (feature_buffer->size() > target_buffer_.size()) {
|
ian@0
|
143 buffer_end = buffer_start + target_buffer_.size();
|
ian@0
|
144 } else {
|
ian@0
|
145 buffer_end = feature_buffer->end();
|
ian@0
|
146 }
|
ian@0
|
147 // return root mean square error of buffer differences
|
ian@0
|
148 return std::sqrt(stats::MeanSquaredError(buffer_start,
|
ian@0
|
149 buffer_end,
|
ian@0
|
150 target_buffer_.begin()));
|
ian@0
|
151 }
|
ian@0
|
152
|
ian@0
|
153 virtual FeatureComparerInterface* Clone() const {
|
ian@0
|
154 return new FeatureComparer<T>(*this);
|
ian@0
|
155 }
|
ian@0
|
156
|
ian@0
|
157 virtual void SetExtractor(FeatureExtractor* extractor) {
|
ian@0
|
158 extractor_ = extractor;
|
ian@0
|
159 }
|
ian@0
|
160
|
ian@0
|
161 virtual int ID() const { return id_; }
|
ian@0
|
162 };
|
ian@0
|
163
|
ian@0
|
164 // specializations for std::vector<ValueList>
|
ian@0
|
165 template<>
|
ian@0
|
166 void FeatureComparer<std::vector<ValueList> >::AnalyzeTarget() {
|
ian@0
|
167 target_buffer_ = (extractor_->*feature_function_)();
|
ian@0
|
168 if (converter_ != NULL) {
|
ian@0
|
169 converter_(target_buffer_);
|
ian@0
|
170 }
|
ian@0
|
171 // for vector features we can scale by energy on a frame by frame basis
|
ian@0
|
172 }
|
ian@0
|
173
|
ian@0
|
174 template<>
|
ian@0
|
175 Value FeatureComparer<std::vector<ValueList> >::Compare() {
|
ian@0
|
176 // get the values to compare
|
ian@0
|
177 const std::vector<ValueList>& feature = (extractor_->*feature_function_)();
|
ian@0
|
178 std::vector<ValueList>* feature_buffer;
|
ian@0
|
179 // call conversion function
|
ian@0
|
180 if (converter_ != NULL) {
|
ian@0
|
181 comparison_buffer_ = feature;
|
ian@0
|
182 converter_(comparison_buffer_);
|
ian@0
|
183 feature_buffer = &comparison_buffer_;
|
ian@0
|
184 } else {
|
ian@0
|
185 // this isn't pretty, but it's the only way to allow avoiding a copy
|
ian@0
|
186 // when no conversion is taking place..
|
ian@0
|
187 feature_buffer = const_cast<std::vector<ValueList>*>(&feature);
|
ian@0
|
188 }
|
ian@0
|
189 // find how many frames to compare
|
ian@0
|
190 std::size_t frames_to_compare;
|
ian@0
|
191 if (feature_buffer->size() > target_buffer_.size()) {
|
ian@0
|
192 frames_to_compare = target_buffer_.size();
|
ian@0
|
193 } else {
|
ian@0
|
194 frames_to_compare = feature_buffer->size();
|
ian@0
|
195 }
|
ian@0
|
196 // take average of RMSE over all frames
|
ian@0
|
197 Value error = 0;
|
ian@0
|
198 const ValueList& energy = extractor_->Energy();
|
ian@0
|
199 if (scale_by_energy_) {
|
ian@0
|
200 for (std::size_t i = 0; i < frames_to_compare; ++i) {
|
ian@0
|
201 Value frame_error;
|
ian@0
|
202 frame_error = std::sqrt(stats::MeanSquaredError(feature_buffer->at(i),
|
ian@0
|
203 target_buffer_[i]));
|
ian@0
|
204 error += frame_error * energy[i];
|
ian@0
|
205 }
|
ian@0
|
206 } else {
|
ian@0
|
207 for (std::size_t i = 0; i < frames_to_compare; ++i) {
|
ian@0
|
208 error += std::sqrt(stats::MeanSquaredError(feature_buffer->at(i),
|
ian@0
|
209 target_buffer_[i]));
|
ian@0
|
210 }
|
ian@0
|
211 }
|
ian@0
|
212 return error / frames_to_compare;
|
ian@0
|
213 }
|
ian@0
|
214
|
ian@0
|
215
|
ian@0
|
216 FileComparer::FileComparer(int window_size /* = 1024 */,
|
ian@0
|
217 int hop_size /* = 256 */)
|
ian@0
|
218 : extractor_("", window_size, hop_size),
|
ian@0
|
219 target_duration_(0)
|
ian@0
|
220 {
|
ian@0
|
221 EnableFeature(Feature::LogMagnitude);
|
ian@0
|
222 EnableFeature(Feature::Pitch);
|
ian@0
|
223 }
|
ian@0
|
224
|
ian@0
|
225 FileComparer::FileComparer(const std::string& feature_list,
|
ian@0
|
226 int window_size /* = 1024 */,
|
ian@0
|
227 int hop_size /* = 256 */)
|
ian@0
|
228 : extractor_("", window_size, hop_size),
|
ian@0
|
229 target_duration_(0)
|
ian@0
|
230 {
|
ian@0
|
231 feature_names_["pitch"] = Feature::Pitch;
|
ian@0
|
232 feature_names_["energy"] = Feature::Energy;
|
ian@0
|
233 feature_names_["mfccs"] = Feature::MFCCs;
|
ian@0
|
234 feature_names_["dmfccs"] = Feature::DeltaMFCCs;
|
ian@0
|
235 feature_names_["ddmfccs"] = Feature::DoubleDeltaMFCCs;
|
ian@0
|
236 feature_names_["mag"] = Feature::Magnitude;
|
ian@0
|
237 feature_names_["logmag"] = Feature::LogMagnitude;
|
ian@0
|
238 feature_names_["centroid"] = Feature::SpectralCentroid;
|
ian@0
|
239 feature_names_["spread"] = Feature::SpectralSpread;
|
ian@0
|
240 feature_names_["flux"] = Feature::SpectralFlux;
|
ian@0
|
241 EnableFeatures(feature_list);
|
ian@0
|
242 }
|
ian@0
|
243
|
ian@0
|
244 FileComparer::FileComparer(const FileComparer& other)
|
ian@0
|
245 : target_file_(other.target_file_),
|
ian@0
|
246 target_duration_(other.target_duration_),
|
ian@0
|
247 extractor_("",
|
ian@0
|
248 other.extractor_.WindowSize(),
|
ian@0
|
249 other.extractor_.HopSize())
|
ian@0
|
250 {
|
ian@0
|
251 foreach (const FeatureComparerPtr& pointer, other.features_) {
|
ian@0
|
252 // clone the feature comparer
|
ian@0
|
253 features_.push_back(FeatureComparerPtr(pointer->Clone()));
|
ian@0
|
254 features_.back()->SetExtractor(&extractor_);
|
ian@0
|
255 }
|
ian@0
|
256 }
|
ian@0
|
257
|
ian@0
|
258 void FileComparer::SetFeatureExtractorSettings(int window_size, int hop_size) {
|
ian@0
|
259 bool reload_target = false;
|
ian@0
|
260 if (extractor_.WindowSize() != window_size) {
|
ian@0
|
261 extractor_.SetWindowSize(window_size);
|
ian@0
|
262 reload_target = true;
|
ian@0
|
263 }
|
ian@0
|
264 if (extractor_.HopSize() != hop_size) {
|
ian@0
|
265 extractor_.SetHopSize(hop_size);
|
ian@0
|
266 reload_target = true;
|
ian@0
|
267 }
|
ian@0
|
268 if (reload_target && !target_file_.empty()) {
|
ian@0
|
269 SetTargetFile(target_file_);
|
ian@0
|
270 }
|
ian@0
|
271 }
|
ian@0
|
272
|
ian@0
|
273 void FileComparer::SetTargetFile(const std::string& target_file) {
|
ian@0
|
274 if (target_file_ != target_file) {
|
ian@0
|
275 target_file_ = target_file;
|
ian@0
|
276 extractor_.LoadFile(target_file);
|
ian@0
|
277 target_duration_ = extractor_.Duration();
|
ian@0
|
278 std::for_each(features_.begin(), features_.end(),
|
ian@0
|
279 boost::bind(&FeatureComparerInterface::AnalyzeTarget, _1));
|
ian@0
|
280 }
|
ian@0
|
281 }
|
ian@0
|
282
|
ian@0
|
283
|
ian@0
|
284 Value FileComparer::CompareFile(const std::string& file_path) {
|
ian@0
|
285 extractor_.LoadFile(file_path);
|
ian@0
|
286 Value error;
|
ian@0
|
287 foreach (FeatureComparerPtr& feature, features_) {
|
ian@0
|
288 error += feature->Compare();
|
ian@0
|
289 }
|
ian@0
|
290 return error / features_.size();
|
ian@0
|
291 }
|
ian@0
|
292
|
ian@0
|
293 namespace {
|
ian@0
|
294
|
ian@0
|
295 // Helper function for creating FeatureComparers
|
ian@0
|
296 template<typename T>
|
ian@0
|
297 FeatureComparerPtr MakeFeatureComparer(FileComparer::Feature::ID id,
|
ian@0
|
298 FeatureExtractor& extractor,
|
ian@0
|
299 const T& (FeatureExtractor::*feature)(),
|
ian@0
|
300 void (*converter)(T&) = NULL,
|
ian@0
|
301 bool scale_by_energy = false) {
|
ian@0
|
302 return FeatureComparerPtr(new FeatureComparer<T>(static_cast<int>(id),
|
ian@0
|
303 &extractor,
|
ian@0
|
304 feature,
|
ian@0
|
305 converter,
|
ian@0
|
306 scale_by_energy));
|
ian@0
|
307 }
|
ian@0
|
308
|
ian@0
|
309 } // namespace
|
ian@0
|
310
|
ian@0
|
311 void FileComparer::EnableFeature(Feature::ID feature_id, bool enable) {
|
ian@0
|
312 // check if the requested feature is already enabled
|
ian@0
|
313 for (std::vector<FeatureComparerPtr>::iterator feature = features_.begin(),
|
ian@0
|
314 end = features_.end();
|
ian@0
|
315 feature != end;
|
ian@0
|
316 ++feature) {
|
ian@0
|
317 if ((*feature)->ID() == feature_id) {
|
ian@0
|
318 // feature enabled, if enable == false then remove the feature
|
ian@0
|
319 if (!enable) {
|
ian@0
|
320 features_.erase(feature);
|
ian@0
|
321 }
|
ian@0
|
322 return;
|
ian@0
|
323 }
|
ian@0
|
324 }
|
ian@0
|
325 // make the feature comparer
|
ian@0
|
326 FeatureComparerPtr feature;
|
ian@0
|
327 switch (feature_id) {
|
ian@0
|
328 case Feature::Pitch:
|
ian@0
|
329 feature = MakeFeatureComparer(feature_id,
|
ian@0
|
330 extractor_,
|
ian@0
|
331 &FeatureExtractor::Pitch,
|
ian@0
|
332 ConvertFrequencyList,
|
ian@0
|
333 true);
|
ian@0
|
334 break;
|
ian@0
|
335 case Feature::Energy:
|
ian@0
|
336 feature = MakeFeatureComparer(feature_id,
|
ian@0
|
337 extractor_,
|
ian@0
|
338 &FeatureExtractor::Energy);
|
ian@0
|
339 break;
|
ian@0
|
340 case Feature::MFCCs:
|
ian@0
|
341 feature = MakeFeatureComparer(feature_id,
|
ian@0
|
342 extractor_,
|
ian@0
|
343 &FeatureExtractor::MFCCs);
|
ian@0
|
344 break;
|
ian@0
|
345 case Feature::DeltaMFCCs:
|
ian@0
|
346 feature = MakeFeatureComparer(feature_id,
|
ian@0
|
347 extractor_,
|
ian@0
|
348 &FeatureExtractor::DeltaMFCCs);
|
ian@0
|
349 break;
|
ian@0
|
350 case Feature::DoubleDeltaMFCCs:
|
ian@0
|
351 feature = MakeFeatureComparer(feature_id,
|
ian@0
|
352 extractor_,
|
ian@0
|
353 &FeatureExtractor::DoubleDeltaMFCCs);
|
ian@0
|
354 break;
|
ian@0
|
355 case Feature::Magnitude:
|
ian@0
|
356 feature = MakeFeatureComparer(feature_id,
|
ian@0
|
357 extractor_,
|
ian@0
|
358 &FeatureExtractor::MagnitudeSpectrum);
|
ian@0
|
359 case Feature::LogMagnitude:
|
ian@0
|
360 feature = MakeFeatureComparer(feature_id,
|
ian@0
|
361 extractor_,
|
ian@0
|
362 &FeatureExtractor::LogMagnitudeSpectrum);
|
ian@0
|
363 break;
|
ian@0
|
364 case Feature::SpectralCentroid:
|
ian@0
|
365 feature = MakeFeatureComparer(feature_id,
|
ian@0
|
366 extractor_,
|
ian@0
|
367 &FeatureExtractor::SpectralCentroid,
|
ian@0
|
368 ConvertFrequencyList,
|
ian@0
|
369 true);
|
ian@0
|
370 break;
|
ian@0
|
371 case Feature::SpectralSpread:
|
ian@0
|
372 feature = MakeFeatureComparer(feature_id,
|
ian@0
|
373 extractor_,
|
ian@0
|
374 &FeatureExtractor::SpectralSpread,
|
ian@0
|
375 ConvertFrequencyList,
|
ian@0
|
376 true);
|
ian@0
|
377 break;
|
ian@0
|
378 case Feature::SpectralFlux:
|
ian@0
|
379 feature = MakeFeatureComparer(feature_id,
|
ian@0
|
380 extractor_,
|
ian@0
|
381 &FeatureExtractor::SpectralFlux);
|
ian@0
|
382 break;
|
ian@0
|
383 default:
|
ian@0
|
384 throw std::runtime_error("FileComparer::EnableFeature - Invalid ID");
|
ian@0
|
385 }
|
ian@0
|
386 // analyze the feature if we already have a target file loaded
|
ian@0
|
387 if (!target_file_.empty()) {
|
ian@0
|
388 feature->AnalyzeTarget();
|
ian@0
|
389 }
|
ian@0
|
390 // store the feature
|
ian@0
|
391 features_.push_back(feature);
|
ian@0
|
392 }
|
ian@0
|
393
|
ian@0
|
394 void FileComparer::EnableFeatures(const std::vector<Feature::ID>& features,
|
ian@0
|
395 bool enable /* = true */) {
|
ian@0
|
396 foreach (Feature::ID id, features) {
|
ian@0
|
397 EnableFeature(id, enable);
|
ian@0
|
398 }
|
ian@0
|
399 }
|
ian@0
|
400
|
ian@0
|
401 void FileComparer::EnableFeatures(std::string feature_name_list,
|
ian@0
|
402 bool enable /* = true */) {
|
ian@0
|
403 std::vector<std::string> feature_names;
|
ian@0
|
404 // split the comma separated list of names
|
ian@0
|
405 boost::algorithm::split(feature_names,
|
ian@0
|
406 feature_name_list,
|
ian@0
|
407 boost::algorithm::is_any_of(", "),
|
ian@0
|
408 boost::algorithm::token_compress_on);
|
ian@0
|
409 // enable each feature
|
ian@0
|
410 foreach (const std::string& feature_name, feature_names) {
|
ian@0
|
411 // check if the requested feature name is valid
|
ian@0
|
412 if (feature_names_.find(feature_name) == feature_names_.end()) {
|
ian@0
|
413 std::stringstream message;
|
ian@0
|
414 message << "FileComparer::EnableFeatures - Unknown feature '"
|
ian@0
|
415 << feature_name << "'";
|
ian@0
|
416 throw std::runtime_error(message.str());
|
ian@0
|
417 }
|
ian@0
|
418 EnableFeature(feature_names_[feature_name], enable);
|
ian@0
|
419 }
|
ian@0
|
420 }
|
ian@0
|
421
|
ian@0
|
422 void FileComparer::SetFeatures(const std::vector<Feature::ID>& features) {
|
ian@0
|
423 features_.clear();
|
ian@0
|
424 EnableFeatures(features);
|
ian@0
|
425 }
|
ian@0
|
426
|
ian@0
|
427 } // dsp namespace
|