Post-export Analysis
This commit is contained in:
parent
883a6a3d4e
commit
c1642fead8
60
libs/ardour/ardour/export_analysis.h
Normal file
60
libs/ardour/ardour/export_analysis.h
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (C) 2016 Robin Gareus <robin@gareus.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __ardour_export_analysis_h__
|
||||
#define __ardour_export_analysis_h__
|
||||
|
||||
#include <map>
|
||||
#include <cstring>
|
||||
#include <boost/shared_ptr.hpp>
|
||||
|
||||
#include "ardour/types.h"
|
||||
|
||||
namespace ARDOUR {
|
||||
struct ExportAnalysis {
|
||||
public:
|
||||
ExportAnalysis ()
|
||||
: loudness (0)
|
||||
, loudness_range (0)
|
||||
, have_loudness (false)
|
||||
{
|
||||
memset (_peaks, 0, sizeof(_peaks));
|
||||
memset (_spectrum, 0, sizeof(_spectrum));
|
||||
}
|
||||
|
||||
ExportAnalysis (const ExportAnalysis& other)
|
||||
: loudness (other.loudness)
|
||||
, loudness_range (other.loudness_range)
|
||||
, have_loudness (other.have_loudness)
|
||||
{
|
||||
memcpy (_peaks, other._peaks, sizeof(_peaks));
|
||||
memcpy (_spectrum, other._spectrum, sizeof(_spectrum));
|
||||
}
|
||||
|
||||
float loudness;
|
||||
float loudness_range;
|
||||
bool have_loudness;
|
||||
PeakData _peaks[800];
|
||||
float _spectrum[800][256];
|
||||
};
|
||||
|
||||
typedef boost::shared_ptr<ExportAnalysis> ExportAnalysisPtr;
|
||||
typedef std::map<std::string, ExportAnalysisPtr> AnalysisResults;
|
||||
|
||||
} // namespace ARDOUR
|
||||
#endif
|
|
@ -22,6 +22,7 @@
|
|||
#define __ardour_export_graph_builder_h__
|
||||
|
||||
#include "ardour/export_handler.h"
|
||||
#include "ardour/export_analysis.h"
|
||||
|
||||
#include "audiographer/utils/identity_vertex.h"
|
||||
|
||||
|
@ -32,6 +33,7 @@ namespace AudioGrapher {
|
|||
class SampleRateConverter;
|
||||
class PeakReader;
|
||||
class Normalizer;
|
||||
class Analyser;
|
||||
template <typename T> class Chunker;
|
||||
template <typename T> class SampleFormatConverter;
|
||||
template <typename T> class Interleaver;
|
||||
|
@ -55,7 +57,9 @@ class LIBARDOUR_API ExportGraphBuilder
|
|||
|
||||
typedef boost::shared_ptr<AudioGrapher::Sink<Sample> > FloatSinkPtr;
|
||||
typedef boost::shared_ptr<AudioGrapher::IdentityVertex<Sample> > IdentityVertexPtr;
|
||||
typedef boost::shared_ptr<AudioGrapher::Analyser> AnalysisPtr;
|
||||
typedef std::map<ExportChannelPtr, IdentityVertexPtr> ChannelMap;
|
||||
typedef std::map<std::string, AnalysisPtr> AnalysisMap;
|
||||
|
||||
public:
|
||||
|
||||
|
@ -71,9 +75,14 @@ class LIBARDOUR_API ExportGraphBuilder
|
|||
void cleanup (bool remove_out_files = false);
|
||||
void set_current_timespan (boost::shared_ptr<ExportTimespan> span);
|
||||
void add_config (FileSpec const & config);
|
||||
void get_analysis_results (AnalysisResults& results);
|
||||
|
||||
private:
|
||||
|
||||
void add_analyser (const std::string& fn, AnalysisPtr ap) {
|
||||
analysis_map.insert (std::make_pair (fn, ap));
|
||||
}
|
||||
|
||||
void add_split_config (FileSpec const & config);
|
||||
|
||||
class Encoder {
|
||||
|
@ -125,6 +134,7 @@ class LIBARDOUR_API ExportGraphBuilder
|
|||
boost::ptr_list<Encoder> children;
|
||||
int data_width;
|
||||
|
||||
AnalysisPtr analyser;
|
||||
// Only one of these should be available at a time
|
||||
FloatConverterPtr float_converter;
|
||||
IntConverterPtr int_converter;
|
||||
|
@ -245,6 +255,8 @@ class LIBARDOUR_API ExportGraphBuilder
|
|||
|
||||
std::list<Normalizer *> normalizers;
|
||||
|
||||
AnalysisMap analysis_map;
|
||||
|
||||
Glib::ThreadPool thread_pool;
|
||||
};
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
#include <stdint.h>
|
||||
|
||||
#include "ardour/libardour_visibility.h"
|
||||
#include "ardour/export_analysis.h"
|
||||
#include "ardour/types.h"
|
||||
|
||||
#include "pbd/signals.h"
|
||||
|
@ -79,6 +80,8 @@ class LIBARDOUR_API ExportStatus {
|
|||
volatile uint32_t total_normalize_cycles;
|
||||
volatile uint32_t current_normalize_cycle;
|
||||
|
||||
AnalysisResults result_map;
|
||||
|
||||
private:
|
||||
volatile bool _aborted;
|
||||
volatile bool _errors;
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
#include "audiographer/general/chunker.h"
|
||||
#include "audiographer/general/interleaver.h"
|
||||
#include "audiographer/general/normalizer.h"
|
||||
#include "audiographer/general/analyser.h"
|
||||
#include "audiographer/general/peak_reader.h"
|
||||
#include "audiographer/general/sample_format_converter.h"
|
||||
#include "audiographer/general/sr_converter.h"
|
||||
|
@ -110,6 +111,7 @@ ExportGraphBuilder::reset ()
|
|||
channel_configs.clear ();
|
||||
channels.clear ();
|
||||
normalizers.clear ();
|
||||
analysis_map.clear();
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -173,6 +175,16 @@ ExportGraphBuilder::add_config (FileSpec const & config)
|
|||
}
|
||||
}
|
||||
|
||||
void
|
||||
ExportGraphBuilder::get_analysis_results (AnalysisResults& results) {
|
||||
for (AnalysisMap::iterator i = analysis_map.begin(); i != analysis_map.end(); ++i) {
|
||||
ExportAnalysisPtr p = i->second->result ();
|
||||
if (p) {
|
||||
results.insert (std::make_pair (i->first, p));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ExportGraphBuilder::add_split_config (FileSpec const & config)
|
||||
{
|
||||
|
@ -287,39 +299,39 @@ ExportGraphBuilder::Encoder::copy_files (std::string orig_path)
|
|||
|
||||
/* SFC */
|
||||
|
||||
ExportGraphBuilder::SFC::SFC (ExportGraphBuilder &, FileSpec const & new_config, framecnt_t max_frames)
|
||||
ExportGraphBuilder::SFC::SFC (ExportGraphBuilder &parent, FileSpec const & new_config, framecnt_t max_frames)
|
||||
: data_width(0)
|
||||
{
|
||||
config = new_config;
|
||||
data_width = sndfile_data_width (Encoder::get_real_format (config));
|
||||
unsigned channels = new_config.channel_config->get_n_chans();
|
||||
analyser.reset (new Analyser (config.format->sample_rate(), channels, max_frames,
|
||||
(framecnt_t) ceil (parent.timespan->get_length () * config.format->sample_rate () / (double) parent.session.nominal_frame_rate ())));
|
||||
parent.add_analyser (config.filename->get_path (config.format), analyser);
|
||||
|
||||
if (data_width == 8 || data_width == 16) {
|
||||
short_converter = ShortConverterPtr (new SampleFormatConverter<short> (channels));
|
||||
short_converter->init (max_frames, config.format->dither_type(), data_width);
|
||||
add_child (config);
|
||||
analyser->add_output (short_converter);
|
||||
} else if (data_width == 24 || data_width == 32) {
|
||||
int_converter = IntConverterPtr (new SampleFormatConverter<int> (channels));
|
||||
int_converter->init (max_frames, config.format->dither_type(), data_width);
|
||||
add_child (config);
|
||||
analyser->add_output (int_converter);
|
||||
} else {
|
||||
int actual_data_width = 8 * sizeof(Sample);
|
||||
float_converter = FloatConverterPtr (new SampleFormatConverter<Sample> (channels));
|
||||
float_converter->init (max_frames, config.format->dither_type(), actual_data_width);
|
||||
add_child (config);
|
||||
analyser->add_output (float_converter);
|
||||
}
|
||||
}
|
||||
|
||||
ExportGraphBuilder::FloatSinkPtr
|
||||
ExportGraphBuilder::SFC::sink ()
|
||||
{
|
||||
if (data_width == 8 || data_width == 16) {
|
||||
return short_converter;
|
||||
} else if (data_width == 24 || data_width == 32) {
|
||||
return int_converter;
|
||||
} else {
|
||||
return float_converter;
|
||||
}
|
||||
return analyser;
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
@ -295,11 +295,12 @@ ExportHandler::command_output(std::string output, size_t size)
|
|||
void
|
||||
ExportHandler::finish_timespan ()
|
||||
{
|
||||
graph_builder->get_analysis_results (export_status->result_map);
|
||||
|
||||
while (config_map.begin() != timespan_bounds.second) {
|
||||
|
||||
ExportFormatSpecPtr fmt = config_map.begin()->second.format;
|
||||
std::string filename = config_map.begin()->second.filename->get_path(fmt);
|
||||
|
||||
if (fmt->with_cue()) {
|
||||
export_cd_marker_file (current_timespan, fmt, filename, CDMarkerCUE);
|
||||
}
|
||||
|
@ -312,15 +313,17 @@ ExportHandler::finish_timespan ()
|
|||
export_cd_marker_file (current_timespan, fmt, filename, MP4Chaps);
|
||||
}
|
||||
|
||||
/* close file first, otherwise TagLib enounters an ERROR_SHARING_VIOLATION
|
||||
* The process cannot access the file because it is being used.
|
||||
* ditto for post-export and upload.
|
||||
*/
|
||||
graph_builder->reset ();
|
||||
|
||||
if (fmt->tag()) {
|
||||
/* close file first, otherwise TagLib enounters an ERROR_SHARING_VIOLATION
|
||||
* The process cannot access the file because it is being used.
|
||||
*
|
||||
* TODO: check Umlauts and encoding in filename.
|
||||
/* TODO: check Umlauts and encoding in filename.
|
||||
* TagLib eventually calls CreateFileA(),
|
||||
*/
|
||||
export_status->active_job = ExportStatus::Tagging;
|
||||
graph_builder->reset ();
|
||||
AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
|
||||
}
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ ExportStatus::init ()
|
|||
|
||||
total_normalize_cycles = 0;
|
||||
current_normalize_cycle = 0;
|
||||
result_map.clear();
|
||||
}
|
||||
|
||||
void
|
||||
|
|
215
libs/audiographer/audiographer/general/analyser.h
Normal file
215
libs/audiographer/audiographer/general/analyser.h
Normal file
|
@ -0,0 +1,215 @@
|
|||
/*
|
||||
* Copyright (C) 2016 Robin Gareus <robin@gareus.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef AUDIOGRAPHER_ANALYSER_H
|
||||
#define AUDIOGRAPHER_ANALYSER_H
|
||||
|
||||
#include <fftw3.h>
|
||||
#include <vamp-hostsdk/PluginLoader.h>
|
||||
#include <vamp-sdk/Plugin.h>
|
||||
|
||||
#include "audiographer/visibility.h"
|
||||
#include "audiographer/sink.h"
|
||||
#include "audiographer/routines.h"
|
||||
#include "audiographer/utils/listed_source.h"
|
||||
|
||||
#include "pbd/fastlog.h"
|
||||
#include "ardour/export_analysis.h"
|
||||
|
||||
namespace AudioGrapher
|
||||
{
|
||||
|
||||
class /*LIBAUDIOGRAPHER_API*/ Analyser : public ListedSource<float>, public Sink<float>
|
||||
{
|
||||
public:
|
||||
Analyser (float sample_rate, unsigned int channels, framecnt_t bufsize, framecnt_t n_samples)
|
||||
: _ebur128_plugin (0)
|
||||
, _sample_rate (sample_rate)
|
||||
, _channels (channels)
|
||||
, _bufsize (bufsize / channels)
|
||||
, _n_samples (n_samples)
|
||||
, _pos (0)
|
||||
{
|
||||
assert (bufsize % channels == 0);
|
||||
//printf("NEW ANALYSER %p r:%.1f c:%d f:%ld l%ld\n", this, sample_rate, channels, bufsize, n_samples);
|
||||
if (channels > 0 && channels <= 2) {
|
||||
using namespace Vamp::HostExt;
|
||||
PluginLoader* loader (PluginLoader::getInstance());
|
||||
_ebur128_plugin = loader->loadPlugin ("libardourvampplugins:ebur128", sample_rate, PluginLoader::ADAPT_ALL_SAFE);
|
||||
assert (_ebur128_plugin);
|
||||
_ebur128_plugin->reset ();
|
||||
_ebur128_plugin->initialise (channels, _bufsize, _bufsize);
|
||||
}
|
||||
_bufs[0] = (float*) malloc (sizeof(float) * _bufsize);
|
||||
_bufs[1] = (float*) malloc (sizeof(float) * _bufsize);
|
||||
const size_t peaks = sizeof(_result._peaks) / sizeof (ARDOUR::PeakData::PeakDatum) / 2;
|
||||
_spp = ceil ((_n_samples + 1.f) / (float) peaks);
|
||||
|
||||
_fft_data_size = _bufsize / 2;
|
||||
_fft_freq_per_bin = sample_rate / _fft_data_size / 2.f;
|
||||
|
||||
_fft_data_in = (float *) fftwf_malloc (sizeof(float) * _bufsize);
|
||||
_fft_data_out = (float *) fftwf_malloc (sizeof(float) * _bufsize);
|
||||
_fft_power = (float *) malloc (sizeof(float) * _fft_data_size);
|
||||
|
||||
for (uint32_t i = 0; i < _fft_data_size; ++i) {
|
||||
_fft_power[i] = 0;
|
||||
}
|
||||
for (uint32_t i = 0; i < _bufsize; ++i) {
|
||||
_fft_data_out[i] = 0;
|
||||
}
|
||||
|
||||
_fft_plan = fftwf_plan_r2r_1d (_bufsize, _fft_data_in, _fft_data_out, FFTW_R2HC, FFTW_MEASURE);
|
||||
|
||||
_hann_window = (float *) malloc(sizeof(float) * _bufsize);
|
||||
double sum = 0.0;
|
||||
|
||||
for (uint32_t i = 0; i < _bufsize; ++i) {
|
||||
_hann_window[i] = 0.5f - (0.5f * (float) cos (2.0f * M_PI * (float)i / (float)(_bufsize)));
|
||||
sum += _hann_window[i];
|
||||
}
|
||||
const double isum = 2.0 / sum;
|
||||
for (uint32_t i = 0; i < _bufsize; ++i) {
|
||||
_hann_window[i] *= isum;
|
||||
}
|
||||
}
|
||||
|
||||
~Analyser ()
|
||||
{
|
||||
delete _ebur128_plugin;
|
||||
free (_bufs[0]);
|
||||
free (_bufs[1]);
|
||||
fftwf_destroy_plan (_fft_plan);
|
||||
fftwf_free (_fft_data_in);
|
||||
fftwf_free (_fft_data_out);
|
||||
free (_fft_power);
|
||||
free (_hann_window);
|
||||
}
|
||||
|
||||
void process (ProcessContext<float> const & c)
|
||||
{
|
||||
framecnt_t n_samples = c.frames() / c.channels();
|
||||
assert (c.frames() % c.channels() == 0);
|
||||
assert (n_samples <= _bufsize);
|
||||
//printf("PROC %p @%ld F: %ld, S: %ld C:%d\n", this, _pos, c.frames(), n_samples, c.channels());
|
||||
float const * d = c.data ();
|
||||
framecnt_t s;
|
||||
for (s = 0; s < n_samples; ++s) {
|
||||
_fft_data_in[s] = 0;
|
||||
const framecnt_t pk = (_pos + s) / _spp;
|
||||
for (unsigned int c = 0; c < _channels; ++c) {
|
||||
const float v = *d;
|
||||
_bufs[c][s] = v;
|
||||
if (_result._peaks[pk].min > v) { _result._peaks[pk].min = *d; }
|
||||
if (_result._peaks[pk].max < v) { _result._peaks[pk].max = *d; }
|
||||
_fft_data_in[s] += v * _hann_window[s] / (float) _channels;
|
||||
++d;
|
||||
}
|
||||
}
|
||||
for (; s < _bufsize; ++s) {
|
||||
for (unsigned int c = 0; c < _channels; ++c) {
|
||||
_bufs[c][s] = 0.f;
|
||||
_fft_data_in[s] = 0;
|
||||
}
|
||||
}
|
||||
if (_ebur128_plugin) {
|
||||
_ebur128_plugin->process (_bufs, Vamp::RealTime::fromSeconds ((double) _pos / _sample_rate));
|
||||
}
|
||||
fftwf_execute (_fft_plan);
|
||||
|
||||
_fft_power[0] = _fft_data_out[0] * _fft_data_out[0];
|
||||
|
||||
#define FRe (_fft_data_out[i])
|
||||
#define FIm (_fft_data_out[_bufsize - i])
|
||||
for (uint32_t i = 1; i < _fft_data_size - 1; ++i) {
|
||||
_fft_power[i] = (FRe * FRe) + (FIm * FIm);
|
||||
}
|
||||
#undef FRe
|
||||
#undef FIm
|
||||
|
||||
// TODO handle case where _pos / _spp != (_pos + _bufsize) / _spp
|
||||
// TODO: get geometry from ExportAnalysis
|
||||
const framecnt_t x = _pos / _spp;
|
||||
const float range = 80; // dB
|
||||
const double ypb = 256.0 / _fft_data_size;
|
||||
|
||||
for (uint32_t i = 1; i < _fft_data_size - 1; ++i) {
|
||||
const float level = fft_power_at_bin (i, i);
|
||||
if (level < -range) continue;
|
||||
const float pk = level > 0.0 ? 1.0 : (range + level) / range;
|
||||
const uint32_t y = 256 - ceil (i * ypb); // log-y?
|
||||
assert (x >= 0 && x < 800);
|
||||
assert (y < 256);
|
||||
if (_result._spectrum[x][y] < pk) { _result._spectrum[x][y] = pk; }
|
||||
}
|
||||
|
||||
_pos += n_samples;
|
||||
|
||||
/* pass audio audio through */
|
||||
ListedSource<float>::output(c);
|
||||
}
|
||||
|
||||
ARDOUR::ExportAnalysisPtr result () {
|
||||
//printf("PROCESSED %ld / %ld samples\n", _pos, _n_samples);
|
||||
if (_pos == 0) {
|
||||
return ARDOUR::ExportAnalysisPtr ();
|
||||
}
|
||||
if (_ebur128_plugin) {
|
||||
Vamp::Plugin::FeatureSet features = _ebur128_plugin->getRemainingFeatures ();
|
||||
if (!features.empty() && features.size() == 2) {
|
||||
_result.loudness = features[0][0].values[0];
|
||||
_result.loudness_range = features[1][0].values[0];
|
||||
_result.have_loudness = true;
|
||||
}
|
||||
}
|
||||
return ARDOUR::ExportAnalysisPtr (new ARDOUR::ExportAnalysis (_result));
|
||||
}
|
||||
|
||||
using Sink<float>::process;
|
||||
|
||||
private:
|
||||
ARDOUR::ExportAnalysis _result;
|
||||
Vamp::Plugin* _ebur128_plugin;
|
||||
|
||||
float _sample_rate;
|
||||
unsigned int _channels;
|
||||
framecnt_t _bufsize;
|
||||
framecnt_t _n_samples;
|
||||
framecnt_t _pos;
|
||||
framecnt_t _spp;
|
||||
|
||||
float* _bufs[2];
|
||||
|
||||
float* _hann_window;
|
||||
uint32_t _fft_data_size;
|
||||
double _fft_freq_per_bin;
|
||||
float* _fft_data_in;
|
||||
float* _fft_data_out;
|
||||
float* _fft_power;
|
||||
fftwf_plan _fft_plan;
|
||||
|
||||
inline float fft_power_at_bin (const uint32_t b, const float norm) const {
|
||||
const float a = _fft_power[b] * norm;
|
||||
return a > 1e-12 ? 10.0 * fast_log10(a) : -INFINITY;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif
|
Loading…
Reference in New Issue
Block a user