mirror of
https://github.com/Ardour/ardour.git
synced 2025-12-12 09:36:33 +01:00
This adds an experimental pipe to ffmpeg to encode mp3. Currently quality is hardcoded and various aspects remain to be implemented. However, it is sufficient for initial testing.
861 lines
26 KiB
C++
861 lines
26 KiB
C++
/*
|
|
Copyright (C) 2008-2012 Paul Davis
|
|
Author: Sakari Bergen
|
|
|
|
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., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
|
|
*/
|
|
|
|
#include "ardour/export_graph_builder.h"
|
|
|
|
#include <vector>
|
|
|
|
#include <glibmm/miscutils.h>
|
|
|
|
#include "audiographer/process_context.h"
|
|
#include "audiographer/general/chunker.h"
|
|
#include "audiographer/general/cmdpipe_writer.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/loudness_reader.h"
|
|
#include "audiographer/general/sample_format_converter.h"
|
|
#include "audiographer/general/sr_converter.h"
|
|
#include "audiographer/general/silence_trimmer.h"
|
|
#include "audiographer/general/threader.h"
|
|
#include "audiographer/sndfile/tmp_file.h"
|
|
#include "audiographer/sndfile/tmp_file_rt.h"
|
|
#include "audiographer/sndfile/tmp_file_sync.h"
|
|
#include "audiographer/sndfile/sndfile_writer.h"
|
|
|
|
#include "ardour/audioengine.h"
|
|
#include "ardour/export_channel_configuration.h"
|
|
#include "ardour/export_failed.h"
|
|
#include "ardour/export_filename.h"
|
|
#include "ardour/export_format_specification.h"
|
|
#include "ardour/export_timespan.h"
|
|
#include "ardour/filesystem_paths.h"
|
|
#include "ardour/session_directory.h"
|
|
#include "ardour/sndfile_helpers.h"
|
|
#include "ardour/system_exec.h"
|
|
|
|
#include "pbd/file_utils.h"
|
|
#include "pbd/cpus.h"
|
|
|
|
using namespace AudioGrapher;
|
|
using std::string;
|
|
|
|
namespace ARDOUR {
|
|
|
|
ExportGraphBuilder::ExportGraphBuilder (Session const & session)
|
|
: session (session)
|
|
, thread_pool (hardware_concurrency())
|
|
{
|
|
process_buffer_samples = session.engine().samples_per_cycle();
|
|
}
|
|
|
|
ExportGraphBuilder::~ExportGraphBuilder ()
|
|
{
|
|
}
|
|
|
|
int
|
|
ExportGraphBuilder::process (samplecnt_t samples, bool last_cycle)
|
|
{
|
|
assert(samples <= process_buffer_samples);
|
|
|
|
for (ChannelMap::iterator it = channels.begin(); it != channels.end(); ++it) {
|
|
Sample const * process_buffer = 0;
|
|
it->first->read (process_buffer, samples);
|
|
ConstProcessContext<Sample> context(process_buffer, samples, 1);
|
|
if (last_cycle) { context().set_flag (ProcessContext<Sample>::EndOfInput); }
|
|
it->second->process (context);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
bool
|
|
ExportGraphBuilder::post_process ()
|
|
{
|
|
for (std::list<Intermediate *>::iterator it = intermediates.begin(); it != intermediates.end(); /* ++ in loop */) {
|
|
if ((*it)->process()) {
|
|
it = intermediates.erase (it);
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
|
|
return intermediates.empty();
|
|
}
|
|
|
|
unsigned
|
|
ExportGraphBuilder::get_postprocessing_cycle_count() const
|
|
{
|
|
unsigned max = 0;
|
|
for (std::list<Intermediate *>::const_iterator it = intermediates.begin(); it != intermediates.end(); ++it) {
|
|
max = std::max(max, (*it)->get_postprocessing_cycle_count());
|
|
}
|
|
return max;
|
|
}
|
|
|
|
void
|
|
ExportGraphBuilder::reset ()
|
|
{
|
|
timespan.reset();
|
|
channel_configs.clear ();
|
|
channels.clear ();
|
|
intermediates.clear ();
|
|
analysis_map.clear();
|
|
_realtime = false;
|
|
}
|
|
|
|
void
|
|
ExportGraphBuilder::cleanup (bool remove_out_files/*=false*/)
|
|
{
|
|
ChannelConfigList::iterator iter = channel_configs.begin();
|
|
|
|
while (iter != channel_configs.end() ) {
|
|
iter->remove_children(remove_out_files);
|
|
iter = channel_configs.erase(iter);
|
|
}
|
|
}
|
|
|
|
void
|
|
ExportGraphBuilder::set_current_timespan (boost::shared_ptr<ExportTimespan> span)
|
|
{
|
|
timespan = span;
|
|
}
|
|
|
|
void
|
|
ExportGraphBuilder::add_config (FileSpec const & config, bool rt)
|
|
{
|
|
ExportChannelConfiguration::ChannelList const & channels =
|
|
config.channel_config->get_channels();
|
|
for(ExportChannelConfiguration::ChannelList::const_iterator it = channels.begin();
|
|
it != channels.end(); ++it) {
|
|
(*it)->set_max_buffer_size(process_buffer_samples);
|
|
}
|
|
|
|
_realtime = rt;
|
|
|
|
// If the sample rate is "session rate", change it to the real value.
|
|
// However, we need to copy it to not change the config which is saved...
|
|
FileSpec new_config (config);
|
|
new_config.format.reset(new ExportFormatSpecification(*new_config.format, false));
|
|
if(new_config.format->sample_rate() == ExportFormatBase::SR_Session) {
|
|
samplecnt_t session_rate = session.nominal_sample_rate();
|
|
new_config.format->set_sample_rate(ExportFormatBase::nearest_sample_rate(session_rate));
|
|
}
|
|
|
|
|
|
if (!new_config.channel_config->get_split ()) {
|
|
add_split_config (new_config);
|
|
return;
|
|
}
|
|
|
|
// Split channel configurations are split into several channel configurations,
|
|
// each corresponding to a file, at this stage
|
|
typedef std::list<boost::shared_ptr<ExportChannelConfiguration> > ConfigList;
|
|
ConfigList file_configs;
|
|
new_config.channel_config->configurations_for_files (file_configs);
|
|
|
|
unsigned chan = 1;
|
|
for (ConfigList::iterator it = file_configs.begin(); it != file_configs.end(); ++it, ++chan) {
|
|
FileSpec copy = new_config;
|
|
copy.channel_config = *it;
|
|
|
|
copy.filename.reset (new ExportFilename (*copy.filename));
|
|
copy.filename->include_channel = true;
|
|
copy.filename->set_channel (chan);
|
|
|
|
add_split_config (copy);
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
for (ChannelConfigList::iterator it = channel_configs.begin(); it != channel_configs.end(); ++it) {
|
|
if (*it == config) {
|
|
it->add_child (config);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// No duplicate channel config found, create new one
|
|
channel_configs.push_back (new ChannelConfig (*this, config, channels));
|
|
}
|
|
|
|
/* Encoder */
|
|
|
|
template <>
|
|
boost::shared_ptr<AudioGrapher::Sink<Sample> >
|
|
ExportGraphBuilder::Encoder::init (FileSpec const & new_config)
|
|
{
|
|
config = new_config;
|
|
if (config.format->format_id() == ExportFormatBase::F_FFMPEG) {
|
|
init_writer (pipe_writer);
|
|
return pipe_writer;
|
|
} else {
|
|
init_writer (float_writer);
|
|
return float_writer;
|
|
}
|
|
}
|
|
|
|
template <>
|
|
boost::shared_ptr<AudioGrapher::Sink<int> >
|
|
ExportGraphBuilder::Encoder::init (FileSpec const & new_config)
|
|
{
|
|
config = new_config;
|
|
init_writer (int_writer);
|
|
return int_writer;
|
|
}
|
|
|
|
template <>
|
|
boost::shared_ptr<AudioGrapher::Sink<short> >
|
|
ExportGraphBuilder::Encoder::init (FileSpec const & new_config)
|
|
{
|
|
config = new_config;
|
|
init_writer (short_writer);
|
|
return short_writer;
|
|
}
|
|
|
|
void
|
|
ExportGraphBuilder::Encoder::add_child (FileSpec const & new_config)
|
|
{
|
|
filenames.push_back (new_config.filename);
|
|
}
|
|
|
|
void
|
|
ExportGraphBuilder::Encoder::destroy_writer (bool delete_out_file)
|
|
{
|
|
if (delete_out_file ) {
|
|
|
|
if (float_writer) {
|
|
float_writer->close ();
|
|
}
|
|
|
|
if (int_writer) {
|
|
int_writer->close ();
|
|
}
|
|
|
|
if (short_writer) {
|
|
short_writer->close ();
|
|
}
|
|
|
|
if (pipe_writer) {
|
|
pipe_writer->close ();
|
|
}
|
|
|
|
if (std::remove(writer_filename.c_str() ) != 0) {
|
|
std::cout << "Encoder::destroy_writer () : Error removing file: " << strerror(errno) << std::endl;
|
|
}
|
|
}
|
|
|
|
float_writer.reset ();
|
|
int_writer.reset ();
|
|
short_writer.reset ();
|
|
pipe_writer.reset ();
|
|
}
|
|
|
|
bool
|
|
ExportGraphBuilder::Encoder::operator== (FileSpec const & other_config) const
|
|
{
|
|
return get_real_format (config) == get_real_format (other_config);
|
|
}
|
|
|
|
int
|
|
ExportGraphBuilder::Encoder::get_real_format (FileSpec const & config)
|
|
{
|
|
ExportFormatSpecification & format = *config.format;
|
|
return format.format_id() | format.sample_format() | format.endianness();
|
|
}
|
|
|
|
template<typename T>
|
|
void
|
|
ExportGraphBuilder::Encoder::init_writer (boost::shared_ptr<AudioGrapher::SndfileWriter<T> > & writer)
|
|
{
|
|
unsigned channels = config.channel_config->get_n_chans();
|
|
int format = get_real_format (config);
|
|
config.filename->set_channel_config(config.channel_config);
|
|
writer_filename = config.filename->get_path (config.format);
|
|
|
|
writer.reset (new AudioGrapher::SndfileWriter<T> (writer_filename, format, channels, config.format->sample_rate(), config.broadcast_info));
|
|
writer->FileWritten.connect_same_thread (copy_files_connection, boost::bind (&ExportGraphBuilder::Encoder::copy_files, this, _1));
|
|
}
|
|
|
|
template<typename T>
|
|
void
|
|
ExportGraphBuilder::Encoder::init_writer (boost::shared_ptr<AudioGrapher::CmdPipeWriter<T> > & writer)
|
|
{
|
|
unsigned channels = config.channel_config->get_n_chans();
|
|
config.filename->set_channel_config(config.channel_config);
|
|
writer_filename = config.filename->get_path (config.format);
|
|
|
|
std::string ffmpeg_exe;
|
|
std::string unused;
|
|
|
|
if (!ArdourVideoToolPaths::transcoder_exe (ffmpeg_exe, unused)) {
|
|
throw ExportFailed ("External encoder (ffmpeg) is not available.");
|
|
}
|
|
|
|
int quality = 3; // TODO get from config.format
|
|
|
|
int a=0;
|
|
char **argp = (char**) calloc (100, sizeof(char*));
|
|
char tmp[64];
|
|
argp[a++] = strdup(ffmpeg_exe.c_str());
|
|
argp[a++] = strdup ("-f");
|
|
argp[a++] = strdup ("f32le");
|
|
argp[a++] = strdup ("-acodec");
|
|
argp[a++] = strdup ("pcm_f32le");
|
|
argp[a++] = strdup ("-ac");
|
|
snprintf (tmp, sizeof(tmp), "%d", channels);
|
|
argp[a++] = strdup (tmp);
|
|
argp[a++] = strdup ("-ar");
|
|
snprintf (tmp, sizeof(tmp), "%d", config.format->sample_rate());
|
|
argp[a++] = strdup (tmp);
|
|
argp[a++] = strdup ("-i");
|
|
argp[a++] = strdup ("pipe:0");
|
|
|
|
argp[a++] = strdup ("-y");
|
|
if (quality < 10) {
|
|
/* variable rate, lower is better */
|
|
snprintf (tmp, sizeof(tmp), "%d", quality);
|
|
argp[a++] = strdup ("-q:a"); argp[a++] = strdup (tmp);
|
|
} else {
|
|
/* fixed bitrate, higher is better */
|
|
snprintf (tmp, sizeof(tmp), "%dk", quality); // eg. "192k"
|
|
argp[a++] = strdup ("-b:a"); argp[a++] = strdup (tmp);
|
|
}
|
|
|
|
/* TODO: add SessionMetadata::Metadata()
|
|
* see gtk2_ardour/export_video_dialog.cc
|
|
* and gtk2_ardour/transcode_ffmpeg.cc
|
|
*/
|
|
|
|
argp[a++] = strdup (writer_filename.c_str());
|
|
argp[a] = (char *)0;
|
|
|
|
/* argp is free()d in ~SystemExec,
|
|
* SystemExec is deleted when writer is destroyed */
|
|
ARDOUR::SystemExec* exec = new ARDOUR::SystemExec (ffmpeg_exe, argp);
|
|
if (exec->start(0)) {
|
|
throw ExportFailed ("External encoder (ffmpeg) cannot be started.");
|
|
}
|
|
writer.reset (new AudioGrapher::CmdPipeWriter<T> (exec, writer_filename));
|
|
writer->FileWritten.connect_same_thread (copy_files_connection, boost::bind (&ExportGraphBuilder::Encoder::copy_files, this, _1));
|
|
}
|
|
|
|
void
|
|
ExportGraphBuilder::Encoder::copy_files (std::string orig_path)
|
|
{
|
|
while (filenames.size()) {
|
|
ExportFilenamePtr & filename = filenames.front();
|
|
PBD::copy_file (orig_path, filename->get_path (config.format).c_str());
|
|
filenames.pop_front();
|
|
}
|
|
}
|
|
|
|
/* SFC */
|
|
|
|
ExportGraphBuilder::SFC::SFC (ExportGraphBuilder &parent, FileSpec const & new_config, samplecnt_t max_samples)
|
|
: 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();
|
|
_analyse = config.format->analyse();
|
|
if (_analyse) {
|
|
samplecnt_t sample_rate = parent.session.nominal_sample_rate();
|
|
samplecnt_t sb = config.format->silence_beginning_at (parent.timespan->get_start(), sample_rate);
|
|
samplecnt_t se = config.format->silence_end_at (parent.timespan->get_end(), sample_rate);
|
|
samplecnt_t duration = parent.timespan->get_length () + sb + se;
|
|
max_samples = min ((samplecnt_t) 8192 * channels, max ((samplecnt_t) 4096 * channels, max_samples));
|
|
chunker.reset (new Chunker<Sample> (max_samples));
|
|
analyser.reset (new Analyser (config.format->sample_rate(), channels, max_samples,
|
|
(samplecnt_t) ceil (duration * config.format->sample_rate () / (double) sample_rate)));
|
|
chunker->add_output (analyser);
|
|
|
|
config.filename->set_channel_config (config.channel_config);
|
|
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_samples, config.format->dither_type(), data_width);
|
|
add_child (config);
|
|
if (_analyse) { analyser->add_output (short_converter); }
|
|
|
|
} else if (data_width == 24 || data_width == 32) {
|
|
int_converter = IntConverterPtr (new SampleFormatConverter<int> (channels));
|
|
int_converter->init (max_samples, config.format->dither_type(), data_width);
|
|
add_child (config);
|
|
if (_analyse) { analyser->add_output (int_converter); }
|
|
} else {
|
|
int actual_data_width = 8 * sizeof(Sample);
|
|
float_converter = FloatConverterPtr (new SampleFormatConverter<Sample> (channels));
|
|
float_converter->init (max_samples, config.format->dither_type(), actual_data_width);
|
|
add_child (config);
|
|
if (_analyse) { analyser->add_output (float_converter); }
|
|
}
|
|
}
|
|
|
|
void
|
|
ExportGraphBuilder::SFC::set_peak (float gain)
|
|
{
|
|
if (_analyse) {
|
|
analyser->set_normalization_gain (gain);
|
|
}
|
|
}
|
|
|
|
ExportGraphBuilder::FloatSinkPtr
|
|
ExportGraphBuilder::SFC::sink ()
|
|
{
|
|
if (_analyse) {
|
|
return chunker;
|
|
} else 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;
|
|
}
|
|
}
|
|
|
|
void
|
|
ExportGraphBuilder::SFC::add_child (FileSpec const & new_config)
|
|
{
|
|
for (boost::ptr_list<Encoder>::iterator it = children.begin(); it != children.end(); ++it) {
|
|
if (*it == new_config) {
|
|
it->add_child (new_config);
|
|
return;
|
|
}
|
|
}
|
|
|
|
children.push_back (new Encoder());
|
|
Encoder & encoder = children.back();
|
|
|
|
if (data_width == 8 || data_width == 16) {
|
|
short_converter->add_output (encoder.init<short> (new_config));
|
|
} else if (data_width == 24 || data_width == 32) {
|
|
int_converter->add_output (encoder.init<int> (new_config));
|
|
} else {
|
|
float_converter->add_output (encoder.init<Sample> (new_config));
|
|
}
|
|
}
|
|
|
|
void
|
|
ExportGraphBuilder::SFC::remove_children (bool remove_out_files)
|
|
{
|
|
boost::ptr_list<Encoder>::iterator iter = children.begin ();
|
|
|
|
while (iter != children.end() ) {
|
|
|
|
if (remove_out_files) {
|
|
iter->destroy_writer(remove_out_files);
|
|
}
|
|
iter = children.erase (iter);
|
|
}
|
|
}
|
|
|
|
bool
|
|
ExportGraphBuilder::SFC::operator== (FileSpec const & other_config) const
|
|
{
|
|
return config.format->sample_format() == other_config.format->sample_format();
|
|
}
|
|
|
|
/* Intermediate (Normalizer, TmpFile) */
|
|
|
|
ExportGraphBuilder::Intermediate::Intermediate (ExportGraphBuilder & parent, FileSpec const & new_config, samplecnt_t max_samples)
|
|
: parent (parent)
|
|
, use_loudness (false)
|
|
, use_peak (false)
|
|
{
|
|
std::string tmpfile_path = parent.session.session_directory().export_path();
|
|
tmpfile_path = Glib::build_filename(tmpfile_path, "XXXXXX");
|
|
std::vector<char> tmpfile_path_buf(tmpfile_path.size() + 1);
|
|
std::copy(tmpfile_path.begin(), tmpfile_path.end(), tmpfile_path_buf.begin());
|
|
tmpfile_path_buf[tmpfile_path.size()] = '\0';
|
|
|
|
config = new_config;
|
|
uint32_t const channels = config.channel_config->get_n_chans();
|
|
max_samples_out = 4086 - (4086 % channels); // TODO good chunk size
|
|
use_loudness = config.format->normalize_loudness ();
|
|
use_peak = config.format->normalize ();
|
|
|
|
buffer.reset (new AllocatingProcessContext<Sample> (max_samples_out, channels));
|
|
|
|
if (use_peak) {
|
|
peak_reader.reset (new PeakReader ());
|
|
}
|
|
if (use_loudness) {
|
|
loudness_reader.reset (new LoudnessReader (config.format->sample_rate(), channels, max_samples));
|
|
}
|
|
|
|
normalizer.reset (new AudioGrapher::Normalizer (use_loudness ? 0.0 : config.format->normalize_dbfs()));
|
|
threader.reset (new Threader<Sample> (parent.thread_pool));
|
|
normalizer->alloc_buffer (max_samples_out);
|
|
normalizer->add_output (threader);
|
|
|
|
int format = ExportFormatBase::F_RAW | ExportFormatBase::SF_Float;
|
|
|
|
if (parent._realtime) {
|
|
tmp_file.reset (new TmpFileRt<float> (&tmpfile_path_buf[0], format, channels, config.format->sample_rate()));
|
|
} else {
|
|
tmp_file.reset (new TmpFileSync<float> (&tmpfile_path_buf[0], format, channels, config.format->sample_rate()));
|
|
}
|
|
|
|
tmp_file->FileWritten.connect_same_thread (post_processing_connection,
|
|
boost::bind (&Intermediate::prepare_post_processing, this));
|
|
tmp_file->FileFlushed.connect_same_thread (post_processing_connection,
|
|
boost::bind (&Intermediate::start_post_processing, this));
|
|
|
|
add_child (new_config);
|
|
|
|
if (use_loudness) {
|
|
loudness_reader->add_output (tmp_file);
|
|
} else if (use_peak) {
|
|
peak_reader->add_output (tmp_file);
|
|
}
|
|
}
|
|
|
|
ExportGraphBuilder::FloatSinkPtr
|
|
ExportGraphBuilder::Intermediate::sink ()
|
|
{
|
|
if (use_loudness) {
|
|
return loudness_reader;
|
|
} else if (use_peak) {
|
|
return peak_reader;
|
|
}
|
|
return tmp_file;
|
|
}
|
|
|
|
void
|
|
ExportGraphBuilder::Intermediate::add_child (FileSpec const & new_config)
|
|
{
|
|
for (boost::ptr_list<SFC>::iterator it = children.begin(); it != children.end(); ++it) {
|
|
if (*it == new_config) {
|
|
it->add_child (new_config);
|
|
return;
|
|
}
|
|
}
|
|
|
|
children.push_back (new SFC (parent, new_config, max_samples_out));
|
|
threader->add_output (children.back().sink());
|
|
}
|
|
|
|
void
|
|
ExportGraphBuilder::Intermediate::remove_children (bool remove_out_files)
|
|
{
|
|
boost::ptr_list<SFC>::iterator iter = children.begin ();
|
|
|
|
while (iter != children.end() ) {
|
|
iter->remove_children (remove_out_files);
|
|
iter = children.erase (iter);
|
|
}
|
|
}
|
|
|
|
bool
|
|
ExportGraphBuilder::Intermediate::operator== (FileSpec const & other_config) const
|
|
{
|
|
return config.format->normalize() == other_config.format->normalize() &&
|
|
config.format->normalize_loudness () == other_config.format->normalize_loudness() &&
|
|
(
|
|
(!config.format->normalize_loudness () && config.format->normalize_dbfs() == other_config.format->normalize_dbfs())
|
|
||
|
|
// FIXME: allow simultaneous export of two formats with different loundness normalization settings
|
|
(config.format->normalize_loudness () /* lufs/dbtp is a result option, not an instantaion option */)
|
|
);
|
|
}
|
|
|
|
unsigned
|
|
ExportGraphBuilder::Intermediate::get_postprocessing_cycle_count() const
|
|
{
|
|
return static_cast<unsigned>(std::ceil(static_cast<float>(tmp_file->get_samples_written()) /
|
|
max_samples_out));
|
|
}
|
|
|
|
bool
|
|
ExportGraphBuilder::Intermediate::process()
|
|
{
|
|
samplecnt_t samples_read = tmp_file->read (*buffer);
|
|
return samples_read != buffer->samples();
|
|
}
|
|
|
|
void
|
|
ExportGraphBuilder::Intermediate::prepare_post_processing()
|
|
{
|
|
// called in sync rt-context
|
|
float gain;
|
|
if (use_loudness) {
|
|
gain = normalizer->set_peak (loudness_reader->get_peak (config.format->normalize_lufs (), config.format->normalize_dbtp ()));
|
|
} else if (use_peak) {
|
|
gain = normalizer->set_peak (peak_reader->get_peak());
|
|
} else {
|
|
gain = normalizer->set_peak (0.0);
|
|
}
|
|
if (use_loudness || use_peak) {
|
|
// push info to analyzers
|
|
for (boost::ptr_list<SFC>::iterator i = children.begin(); i != children.end(); ++i) {
|
|
(*i).set_peak (gain);
|
|
}
|
|
}
|
|
tmp_file->add_output (normalizer);
|
|
parent.intermediates.push_back (this);
|
|
}
|
|
|
|
void
|
|
ExportGraphBuilder::Intermediate::start_post_processing()
|
|
{
|
|
// called in disk-thread (when exporting in realtime)
|
|
tmp_file->seek (0, SEEK_SET);
|
|
if (!AudioEngine::instance()->freewheeling ()) {
|
|
AudioEngine::instance()->freewheel (true);
|
|
}
|
|
}
|
|
|
|
/* SRC */
|
|
|
|
ExportGraphBuilder::SRC::SRC (ExportGraphBuilder & parent, FileSpec const & new_config, samplecnt_t max_samples)
|
|
: parent (parent)
|
|
{
|
|
config = new_config;
|
|
converter.reset (new SampleRateConverter (new_config.channel_config->get_n_chans()));
|
|
ExportFormatSpecification & format = *new_config.format;
|
|
converter->init (parent.session.nominal_sample_rate(), format.sample_rate(), format.src_quality());
|
|
max_samples_out = converter->allocate_buffers (max_samples);
|
|
|
|
add_child (new_config);
|
|
}
|
|
|
|
ExportGraphBuilder::FloatSinkPtr
|
|
ExportGraphBuilder::SRC::sink ()
|
|
{
|
|
return converter;
|
|
}
|
|
|
|
void
|
|
ExportGraphBuilder::SRC::add_child (FileSpec const & new_config)
|
|
{
|
|
if (new_config.format->normalize() || parent._realtime) {
|
|
add_child_to_list (new_config, intermediate_children);
|
|
} else {
|
|
add_child_to_list (new_config, children);
|
|
}
|
|
}
|
|
|
|
void
|
|
ExportGraphBuilder::SRC::remove_children (bool remove_out_files)
|
|
{
|
|
boost::ptr_list<SFC>::iterator sfc_iter = children.begin();
|
|
|
|
while (sfc_iter != children.end() ) {
|
|
converter->remove_output (sfc_iter->sink() );
|
|
sfc_iter->remove_children (remove_out_files);
|
|
sfc_iter = children.erase (sfc_iter);
|
|
}
|
|
|
|
boost::ptr_list<Intermediate>::iterator norm_iter = intermediate_children.begin();
|
|
|
|
while (norm_iter != intermediate_children.end() ) {
|
|
converter->remove_output (norm_iter->sink() );
|
|
norm_iter->remove_children (remove_out_files);
|
|
norm_iter = intermediate_children.erase (norm_iter);
|
|
}
|
|
|
|
}
|
|
|
|
template<typename T>
|
|
void
|
|
ExportGraphBuilder::SRC::add_child_to_list (FileSpec const & new_config, boost::ptr_list<T> & list)
|
|
{
|
|
for (typename boost::ptr_list<T>::iterator it = list.begin(); it != list.end(); ++it) {
|
|
if (*it == new_config) {
|
|
it->add_child (new_config);
|
|
return;
|
|
}
|
|
}
|
|
|
|
list.push_back (new T (parent, new_config, max_samples_out));
|
|
converter->add_output (list.back().sink ());
|
|
}
|
|
|
|
bool
|
|
ExportGraphBuilder::SRC::operator== (FileSpec const & other_config) const
|
|
{
|
|
return config.format->sample_rate() == other_config.format->sample_rate();
|
|
}
|
|
|
|
/* SilenceHandler */
|
|
ExportGraphBuilder::SilenceHandler::SilenceHandler (ExportGraphBuilder & parent, FileSpec const & new_config, samplecnt_t max_samples)
|
|
: parent (parent)
|
|
{
|
|
config = new_config;
|
|
max_samples_in = max_samples;
|
|
samplecnt_t sample_rate = parent.session.nominal_sample_rate();
|
|
|
|
/* work around partsing "-inf" config to "0" -- 7b1f97b
|
|
* silence trim 0dBFS makes no sense, anyway.
|
|
*/
|
|
float est = Config->get_export_silence_threshold ();
|
|
if (est >= 0.f) est = -INFINITY;
|
|
#ifdef MIXBUS
|
|
// Mixbus channelstrip always dithers the signal, cut above dither level
|
|
silence_trimmer.reset (new SilenceTrimmer<Sample>(max_samples_in, std::max (-90.f, est)));
|
|
#else
|
|
// TODO silence-threshold should be per export-preset, with Config->get_silence_threshold being the default
|
|
silence_trimmer.reset (new SilenceTrimmer<Sample>(max_samples_in, est));
|
|
#endif
|
|
silence_trimmer->set_trim_beginning (config.format->trim_beginning());
|
|
silence_trimmer->set_trim_end (config.format->trim_end());
|
|
|
|
samplecnt_t sb = config.format->silence_beginning_at (parent.timespan->get_start(), sample_rate);
|
|
samplecnt_t se = config.format->silence_end_at (parent.timespan->get_end(), sample_rate);
|
|
|
|
silence_trimmer->add_silence_to_beginning (sb);
|
|
silence_trimmer->add_silence_to_end (se);
|
|
|
|
add_child (new_config);
|
|
}
|
|
|
|
ExportGraphBuilder::FloatSinkPtr
|
|
ExportGraphBuilder::SilenceHandler::sink ()
|
|
{
|
|
return silence_trimmer;
|
|
}
|
|
|
|
void
|
|
ExportGraphBuilder::SilenceHandler::add_child (FileSpec const & new_config)
|
|
{
|
|
for (boost::ptr_list<SRC>::iterator it = children.begin(); it != children.end(); ++it) {
|
|
if (*it == new_config) {
|
|
it->add_child (new_config);
|
|
return;
|
|
}
|
|
}
|
|
|
|
children.push_back (new SRC (parent, new_config, max_samples_in));
|
|
silence_trimmer->add_output (children.back().sink());
|
|
}
|
|
|
|
void
|
|
ExportGraphBuilder::SilenceHandler::remove_children (bool remove_out_files)
|
|
{
|
|
boost::ptr_list<SRC>::iterator iter = children.begin();
|
|
|
|
while (iter != children.end() ) {
|
|
silence_trimmer->remove_output (iter->sink() );
|
|
iter->remove_children (remove_out_files);
|
|
iter = children.erase (iter);
|
|
}
|
|
}
|
|
|
|
bool
|
|
ExportGraphBuilder::SilenceHandler::operator== (FileSpec const & other_config) const
|
|
{
|
|
ExportFormatSpecification & format = *config.format;
|
|
ExportFormatSpecification & other_format = *other_config.format;
|
|
return (format.trim_beginning() == other_format.trim_beginning()) &&
|
|
(format.trim_end() == other_format.trim_end()) &&
|
|
(format.silence_beginning_time() == other_format.silence_beginning_time()) &&
|
|
(format.silence_end_time() == other_format.silence_end_time());
|
|
}
|
|
|
|
/* ChannelConfig */
|
|
|
|
ExportGraphBuilder::ChannelConfig::ChannelConfig (ExportGraphBuilder & parent, FileSpec const & new_config, ChannelMap & channel_map)
|
|
: parent (parent)
|
|
{
|
|
typedef ExportChannelConfiguration::ChannelList ChannelList;
|
|
|
|
config = new_config;
|
|
|
|
samplecnt_t max_samples = parent.session.engine().samples_per_cycle();
|
|
interleaver.reset (new Interleaver<Sample> ());
|
|
interleaver->init (new_config.channel_config->get_n_chans(), max_samples);
|
|
|
|
// Make the chunk size divisible by the channel count
|
|
int chan_count = new_config.channel_config->get_n_chans();
|
|
max_samples_out = 8192;
|
|
if (chan_count > 0) {
|
|
max_samples_out -= max_samples_out % chan_count;
|
|
}
|
|
chunker.reset (new Chunker<Sample> (max_samples_out));
|
|
interleaver->add_output(chunker);
|
|
|
|
ChannelList const & channel_list = config.channel_config->get_channels();
|
|
unsigned chan = 0;
|
|
for (ChannelList::const_iterator it = channel_list.begin(); it != channel_list.end(); ++it, ++chan) {
|
|
ChannelMap::iterator map_it = channel_map.find (*it);
|
|
if (map_it == channel_map.end()) {
|
|
std::pair<ChannelMap::iterator, bool> result_pair =
|
|
channel_map.insert (std::make_pair (*it, IdentityVertexPtr (new IdentityVertex<Sample> ())));
|
|
assert (result_pair.second);
|
|
map_it = result_pair.first;
|
|
}
|
|
map_it->second->add_output (interleaver->input (chan));
|
|
}
|
|
|
|
add_child (new_config);
|
|
}
|
|
|
|
void
|
|
ExportGraphBuilder::ChannelConfig::add_child (FileSpec const & new_config)
|
|
{
|
|
assert (*this == new_config);
|
|
|
|
for (boost::ptr_list<SilenceHandler>::iterator it = children.begin(); it != children.end(); ++it) {
|
|
if (*it == new_config) {
|
|
it->add_child (new_config);
|
|
return;
|
|
}
|
|
}
|
|
|
|
children.push_back (new SilenceHandler (parent, new_config, max_samples_out));
|
|
chunker->add_output (children.back().sink ());
|
|
}
|
|
|
|
void
|
|
ExportGraphBuilder::ChannelConfig::remove_children (bool remove_out_files)
|
|
{
|
|
boost::ptr_list<SilenceHandler>::iterator iter = children.begin();
|
|
|
|
while(iter != children.end() ) {
|
|
|
|
chunker->remove_output (iter->sink ());
|
|
iter->remove_children (remove_out_files);
|
|
iter = children.erase(iter);
|
|
}
|
|
}
|
|
|
|
bool
|
|
ExportGraphBuilder::ChannelConfig::operator== (FileSpec const & other_config) const
|
|
{
|
|
return config.channel_config == other_config.channel_config;
|
|
}
|
|
|
|
} // namespace ARDOUR
|