changeset 738:48001ed9143b audio-source-refactor

Introduce TimeStretchWrapper; some work towards making the AudioCallbackPlaySource not actually try to be an ApplicationPlaybackSource itself but only return one that is constructed from wrappers that it controls the lifespan of
author Chris Cannam
date Wed, 18 Mar 2020 12:51:41 +0000
parents 497d80d3b9c4
children ddfac001b543
files audio/AudioCallbackPlaySource.cpp audio/AudioCallbackPlaySource.h audio/TimeStretchWrapper.cpp audio/TimeStretchWrapper.h files.pri framework/MainWindowBase.cpp framework/MainWindowBase.h
diffstat 7 files changed, 426 insertions(+), 272 deletions(-) [+]
line wrap: on
line diff
--- a/audio/AudioCallbackPlaySource.cpp	Wed Feb 05 12:33:24 2020 +0000
+++ b/audio/AudioCallbackPlaySource.cpp	Wed Mar 18 12:51:41 2020 +0000
@@ -16,6 +16,7 @@
 #include "AudioCallbackPlaySource.h"
 
 #include "AudioGenerator.h"
+#include "TimeStretchWrapper.h"
 
 #include "data/model/Model.h"
 #include "base/ViewManagerBase.h"
@@ -32,9 +33,6 @@
 
 #include "bqvec/VectorOps.h"
 
-#include <rubberband/RubberBandStretcher.h>
-using namespace RubberBand;
-
 using breakfastquay::v_zero_channels;
 
 #include <iostream>
@@ -78,15 +76,9 @@
     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_viewManager->setAudioPlaySource(this);
 
@@ -135,15 +127,6 @@
 
     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;
-
     m_bufferScavenger.scavenge(true);
     m_pluginScavenger.scavenge(true);
 #ifdef DEBUG_AUDIO_PLAY_SOURCE
@@ -151,6 +134,32 @@
 #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_timeStretchWrapper) {
+        m_timeStretchWrapper = new TimeStretchWrapper(m_resamplerWrapper);
+    }
+}
+
 void
 AudioCallbackPlaySource::addModel(ModelId modelId)
 {
@@ -250,24 +259,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 +482,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;
@@ -605,20 +601,13 @@
         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
 AudioCallbackPlaySource::setSystemPlaybackTarget(breakfastquay::SystemPlaybackTarget *target)
 {
+    //!!! This should go, we should be using the ApplicationPlaybackSource callbacks
+    
     if (target == nullptr) {
         // reset target-related facts and figures
         m_deviceSampleRate = 0;
@@ -628,16 +617,6 @@
 }
 
 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;
@@ -736,6 +715,7 @@
 
     RealTime inbuffer_t = RealTime::frame2RealTime(inbuffer, rate);
 
+    /*!!!
     sv_frame_t stretchlat = 0;
     double timeRatio = 1.0;
 
@@ -745,7 +725,7 @@
     }
 
     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 +764,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 << ", time ratio " << timeRatio << "\n  stretcher latency: " << stretchlat_t << ", device latency: " << latency_t << "\n  since request: " << sincerequest_t << ", last retrieved quantity: " << lastretrieved_t << endl;
 #endif
@@ -805,7 +779,8 @@
     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 - 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 +806,8 @@
     RealTime playing_t = bufferedto_t;
 
     playing_t = playing_t
-        - latency_t - stretchlat_t - lastretrieved_t - inbuffer_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
@@ -850,7 +826,8 @@
 //            cout << "playing_t " << playing_t << " < playstart_t " 
 //                      << playstart_t << endl;
             if (/*!!! sincerequest_t > RealTime::zeroTime && */
-                m_playStartedAt + latency_t + stretchlat_t <
+//!!!                m_playStartedAt + latency_t + stretchlat_t <
+                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;
@@ -1069,35 +1046,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,166 +1154,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);
+    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
--- a/audio/AudioCallbackPlaySource.h	Wed Feb 05 12:33:24 2020 +0000
+++ b/audio/AudioCallbackPlaySource.h	Wed Mar 18 12:51:41 2020 +0000
@@ -36,10 +36,6 @@
 #include <set>
 #include <map>
 
-namespace RubberBand {
-    class RubberBandStretcher;
-}
-
 namespace breakfastquay {
     class ResamplerWrapper;
 }
@@ -50,6 +46,7 @@
 class PlayParameters;
 class RealTimePluginInstance;
 class AudioCallbackPlayTarget;
+class TimeStretchWrapper;
 
 /**
  * AudioCallbackPlaySource manages audio data supply to callback-based
@@ -60,6 +57,7 @@
  */
 class AudioCallbackPlaySource : public QObject,
                                 public AudioPlaySource,
+                                //!!! to remove:
                                 public breakfastquay::ApplicationPlaybackSource
 {
     Q_OBJECT
@@ -67,6 +65,15 @@
 public:
     AudioCallbackPlaySource(ViewManagerBase *, QString clientName);
     virtual ~AudioCallbackPlaySource();
+
+    /**
+     * Return an ApplicationPlaybackSource interface to this class.
+     * The returned pointer is only borrowed, and the object continues
+     * to be owned by us. Caller must ensure the lifetime of the
+     * AudioCallbackPlaySource exceeds the scope in which the pointer
+     * is retained.
+     */
+    breakfastquay::ApplicationPlaybackSource *getApplicationPlaybackSource();
     
     /**
      * Add a data model to be played from.  The source can mix
@@ -124,11 +131,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
@@ -315,7 +317,6 @@
     void channelCountIncreased(int count); // target channel count (see getTargetChannelCount())
 
     void audioOverloadPluginDisabled();
-    void audioTimeStretchMultiChannelDisabled();
 
     void activity(QString);
 
@@ -397,15 +398,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();
@@ -442,9 +434,10 @@
     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;
+    void checkWrappers();
 };
 
 #endif
 
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/audio/TimeStretchWrapper.cpp	Wed Mar 18 12:51:41 2020 +0000
@@ -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);
+    }
+
+    sv_frame_t available;
+    sv_frame_t fedToStretcher = 0;
+
+    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.
+
+    while ((available = m_stretcher->available()) < nframes) {
+        
+        sv_frame_t reqd = sv_frame_t
+            (ceil(double(nframes - available) / m_timeRatio));
+        reqd = std::max(reqd, sv_frame_t(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) {
+        SVDEBUG << "TimeStretchWrapper::checkStretcher: m_timeRatio = "
+                << m_timeRatio << ", m_channelCount = " << m_channelCount
+                << ", m_sampleRate = " << m_sampleRate
+                << ", no need for stretcher" << endl;
+        if (m_stretcher) {
+            SVDEBUG << "(Deleting existing one)" << 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
+        (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)
+{
+}
+
+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 Mar 18 12:51:41 2020 +0000
@@ -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;
+    sv_frame_t 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	Wed Feb 05 12:33:24 2020 +0000
+++ b/files.pri	Wed Mar 18 12:51:41 2020 +0000
@@ -6,6 +6,7 @@
            audio/ClipMixer.h \
            audio/ContinuousSynth.h \
            audio/PlaySpeedRangeMapper.h \
+           audio/TimeStretchWrapper.h \
            framework/Align.h \
 	   framework/Document.h \
            framework/MainWindowBase.h \
@@ -21,6 +22,7 @@
            audio/ClipMixer.cpp \
            audio/ContinuousSynth.cpp \
            audio/PlaySpeedRangeMapper.cpp \
+           audio/TimeStretchWrapper.cpp \
 	   framework/Align.cpp \
 	   framework/Document.cpp \
            framework/MainWindowBase.cpp \
--- a/framework/MainWindowBase.cpp	Wed Feb 05 12:33:24 2020 +0000
+++ b/framework/MainWindowBase.cpp	Wed Mar 18 12:51:41 2020 +0000
@@ -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),
@@ -2568,17 +2566,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
@@ -2593,8 +2588,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
@@ -2648,7 +2642,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.
@@ -2656,14 +2649,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	Wed Feb 05 12:33:24 2020 +0000
+++ b/framework/MainWindowBase.h	Wed Mar 18 12:51:41 2020 +0000
@@ -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;
 }
 
 /**
@@ -413,7 +413,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