Fix export alignment (#7916)
Ardour's playback is aligned to master-out: "When the playback clock reads 01:00:00:00, the sample corresponding to 01:00:00:00 is audible at the speaker(s)" When exporting, and grabbing data from output ports, the signal is offset by the master-bus physical playback latency. This was compensated for, but lead to initial silence in the exported file. New approach is to start capturing export data during pre-roll, at the time when playback is written to the output buffers. To also shaves off a common offset to make this work with realtime export. Effectively this emulates processing with disconnected master-output port, while still keeping any latency of effects on the master-bus itself. Last but not least: jack updates latencies when freewheeling, (setting HW latency to zero). The callback arrives asynchronously some time after enabling freewheeling, but after Export Ports have been configured. Those callbacks are ignored.
This commit is contained in:
@@ -50,7 +50,8 @@ class LIBARDOUR_API ExportChannel : public boost::less_than_comparable<ExportCha
|
||||
|
||||
virtual ~ExportChannel () {}
|
||||
|
||||
virtual void set_max_buffer_size(samplecnt_t) { }
|
||||
virtual samplecnt_t common_port_playback_latency () const { return 0; }
|
||||
virtual void prepare_export (samplecnt_t max_samples, sampleoffset_t common_latency) { }
|
||||
|
||||
virtual void read (Sample const *& data, samplecnt_t samples) const = 0;
|
||||
virtual bool empty () const = 0;
|
||||
@@ -74,7 +75,8 @@ class LIBARDOUR_API PortExportChannel : public ExportChannel
|
||||
PortExportChannel ();
|
||||
~PortExportChannel ();
|
||||
|
||||
void set_max_buffer_size(samplecnt_t samples);
|
||||
samplecnt_t common_port_playback_latency () const;
|
||||
void prepare_export (samplecnt_t max_samples, sampleoffset_t common_latency);
|
||||
|
||||
void read (Sample const *& data, samplecnt_t samples) const;
|
||||
bool empty () const { return ports.empty(); }
|
||||
@@ -171,7 +173,7 @@ class LIBARDOUR_API RouteExportChannel : public ExportChannel
|
||||
static void create_from_route(std::list<ExportChannelPtr> & result, boost::shared_ptr<Route> route);
|
||||
|
||||
public: // ExportChannel interface
|
||||
void set_max_buffer_size(samplecnt_t samples);
|
||||
void prepare_export (samplecnt_t max_samples, sampleoffset_t common_latency);
|
||||
|
||||
void read (Sample const *& data, samplecnt_t samples) const;
|
||||
bool empty () const { return false; }
|
||||
|
||||
@@ -69,7 +69,7 @@ class LIBARDOUR_API ExportGraphBuilder
|
||||
ExportGraphBuilder (Session const & session);
|
||||
~ExportGraphBuilder ();
|
||||
|
||||
int process (samplecnt_t samples, bool last_cycle);
|
||||
samplecnt_t process (samplecnt_t samples, bool last_cycle);
|
||||
bool post_process (); // returns true when finished
|
||||
bool need_postprocessing () const { return !intermediates.empty(); }
|
||||
bool realtime() const { return _realtime; }
|
||||
@@ -275,7 +275,8 @@ class LIBARDOUR_API ExportGraphBuilder
|
||||
|
||||
AnalysisMap analysis_map;
|
||||
|
||||
bool _realtime;
|
||||
bool _realtime;
|
||||
samplecnt_t _master_align;
|
||||
|
||||
Glib::ThreadPool thread_pool;
|
||||
};
|
||||
|
||||
@@ -46,17 +46,35 @@ PortExportChannel::~PortExportChannel ()
|
||||
_delaylines.clear ();
|
||||
}
|
||||
|
||||
void PortExportChannel::set_max_buffer_size(samplecnt_t samples)
|
||||
samplecnt_t PortExportChannel::common_port_playback_latency () const
|
||||
{
|
||||
_buffer_size = samples;
|
||||
_buffer.reset (new Sample[samples]);
|
||||
samplecnt_t l = 0;
|
||||
bool first = true;
|
||||
for (PortSet::const_iterator it = ports.begin(); it != ports.end(); ++it) {
|
||||
boost::shared_ptr<AudioPort> p = it->lock ();
|
||||
if (!p) { continue; }
|
||||
samplecnt_t latency = p->private_latency_range (true).max;
|
||||
if (first) {
|
||||
first = false;
|
||||
l = p->private_latency_range (true).max;
|
||||
continue;
|
||||
}
|
||||
l = std::min (l, latency);
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
void PortExportChannel::prepare_export (samplecnt_t max_samples, sampleoffset_t common_latency)
|
||||
{
|
||||
_buffer_size = max_samples;
|
||||
_buffer.reset (new Sample[max_samples]);
|
||||
|
||||
_delaylines.clear ();
|
||||
|
||||
for (PortSet::const_iterator it = ports.begin(); it != ports.end(); ++it) {
|
||||
boost::shared_ptr<AudioPort> p = it->lock ();
|
||||
if (!p) { continue; }
|
||||
samplecnt_t latency = p->private_latency_range (true).max;
|
||||
samplecnt_t latency = p->private_latency_range (true).max - common_latency;
|
||||
PBD::RingBuffer<Sample>* rb = new PBD::RingBuffer<Sample> (latency + 1 + _buffer_size);
|
||||
for (samplepos_t i = 0; i < latency; ++i) {
|
||||
Sample zero = 0;
|
||||
@@ -258,10 +276,10 @@ RouteExportChannel::create_from_route(std::list<ExportChannelPtr> & result, boos
|
||||
}
|
||||
|
||||
void
|
||||
RouteExportChannel::set_max_buffer_size(samplecnt_t samples)
|
||||
RouteExportChannel::prepare_export (samplecnt_t max_samples, sampleoffset_t)
|
||||
{
|
||||
if (processor) {
|
||||
processor->set_block_size (samples);
|
||||
processor->set_block_size (max_samples);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,20 +77,33 @@ ExportGraphBuilder::~ExportGraphBuilder ()
|
||||
{
|
||||
}
|
||||
|
||||
int
|
||||
samplecnt_t
|
||||
ExportGraphBuilder::process (samplecnt_t samples, bool last_cycle)
|
||||
{
|
||||
assert(samples <= process_buffer_samples);
|
||||
|
||||
sampleoffset_t off = 0;
|
||||
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 (session.remaining_latency_preroll () >= _master_align + samples) {
|
||||
/* Skip processing during pre-roll, only read/write export ringbuffers */
|
||||
return 0;
|
||||
}
|
||||
|
||||
off = 0;
|
||||
if (session.remaining_latency_preroll () > _master_align) {
|
||||
off = session.remaining_latency_preroll () - _master_align;
|
||||
assert (off < samples);
|
||||
}
|
||||
|
||||
ConstProcessContext<Sample> context(&process_buffer[off], samples - off, 1);
|
||||
if (last_cycle) { context().set_flag (ProcessContext<Sample>::EndOfInput); }
|
||||
it->second->process (context);
|
||||
}
|
||||
|
||||
return 0;
|
||||
return samples - off;
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -126,6 +139,7 @@ ExportGraphBuilder::reset ()
|
||||
intermediates.clear ();
|
||||
analysis_map.clear();
|
||||
_realtime = false;
|
||||
_master_align = 0;
|
||||
}
|
||||
|
||||
void
|
||||
@@ -148,17 +162,25 @@ ExportGraphBuilder::set_current_timespan (boost::shared_ptr<ExportTimespan> 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);
|
||||
/* calculate common latency, shave off master-bus hardware playback latency (if any) */
|
||||
_master_align = session.master_out() ? session.master_out()->output()->connected_latency (true) : 0;
|
||||
|
||||
ExportChannelConfiguration::ChannelList const & channels = config.channel_config->get_channels();
|
||||
|
||||
for(ExportChannelConfiguration::ChannelList::const_iterator it = channels.begin(); it != channels.end(); ++it) {
|
||||
_master_align = std::min (_master_align, (*it)->common_port_playback_latency ());
|
||||
}
|
||||
|
||||
/* now set-up port-data sniffing and delay-ringbuffers */
|
||||
for(ExportChannelConfiguration::ChannelList::const_iterator it = channels.begin(); it != channels.end(); ++it) {
|
||||
(*it)->prepare_export (process_buffer_samples, _master_align);
|
||||
}
|
||||
|
||||
_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...
|
||||
/* 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) {
|
||||
@@ -166,14 +188,14 @@ ExportGraphBuilder::add_config (FileSpec const & config, bool rt)
|
||||
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
|
||||
/* 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);
|
||||
|
||||
@@ -299,12 +299,13 @@ ExportHandler::process_timespan (samplecnt_t samples)
|
||||
samples_to_read = samples;
|
||||
}
|
||||
|
||||
process_position += samples_to_read;
|
||||
export_status->processed_samples += samples_to_read;
|
||||
export_status->processed_samples_current_timespan += samples_to_read;
|
||||
|
||||
/* Do actual processing */
|
||||
int ret = graph_builder->process (samples_to_read, last_cycle);
|
||||
samplecnt_t ret = graph_builder->process (samples_to_read, last_cycle);
|
||||
if (ret > 0) {
|
||||
process_position += ret;
|
||||
export_status->processed_samples += ret;
|
||||
export_status->processed_samples_current_timespan += ret;
|
||||
}
|
||||
|
||||
/* Start post-processing/normalizing if necessary */
|
||||
if (last_cycle) {
|
||||
@@ -318,7 +319,7 @@ ExportHandler::process_timespan (samplecnt_t samples)
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
|
||||
@@ -6533,7 +6533,7 @@ Session::update_latency (bool playback)
|
||||
if (inital_connect_or_deletion_in_progress () || _adding_routes_in_progress || _route_deletion_in_progress) {
|
||||
return;
|
||||
}
|
||||
if (!_engine.running()) {
|
||||
if (!_engine.running() || _exporting) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -285,20 +285,33 @@ Session::process_export_fw (pframes_t nframes)
|
||||
_engine.main_thread()->get_buffers ();
|
||||
}
|
||||
|
||||
process_without_events (remain);
|
||||
assert (_count_in_samples == 0);
|
||||
while (remain > 0) {
|
||||
samplecnt_t ns = calc_preroll_subcycle (remain);
|
||||
|
||||
bool session_needs_butler = false;
|
||||
if (process_routes (ns, session_needs_butler)) {
|
||||
fail_roll (ns);
|
||||
}
|
||||
|
||||
ProcessExport (ns);
|
||||
|
||||
_remaining_latency_preroll -= ns;
|
||||
remain -= ns;
|
||||
nframes -= ns;
|
||||
|
||||
if (remain != 0) {
|
||||
_engine.split_cycle (ns);
|
||||
}
|
||||
}
|
||||
|
||||
if (need_buffers) {
|
||||
_engine.main_thread()->drop_buffers ();
|
||||
}
|
||||
|
||||
_remaining_latency_preroll -= remain;
|
||||
_transport_sample -= remain;
|
||||
nframes -= remain;
|
||||
|
||||
if (nframes == 0) {
|
||||
return;
|
||||
}
|
||||
_engine.split_cycle (remain);
|
||||
}
|
||||
|
||||
if (need_buffers) {
|
||||
|
||||
@@ -568,7 +568,7 @@ Session::start_transport ()
|
||||
|
||||
_last_roll_location = _transport_sample;
|
||||
_last_roll_or_reversal_location = _transport_sample;
|
||||
if (!have_looped) {
|
||||
if (!have_looped && !_exporting) {
|
||||
_remaining_latency_preroll = worst_latency_preroll_buffer_size_ceil ();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user