triggerbox: first pass at using "realtime" rubberband rather than pre-stretch

This commit is contained in:
Paul Davis 2021-11-11 15:10:33 -07:00
parent e1b2a24221
commit 5105083394
2 changed files with 159 additions and 205 deletions

View File

@ -48,6 +48,9 @@
class XMLNode;
namespace RubberBand {
class RubberBandStretcher;
}
namespace ARDOUR {
@ -86,7 +89,6 @@ class LIBARDOUR_API Trigger : public PBD::Stateful {
virtual void set_start (timepos_t const &) = 0;
virtual void set_end (timepos_t const &) = 0;
virtual void set_length (timecnt_t const &) = 0;
virtual void tempo_map_change () = 0;
virtual void reload (BufferSet&, void*) = 0;
virtual double position_as_fraction() const = 0;
@ -196,30 +198,33 @@ class LIBARDOUR_API Trigger : public PBD::Stateful {
float midi_velocity_effect() const { return _midi_velocity_effect; }
void set_midi_velocity_effect (float);
double apparent_tempo() const { return _apparent_tempo; }
double set_tempo (double t);
protected:
TriggerBox& _box;
State _state;
std::atomic<State> _requested_state;
std::atomic<int> _bang;
std::atomic<int> _unbang;
uint64_t _index;
int _next_trigger;
LaunchStyle _launch_style;
PBD::Property<bool> _use_follow;
FollowAction _follow_action[2];
int _follow_action_probability;
uint32_t _loop_cnt;
uint32_t _follow_count;
boost::shared_ptr<Region> _region;
Temporal::BBT_Offset _quantization;
PBD::Property<bool> _legato;
std::string _name;
double _stretch;
double _barcnt; /* our estimate of the number of bars in the region */
void* _ui;
gain_t _gain;
gain_t _pending_gain;
float _midi_velocity_effect;
TriggerBox& _box;
State _state;
std::atomic<State> _requested_state;
std::atomic<int> _bang;
std::atomic<int> _unbang;
uint64_t _index;
int _next_trigger;
LaunchStyle _launch_style;
PBD::Property<bool> _use_follow;
FollowAction _follow_action[2];
int _follow_action_probability;
uint32_t _loop_cnt; /* how many times in a row has this played */
uint32_t _follow_count;
Temporal::BBT_Offset _quantization;
PBD::Property<bool> _legato;
std::string _name;
double _barcnt; /* our estimate of the number of bars in the region */
double _apparent_tempo;
gain_t _gain;
gain_t _pending_gain;
float _midi_velocity_effect;
void* _ui;
void set_region_internal (boost::shared_ptr<Region>);
void request_state (State s);
@ -232,7 +237,7 @@ class LIBARDOUR_API AudioTrigger : public Trigger {
AudioTrigger (uint64_t index, TriggerBox&);
~AudioTrigger ();
int run (BufferSet&, pframes_t nframes, pframes_t offset, bool first);
int run (BufferSet&, pframes_t nframes, pframes_t offset, bool first, double bpm);
void set_start (timepos_t const &);
void set_end (timepos_t const &);
@ -254,7 +259,7 @@ class LIBARDOUR_API AudioTrigger : public Trigger {
XMLNode& get_state (void);
int set_state (const XMLNode&, int version);
void tempo_map_change ();
RubberBand::RubberBandStretcher* stretcher() { return (_stretcher); }
protected:
void retrigger ();
@ -270,10 +275,13 @@ class LIBARDOUR_API AudioTrigger : public Trigger {
samplecnt_t usable_length;
samplepos_t last_sample;
RubberBand::RubberBandStretcher* _stretcher;
void drop_data ();
int load_data (boost::shared_ptr<AudioRegion>);
RunResult at_end ();
void compute_and_set_length ();
void determine_tempo ();
void setup_stretcher ();
};
@ -282,7 +290,7 @@ class LIBARDOUR_API MIDITrigger : public Trigger {
MIDITrigger (uint64_t index, TriggerBox&);
~MIDITrigger ();
int run (BufferSet&, samplepos_t start_sample, samplepos_t end_sample, pframes_t nframes, pframes_t offset, bool first);
int run (BufferSet&, samplepos_t start_sample, samplepos_t end_sample, pframes_t nframes, pframes_t offset, bool first, double bpm);
void set_start (timepos_t const &);
void set_end (timepos_t const &);
@ -305,8 +313,6 @@ class LIBARDOUR_API MIDITrigger : public Trigger {
XMLNode& get_state (void);
int set_state (const XMLNode&, int version);
void tempo_map_change ();
protected:
void retrigger ();
void set_usable_length ();
@ -484,9 +490,6 @@ class LIBARDOUR_API TriggerBox : public Processor
int note_to_trigger (int node, int channel);
void tempo_map_change ();
PBD::ScopedConnection tempo_map_connection;
void note_on (int note_number, int velocity);
void note_off (int note_number, int velocity);

View File

@ -14,6 +14,7 @@
#include "temporal/tempo.h"
#include "ardour/audioengine.h"
#include "ardour/audioregion.h"
#include "ardour/audio_buffer.h"
#include "ardour/debug.h"
@ -66,12 +67,12 @@ Trigger::Trigger (uint64_t n, TriggerBox& b)
, _follow_count (1)
, _quantization (Temporal::BBT_Offset (0, 1, 0))
, _legato (Properties::legato, false)
, _stretch (1.0)
, _barcnt (0.)
, _ui (0)
, _apparent_tempo (0.)
, _gain (1.0)
, _pending_gain (1.0)
, _midi_velocity_effect (0.)
, _ui (0)
{
add_property (_legato);
add_property (_use_follow);
@ -170,7 +171,8 @@ Trigger::get_state (void)
node->set_property (X_("quantization"), _quantization);
node->set_property (X_("name"), _name);
node->set_property (X_("index"), _index);
node->set_property (X_("stretch"), _stretch);
node->set_property (X_("apparent-tempo"), _apparent_tempo);
node->set_property (X_("barcnt"), _barcnt);
if (_region) {
node->set_property (X_("region"), _region->id());
@ -482,6 +484,7 @@ AudioTrigger::AudioTrigger (uint64_t n, TriggerBox& b)
, _legato_offset (0)
, usable_length (0)
, last_sample (0)
, _stretcher (0)
{
}
@ -490,6 +493,8 @@ AudioTrigger::~AudioTrigger ()
for (std::vector<Sample*>::iterator d = data.begin(); d != data.end(); ++d) {
delete *d;
}
delete _stretcher;
}
void
@ -578,132 +583,6 @@ AudioTrigger::current_pos() const
return timepos_t (read_index);
}
void
AudioTrigger::set_length (timecnt_t const & newlen)
{
using namespace RubberBand;
using namespace Temporal;
if (!_region) {
return;
}
boost::shared_ptr<AudioRegion> ar (boost::dynamic_pointer_cast<AudioRegion> (_region));
if (newlen == _region->length()) {
/* no stretch required */
return;
}
/* offline stretch */
/* study */
const uint32_t nchans = ar->n_channels();
RubberBandStretcher::Options options = RubberBandStretcher::Option (RubberBandStretcher::OptionProcessOffline|RubberBandStretcher::OptionStretchPrecise);
RubberBandStretcher stretcher (_box.session().sample_rate(), nchans, options, 1.0, 1.0);
/* Compute stretch ratio */
if (newlen.time_domain() == AudioTime) {
_stretch = (double) newlen.samples() / data_length;
cerr << "gonna stretch, ratio is " << _stretch << " from " << newlen.samples() << " vs. " << data_length << endl;
} else {
/* XXX what to use for position ??? */
timecnt_t l (newlen, timepos_t (AudioTime));
const timecnt_t dur = TempoMap::use()->convert_duration (l, timepos_t (0), AudioTime);
_stretch = (double) dur.samples() / data_length;
}
stretcher.setTimeRatio (_stretch);
const samplecnt_t expected_length = ceil (data_length * _stretch) + 16; /* extra space for safety */
std::vector<Sample*> stretched;
for (uint32_t n = 0; n < nchans; ++n) {
stretched.push_back (new Sample[expected_length]);
}
/* RB expects array-of-ptr-to-Sample, so set one up */
std::vector<Sample*> raw(nchans);
std::vector<Sample*> results(nchans);
/* study, then process */
const samplecnt_t block_size = 16384;
samplecnt_t read = 0;
stretcher.setDebugLevel (0);
stretcher.setMaxProcessSize (block_size);
stretcher.setExpectedInputDuration (data_length);
while (read < data_length) {
for (uint32_t n = 0; n < nchans; ++n) {
raw[n] = data[n] + read;
}
samplecnt_t to_read = std::min (block_size, data_length - read);
read += to_read;
stretcher.study (&raw[0], to_read, (read >= data_length));
}
read = 0;
samplecnt_t processed = 0;
samplecnt_t avail;
while (read < data_length) {
for (uint32_t n = 0; n < nchans; ++n) {
raw[n] = data[n] + read;
}
samplecnt_t to_read = std::min (block_size, data_length - read);
read += to_read;
stretcher.process (&raw[0], to_read, (read >= data_length));
while ((avail = stretcher.available()) > 0) {
for (uint32_t n = 0; n < nchans; ++n) {
results[n] = stretched[n] + processed;
}
processed += stretcher.retrieve (&results[0], avail);
}
}
/* collect final chunk of data, possible delayed by thread activity in stretcher */
while ((avail = stretcher.available()) >= 0) {
if (avail == 0) {
Glib::usleep (10000);
continue;
}
for (uint32_t n = 0; n < nchans; ++n) {
results[n] = stretched[n] + processed;
}
processed += stretcher.retrieve (&results[0], avail);
}
/* allocate new data buffers */
drop_data ();
data = stretched;
data_length = processed;
if (!usable_length || usable_length > data_length) {
usable_length = data_length;
last_sample = _start_offset + usable_length;
}
}
void
AudioTrigger::set_usable_length ()
{
@ -733,6 +612,12 @@ AudioTrigger::set_usable_length ()
last_sample = _start_offset + usable_length;
}
void
AudioTrigger::set_length (timecnt_t const & newlen)
{
/* XXX what? */
}
timepos_t
AudioTrigger::current_length() const
{
@ -754,6 +639,8 @@ AudioTrigger::natural_length() const
int
AudioTrigger::set_region_threaded (boost::shared_ptr<Region> r)
{
using namespace RubberBand;
boost::shared_ptr<AudioRegion> ar = boost::dynamic_pointer_cast<AudioRegion> (r);
if (!ar) {
@ -762,7 +649,8 @@ AudioTrigger::set_region_threaded (boost::shared_ptr<Region> r)
set_region_internal (r);
load_data (ar);
compute_and_set_length ();
determine_tempo ();
setup_stretcher ();
PropertyChanged (ARDOUR::Properties::name);
@ -770,7 +658,7 @@ AudioTrigger::set_region_threaded (boost::shared_ptr<Region> r)
}
void
AudioTrigger::compute_and_set_length ()
AudioTrigger::determine_tempo ()
{
using namespace Temporal;
@ -788,16 +676,18 @@ AudioTrigger::compute_and_set_length ()
breakfastquay::MiniBPM mbpm (_box.session().sample_rate());
mbpm.setBPMRange (metric.tempo().quarter_notes_per_minute () * 0.75, metric.tempo().quarter_notes_per_minute() * 1.5);
double bpm = mbpm.estimateTempoOfSamples (data[0], data_length);
if (bpm == 0.0) {
_apparent_tempo = mbpm.estimateTempoOfSamples (data[0], data_length);
if (_apparent_tempo == 0.0) {
/* no apparent tempo, just return since we'll use it as-is */
return;
}
cerr << name() << " Estimated bpm " << bpm << " from " << (double) data_length / _box.session().sample_rate() << " seconds\n";
cerr << name() << " Estimated bpm " << _apparent_tempo << " from " << (double) data_length / _box.session().sample_rate() << " seconds\n";
const double seconds = (double) data_length / _box.session().sample_rate();
const double quarters = (seconds / 60.) * bpm;
const double quarters = (seconds / 60.) * _apparent_tempo;
_barcnt = quarters / metric.meter().divisions_per_bar();
}
@ -807,20 +697,36 @@ AudioTrigger::compute_and_set_length ()
cerr << "one bar in samples: " << one_bar << endl;
cerr << "barcnt = " << round (_barcnt) << endl;
set_length (timecnt_t ((samplecnt_t) round (_barcnt) * one_bar));
}
void
AudioTrigger::tempo_map_change ()
AudioTrigger::setup_stretcher ()
{
using namespace RubberBand;
using namespace Temporal;
if (!_region) {
return;
}
drop_data ();
load_data (boost::dynamic_pointer_cast<AudioRegion> (_region));
compute_and_set_length ();
boost::shared_ptr<AudioRegion> ar (boost::dynamic_pointer_cast<AudioRegion> (_region));
const uint32_t nchans = ar->n_channels();
/* XXX maybe get some of these options from region properties (when/if we have them) ? */
RubberBandStretcher::Options options = RubberBandStretcher::Option (RubberBandStretcher::OptionProcessRealTime |
RubberBandStretcher::OptionTransientsCrisp |
RubberBandStretcher::OptionPhaseIndependent);
if (!_stretcher) {
_stretcher = new RubberBandStretcher (_box.session().sample_rate(), nchans, options, 1.0, 1.0);
} else {
_stretcher->reset ();
}
/* XXX this needs to change if the engine buffer size changes */
_stretcher->setMaxProcessSize (AudioEngine::instance()->raw_buffer_size (DataType::AUDIO));
}
void
@ -869,33 +775,94 @@ AudioTrigger::retrigger ()
{
read_index = _start_offset + _legato_offset;
_legato_offset = 0; /* used one time only */
_stretcher->reset ();
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 retriggered to %2\n", _index, read_index));
}
int
AudioTrigger::run (BufferSet& bufs, pframes_t nframes, pframes_t dest_offset, bool first)
AudioTrigger::run (BufferSet& bufs, pframes_t nframes, pframes_t dest_offset, bool first, double bpm)
{
boost::shared_ptr<AudioRegion> ar = boost::dynamic_pointer_cast<AudioRegion>(_region);
const uint32_t nchans = ar->n_channels();
const bool long_enough_to_fade = (nframes >= 64);
assert (ar);
assert (active());
/* tell the stretcher what we are doing for this ::run() call */
if (_apparent_tempo == 0.) {
/* XXX just use data directly */
return 0;
}
const double stretch = _apparent_tempo / bpm;
_stretcher->setTimeRatio (stretch);
cerr << "apparent " << _apparent_tempo << " bpm " << bpm << " TR " << std::setprecision (4) << stretch << endl;
int avail = _stretcher->available();
if (avail < 0) {
error << _("Could not configure rubberband stretcher") << endmsg;
return -1;
}
while (nframes) {
pframes_t this_read = (pframes_t) std::min ((samplecnt_t) nframes, (last_sample - read_index));
pframes_t this_read;
/* Fill all buffers (so mono region will fill all channels),
* and discard extra channels. This models what we do in the
* timeline.
*/
if (read_index < last_sample) {
for (uint64_t chn = 0; chn < bufs.count().n_audio(); ++chn) {
/* still have data to push into the stretcher */
this_read = (pframes_t) std::min ((samplecnt_t) nframes, (last_sample - read_index));
const bool at_end = (this_read < nframes);
while ((pframes_t) avail < this_read && (read_index < last_sample)) {
/* keep feeding the stretcher in chunks of "this_read",
* until there's nframes of data available, or we reach
* the end of the region
*/
Sample* in[nchans];
for (uint32_t chn = 0; chn < nchans; ++chn) {
in[chn] = data[chn] + read_index;
}
_stretcher->process (in, this_read, at_end);
read_index += this_read;
avail = _stretcher->available ();
}
} else {
/* finished reading data, but have not yet delivered it all */
avail = _stretcher->available ();
this_read = (pframes_t) std::min ((pframes_t) nframes, (pframes_t) avail);
}
/* Set up buffers for RB to write into */
Sample* out[nchans];
BufferSet& scratch (_box.session().get_scratch_buffers (ChanCount (DataType::AUDIO, nchans)));
for (uint32_t chn = 0; chn < bufs.count().n_audio(); ++chn) {
out[chn] = scratch.get_audio (chn).data();
}
/* fetch the stretch */
_stretcher->retrieve (out, this_read);
/* deliver to buffers */
for (uint32_t chn = 0; chn < bufs.count().n_audio(); ++chn) {
uint64_t channel = chn % data.size();
Sample* src = data[channel] + read_index;
AudioBuffer& buf (bufs.get_audio (chn));
Sample* src = out[channel];
if (first) {
buf.read_from (src, this_read, dest_offset);
@ -911,9 +878,9 @@ AudioTrigger::run (BufferSet& bufs, pframes_t nframes, pframes_t dest_offset, bo
}
}
read_index += this_read;
avail = _stretcher->available ();
if (read_index >= last_sample) {
if (read_index >= last_sample && avail <= 0) {
/* We reached the end */
@ -1213,8 +1180,6 @@ MIDITrigger::reload (BufferSet& bufs, void* ptr)
std::swap (data, rtmb);
delete rtmb;
std::cerr << _index << " data now " << data << std::endl;
}
void
@ -1226,11 +1191,6 @@ MIDITrigger::re_render ()
_box.request_reload (_index, new_data);
}
void
MIDITrigger::tempo_map_change ()
{
}
void
MIDITrigger::drop_data ()
{
@ -1272,7 +1232,7 @@ MIDITrigger::retrigger ()
}
int
MIDITrigger::run (BufferSet& bufs, samplepos_t start_sample, samplepos_t end_sample, pframes_t nframes, pframes_t dest_offset, bool first)
MIDITrigger::run (BufferSet& bufs, samplepos_t start_sample, samplepos_t end_sample, pframes_t nframes, pframes_t dest_offset, bool first, double bpm)
{
MidiBuffer& mb (bufs.get_midi (0));
@ -1426,8 +1386,6 @@ TriggerBox::TriggerBox (Session& s, DataType dt)
}
}
Temporal::TempoMap::MapChanged.connect_same_thread (tempo_map_connection, boost::bind (&TriggerBox::tempo_map_change, this));
Config->ParameterChanged.connect_same_thread (*this, boost::bind (&TriggerBox::parameter_changed, this, _1));
StopAllTriggers.connect_same_thread (stop_all_connection, boost::bind (&TriggerBox::request_stop_all, this));
@ -1862,6 +1820,8 @@ TriggerBox::run (BufferSet& bufs, samplepos_t start_sample, samplepos_t end_samp
Temporal::Beats start_beats (start.beats());
Temporal::Beats end_beats (end.beats());
Temporal::TempoMap::SharedPtr tmap (Temporal::TempoMap::use());
Temporal::TempoPoint const & tempo (tmap->tempo_at (start));
const double bpm = tempo.quarter_notes_per_minute ();
uint64_t max_chans = 0;
bool first = false;
@ -1983,7 +1943,7 @@ TriggerBox::run (BufferSet& bufs, samplepos_t start_sample, samplepos_t end_samp
max_chans = std::max (max_chans, nchans);
at->run (bufs, trigger_samples, dest_offset, first);
at->run (bufs, trigger_samples, dest_offset, first, bpm);
first = false;
@ -1991,7 +1951,7 @@ TriggerBox::run (BufferSet& bufs, samplepos_t start_sample, samplepos_t end_samp
MIDITrigger* mt = dynamic_cast<MIDITrigger*> (currently_playing);
if (mt) {
mt->run (bufs, start_sample, end_sample, trigger_samples, dest_offset, first);
mt->run (bufs, start_sample, end_sample, trigger_samples, dest_offset, first, bpm);
first = false;
}
@ -2271,15 +2231,6 @@ TriggerBox::set_state (const XMLNode& node, int version)
return 0;
}
void
TriggerBox::tempo_map_change ()
{
Glib::Threads::RWLock::ReaderLock lm (trigger_lock);
for (auto & t : all_triggers) {
t->tempo_map_change ();
}
}
void
TriggerBox::parameter_changed (std::string const & param)
{