changeset 751:ed5db7d37005 pitch-align

Merge from default branch
author Chris Cannam
date Wed, 22 Apr 2020 17:40:09 +0100
parents 36772d79cf44 (current diff) e7c77c366360 (diff)
children 32654e402f8b
files files.pri
diffstat 11 files changed, 876 insertions(+), 399 deletions(-) [+]
line wrap: on
line diff
--- a/audio/AudioCallbackPlaySource.cpp	Fri Apr 03 10:17:46 2020 +0100
+++ b/audio/AudioCallbackPlaySource.cpp	Wed Apr 22 17:40:09 2020 +0100
@@ -16,6 +16,8 @@
 #include "AudioCallbackPlaySource.h"
 
 #include "AudioGenerator.h"
+#include "TimeStretchWrapper.h"
+#include "EffectWrapper.h"
 
 #include "data/model/Model.h"
 #include "base/ViewManagerBase.h"
@@ -32,9 +34,6 @@
 
 #include "bqvec/VectorOps.h"
 
-#include <rubberband/RubberBandStretcher.h>
-using namespace RubberBand;
-
 using breakfastquay::v_zero_channels;
 
 #include <iostream>
@@ -73,20 +72,12 @@
     m_outputLeft(0.0),
     m_outputRight(0.0),
     m_levelsSet(false),
-    m_auditioningPlugin(nullptr),
-    m_auditioningPluginBypassed(false),
-    m_auditioningPluginFailed(false),
     m_playStartFrame(0),
     m_playStartFramePassed(false),
-    m_timeStretcher(nullptr),
-    m_monoStretcher(nullptr),
-    m_stretchRatio(1.0),
-    m_stretchMono(false),
-    m_stretcherInputCount(0),
-    m_stretcherInputs(nullptr),
-    m_stretcherInputSizes(nullptr),
     m_fillThread(nullptr),
-    m_resamplerWrapper(nullptr)
+    m_resamplerWrapper(nullptr),
+    m_timeStretchWrapper(nullptr),
+    m_auditioningEffectWrapper(nullptr)
 {
     m_viewManager->setAudioPlaySource(this);
 
@@ -135,14 +126,9 @@
 
     delete m_audioGenerator;
 
-    for (int i = 0; i < m_stretcherInputCount; ++i) {
-        delete[] m_stretcherInputs[i];
-    }
-    delete[] m_stretcherInputSizes;
-    delete[] m_stretcherInputs;
-
-    delete m_timeStretcher;
-    delete m_monoStretcher;
+    delete m_timeStretchWrapper;
+    delete m_auditioningEffectWrapper;
+    delete m_resamplerWrapper;
 
     m_bufferScavenger.scavenge(true);
     m_pluginScavenger.scavenge(true);
@@ -151,6 +137,35 @@
 #endif
 }
 
+breakfastquay::ApplicationPlaybackSource *
+AudioCallbackPlaySource::getApplicationPlaybackSource()
+{
+    QMutexLocker locker(&m_mutex);
+    
+    if (m_timeStretchWrapper) {
+        return m_timeStretchWrapper;
+    }
+
+    checkWrappers();
+    return m_timeStretchWrapper;
+}
+
+void
+AudioCallbackPlaySource::checkWrappers()
+{
+    // to be called only with m_mutex held
+
+    if (!m_resamplerWrapper) {
+        m_resamplerWrapper = new breakfastquay::ResamplerWrapper(this);
+    }
+    if (!m_auditioningEffectWrapper) {
+        m_auditioningEffectWrapper = new EffectWrapper(m_resamplerWrapper);
+    }
+    if (!m_timeStretchWrapper) {
+        m_timeStretchWrapper = new TimeStretchWrapper(m_auditioningEffectWrapper);
+    }
+}
+
 void
 AudioCallbackPlaySource::addModel(ModelId modelId)
 {
@@ -250,24 +265,14 @@
 
     if (srChanged) {
 
-        SVCERR << "AudioCallbackPlaySource: Source rate changed" << endl;
+        checkWrappers();
 
-        if (m_resamplerWrapper) {
-            SVCERR << "AudioCallbackPlaySource: Source sample rate changed to "
-                << m_sourceSampleRate << ", updating resampler wrapper" << endl;
-            m_resamplerWrapper->changeApplicationSampleRate
-                (int(round(m_sourceSampleRate)));
-            m_resamplerWrapper->reset();
-        }
-
-        delete m_timeStretcher;
-        delete m_monoStretcher;
-        m_timeStretcher = nullptr;
-        m_monoStretcher = nullptr;
-        
-        if (m_stretchRatio != 1.f) {
-            setTimeStretch(m_stretchRatio);
-        }
+        SVCERR << "AudioCallbackPlaySource: Source sample rate changed to "
+               << m_sourceSampleRate << ", updating resampler wrapper"
+               << endl;
+        m_resamplerWrapper->changeApplicationSampleRate
+            (int(round(m_sourceSampleRate)));
+        m_resamplerWrapper->reset();
     }
 
     rebuildRangeLists();
@@ -483,11 +488,8 @@
 
     m_mutex.lock();
 
-    if (m_timeStretcher) {
-        m_timeStretcher->reset();
-    }
-    if (m_monoStretcher) {
-        m_monoStretcher->reset();
+    if (m_timeStretchWrapper) {
+        m_timeStretchWrapper->reset();
     }
 
     m_readBufferFill = m_writeBufferFill = startFrame;
@@ -599,21 +601,13 @@
 
     if (!m_playing) return;
 
-    RealTimePluginInstance *ap = m_auditioningPlugin;
-    if (ap && !m_auditioningPluginBypassed) {
-        m_auditioningPluginBypassed = true;
+    if (m_auditioningEffectWrapper &&
+        m_auditioningEffectWrapper->haveEffect() &&
+        !m_auditioningEffectWrapper->isBypassed()) {
+        m_auditioningEffectWrapper->setBypassed(true);
         emit audioOverloadPluginDisabled();
         return;
     }
-
-    if (m_timeStretcher &&
-        m_timeStretcher->getTimeRatio() < 1.0 &&
-        m_stretcherInputCount > 1 &&
-        m_monoStretcher && !m_stretchMono) {
-        m_stretchMono = true;
-        emit audioTimeStretchMultiChannelDisabled();
-        return;
-    }
 }
 
 void
@@ -628,28 +622,18 @@
 }
 
 void
-AudioCallbackPlaySource::setResamplerWrapper(breakfastquay::ResamplerWrapper *w)
-{
-    m_resamplerWrapper = w;
-    if (m_resamplerWrapper && m_sourceSampleRate != 0) {
-        m_resamplerWrapper->changeApplicationSampleRate
-            (int(round(m_sourceSampleRate)));
-    }
-}
-
-void
 AudioCallbackPlaySource::setSystemPlaybackBlockSize(int size)
 {
-    cout << "AudioCallbackPlaySource::setTarget: Block size -> " << size << endl;
+    SVDEBUG << "AudioCallbackPlaySource::setTarget: Block size -> " << size << endl;
     if (size != 0) {
         m_blockSize = size;
     }
     if (size * 4 > m_ringBufferSize) {
 #ifdef DEBUG_AUDIO_PLAY_SOURCE
-        cout << "AudioCallbackPlaySource::setTarget: Buffer size "
-             << size << " > a quarter of ring buffer size "
-             << m_ringBufferSize << ", calling for more ring buffer"
-             << endl;
+        SVCERR << "AudioCallbackPlaySource::setTarget: Buffer size "
+               << size << " > a quarter of ring buffer size "
+               << m_ringBufferSize << ", calling for more ring buffer"
+               << endl;
 #endif
         m_ringBufferSize = size * 4;
         if (m_writeBuffers && !m_writeBuffers->empty()) {
@@ -704,9 +688,8 @@
 AudioCallbackPlaySource::getCurrentFrame(RealTime latency_t)
 {
     // The ring buffers contain data at the source sample rate and all
-    // processing (including time stretching) happens at this
-    // rate. Resampling only happens after the audio data leaves this
-    // class.
+    // processing here happens at this rate. Resampling only happens
+    // after the audio data leaves this class.
     
     // (But because historically more than one sample rate could have
     // been involved here, we do latency calculations using RealTime
@@ -736,16 +719,6 @@
 
     RealTime inbuffer_t = RealTime::frame2RealTime(inbuffer, rate);
 
-    sv_frame_t stretchlat = 0;
-    double timeRatio = 1.0;
-
-    if (m_timeStretcher) {
-        stretchlat = m_timeStretcher->getLatency();
-        timeRatio = m_timeStretcher->getTimeRatio();
-    }
-
-    RealTime stretchlat_t = RealTime::frame2RealTime(stretchlat, rate);
-
     // When the target has just requested a block from us, the last
     // sample it obtained was our buffer fill frame count minus the
     // amount of read space (converted back to source sample rate)
@@ -784,12 +757,6 @@
 
     RealTime bufferedto_t = RealTime::frame2RealTime(readBufferFill, rate);
 
-    if (timeRatio != 1.0) {
-        lastretrieved_t = lastretrieved_t / timeRatio;
-        sincerequest_t = sincerequest_t / timeRatio;
-        latency_t = latency_t / timeRatio;
-    }
-
 #ifdef DEBUG_AUDIO_PLAY_SOURCE_PLAYING
     cout << "\nbuffered to: " << bufferedto_t << ", in buffer: " << inbuffer_t << ", device latency: " << latency_t << "\n  since request: " << sincerequest_t << ", last retrieved quantity: " << lastretrieved_t << endl;
 #endif
@@ -805,7 +772,7 @@
     if (m_rangeStarts.empty()) {
         // this code is only used in case of error in rebuildRangeLists
         RealTime playing_t = bufferedto_t
-            - latency_t - stretchlat_t - lastretrieved_t - inbuffer_t
+            - latency_t - lastretrieved_t - inbuffer_t
             + sincerequest_t;
         if (playing_t < RealTime::zeroTime) playing_t = RealTime::zeroTime;
         sv_frame_t frame = RealTime::realTime2Frame(playing_t, rate);
@@ -831,7 +798,7 @@
     RealTime playing_t = bufferedto_t;
 
     playing_t = playing_t
-        - latency_t - stretchlat_t - lastretrieved_t - inbuffer_t
+        - latency_t - lastretrieved_t - inbuffer_t
         + sincerequest_t;
 
     // This rather gross little hack is used to ensure that latency
@@ -849,8 +816,7 @@
         if (playing_t < playstart_t) {
 //            cout << "playing_t " << playing_t << " < playstart_t " 
 //                      << playstart_t << endl;
-            if (/*!!! sincerequest_t > RealTime::zeroTime && */
-                m_playStartedAt + latency_t + stretchlat_t <
+            if (m_playStartedAt + latency_t <
                 RealTime::fromSeconds(currentTime)) {
 //                cout << "but we've been playing for long enough that I think we should disregard it (it probably results from loop wrapping)" << endl;
                 m_playStartFramePassed = true;
@@ -1003,22 +969,23 @@
 }
 
 void
-AudioCallbackPlaySource::setAuditioningEffect(Auditionable *a)
+AudioCallbackPlaySource::setAuditioningEffect(std::shared_ptr<Auditionable> a)
 {
-    RealTimePluginInstance *plugin = dynamic_cast<RealTimePluginInstance *>(a);
+    SVDEBUG << "AudioCallbackPlaySource::setAuditioningEffect(" << a << ")"
+            << endl;
+    
+    auto plugin = std::dynamic_pointer_cast<RealTimePluginInstance>(a);
     if (a && !plugin) {
         SVCERR << "WARNING: AudioCallbackPlaySource::setAuditioningEffect: auditionable object " << a << " is not a real-time plugin instance" << endl;
     }
 
     m_mutex.lock();
-    m_auditioningPlugin = plugin;
-    m_auditioningPluginBypassed = false;
-    m_auditioningPluginFailed = false;
+    m_auditioningEffectWrapper->setEffect(plugin);
+    m_auditioningEffectWrapper->setBypassed(false);
     m_mutex.unlock();
 
     SVDEBUG << "AudioCallbackPlaySource::setAuditioningEffect: set plugin to "
-            << plugin << " and bypassed to " << m_auditioningPluginBypassed
-            << endl;
+            << plugin << endl;
 }
 
 void
@@ -1069,35 +1036,10 @@
 void
 AudioCallbackPlaySource::setTimeStretch(double factor)
 {
-    m_stretchRatio = factor;
+    checkWrappers();
 
-    int rate = int(getSourceSampleRate());
-    if (!rate) return; // have to make our stretcher later
-
-    if (m_timeStretcher || (factor == 1.0)) {
-        // stretch ratio will be set in next process call if appropriate
-    } else {
-        m_stretcherInputCount = getTargetChannelCount();
-        RubberBandStretcher *stretcher = new RubberBandStretcher
-            (rate,
-             m_stretcherInputCount,
-             RubberBandStretcher::OptionProcessRealTime,
-             factor);
-        RubberBandStretcher *monoStretcher = new RubberBandStretcher
-            (rate,
-             1,
-             RubberBandStretcher::OptionProcessRealTime,
-             factor);
-        m_stretcherInputs = new float *[m_stretcherInputCount];
-        m_stretcherInputSizes = new sv_frame_t[m_stretcherInputCount];
-        for (int c = 0; c < m_stretcherInputCount; ++c) {
-            m_stretcherInputSizes[c] = 16384;
-            m_stretcherInputs[c] = new float[m_stretcherInputSizes[c]];
-        }
-        m_monoStretcher = monoStretcher;
-        m_timeStretcher = stretcher;
-    }
-
+    m_timeStretchWrapper->setTimeStretchRatio(factor);
+    
     emit activity(tr("Change time-stretch factor to %1").arg(factor));
 }
 
@@ -1202,230 +1144,51 @@
 
     if (count == 0) return 0;
 
-    RubberBandStretcher *ts = m_timeStretcher;
-    RubberBandStretcher *ms = m_monoStretcher;
-
-    double ratio = ts ? ts->getTimeRatio() : 1.0;
-
-    if (ratio != m_stretchRatio) {
-        if (!ts) {
-            SVCERR << "WARNING: AudioCallbackPlaySource::getSourceSamples: Time ratio change to " << m_stretchRatio << " is pending, but no stretcher is set" << endl;
-            m_stretchRatio = 1.0;
-        } else {
-            ts->setTimeRatio(m_stretchRatio);
-            if (ms) ms->setTimeRatio(m_stretchRatio);
-            if (m_stretchRatio >= 1.0) m_stretchMono = false;
-        }
-    }
-
-    int stretchChannels = m_stretcherInputCount;
-    if (m_stretchMono) {
-        if (ms) {
-            ts = ms;
-            stretchChannels = 1;
-        } else {
-            m_stretchMono = false;
-        }
-    }
-
     if (m_target) {
         m_lastRetrievedBlockSize = count;
         m_lastRetrievalTimestamp = m_target->getCurrentTime();
     }
 
-    if (!ts || ratio == 1.f) {
-
-        int got = 0;
+    int got = 0;
 
 #ifdef DEBUG_AUDIO_PLAY_SOURCE_PLAYING
-        cout << "channels == " << channels << endl;
+    cout << "channels == " << channels << endl;
 #endif
         
-        for (int ch = 0; ch < channels; ++ch) {
+    for (int ch = 0; ch < channels; ++ch) {
 
-            RingBuffer<float> *rb = getReadRingBuffer(ch);
+        RingBuffer<float> *rb = getReadRingBuffer(ch);
 
-            if (rb) {
+        if (rb) {
 
-                // this is marginally more likely to leave our channels in
-                // sync after a processing failure than just passing "count":
-                sv_frame_t request = count;
-                if (ch > 0) request = got;
+            // this is marginally more likely to leave our channels in
+            // sync after a processing failure than just passing "count":
+            sv_frame_t request = count;
+            if (ch > 0) request = got;
 
-                got = rb->read(buffer[ch], int(request));
+            got = rb->read(buffer[ch], int(request));
             
 #ifdef DEBUG_AUDIO_PLAY_SOURCE_PLAYING
-                cout << "AudioCallbackPlaySource::getSamples: got " << got << " (of " << count << ") samples on channel " << ch << ", signalling for more (possibly)" << endl;
+            cout << "AudioCallbackPlaySource::getSamples: got " << got << " (of " << count << ") samples on channel " << ch << ", signalling for more (possibly)" << endl;
 #endif
-            }
+        }
 
-            for (int ch = 0; ch < channels; ++ch) {
-                for (int i = got; i < count; ++i) {
-                    buffer[ch][i] = 0.0;
-                }
+        for (int ch = 0; ch < channels; ++ch) {
+            for (int i = got; i < count; ++i) {
+                buffer[ch][i] = 0.0;
             }
         }
-
-        applyAuditioningEffect(count, buffer);
+    }
 
 #ifdef DEBUG_AUDIO_PLAY_SOURCE
     cout << "AudioCallbackPlaySource::getSamples: awakening thread" << endl;
 #endif
 
-        m_condition.wakeAll();
-
-        return got;
-    }
-
-    sv_frame_t available;
-    sv_frame_t fedToStretcher = 0;
-    int warned = 0;
-
-    // The input block for a given output is approx output / ratio,
-    // but we can't predict it exactly, for an adaptive timestretcher.
-
-    while ((available = ts->available()) < count) {
-
-        sv_frame_t reqd = lrint(double(count - available) / ratio);
-        reqd = std::max(reqd, sv_frame_t(ts->getSamplesRequired()));
-        if (reqd == 0) reqd = 1;
-                
-        sv_frame_t got = reqd;
-
-#ifdef DEBUG_AUDIO_PLAY_SOURCE_PLAYING
-        cout << "reqd = " <<reqd << ", channels = " << channels << ", ic = " << m_stretcherInputCount << endl;
-#endif
-
-        for (int c = 0; c < channels; ++c) {
-            if (c >= m_stretcherInputCount) continue;
-            if (reqd > m_stretcherInputSizes[c]) {
-                if (c == 0) {
-                    SVDEBUG << "NOTE: resizing stretcher input buffer from " << m_stretcherInputSizes[c] << " to " << (reqd * 2) << endl;
-                }
-                delete[] m_stretcherInputs[c];
-                m_stretcherInputSizes[c] = reqd * 2;
-                m_stretcherInputs[c] = new float[m_stretcherInputSizes[c]];
-            }
-        }
-
-        for (int c = 0; c < channels; ++c) {
-            if (c >= m_stretcherInputCount) continue;
-            RingBuffer<float> *rb = getReadRingBuffer(c);
-            if (rb) {
-                sv_frame_t gotHere;
-                if (stretchChannels == 1 && c > 0) {
-                    gotHere = rb->readAdding(m_stretcherInputs[0], int(got));
-                } else {
-                    gotHere = rb->read(m_stretcherInputs[c], int(got));
-                }
-                if (gotHere < got) got = gotHere;
-                
-#ifdef DEBUG_AUDIO_PLAY_SOURCE_PLAYING
-                if (c == 0) {
-                    cout << "feeding stretcher: got " << gotHere
-                              << ", " << rb->getReadSpace() << " remain" << endl;
-                }
-#endif
-                
-            } else {
-                SVCERR << "WARNING: No ring buffer available for channel " << c << " in stretcher input block" << endl;
-            }
-        }
-
-        if (got < reqd) {
-            SVCERR << "WARNING: Read underrun in playback ("
-                      << got << " < " << reqd << ")" << endl;
-        }
-
-        ts->process(m_stretcherInputs, size_t(got), false);
-
-        fedToStretcher += got;
-
-        if (got == 0) break;
-
-        if (ts->available() == available) {
-            SVCERR << "WARNING: AudioCallbackPlaySource::getSamples: Added " << got << " samples to time stretcher, created no new available output samples (warned = " << warned << ")" << endl;
-            if (++warned == 5) break;
-        }
-    }
-
-    ts->retrieve(buffer, size_t(count));
-
-    v_zero_channels(buffer + stretchChannels, channels - stretchChannels, count);
-
-    applyAuditioningEffect(count, buffer);
-
-#ifdef DEBUG_AUDIO_PLAY_SOURCE
-    cout << "AudioCallbackPlaySource::getSamples [stretched]: awakening thread" << endl;
-#endif
-
     m_condition.wakeAll();
 
-    return count;
+    return got;
 }
 
-void
-AudioCallbackPlaySource::applyAuditioningEffect(sv_frame_t count, float *const *buffers)
-{
-    if (m_auditioningPluginBypassed) return;
-    RealTimePluginInstance *plugin = m_auditioningPlugin;
-    if (!plugin) return;
-
-    if ((int)plugin->getAudioInputCount() != getTargetChannelCount()) {
-        if (!m_auditioningPluginFailed) {
-            SVCERR << "AudioCallbackPlaySource::applyAuditioningEffect: "
-                   << "Can't run plugin: plugin input count "
-                   << plugin->getAudioInputCount() 
-                   << " != our channel count " << getTargetChannelCount()
-                   << " (future errors for this plugin will be suppressed)"
-                   << endl;
-            m_auditioningPluginFailed = true;
-        }
-        return;
-    }
-    if ((int)plugin->getAudioOutputCount() != getTargetChannelCount()) {
-        if (!m_auditioningPluginFailed) {
-            SVCERR << "AudioCallbackPlaySource::applyAuditioningEffect: "
-                   << "Can't run plugin: plugin output count "
-                   << plugin->getAudioOutputCount() 
-                   << " != our channel count " << getTargetChannelCount()
-                   << " (future errors for this plugin will be suppressed)"
-                   << endl;
-            m_auditioningPluginFailed = true;
-        }
-        return;
-    }
-    if ((int)plugin->getBufferSize() < count) {
-        if (!m_auditioningPluginFailed) {
-            SVCERR << "AudioCallbackPlaySource::applyAuditioningEffect: "
-                   << "Can't run plugin: plugin buffer size "
-                   << plugin->getBufferSize() 
-                   << " < our block size " << count
-                   << " (future errors for this plugin will be suppressed)"
-                   << endl;
-            m_auditioningPluginFailed = true;
-        }
-        return;
-    }
-    
-    float **ib = plugin->getAudioInputBuffers();
-    float **ob = plugin->getAudioOutputBuffers();
-
-    for (int c = 0; c < getTargetChannelCount(); ++c) {
-        for (int i = 0; i < count; ++i) {
-            ib[c][i] = buffers[c][i];
-        }
-    }
-
-    plugin->run(Vamp::RealTime::zeroTime, int(count));
-    
-    for (int c = 0; c < getTargetChannelCount(); ++c) {
-        for (int i = 0; i < count; ++i) {
-            buffers[c][i] = ob[c][i];
-        }
-    }
-}    
-
 // Called from fill thread, m_playing true, mutex held
 bool
 AudioCallbackPlaySource::fillBuffers()
--- a/audio/AudioCallbackPlaySource.h	Fri Apr 03 10:17:46 2020 +0100
+++ b/audio/AudioCallbackPlaySource.h	Wed Apr 22 17:40:09 2020 +0100
@@ -36,10 +36,6 @@
 #include <set>
 #include <map>
 
-namespace RubberBand {
-    class RubberBandStretcher;
-}
-
 namespace breakfastquay {
     class ResamplerWrapper;
 }
@@ -50,6 +46,8 @@
 class PlayParameters;
 class RealTimePluginInstance;
 class AudioCallbackPlayTarget;
+class TimeStretchWrapper;
+class EffectWrapper;
 
 /**
  * AudioCallbackPlaySource manages audio data supply to callback-based
@@ -67,6 +65,18 @@
 public:
     AudioCallbackPlaySource(ViewManagerBase *, QString clientName);
     virtual ~AudioCallbackPlaySource();
+
+    /**
+     * Return an ApplicationPlaybackSource interface to this
+     * class. Although this class implements ApplicationPlaybackSource
+     * itself, the object returned here may be a wrapper which
+     * provides facilities not implemented in this class, such as
+     * time-stretching, resampling, and an auditioning effect.  The
+     * returned pointer points to an object which is owned by this
+     * object. Caller must ensure the lifetime of this object exceeds
+     * the scope which the returned pointer is retained.
+     */
+    breakfastquay::ApplicationPlaybackSource *getApplicationPlaybackSource();
     
     /**
      * Add a data model to be played from.  The source can mix
@@ -124,11 +134,6 @@
      * Set the playback target.
      */
     virtual void setSystemPlaybackTarget(breakfastquay::SystemPlaybackTarget *);
-
-    /**
-     * Set the resampler wrapper, if one is in use.
-     */
-    virtual void setResamplerWrapper(breakfastquay::ResamplerWrapper *);
     
     /**
      * Set the block size of the target audio device.  This should be
@@ -288,7 +293,8 @@
      * Pass a null pointer to remove the current auditioning plugin,
      * if any.
      */
-    virtual void setAuditioningEffect(Auditionable *plugin) override;
+    virtual void setAuditioningEffect(std::shared_ptr<Auditionable> plugin)
+        override;
 
     /**
      * Specify that only the given set of models should be played.
@@ -315,7 +321,6 @@
     void channelCountIncreased(int count); // target channel count (see getTargetChannelCount())
 
     void audioOverloadPluginDisabled();
-    void audioTimeStretchMultiChannelDisabled();
 
     void activity(QString);
 
@@ -369,9 +374,6 @@
     float                             m_outputLeft;
     float                             m_outputRight;
     bool                              m_levelsSet;
-    RealTimePluginInstance           *m_auditioningPlugin;
-    bool                              m_auditioningPluginBypassed;
-    bool                              m_auditioningPluginFailed;
     Scavenger<RealTimePluginInstance> m_pluginScavenger;
     sv_frame_t                        m_playStartFrame;
     bool                              m_playStartFramePassed;
@@ -397,15 +399,6 @@
     void clearRingBuffers(bool haveLock = false, int count = 0);
     void unifyRingBuffers();
 
-    RubberBand::RubberBandStretcher *m_timeStretcher;
-    RubberBand::RubberBandStretcher *m_monoStretcher;
-    double m_stretchRatio;
-    bool m_stretchMono;
-    
-    int m_stretcherInputCount;
-    float **m_stretcherInputs;
-    sv_frame_t *m_stretcherInputSizes;
-
     // Called from fill thread, m_playing true, mutex held
     // Return true if work done
     bool fillBuffers();
@@ -416,9 +409,6 @@
     // frame argument passed in, in the case of looping).
     sv_frame_t mixModels(sv_frame_t &frame, sv_frame_t count, float **buffers);
 
-    // Called from getSourceSamples.
-    void applyAuditioningEffect(sv_frame_t count, float *const *buffers);
-
     // Ranges of current selections, if play selection is active
     std::vector<RealTime> m_rangeStarts;
     std::vector<RealTime> m_rangeDurations;
@@ -442,9 +432,11 @@
     QMutex m_mutex;
     QWaitCondition m_condition;
     FillThread *m_fillThread;
-    breakfastquay::ResamplerWrapper *m_resamplerWrapper; // I don't own this
+    breakfastquay::ResamplerWrapper *m_resamplerWrapper;
+    TimeStretchWrapper *m_timeStretchWrapper;
+    EffectWrapper *m_auditioningEffectWrapper;
+    void checkWrappers();
 };
 
 #endif
 
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/audio/EffectWrapper.cpp	Wed Apr 22 17:40:09 2020 +0100
@@ -0,0 +1,271 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "EffectWrapper.h"
+
+#include <rubberband/RubberBandStretcher.h>
+
+#include "base/Debug.h"
+
+//#define DEBUG_EFFECT_WRAPPER 1
+
+using namespace std;
+
+static const int DEFAULT_RING_BUFFER_SIZE = 131071;
+
+EffectWrapper::EffectWrapper(ApplicationPlaybackSource *source) :
+    m_source(source),
+    m_bypassed(false),
+    m_failed(false),
+    m_channelCount(0)
+{
+}
+
+EffectWrapper::~EffectWrapper()
+{
+}
+
+void
+EffectWrapper::setEffect(weak_ptr<RealTimePluginInstance> effect)
+{
+    lock_guard<mutex> guard(m_mutex);
+
+#ifdef DEBUG_EFFECT_WRAPPER
+    SVCERR << "EffectWrapper[" << this
+           << "]::setEffect(" << effect.lock() << ")" << endl;
+#endif
+    
+    m_effect = effect;
+    m_failed = false;
+}
+
+bool
+EffectWrapper::haveEffect() const
+{
+    return m_effect.lock() != nullptr;
+}
+
+void
+EffectWrapper::clearEffect()
+{
+    m_effect = {};
+}
+
+void
+EffectWrapper::setBypassed(bool bypassed)
+{
+    lock_guard<mutex> guard(m_mutex);
+
+#ifdef DEBUG_EFFECT_WRAPPER
+    SVCERR << "EffectWrapper[" << this
+           << "]::setBypassed(" << bypassed << ")" << endl;
+#endif
+
+    m_bypassed = bypassed;
+}
+
+bool
+EffectWrapper::isBypassed() const
+{
+    lock_guard<mutex> guard(m_mutex);
+
+    return m_bypassed;
+}
+
+void
+EffectWrapper::reset()
+{
+    lock_guard<mutex> guard(m_mutex);
+
+#ifdef DEBUG_EFFECT_WRAPPER
+    SVCERR << "EffectWrapper[" << this << "]::reset" << endl;
+#endif
+
+    for (auto &rb: m_effectOutputBuffers) {
+        rb.reset();
+    }
+
+    m_failed = false;
+}
+
+int
+EffectWrapper::getSourceSamples(float *const *samples,
+                                int nchannels, int nframes)
+{
+    lock_guard<mutex> guard(m_mutex);
+
+#ifdef DEBUG_EFFECT_WRAPPER
+    SVCERR << "EffectWrapper[" << this << "]::getSourceSamples: " << nframes
+           << " frames across " << nchannels << " channels" << endl;
+#endif
+    
+    auto effect(m_effect.lock());
+    
+    if (!effect) {
+#ifdef DEBUG_EFFECT_WRAPPER
+        SVCERR << "EffectWrapper::getSourceSamples: "
+               << "no effect is set" << endl;
+#endif
+        return m_source->getSourceSamples(samples, nchannels, nframes);
+    }
+
+    if (m_bypassed || m_failed) {
+#ifdef DEBUG_EFFECT_WRAPPER
+        SVCERR << "EffectWrapper::getSourceSamples: "
+               << "effect is bypassed or has failed" << endl;
+#endif
+        return m_source->getSourceSamples(samples, nchannels, nframes);
+    }
+    
+    static int warnings = 0;
+    if (nchannels != m_channelCount) {
+        if (warnings >= 0) {
+            SVCERR << "WARNING: EffectWrapper::getSourceSamples called for a number of channels different from that set with setSystemPlaybackChannelCount ("
+                   << nchannels << " vs " << m_channelCount << ")" << endl;
+            if (++warnings == 6) {
+                SVCERR << "(further warnings will be suppressed)" << endl;
+                warnings = -1;
+            }
+        }
+        return 0;
+    }
+    
+    if ((int)effect->getAudioInputCount() != m_channelCount) {
+        if (!m_failed) {
+            SVCERR << "EffectWrapper::getSourceSamples: "
+                   << "Can't run plugin: plugin input count "
+                   << effect->getAudioInputCount() 
+                   << " != our channel count " << m_channelCount
+                   << " (future errors for this plugin will be suppressed)"
+                   << endl;
+            m_failed = true;
+        }
+    }
+    if ((int)effect->getAudioOutputCount() != m_channelCount) {
+        if (!m_failed) {
+            SVCERR << "EffectWrapper::getSourceSamples: "
+                   << "Can't run plugin: plugin output count "
+                   << effect->getAudioOutputCount() 
+                   << " != our channel count " << m_channelCount
+                   << " (future errors for this plugin will be suppressed)"
+                   << endl;
+            m_failed = true;
+        }
+    }
+
+    if (m_failed) {
+        return m_source->getSourceSamples(samples, nchannels, nframes);
+    }
+    
+    float **ib = effect->getAudioInputBuffers();
+    float **ob = effect->getAudioOutputBuffers();
+    int blockSize = effect->getBufferSize();
+    
+    int got = 0;
+
+    while (got < nframes) {
+
+        int read = 0;
+        for (int c = 0; c < nchannels; ++c) {
+            read = m_effectOutputBuffers[c].read(samples[c] + got,
+                                                 nframes - got);
+        }
+
+        got += read;
+
+        if (got < nframes) {
+
+            int toRun = m_source->getSourceSamples(ib, nchannels, blockSize);
+            if (toRun <= 0) break;
+
+#ifdef DEBUG_EFFECT_WRAPPER
+            SVCERR << "EffectWrapper::getSourceSamples: Running effect "
+                   << "for " << toRun << " frames" << endl;
+#endif
+            effect->run(Vamp::RealTime::zeroTime, toRun);
+
+            for (int c = 0; c < nchannels; ++c) {
+                m_effectOutputBuffers[c].write(ob[c], toRun);
+            }
+        }
+    }
+        
+    return got;
+}
+
+void
+EffectWrapper::setSystemPlaybackChannelCount(int count)
+{
+    {
+        lock_guard<mutex> guard(m_mutex);
+#ifdef DEBUG_EFFECT_WRAPPER
+        SVCERR << "EffectWrapper[" << this
+               << "]::setSystemPlaybackChannelCount(" << count << ")" << endl;
+#endif
+        m_effectOutputBuffers.resize
+            (count, RingBuffer<float>(DEFAULT_RING_BUFFER_SIZE));
+        m_channelCount = count;
+    }
+    m_source->setSystemPlaybackChannelCount(count);
+}
+
+void
+EffectWrapper::setSystemPlaybackSampleRate(int rate)
+{
+    m_source->setSystemPlaybackSampleRate(rate);
+}
+
+std::string
+EffectWrapper::getClientName() const
+{
+    return m_source->getClientName();
+}
+
+int
+EffectWrapper::getApplicationSampleRate() const
+{
+    return m_source->getApplicationSampleRate();
+}
+
+int
+EffectWrapper::getApplicationChannelCount() const
+{
+    return m_source->getApplicationChannelCount();
+}
+
+void
+EffectWrapper::setSystemPlaybackBlockSize(int sz)
+{
+    SVDEBUG << "NOTE: EffectWrapper::setSystemPlaybackBlockSize called "
+            << "with size = " << sz << "; not passing to wrapped source, as "
+            << "actual block size will vary" << endl;
+}
+
+void
+EffectWrapper::setSystemPlaybackLatency(int latency)
+{
+    m_source->setSystemPlaybackLatency(latency);
+}
+
+void
+EffectWrapper::setOutputLevels(float left, float right)
+{
+    m_source->setOutputLevels(left, right);
+}
+
+void
+EffectWrapper::audioProcessingOverload()
+{
+    m_source->audioProcessingOverload();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/audio/EffectWrapper.h	Wed Apr 22 17:40:09 2020 +0100
@@ -0,0 +1,120 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef SV_EFFECT_WRAPPER_H
+#define SV_EFFECT_WRAPPER_H
+
+#include "bqaudioio/ApplicationPlaybackSource.h"
+
+#include "base/BaseTypes.h"
+#include "base/RingBuffer.h"
+
+#include "plugin/RealTimePluginInstance.h"
+
+#include <vector>
+#include <mutex>
+#include <memory>
+
+/**
+ * A breakfastquay::ApplicationPlaybackSource wrapper that applies a
+ * real-time effect plugin.
+ */
+class EffectWrapper : public breakfastquay::ApplicationPlaybackSource
+{
+public:
+    /**
+     * Create a wrapper around the given ApplicationPlaybackSource,
+     * implementing another ApplicationPlaybackSource interface that
+     * draws from the same source data but with an effect optionally
+     * applied.
+     *
+     * The wrapper does not take ownership of the wrapped
+     * ApplicationPlaybackSource, whose lifespan must exceed that of
+     * this object.    
+     */
+    EffectWrapper(ApplicationPlaybackSource *source);
+    ~EffectWrapper();
+
+    /**
+     * Set the effect to apply. The effect instance is shared with the
+     * caller: the expectation is that the caller may continue to
+     * modify its parameters etc during auditioning. Replaces any
+     * instance previously set.
+     */
+    void setEffect(std::weak_ptr<RealTimePluginInstance>);
+
+    /**
+     * Return true if an effect is currently set to be applied.
+     */
+    bool haveEffect() const;
+    
+    /**
+     * Remove any applied effect without setting another one.
+     */
+    void clearEffect();
+
+    /**
+     * Bypass or un-bypass the effect.
+     */
+    void setBypassed(bool bypassed);
+
+    /**
+     * Return true if the effect is bypassed.
+     */
+    bool isBypassed() const;
+    
+    /**
+     * Clear any buffered data.
+     */
+    void reset();
+
+    // These functions are passed through to the wrapped
+    // ApplicationPlaybackSource
+    
+    std::string getClientName() const override;
+    int getApplicationSampleRate() const override;
+    int getApplicationChannelCount() const override;
+
+    void setSystemPlaybackBlockSize(int) override;
+    void setSystemPlaybackSampleRate(int) override;
+    void setSystemPlaybackChannelCount(int) override;
+    void setSystemPlaybackLatency(int) override;
+
+    void setOutputLevels(float peakLeft, float peakRight) override;
+    void audioProcessingOverload() override;
+
+    /** 
+     * Request some samples from the wrapped
+     * ApplicationPlaybackSource, apply effect if set, and return them
+     * to the target
+     */
+    int getSourceSamples(float *const *samples, int nchannels, int nframes)
+        override;
+
+private:
+    ApplicationPlaybackSource *m_source;
+    std::weak_ptr<RealTimePluginInstance> m_effect;
+    bool m_bypassed;
+    bool m_failed;
+    int m_channelCount;
+    std::vector<RingBuffer<float>> m_effectOutputBuffers;
+    mutable std::mutex m_mutex;
+
+    EffectWrapper(const EffectWrapper &)=delete;
+    EffectWrapper &operator=(const EffectWrapper &)=delete;
+};
+
+#endif
+
+    
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/audio/TimeStretchWrapper.cpp	Wed Apr 22 17:40:09 2020 +0100
@@ -0,0 +1,228 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "TimeStretchWrapper.h"
+
+#include <rubberband/RubberBandStretcher.h>
+
+#include "base/Debug.h"
+
+using namespace RubberBand;
+using namespace std;
+
+TimeStretchWrapper::TimeStretchWrapper(ApplicationPlaybackSource *source) :
+    m_source(source),
+    m_stretcher(nullptr),
+    m_timeRatio(1.0),
+    m_stretcherInputSize(16384),
+    m_channelCount(0),
+    m_sampleRate(0)
+{
+}
+
+TimeStretchWrapper::~TimeStretchWrapper()
+{
+    delete m_stretcher;
+}
+
+void
+TimeStretchWrapper::setTimeStretchRatio(double ratio)
+{
+    lock_guard<mutex> guard(m_mutex);
+
+    SVDEBUG << "TimeStretchWrapper::setTimeStretchRatio: setting ratio to "
+            << ratio << " (was " << m_timeRatio << ")" << endl;
+    
+    m_timeRatio = ratio;
+
+    // Stretcher will be updated by checkStretcher() from next call to
+    // getSourceSamples()
+}
+
+void
+TimeStretchWrapper::reset()
+{
+    lock_guard<mutex> guard(m_mutex);
+
+    if (m_stretcher) {
+        m_stretcher->reset();
+    }
+}
+
+int
+TimeStretchWrapper::getSourceSamples(float *const *samples,
+                                     int nchannels, int nframes)
+{
+    checkStretcher();
+
+    lock_guard<mutex> guard(m_mutex);
+
+    static int warnings = 0;
+    if (nchannels != m_channelCount) {
+        if (warnings >= 0) {
+            SVCERR << "WARNING: getSourceSamples called for a number of channels different from that set with setSystemPlaybackChannelCount ("
+                   << nchannels << " vs " << m_channelCount << ")" << endl;
+            if (++warnings == 6) {
+                SVCERR << "(further warnings will be suppressed)" << endl;
+                warnings = -1;
+            }
+        }
+        return 0;
+    }
+    
+    if (!m_stretcher) {
+        return m_source->getSourceSamples(samples, nchannels, nframes);
+    }
+
+    vector<float *> inputPtrs(m_channelCount, nullptr);
+    for (int i = 0; i < m_channelCount; ++i) {
+        inputPtrs[i] = m_inputs[i].data();
+    }
+    
+    // The input block for a given output is approx output / ratio,
+    // but we can't predict it exactly, for an adaptive timestretcher.
+
+    sv_frame_t available;
+
+    while ((available = m_stretcher->available()) < nframes) {
+        
+        int reqd = int(ceil(double(nframes - available) / m_timeRatio));
+        reqd = std::max(reqd, int(m_stretcher->getSamplesRequired()));
+        reqd = std::min(reqd, m_stretcherInputSize);
+        if (reqd == 0) reqd = 1;
+        
+        int got = m_source->getSourceSamples
+            (inputPtrs.data(), nchannels, reqd);
+
+        if (got <= 0) {
+            SVCERR << "WARNING: Failed to obtain any source samples at all"
+                   << endl;
+            return 0;
+        }
+            
+        m_stretcher->process
+            (inputPtrs.data(), size_t(got), false);
+    }
+
+    return int(m_stretcher->retrieve(samples, nframes));
+}
+
+void
+TimeStretchWrapper::checkStretcher()
+{
+    lock_guard<mutex> guard(m_mutex);
+
+    if (m_timeRatio == 1.0 || !m_channelCount || !m_sampleRate) {
+        if (m_stretcher) {
+            SVDEBUG << "TimeStretchWrapper::checkStretcher: m_timeRatio = "
+                    << m_timeRatio << ", m_channelCount = " << m_channelCount
+                    << ", m_sampleRate = " << m_sampleRate
+                    << ", deleting existing stretcher" << endl;
+            delete m_stretcher;
+            m_stretcher = nullptr;
+        }
+        return;
+    }
+    
+    if (m_stretcher) {
+        SVDEBUG << "TimeStretchWrapper::checkStretcher: setting stretcher ratio to " << m_timeRatio << endl;
+        m_stretcher->setTimeRatio(m_timeRatio);
+        return;
+    }
+
+    SVDEBUG << "TimeStretchWrapper::checkStretcher: creating stretcher with ratio " << m_timeRatio << endl;
+    
+    m_stretcher = new RubberBandStretcher
+        (size_t(round(m_sampleRate)),
+         m_channelCount,
+         RubberBandStretcher::OptionProcessRealTime,
+         m_timeRatio);
+
+    m_inputs.resize(m_channelCount);
+    for (auto &v: m_inputs) {
+        v.resize(m_stretcherInputSize);
+    }
+}
+
+void
+TimeStretchWrapper::setSystemPlaybackChannelCount(int count)
+{
+    {
+        lock_guard<mutex> guard(m_mutex);
+        if (m_channelCount != count) {
+            delete m_stretcher;
+            m_stretcher = nullptr;
+        }
+        m_channelCount = count;
+    }
+    m_source->setSystemPlaybackChannelCount(count);
+}
+
+void
+TimeStretchWrapper::setSystemPlaybackSampleRate(int rate)
+{
+    {
+        lock_guard<mutex> guard(m_mutex);
+        if (m_sampleRate != rate) {
+            delete m_stretcher;
+            m_stretcher = nullptr;
+        }
+        m_sampleRate = rate;
+    }
+    m_source->setSystemPlaybackSampleRate(rate);
+}
+
+std::string
+TimeStretchWrapper::getClientName() const
+{
+    return m_source->getClientName();
+}
+
+int
+TimeStretchWrapper::getApplicationSampleRate() const
+{
+    return m_source->getApplicationSampleRate();
+}
+
+int
+TimeStretchWrapper::getApplicationChannelCount() const
+{
+    return m_source->getApplicationChannelCount();
+}
+
+void
+TimeStretchWrapper::setSystemPlaybackBlockSize(int sz)
+{
+    SVDEBUG << "NOTE: TimeStretchWrapper::setSystemPlaybackBlockSize called "
+            << "with size = " << sz << "; not passing to wrapped source, as "
+            << "actual block size will vary" << endl;
+}
+
+void
+TimeStretchWrapper::setSystemPlaybackLatency(int latency)
+{
+    m_source->setSystemPlaybackLatency(latency);
+}
+
+void
+TimeStretchWrapper::setOutputLevels(float left, float right)
+{
+    m_source->setOutputLevels(left, right);
+}
+
+void
+TimeStretchWrapper::audioProcessingOverload()
+{
+    m_source->audioProcessingOverload();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/audio/TimeStretchWrapper.h	Wed Apr 22 17:40:09 2020 +0100
@@ -0,0 +1,108 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef SV_TIME_STRETCH_WRAPPER_H
+#define SV_TIME_STRETCH_WRAPPER_H
+
+#include "bqaudioio/ApplicationPlaybackSource.h"
+
+#include "base/BaseTypes.h"
+
+#include <vector>
+#include <mutex>
+
+namespace RubberBand {
+    class RubberBandStretcher;
+}
+
+/**
+ * A breakfastquay::ApplicationPlaybackSource wrapper that implements
+ * time-stretching using Rubber Band. Note that the stretcher is
+ * bypassed entirely when a ratio of 1.0 is set; this means it's
+ * (almost) free to use one of these wrappers normally, but it also
+ * means you can't switch from 1.0 to another ratio (or back again)
+ * without some audible artifacts.
+ *
+ * This is real-time safe while the ratio is fixed, and may perform
+ * reallocations when the ratio changes.
+ */
+class TimeStretchWrapper : public breakfastquay::ApplicationPlaybackSource
+{
+public:
+    /**
+     * Create a wrapper around the given ApplicationPlaybackSource,
+     * implementing another ApplicationPlaybackSource interface that
+     * draws from the same source data but with a time-stretcher
+     * optionally applied.
+     *
+     * The wrapper does not take ownership of the wrapped
+     * ApplicationPlaybackSource, whose lifespan must exceed that of
+     * this object.    
+     */
+    TimeStretchWrapper(ApplicationPlaybackSource *source);
+    ~TimeStretchWrapper();
+
+    /**
+     * Set a time stretch factor, i.e. playback speed, where 1.0 is
+     * normal speed
+     */
+    void setTimeStretchRatio(double ratio);
+
+    /**
+     * Clear stretcher buffers.
+     */
+    void reset();
+
+    // These functions are passed through to the wrapped
+    // ApplicationPlaybackSource
+    
+    std::string getClientName() const override;
+    int getApplicationSampleRate() const override;
+    int getApplicationChannelCount() const override;
+
+    void setSystemPlaybackBlockSize(int) override;
+    void setSystemPlaybackSampleRate(int) override;
+    void setSystemPlaybackChannelCount(int) override;
+    void setSystemPlaybackLatency(int) override;
+
+    void setOutputLevels(float peakLeft, float peakRight) override;
+    void audioProcessingOverload() override;
+
+    /** 
+     * Request some samples from the wrapped
+     * ApplicationPlaybackSource, time-stretch if appropriate, and
+     * return them to the target
+     */
+    int getSourceSamples(float *const *samples, int nchannels, int nframes)
+        override;
+
+private:
+    ApplicationPlaybackSource *m_source;
+    RubberBand::RubberBandStretcher *m_stretcher;
+    double m_timeRatio;
+    std::vector<std::vector<float>> m_inputs;
+    std::mutex m_mutex;
+    int m_stretcherInputSize;
+    int m_channelCount;
+    sv_samplerate_t m_sampleRate;
+
+    void checkStretcher(); // call without m_mutex held
+    
+    TimeStretchWrapper(const TimeStretchWrapper &)=delete;
+    TimeStretchWrapper &operator=(const TimeStretchWrapper &)=delete;
+};
+
+#endif
+
+    
--- a/files.pri	Fri Apr 03 10:17:46 2020 +0100
+++ b/files.pri	Wed Apr 22 17:40:09 2020 +0100
@@ -6,7 +6,9 @@
            audio/AudioGenerator.h \
            audio/ClipMixer.h \
            audio/ContinuousSynth.h \
+           audio/EffectWrapper.h \
            audio/PlaySpeedRangeMapper.h \
+           audio/TimeStretchWrapper.h \
 	   framework/Document.h \
            framework/MainWindowBase.h \
            framework/OSCScript.h \
@@ -21,7 +23,9 @@
            audio/AudioGenerator.cpp \
            audio/ClipMixer.cpp \
            audio/ContinuousSynth.cpp \
+           audio/EffectWrapper.cpp \
            audio/PlaySpeedRangeMapper.cpp \
+           audio/TimeStretchWrapper.cpp \
 	   framework/Document.cpp \
            framework/MainWindowBase.cpp \
            framework/SVFileReader.cpp \
--- a/framework/MainWindowBase.cpp	Fri Apr 03 10:17:46 2020 +0100
+++ b/framework/MainWindowBase.cpp	Wed Apr 22 17:40:09 2020 +0100
@@ -82,7 +82,6 @@
 #include <bqaudioio/SystemPlaybackTarget.h>
 #include <bqaudioio/SystemAudioIO.h>
 #include <bqaudioio/AudioFactory.h>
-#include <bqaudioio/ResamplerWrapper.h>
 
 #include <QApplication>
 #include <QMessageBox>
@@ -151,7 +150,6 @@
     m_midiMode(midiMode),
     m_playSource(nullptr),
     m_recordTarget(nullptr),
-    m_resamplerWrapper(nullptr),
     m_playTarget(nullptr),
     m_audioIO(nullptr),
     m_oscQueue(nullptr),
@@ -283,8 +281,6 @@
             this,           SLOT(audioChannelCountIncreased(int)));
     connect(m_playSource, SIGNAL(audioOverloadPluginDisabled()),
             this,           SLOT(audioOverloadPluginDisabled()));
-    connect(m_playSource, SIGNAL(audioTimeStretchMultiChannelDisabled()),
-            this,           SLOT(audioTimeStretchMultiChannelDisabled()));
 
     connect(m_viewManager, SIGNAL(monitoringLevelsChanged(float, float)),
             this, SLOT(monitoringLevelsChanged(float, float)));
@@ -2572,17 +2568,14 @@
     SVCERR << "createAudioIO: Preferred record device = \""
             << preference.recordDevice << "\"" << endl;
 
-    if (!m_resamplerWrapper) {
-        m_resamplerWrapper = new breakfastquay::ResamplerWrapper(m_playSource);
-        m_playSource->setResamplerWrapper(m_resamplerWrapper);
-    }
+    breakfastquay::ApplicationPlaybackSource *source =
+        m_playSource->getApplicationPlaybackSource();
 
     std::string errorString;
     
     if (m_audioMode == AUDIO_PLAYBACK_AND_RECORD) {
         m_audioIO = breakfastquay::AudioFactory::
-            createCallbackIO(m_recordTarget, m_resamplerWrapper,
-                             preference, errorString);
+            createCallbackIO(m_recordTarget, source, preference, errorString);
         if (m_audioIO) {
             SVCERR << "MainWindowBase::createAudioIO: Suspending on creation" << endl;
             m_audioIO->suspend(); // start in suspended state
@@ -2597,8 +2590,7 @@
 
     if (!m_audioIO) {
         m_playTarget = breakfastquay::AudioFactory::
-            createCallbackPlayTarget(m_resamplerWrapper,
-                                     preference, errorString);
+            createCallbackPlayTarget(source, preference, errorString);
         if (m_playTarget) {
             SVCERR << "MainWindowBase::createAudioIO: Suspending on creation" << endl;
             m_playTarget->suspend(); // start in suspended state
@@ -2652,7 +2644,6 @@
     // First prevent this trying to call target.
     if (m_playSource) {
         m_playSource->setSystemPlaybackTarget(nullptr);
-        m_playSource->setResamplerWrapper(nullptr);
     }
 
     // Then delete the breakfastquay::System object.
@@ -2660,14 +2651,8 @@
     delete m_audioIO;
     delete m_playTarget;
 
-    // And the breakfastquay resampler wrapper. We need to
-    // delete/recreate this if the channel count changes, which is one
-    // of the use cases for recreateAudioIO() calling this
-    delete m_resamplerWrapper;
-
     m_audioIO = nullptr;
     m_playTarget = nullptr;
-    m_resamplerWrapper = nullptr;
 }
 
 void
--- a/framework/MainWindowBase.h	Fri Apr 03 10:17:46 2020 +0100
+++ b/framework/MainWindowBase.h	Wed Apr 22 17:40:09 2020 +0100
@@ -50,6 +50,7 @@
 class WaveformLayer;
 class WaveFileModel;
 class AudioCallbackPlaySource;
+class TimeStretchWrapper;
 class AudioCallbackRecordTarget;
 class CommandHistory;
 class QMenu;
@@ -75,7 +76,6 @@
 namespace breakfastquay {
     class SystemPlaybackTarget;
     class SystemAudioIO;
-    class ResamplerWrapper;
 }
 
 /**
@@ -307,7 +307,6 @@
 
     virtual void sampleRateMismatch(sv_samplerate_t, sv_samplerate_t, bool) = 0;
     virtual void audioOverloadPluginDisabled() = 0;
-    virtual void audioTimeStretchMultiChannelDisabled() = 0;
 
     virtual void playbackFrameChanged(sv_frame_t);
     virtual void globalCentreFrameChanged(sv_frame_t);
@@ -415,7 +414,6 @@
 
     AudioCallbackPlaySource *m_playSource;
     AudioCallbackRecordTarget *m_recordTarget;
-    breakfastquay::ResamplerWrapper *m_resamplerWrapper;
     breakfastquay::SystemPlaybackTarget *m_playTarget; // only one of this...
     breakfastquay::SystemAudioIO *m_audioIO;           // ... and this exists
 
--- a/framework/TransformUserConfigurator.cpp	Fri Apr 03 10:17:46 2020 +0100
+++ b/framework/TransformUserConfigurator.cpp	Wed Apr 22 17:40:09 2020 +0100
@@ -40,15 +40,20 @@
 
 bool
 TransformUserConfigurator::getChannelRange(TransformId identifier,
-                                           Vamp::PluginBase *plugin,
+                                           std::shared_ptr<Vamp::PluginBase> plugin,
                                            int &minChannels, int &maxChannels)
 {
     if (plugin && plugin->getType() == "Feature Extraction Plugin") {
-        Vamp::Plugin *vp = static_cast<Vamp::Plugin *>(plugin);
-        SVDEBUG << "TransformUserConfigurator::getChannelRange: is a Vamp plugin" << endl;
-        minChannels = int(vp->getMinChannelCount());
-        maxChannels = int(vp->getMaxChannelCount());
-        return true;
+        auto vp = std::dynamic_pointer_cast<Vamp::Plugin>(plugin);
+        if (vp) {
+            SVDEBUG << "TransformUserConfigurator::getChannelRange: is a Vamp plugin" << endl;
+            minChannels = int(vp->getMinChannelCount());
+            maxChannels = int(vp->getMaxChannelCount());
+            return true;
+        } else {
+            SVCERR << "TransformUserConfigurator::getChannelRange: inconsistent plugin identity!" << endl;
+            return false;
+        }
     } else {
         SVDEBUG << "TransformUserConfigurator::getChannelRange: is not a Vamp plugin" << endl;
         return TransformFactory::getInstance()->
@@ -59,7 +64,7 @@
 bool
 TransformUserConfigurator::configure(ModelTransformer::Input &input,
                                      Transform &transform,
-                                     Vamp::PluginBase *plugin,
+                                     std::shared_ptr<Vamp::PluginBase> plugin,
                                      ModelId &inputModel,
                                      AudioPlaySource *source,
                                      sv_frame_t startFrame,
@@ -85,36 +90,35 @@
     if (RealTimePluginFactory::instanceFor(id)) {
 
         RealTimePluginFactory *factory = RealTimePluginFactory::instanceFor(id);
-        const RealTimePluginDescriptor *desc = factory->getPluginDescriptor(id);
+        RealTimePluginDescriptor desc = factory->getPluginDescriptor(id);
 
-        if (desc->audioInputPortCount > 0 && 
-            desc->audioOutputPortCount > 0 &&
-            !desc->isSynth) {
+        if (desc.audioInputPortCount > 0 && 
+            desc.audioOutputPortCount > 0 &&
+            !desc.isSynth) {
             effect = true;
         }
 
-        if (desc->audioInputPortCount == 0) {
+        if (desc.audioInputPortCount == 0) {
             generator = true;
         }
 
         if (output != "A") {
             int outputNo = output.toInt();
-            if (outputNo >= 0 && outputNo < int(desc->controlOutputPortCount)) {
-                outputLabel = desc->controlOutputPortNames[outputNo].c_str();
+            if (outputNo >= 0 && outputNo < int(desc.controlOutputPortCount)) {
+                outputLabel = desc.controlOutputPortNames[outputNo].c_str();
             }
         }
 
-        RealTimePluginInstance *rtp =
-            static_cast<RealTimePluginInstance *>(plugin);
-
-        if (effect && source) {
+        auto auditionable = std::dynamic_pointer_cast<Auditionable>(plugin);
+        
+        if (effect && source && auditionable) {
             SVDEBUG << "Setting auditioning effect" << endl;
-            source->setAuditioningEffect(rtp);
+            source->setAuditioningEffect(auditionable);
         }
 
     } else {
 
-        Vamp::Plugin *vp = static_cast<Vamp::Plugin *>(plugin);
+        auto vp = std::dynamic_pointer_cast<Vamp::Plugin>(plugin);
 
         frequency = (vp->getInputDomain() == Vamp::Plugin::FrequencyDomain);
 
@@ -154,7 +158,8 @@
         (plugin, parentWidget);
 
     dialog->setMoreInfoUrl(TransformFactory::getInstance()->
-                           getTransformInfoUrl(transform.getIdentifier()));
+                           getTransformProvider(transform.getIdentifier())
+                           .infoUrl);
 
     if (candidateModelNames.size() > 1 && !generator) {
         dialog->setCandidateInputModels(candidateModelNames,
@@ -229,7 +234,7 @@
     delete dialog;
 
     if (effect && source) {
-        source->setAuditioningEffect(nullptr);
+        source->setAuditioningEffect({});
     }
 
     return ok;
--- a/framework/TransformUserConfigurator.h	Fri Apr 03 10:17:46 2020 +0100
+++ b/framework/TransformUserConfigurator.h	Wed Apr 22 17:40:09 2020 +0100
@@ -17,6 +17,8 @@
 
 #include "transform/ModelTransformerFactory.h"
 
+#include <memory>
+
 class TransformUserConfigurator : public ModelTransformerFactory::UserConfigurator
 {
 public:
@@ -24,7 +26,7 @@
 
     bool configure(ModelTransformer::Input &input,
                    Transform &transform,
-                   Vamp::PluginBase *plugin,
+                   std::shared_ptr<Vamp::PluginBase> plugin,
                    ModelId &inputModel,
                    AudioPlaySource *source,
                    sv_frame_t startFrame,
@@ -37,7 +39,8 @@
 
 private:
     bool getChannelRange(TransformId identifier,
-                         Vamp::PluginBase *plugin, int &min, int &max);
+                         std::shared_ptr<Vamp::PluginBase> plugin,
+                         int &min, int &max);
 
 };