changeset 564:07e111dd5902 levelpanwidget

Merge from branch 3.0-integration
author Chris Cannam
date Wed, 14 Dec 2016 14:28:41 +0000
parents fcac6c6b8deb (current diff) ce3818cd16c5 (diff)
children be83e81d9156
files framework/MainWindowBase.h
diffstat 6 files changed, 465 insertions(+), 402 deletions(-) [+]
line wrap: on
line diff
--- a/audio/AudioCallbackPlaySource.cpp	Mon Dec 05 17:03:09 2016 +0000
+++ b/audio/AudioCallbackPlaySource.cpp	Wed Dec 14 14:28:41 2016 +0000
@@ -28,10 +28,15 @@
 #include "plugin/RealTimePluginInstance.h"
 
 #include "bqaudioio/SystemPlaybackTarget.h"
+#include "bqaudioio/ResamplerWrapper.h"
+
+#include "bqvec/VectorOps.h"
 
 #include <rubberband/RubberBandStretcher.h>
 using namespace RubberBand;
 
+using breakfastquay::v_zero_channels;
+
 #include <iostream>
 #include <cassert>
 
@@ -53,7 +58,8 @@
     m_sourceChannelCount(0),
     m_blockSize(1024),
     m_sourceSampleRate(0),
-    m_targetSampleRate(0),
+    m_deviceSampleRate(0),
+    m_deviceChannelCount(0),
     m_playLatency(0),
     m_target(0),
     m_lastRetrievalTimestamp(0.0),
@@ -78,7 +84,7 @@
     m_stretcherInputs(0),
     m_stretcherInputSizes(0),
     m_fillThread(0),
-    m_converter(0)
+    m_resamplerWrapper(0)
 {
     m_viewManager->setAudioPlaySource(this);
 
@@ -157,7 +163,7 @@
 	m_lastModelEndFrame = model->getEndFrame();
     }
 
-    bool buffersChanged = false, srChanged = false;
+    bool buffersIncreased = false, srChanged = false;
 
     int modelChannels = 1;
     ReadOnlyWaveFileModel *rowfm = qobject_cast<ReadOnlyWaveFileModel *>(model);
@@ -224,49 +230,60 @@
 
     if (!m_writeBuffers || (int)m_writeBuffers->size() < getTargetChannelCount()) {
 	clearRingBuffers(true, getTargetChannelCount());
-	buffersChanged = true;
+	buffersIncreased = true;
     } else {
 	if (willPlay) clearRingBuffers(true);
     }
 
-    if (buffersChanged || srChanged) {
-	if (m_converter) {
-#ifdef DEBUG_AUDIO_PLAY_SOURCE
-            cerr << "AudioCallbackPlaySource::addModel: Buffers or sample rate changed, deleting existing SR converter" << endl;
-#endif
-	    src_delete(m_converter);
-	    m_converter = 0;
-	}
+    if (srChanged) {
+
+        SVCERR << "AudioCallbackPlaySource: Source rate changed" << endl;
+
+        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 = 0;
+        m_monoStretcher = 0;
+        
+        if (m_stretchRatio != 1.f) {
+            setTimeStretch(m_stretchRatio);
+        }
     }
 
     rebuildRangeLists();
 
     m_mutex.unlock();
 
-    initialiseConverter();
-    
     m_audioGenerator->setTargetChannelCount(getTargetChannelCount());
 
+    if (buffersIncreased) {
+        SVDEBUG << "AudioCallbackPlaySource::addModel: Number of buffers increased, signalling channelCountIncreased" << endl;
+        emit channelCountIncreased();
+    }
+    
     if (!m_fillThread) {
 	m_fillThread = new FillThread(*this);
 	m_fillThread->start();
     }
 
 #ifdef DEBUG_AUDIO_PLAY_SOURCE
-    cout << "AudioCallbackPlaySource::addModel: now have " << m_models.size() << " model(s) -- emitting modelReplaced" << endl;
+    SVDEBUG << "AudioCallbackPlaySource::addModel: now have " << m_models.size() << " model(s)" << endl;
 #endif
 
-    if (buffersChanged || srChanged) {
-	emit modelReplaced();
-    }
-
     connect(model, SIGNAL(modelChangedWithin(sv_frame_t, sv_frame_t)),
             this, SLOT(modelChangedWithin(sv_frame_t, sv_frame_t)));
 
 #ifdef DEBUG_AUDIO_PLAY_SOURCE
     cout << "AudioCallbackPlaySource::addModel: awakening thread" << endl;
 #endif
-
+    
     m_condition.wakeAll();
 }
 
@@ -301,13 +318,6 @@
     m_models.erase(model);
 
     if (m_models.empty()) {
-	if (m_converter) {
-#ifdef DEBUG_AUDIO_PLAY_SOURCE
-            cerr << "AudioCallbackPlaySource::removeModel: No models left, deleting SR converter" << endl;
-#endif
-	    src_delete(m_converter);
-	    m_converter = 0;
-	}
 	m_sourceSampleRate = 0;
     }
 
@@ -344,14 +354,6 @@
 
     m_models.clear();
 
-    if (m_converter) {
-#ifdef DEBUG_AUDIO_PLAY_SOURCE
-        cerr << "AudioCallbackPlaySource::clearModels: Deleting SR converter" << endl;
-#endif
-	src_delete(m_converter);
-	m_converter = 0;
-    }
-
     m_lastModelEndFrame = 0;
 
     m_sourceSampleRate = 0;
@@ -369,7 +371,7 @@
     if (!haveLock) m_mutex.lock();
 
 #ifdef DEBUG_AUDIO_PLAY_SOURCE
-    cerr << "clearRingBuffers" << endl;
+    cout << "clearRingBuffers" << endl;
 #endif
 
     rebuildRangeLists();
@@ -379,15 +381,15 @@
     }
 
 #ifdef DEBUG_AUDIO_PLAY_SOURCE
-    cerr << "current playing frame = " << getCurrentPlayingFrame() << endl;
+    cout << "current playing frame = " << getCurrentPlayingFrame() << endl;
 
-    cerr << "write buffer fill (before) = " << m_writeBufferFill << endl;
+    cout << "write buffer fill (before) = " << m_writeBufferFill << endl;
 #endif
     
     m_writeBufferFill = getCurrentBufferedFrame();
 
 #ifdef DEBUG_AUDIO_PLAY_SOURCE
-    cerr << "current buffered frame = " << m_writeBufferFill << endl;
+    cout << "current buffered frame = " << m_writeBufferFill << endl;
 #endif
 
     if (m_readBuffers != m_writeBuffers) {
@@ -416,18 +418,22 @@
     if (!m_target) return;
     
     if (!m_sourceSampleRate) {
-        cerr << "AudioCallbackPlaySource::play: No source sample rate available, not playing" << endl;
+        SVCERR << "AudioCallbackPlaySource::play: No source sample rate available, not playing" << endl;
         return;
     }
     
     if (m_viewManager->getPlaySelectionMode() &&
 	!m_viewManager->getSelections().empty()) {
 
-        SVDEBUG << "AudioCallbackPlaySource::play: constraining frame " << startFrame << " to selection = ";
+#ifdef DEBUG_AUDIO_PLAY_SOURCE
+        cout << "AudioCallbackPlaySource::play: constraining frame " << startFrame << " to selection = ";
+#endif
 
         startFrame = m_viewManager->constrainFrameToSelection(startFrame);
 
-        SVDEBUG << startFrame << endl;
+#ifdef DEBUG_AUDIO_PLAY_SOURCE
+        cout << startFrame << endl;
+#endif
 
     } else {
         if (startFrame < 0) {
@@ -439,13 +445,13 @@
     }
 
 #ifdef DEBUG_AUDIO_PLAY_SOURCE
-    cerr << "play(" << startFrame << ") -> playback model ";
+    cout << "play(" << startFrame << ") -> aligned playback model ";
 #endif
 
     startFrame = m_viewManager->alignReferenceToPlaybackFrame(startFrame);
 
 #ifdef DEBUG_AUDIO_PLAY_SOURCE
-    cerr << startFrame << endl;
+    cout << startFrame << endl;
 #endif
 
     // The fill thread will automatically empty its buffers before
@@ -467,12 +473,11 @@
         for (int c = 0; c < getTargetChannelCount(); ++c) {
             RingBuffer<float> *rb = getReadRingBuffer(c);
 #ifdef DEBUG_AUDIO_PLAY_SOURCE
-            cerr << "reset ring buffer for channel " << c << endl;
+            cout << "reset ring buffer for channel " << c << endl;
 #endif
             if (rb) rb->reset();
         }
     }
-    if (m_converter) src_reset(m_converter);
 
     m_mutex.unlock();
 
@@ -556,14 +561,14 @@
 }
 
 void
-AudioCallbackPlaySource::preferenceChanged(PropertyContainer::PropertyName n)
+AudioCallbackPlaySource::preferenceChanged(PropertyContainer::PropertyName )
 {
 }
 
 void
 AudioCallbackPlaySource::audioProcessingOverload()
 {
-    cerr << "Audio processing overload!" << endl;
+    SVCERR << "Audio processing overload!" << endl;
 
     if (!m_playing) return;
 
@@ -587,10 +592,25 @@
 void
 AudioCallbackPlaySource::setSystemPlaybackTarget(breakfastquay::SystemPlaybackTarget *target)
 {
+    if (target == 0) {
+        // reset target-related facts and figures
+        m_deviceSampleRate = 0;
+        m_deviceChannelCount = 0;
+    }
     m_target = target;
 }
 
 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;
@@ -599,7 +619,7 @@
     }
     if (size * 4 > m_ringBufferSize) {
 #ifdef DEBUG_AUDIO_PLAY_SOURCE
-        cerr << "AudioCallbackPlaySource::setTarget: Buffer size "
+        cout << "AudioCallbackPlaySource::setTarget: Buffer size "
              << size << " > a quarter of ring buffer size "
              << m_ringBufferSize << ", calling for more ring buffer"
              << endl;
@@ -636,12 +656,12 @@
     // This method attempts to estimate which audio sample frame is
     // "currently coming through the speakers".
 
-    sv_samplerate_t targetRate = getTargetSampleRate();
+    sv_samplerate_t deviceRate = getDeviceSampleRate();
     sv_frame_t latency = m_playLatency; // at target rate
     RealTime latency_t = RealTime::zeroTime;
 
-    if (targetRate != 0) {
-        latency_t = RealTime::frame2RealTime(latency, targetRate);
+    if (deviceRate != 0) {
+        latency_t = RealTime::frame2RealTime(latency, deviceRate);
     }
 
     return getCurrentFrame(latency_t);
@@ -656,16 +676,18 @@
 sv_frame_t
 AudioCallbackPlaySource::getCurrentFrame(RealTime latency_t)
 {
-    // We resample when filling the ring buffer, and time-stretch when
-    // draining it.  The buffer contains data at the "target rate" and
-    // the latency provided by the target is also at the target rate.
-    // Because of the multiple rates involved, we do the actual
-    // calculation using RealTime instead.
+    // 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.
+    
+    // (But because historically more than one sample rate could have
+    // been involved here, we do latency calculations using RealTime
+    // values instead of samples.)
 
-    sv_samplerate_t sourceRate = getSourceSampleRate();
-    sv_samplerate_t targetRate = getTargetSampleRate();
+    sv_samplerate_t rate = getSourceSampleRate();
 
-    if (sourceRate == 0 || targetRate == 0) return 0;
+    if (rate == 0) return 0;
 
     int inbuffer = 0; // at target rate
 
@@ -685,7 +707,7 @@
 
     bool looping = m_viewManager->getPlayLoopMode();
 
-    RealTime inbuffer_t = RealTime::frame2RealTime(inbuffer, targetRate);
+    RealTime inbuffer_t = RealTime::frame2RealTime(inbuffer, rate);
 
     sv_frame_t stretchlat = 0;
     double timeRatio = 1.0;
@@ -695,7 +717,7 @@
         timeRatio = m_timeStretcher->getTimeRatio();
     }
 
-    RealTime stretchlat_t = RealTime::frame2RealTime(stretchlat, targetRate);
+    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
@@ -713,8 +735,7 @@
         m_trustworthyTimestamps &&
         lastRetrievalTimestamp != 0.0) {
 
-        lastretrieved_t = RealTime::frame2RealTime
-            (lastRetrievedBlockSize, targetRate);
+        lastretrieved_t = RealTime::frame2RealTime(lastRetrievedBlockSize, rate);
 
         // calculate number of frames at target rate that have elapsed
         // since the end of the last call to getSourceSamples
@@ -731,11 +752,10 @@
 
     } else {
 
-        lastretrieved_t = RealTime::frame2RealTime
-            (getTargetBlockSize(), targetRate);
+        lastretrieved_t = RealTime::frame2RealTime(getTargetBlockSize(), rate);
     }
 
-    RealTime bufferedto_t = RealTime::frame2RealTime(readBufferFill, sourceRate);
+    RealTime bufferedto_t = RealTime::frame2RealTime(readBufferFill, rate);
 
     if (timeRatio != 1.0) {
         lastretrieved_t = lastretrieved_t / timeRatio;
@@ -744,7 +764,7 @@
     }
 
 #ifdef DEBUG_AUDIO_PLAY_SOURCE_PLAYING
-    cerr << "\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;
+    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
 
     // Normally the range lists should contain at least one item each
@@ -761,7 +781,7 @@
             - latency_t - stretchlat_t - lastretrieved_t - inbuffer_t
             + sincerequest_t;
         if (playing_t < RealTime::zeroTime) playing_t = RealTime::zeroTime;
-        sv_frame_t frame = RealTime::realTime2Frame(playing_t, sourceRate);
+        sv_frame_t frame = RealTime::realTime2Frame(playing_t, rate);
         return m_viewManager->alignPlaybackFrameToReference(frame);
     }
 
@@ -798,15 +818,14 @@
     // duration of playback!
 
     if (!m_playStartFramePassed) {
-        RealTime playstart_t = RealTime::frame2RealTime(m_playStartFrame,
-                                                        sourceRate);
+        RealTime playstart_t = RealTime::frame2RealTime(m_playStartFrame, rate);
         if (playing_t < playstart_t) {
-//            cerr << "playing_t " << 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 <
                 RealTime::fromSeconds(currentTime)) {
-//                cerr << "but we've been playing for long enough that I think we should disregard it (it probably results from loop wrapping)" << endl;
+//                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;
             } else {
                 playing_t = playstart_t;
@@ -817,13 +836,13 @@
     }
  
 #ifdef DEBUG_AUDIO_PLAY_SOURCE_PLAYING
-    cerr << "playing_t " << playing_t;
+    cout << "playing_t " << playing_t;
 #endif
 
     playing_t = playing_t - m_rangeStarts[inRange];
  
 #ifdef DEBUG_AUDIO_PLAY_SOURCE_PLAYING
-    cerr << " as offset into range " << inRange << " (start =" << m_rangeStarts[inRange] << " duration =" << m_rangeDurations[inRange] << ") = " << playing_t << endl;
+    cout << " as offset into range " << inRange << " (start =" << m_rangeStarts[inRange] << " duration =" << m_rangeDurations[inRange] << ") = " << playing_t << endl;
 #endif
 
     while (playing_t < RealTime::zeroTime) {
@@ -844,20 +863,20 @@
     playing_t = playing_t + m_rangeStarts[inRange];
 
 #ifdef DEBUG_AUDIO_PLAY_SOURCE_PLAYING
-    cerr << "  playing time: " << playing_t << endl;
+    cout << "  playing time: " << playing_t << endl;
 #endif
 
     if (!looping) {
         if (inRange == (int)m_rangeStarts.size()-1 &&
             playing_t >= m_rangeStarts[inRange] + m_rangeDurations[inRange]) {
-cerr << "Not looping, inRange " << inRange << " == rangeStarts.size()-1, playing_t " << playing_t << " >= m_rangeStarts[inRange] " << m_rangeStarts[inRange] << " + m_rangeDurations[inRange] " << m_rangeDurations[inRange] << " -- stopping" << endl;
+cout << "Not looping, inRange " << inRange << " == rangeStarts.size()-1, playing_t " << playing_t << " >= m_rangeStarts[inRange] " << m_rangeStarts[inRange] << " + m_rangeDurations[inRange] " << m_rangeDurations[inRange] << " -- stopping" << endl;
             stop();
         }
     }
 
     if (playing_t < RealTime::zeroTime) playing_t = RealTime::zeroTime;
 
-    sv_frame_t frame = RealTime::realTime2Frame(playing_t, sourceRate);
+    sv_frame_t frame = RealTime::realTime2Frame(playing_t, rate);
 
     if (m_lastCurrentFrame > 0 && !looping) {
         if (frame < m_lastCurrentFrame) {
@@ -920,7 +939,7 @@
     }
 
 #ifdef DEBUG_AUDIO_PLAY_SOURCE
-    cerr << "Now have " << m_rangeStarts.size() << " play ranges" << endl;
+    cout << "Now have " << m_rangeStarts.size() << " play ranges" << endl;
 #endif
 }
 
@@ -942,59 +961,13 @@
 void
 AudioCallbackPlaySource::setSystemPlaybackSampleRate(int sr)
 {
-    bool first = (m_targetSampleRate == 0);
-
-    m_targetSampleRate = sr;
-    initialiseConverter();
-
-    if (first && (m_stretchRatio != 1.f)) {
-        // couldn't create a stretcher before because we had no sample
-        // rate: make one now
-        setTimeStretch(m_stretchRatio);
-    }
+    m_deviceSampleRate = sr;
 }
 
 void
-AudioCallbackPlaySource::initialiseConverter()
+AudioCallbackPlaySource::setSystemPlaybackChannelCount(int count)
 {
-    m_mutex.lock();
-
-#ifdef DEBUG_AUDIO_PLAY_SOURCE
-    cerr << "AudioCallbackPlaySource::initialiseConverter(): from "
-         << getSourceSampleRate() << " to " << getTargetSampleRate() << endl;
-#endif
-    
-    if (m_converter) {
-        src_delete(m_converter);
-        m_converter = 0;
-    }
-
-    if (getSourceSampleRate() != getTargetSampleRate()) {
-
-	int err = 0;
-
-	m_converter = src_new(SRC_SINC_FASTEST, getTargetChannelCount(), &err);
-
-	if (!m_converter) {
-	    cerr << "AudioCallbackPlaySource::setModel: ERROR in creating samplerate converter: "
-                 << src_strerror(err) << endl;
-
-            m_mutex.unlock();
-
-            emit sampleRateMismatch(getSourceSampleRate(),
-                                    getTargetSampleRate(),
-                                    false);
-	} else {
-
-            m_mutex.unlock();
-
-            emit sampleRateMismatch(getSourceSampleRate(),
-                                    getTargetSampleRate(),
-                                    true);
-        }
-    } else {
-        m_mutex.unlock();
-    }
+    m_deviceChannelCount = count;
 }
 
 void
@@ -1002,7 +975,7 @@
 {
     RealTimePluginInstance *plugin = dynamic_cast<RealTimePluginInstance *>(a);
     if (a && !plugin) {
-        cerr << "WARNING: AudioCallbackPlaySource::setAuditioningEffect: auditionable object " << a << " is not a real-time plugin instance" << endl;
+        SVCERR << "WARNING: AudioCallbackPlaySource::setAuditioningEffect: auditionable object " << a << " is not a real-time plugin instance" << endl;
     }
 
     m_mutex.lock();
@@ -1026,10 +999,9 @@
 }
 
 sv_samplerate_t
-AudioCallbackPlaySource::getTargetSampleRate() const
+AudioCallbackPlaySource::getDeviceSampleRate() const
 {
-    if (m_targetSampleRate) return m_targetSampleRate;
-    else return getSourceSampleRate();
+    return m_deviceSampleRate;
 }
 
 int
@@ -1045,6 +1017,12 @@
     return m_sourceChannelCount;
 }
 
+int
+AudioCallbackPlaySource::getDeviceChannelCount() const
+{
+    return m_deviceChannelCount;
+}
+
 sv_samplerate_t
 AudioCallbackPlaySource::getSourceSampleRate() const
 {
@@ -1056,19 +1034,20 @@
 {
     m_stretchRatio = factor;
 
-    if (!getTargetSampleRate()) return; // have to make our stretcher later
+    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
-            (int(getTargetSampleRate()),
+            (rate,
              m_stretcherInputCount,
              RubberBandStretcher::OptionProcessRealTime,
              factor);
         RubberBandStretcher *monoStretcher = new RubberBandStretcher
-            (int(getTargetSampleRate()),
+            (rate,
              1,
              RubberBandStretcher::OptionProcessRealTime,
              factor);
@@ -1086,33 +1065,84 @@
 }
 
 int
-AudioCallbackPlaySource::getSourceSamples(int count, float **buffer)
+AudioCallbackPlaySource::getSourceSamples(float *const *buffer,
+                                          int requestedChannels,
+                                          int count)
 {
+    // In principle, the target will handle channel mapping in cases
+    // where our channel count differs from the device's. But that
+    // only holds if our channel count doesn't change -- i.e. if
+    // getApplicationChannelCount() always returns the same value as
+    // it did when the target was created, and if this function always
+    // returns that number of channels.
+    //
+    // Unfortunately that can't hold for us -- we always have at least
+    // 2 channels but if the user opens a new main model with more
+    // channels than that (and more than the last main model) then our
+    // target channel count necessarily gets increased.
+    //
+    // We have:
+    // 
+    // getSourceChannelCount() -> number of channels available to
+    // provide from real model data
+    //
+    // getTargetChannelCount() -> number we will actually provide;
+    // same as getSourceChannelCount() except that it is always at
+    // least 2
+    //
+    // getDeviceChannelCount() -> number the device will emit, usually
+    // equal to the value of getTargetChannelCount() at the time the
+    // device was initialised, unless the device could not provide
+    // that number
+    //
+    // requestedChannels -> number the device is expecting from us,
+    // always equal to the value of getTargetChannelCount() at the
+    // time the device was initialised
+    //
+    // If the requested channel count is at least the target channel
+    // count, then we go ahead and provide the target channels as
+    // expected. We just zero any spare channels.
+    //
+    // If the requested channel count is smaller than the target
+    // channel count, then we don't know what to do and we provide
+    // nothing. This shouldn't happen as long as management is on the
+    // ball -- we emit channelCountIncreased() when the target channel
+    // count increases, and whatever code "owns" the driver should
+    // have reopened the audio device when it got that signal. But
+    // there's a race condition there, which we accommodate with this
+    // check.
+
+    int channels = getTargetChannelCount();
+
     if (!m_playing) {
 #ifdef DEBUG_AUDIO_PLAY_SOURCE_PLAYING
-        SVDEBUG << "AudioCallbackPlaySource::getSourceSamples: Not playing" << endl;
+        cout << "AudioCallbackPlaySource::getSourceSamples: Not playing" << endl;
 #endif
-	for (int ch = 0; ch < getTargetChannelCount(); ++ch) {
-	    for (int i = 0; i < count; ++i) {
-		buffer[ch][i] = 0.0;
-	    }
-	}
+        v_zero_channels(buffer, requestedChannels, count);
 	return 0;
     }
+    if (requestedChannels < channels) {
+        SVDEBUG << "AudioCallbackPlaySource::getSourceSamples: Not enough device channels (" << requestedChannels << ", need " << channels << "); hoping device is about to be reopened" << endl;
+        v_zero_channels(buffer, requestedChannels, count);
+        return 0;
+    }
+    if (requestedChannels > channels) {
+        v_zero_channels(buffer + channels, requestedChannels - channels, count);
+    }
 
 #ifdef DEBUG_AUDIO_PLAY_SOURCE_PLAYING
-    SVDEBUG << "AudioCallbackPlaySource::getSourceSamples: Playing" << endl;
+    cout << "AudioCallbackPlaySource::getSourceSamples: Playing" << endl;
 #endif
 
     // Ensure that all buffers have at least the amount of data we
     // need -- else reduce the size of our requests correspondingly
 
-    for (int ch = 0; ch < getTargetChannelCount(); ++ch) {
+    for (int ch = 0; ch < channels; ++ch) {
 
         RingBuffer<float> *rb = getReadRingBuffer(ch);
         
         if (!rb) {
-            cerr << "WARNING: AudioCallbackPlaySource::getSourceSamples: "
+            SVCERR << "WARNING: AudioCallbackPlaySource::getSourceSamples: "
                       << "No ring buffer available for channel " << ch
                       << ", returning no data here" << endl;
             count = 0;
@@ -1142,7 +1172,7 @@
 
     if (ratio != m_stretchRatio) {
         if (!ts) {
-            cerr << "WARNING: AudioCallbackPlaySource::getSourceSamples: Time ratio change to " << m_stretchRatio << " is pending, but no stretcher is set" << endl;
+            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);
@@ -1170,7 +1200,11 @@
 
 	int got = 0;
 
-	for (int ch = 0; ch < getTargetChannelCount(); ++ch) {
+#ifdef DEBUG_AUDIO_PLAY_SOURCE_PLAYING
+        cout << "channels == " << channels << endl;
+#endif
+        
+	for (int ch = 0; ch < channels; ++ch) {
 
 	    RingBuffer<float> *rb = getReadRingBuffer(ch);
 
@@ -1188,7 +1222,7 @@
 #endif
 	    }
 
-	    for (int ch = 0; ch < getTargetChannelCount(); ++ch) {
+	    for (int ch = 0; ch < channels; ++ch) {
 		for (int i = got; i < count; ++i) {
 		    buffer[ch][i] = 0.0;
 		}
@@ -1206,7 +1240,6 @@
 	return got;
     }
 
-    int channels = getTargetChannelCount();
     sv_frame_t available;
     sv_frame_t fedToStretcher = 0;
     int warned = 0;
@@ -1223,14 +1256,14 @@
         sv_frame_t got = reqd;
 
 #ifdef DEBUG_AUDIO_PLAY_SOURCE_PLAYING
-        cerr << "reqd = " <<reqd << ", channels = " << channels << ", ic = " << m_stretcherInputCount << endl;
+        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) {
-                    cerr << "WARNING: resizing stretcher input buffer from " << m_stretcherInputSizes[c] << " to " << (reqd * 2) << endl;
+                    SVDEBUG << "NOTE: resizing stretcher input buffer from " << m_stretcherInputSizes[c] << " to " << (reqd * 2) << endl;
                 }
                 delete[] m_stretcherInputs[c];
                 m_stretcherInputSizes[c] = reqd * 2;
@@ -1252,18 +1285,18 @@
                 
 #ifdef DEBUG_AUDIO_PLAY_SOURCE_PLAYING
                 if (c == 0) {
-                    SVDEBUG << "feeding stretcher: got " << gotHere
+                    cout << "feeding stretcher: got " << gotHere
                               << ", " << rb->getReadSpace() << " remain" << endl;
                 }
 #endif
                 
             } else {
-                cerr << "WARNING: No ring buffer available for channel " << c << " in stretcher input block" << endl;
+                SVCERR << "WARNING: No ring buffer available for channel " << c << " in stretcher input block" << endl;
             }
         }
 
         if (got < reqd) {
-            cerr << "WARNING: Read underrun in playback ("
+            SVCERR << "WARNING: Read underrun in playback ("
                       << got << " < " << reqd << ")" << endl;
         }
 
@@ -1274,18 +1307,14 @@
         if (got == 0) break;
 
         if (ts->available() == available) {
-            cerr << "WARNING: AudioCallbackPlaySource::getSamples: Added " << got << " samples to time stretcher, created no new available output samples (warned = " << warned << ")" << endl;
+            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));
 
-    for (int c = stretchChannels; c < getTargetChannelCount(); ++c) {
-        for (int i = 0; i < count; ++i) {
-            buffer[c][i] = buffer[0][i];
-        }
-    }
+    v_zero_channels(buffer + stretchChannels, channels - stretchChannels, count);
 
     applyAuditioningEffect(count, buffer);
 
@@ -1299,26 +1328,26 @@
 }
 
 void
-AudioCallbackPlaySource::applyAuditioningEffect(sv_frame_t count, float **buffers)
+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()) {
-//        cerr << "plugin input count " << plugin->getAudioInputCount() 
+//        cout << "plugin input count " << plugin->getAudioInputCount() 
 //                  << " != our channel count " << getTargetChannelCount()
 //                  << endl;
         return;
     }
     if ((int)plugin->getAudioOutputCount() != getTargetChannelCount()) {
-//        cerr << "plugin output count " << plugin->getAudioOutputCount() 
+//        cout << "plugin output count " << plugin->getAudioOutputCount() 
 //                  << " != our channel count " << getTargetChannelCount()
 //                  << endl;
         return;
     }
     if ((int)plugin->getBufferSize() < count) {
-//        cerr << "plugin buffer size " << plugin->getBufferSize() 
+//        cout << "plugin buffer size " << plugin->getBufferSize() 
 //                  << " < our block size " << count
 //                  << endl;
         return;
@@ -1365,6 +1394,9 @@
         return false;
     }
 
+    // space is now the number of samples that can be written on each
+    // channel's write ringbuffer
+    
     sv_frame_t f = m_writeBufferFill;
 	
     bool readWriteEqual = (m_readBuffers == m_writeBuffers);
@@ -1380,17 +1412,8 @@
     cout << "buffered to " << f << " already" << endl;
 #endif
 
-    bool resample = (getSourceSampleRate() != getTargetSampleRate());
-
-#ifdef DEBUG_AUDIO_PLAY_SOURCE
-    cout << (resample ? "" : "not ") << "resampling (source " << getSourceSampleRate() << ", target " << getTargetSampleRate() << ")" << endl;
-#endif
-
     int channels = getTargetChannelCount();
 
-    sv_frame_t orig = space;
-    sv_frame_t got = 0;
-
     static float **bufferPtrs = 0;
     static int bufferPtrCount = 0;
 
@@ -1402,157 +1425,62 @@
 
     sv_frame_t generatorBlockSize = m_audioGenerator->getBlockSize();
 
-    if (resample && !m_converter) {
-        throw std::logic_error("Sample rates differ, but no converter available!");
+    // space must be a multiple of generatorBlockSize
+    sv_frame_t reqSpace = space;
+    space = (reqSpace / generatorBlockSize) * generatorBlockSize;
+    if (space == 0) {
+#ifdef DEBUG_AUDIO_PLAY_SOURCE
+        cout << "requested fill of " << reqSpace
+             << " is less than generator block size of "
+             << generatorBlockSize << ", leaving it" << endl;
+#endif
+        return false;
     }
 
-    if (resample && m_converter) {
+    if (tmpSize < channels * space) {
+        delete[] tmp;
+        tmp = new float[channels * space];
+        tmpSize = channels * space;
+    }
 
-	double ratio =
-	    double(getTargetSampleRate()) / double(getSourceSampleRate());
-	orig = sv_frame_t(double(orig) / ratio + 0.1);
+    for (int c = 0; c < channels; ++c) {
 
-	// orig must be a multiple of generatorBlockSize
-	orig = (orig / generatorBlockSize) * generatorBlockSize;
-	if (orig == 0) return false;
+        bufferPtrs[c] = tmp + c * space;
+	    
+        for (int i = 0; i < space; ++i) {
+            tmp[c * space + i] = 0.0f;
+        }
+    }
 
-	sv_frame_t work = std::max(orig, space);
+    sv_frame_t got = mixModels(f, space, bufferPtrs); // also modifies f
 
-	// We only allocate one buffer, but we use it in two halves.
-	// We place the non-interleaved values in the second half of
-	// the buffer (orig samples for channel 0, orig samples for
-	// channel 1 etc), and then interleave them into the first
-	// half of the buffer.  Then we resample back into the second
-	// half (interleaved) and de-interleave the results back to
-	// the start of the buffer for insertion into the ringbuffers.
-	// What a faff -- especially as we've already de-interleaved
-	// the audio data from the source file elsewhere before we
-	// even reach this point.
-	
-	if (tmpSize < channels * work * 2) {
-	    delete[] tmp;
-	    tmp = new float[channels * work * 2];
-	    tmpSize = channels * work * 2;
-	}
+    for (int c = 0; c < channels; ++c) {
 
-	float *nonintlv = tmp + channels * work;
-	float *intlv = tmp;
-	float *srcout = tmp + channels * work;
-	
-	for (int c = 0; c < channels; ++c) {
-	    for (int i = 0; i < orig; ++i) {
-		nonintlv[channels * i + c] = 0.0f;
-	    }
-	}
+        RingBuffer<float> *wb = getWriteRingBuffer(c);
+        if (wb) {
+            int actual = wb->write(bufferPtrs[c], int(got));
+#ifdef DEBUG_AUDIO_PLAY_SOURCE
+            cout << "Wrote " << actual << " samples for ch " << c << ", now "
+                 << wb->getReadSpace() << " to read" 
+                 << endl;
+#endif
+            if (actual < got) {
+                SVCERR << "WARNING: Buffer overrun in channel " << c
+                       << ": wrote " << actual << " of " << got
+                       << " samples" << endl;
+            }
+        }
+    }
 
-	for (int c = 0; c < channels; ++c) {
-	    bufferPtrs[c] = nonintlv + c * orig;
-	}
-
-	got = mixModels(f, orig, bufferPtrs); // also modifies f
-
-	// and interleave into first half
-	for (int c = 0; c < channels; ++c) {
-	    for (int i = 0; i < got; ++i) {
-		float sample = nonintlv[c * got + i];
-		intlv[channels * i + c] = sample;
-	    }
-	}
-		
-	SRC_DATA data;
-	data.data_in = intlv;
-	data.data_out = srcout;
-	data.input_frames = long(got);
-	data.output_frames = long(work);
-	data.src_ratio = ratio;
-	data.end_of_input = 0;
-	
-	int err = src_process(m_converter, &data);
-
-	sv_frame_t toCopy = sv_frame_t(double(got) * ratio + 0.1);
-
-	if (err) {
-	    cerr
-		<< "AudioCallbackPlaySourceFillThread: ERROR in samplerate conversion: "
-		<< src_strerror(err) << endl;
-	    //!!! Then what?
-	} else {
-	    got = data.input_frames_used;
-	    toCopy = data.output_frames_gen;
-#ifdef DEBUG_AUDIO_PLAY_SOURCE
-	    cout << "Resampled " << got << " frames to " << toCopy << " frames" << endl;
-#endif
-	}
-	
-	for (int c = 0; c < channels; ++c) {
-	    for (int i = 0; i < toCopy; ++i) {
-		tmp[i] = srcout[channels * i + c];
-	    }
-	    RingBuffer<float> *wb = getWriteRingBuffer(c);
-	    if (wb) wb->write(tmp, int(toCopy));
-	}
-
-	m_writeBufferFill = f;
-	if (readWriteEqual) m_readBufferFill = f;
-
-    } else {
-
-	// space must be a multiple of generatorBlockSize
-        sv_frame_t reqSpace = space;
-	space = (reqSpace / generatorBlockSize) * generatorBlockSize;
-	if (space == 0) {
-#ifdef DEBUG_AUDIO_PLAY_SOURCE
-            cout << "requested fill of " << reqSpace
-                      << " is less than generator block size of "
-                      << generatorBlockSize << ", leaving it" << endl;
-#endif
-            return false;
-        }
-
-	if (tmpSize < channels * space) {
-	    delete[] tmp;
-	    tmp = new float[channels * space];
-	    tmpSize = channels * space;
-	}
-
-	for (int c = 0; c < channels; ++c) {
-
-	    bufferPtrs[c] = tmp + c * space;
-	    
-	    for (int i = 0; i < space; ++i) {
-		tmp[c * space + i] = 0.0f;
-	    }
-	}
-
-	sv_frame_t got = mixModels(f, space, bufferPtrs); // also modifies f
-
-	for (int c = 0; c < channels; ++c) {
-
-	    RingBuffer<float> *wb = getWriteRingBuffer(c);
-	    if (wb) {
-                int actual = wb->write(bufferPtrs[c], int(got));
-#ifdef DEBUG_AUDIO_PLAY_SOURCE
-		cout << "Wrote " << actual << " samples for ch " << c << ", now "
-			  << wb->getReadSpace() << " to read" 
-			  << endl;
-#endif
-                if (actual < got) {
-                    cerr << "WARNING: Buffer overrun in channel " << c
-                              << ": wrote " << actual << " of " << got
-                              << " samples" << endl;
-                }
-            }
-	}
-
-	m_writeBufferFill = f;
-	if (readWriteEqual) m_readBufferFill = f;
+    m_writeBufferFill = f;
+    if (readWriteEqual) m_readBufferFill = f;
 
 #ifdef DEBUG_AUDIO_PLAY_SOURCE
-        cout << "Read buffer fill is now " << m_readBufferFill << endl;
+    cout << "Read buffer fill is now " << m_readBufferFill << ", write buffer fill "
+         << m_writeBufferFill << endl;
 #endif
 
-	//!!! how do we know when ended? need to mark up a fully-buffered flag and check this if we find the buffers empty in getSourceSamples
-    }
+    //!!! how do we know when ended? need to mark up a fully-buffered flag and check this if we find the buffers empty in getSourceSamples
 
     return true;
 }    
@@ -1570,13 +1498,24 @@
     bool constrained = (m_viewManager->getPlaySelectionMode() &&
 			!m_viewManager->getSelections().empty());
 
-    static float **chunkBufferPtrs = 0;
-    static int chunkBufferPtrCount = 0;
     int channels = getTargetChannelCount();
 
 #ifdef DEBUG_AUDIO_PLAY_SOURCE
-    cout << "Selection playback: start " << frame << ", size " << count <<", channels " << channels << endl;
+    cout << "mixModels: start " << frame << ", size " << count << ", channels " << channels << endl;
 #endif
+#ifdef DEBUG_AUDIO_PLAY_SOURCE_PLAYING
+    if (constrained) {
+        cout << "Manager has " << m_viewManager->getSelections().size() << " selection(s):" << endl;
+        for (auto sel: m_viewManager->getSelections()) {
+            cout << sel.getStartFrame() << " -> " << sel.getEndFrame()
+                 << " (" << (sel.getEndFrame() - sel.getStartFrame()) << " frames)"
+                 << endl;
+        }
+    }
+#endif
+    
+    static float **chunkBufferPtrs = 0;
+    static int chunkBufferPtrCount = 0;
 
     if (chunkBufferPtrCount < channels) {
 	if (chunkBufferPtrs) delete[] chunkBufferPtrs;
@@ -1652,22 +1591,28 @@
 	    }
 	    nextChunkStart = chunkStart + chunkSize;
 	}
-	
-//	cout << "chunkStart " << chunkStart << ", chunkSize " << chunkSize << ", nextChunkStart " << nextChunkStart << ", frame " << frame << ", count " << count << ", processed " << processed << endl;
 
+#ifdef DEBUG_AUDIO_PLAY_SOURCE_PLAYING
+	cout << "chunkStart " << chunkStart << ", chunkSize " << chunkSize << ", nextChunkStart " << nextChunkStart << ", frame " << frame << ", count " << count << ", processed " << processed << endl;
+#endif
+        
 	if (!chunkSize) {
-#ifdef DEBUG_AUDIO_PLAY_SOURCE
-	    cout << "Ending selection playback at " << nextChunkStart << endl;
-#endif
 	    // We need to maintain full buffers so that the other
 	    // thread can tell where it's got to in the playback -- so
 	    // return the full amount here
 	    frame = frame + count;
+            if (frame < nextChunkStart) {
+                frame = nextChunkStart;
+            }
+#ifdef DEBUG_AUDIO_PLAY_SOURCE
+	    cout << "mixModels: ending at " << nextChunkStart << ", returning frame as "
+                 << frame << endl;
+#endif
 	    return count;
 	}
 
 #ifdef DEBUG_AUDIO_PLAY_SOURCE
-	cout << "Selection playback: chunk at " << chunkStart << " -> " << nextChunkStart << " (size " << chunkSize << ")" << endl;
+	cout << "mixModels: chunk at " << chunkStart << " -> " << nextChunkStart << " (size " << chunkSize << ")" << endl;
 #endif
 
 	if (selectionSize < 100) {
@@ -1707,7 +1652,7 @@
     }
 
 #ifdef DEBUG_AUDIO_PLAY_SOURCE
-    cout << "Returning selection playback " << processed << " frames to " << nextChunkStart << endl;
+    cout << "mixModels returning " << processed << " frames to " << nextChunkStart << endl;
 #endif
 
     frame = nextChunkStart;
@@ -1729,7 +1674,7 @@
 		    // OK, we don't have enough and there's more to
 		    // read -- don't unify until we can do better
 #ifdef DEBUG_AUDIO_PLAY_SOURCE_PLAYING
-                    SVDEBUG << "AudioCallbackPlaySource::unifyRingBuffers: Not unifying: write buffer has less (" << wb->getReadSpace() << ") than " << m_blockSize*2 << " to read and write buffer fill (" << m_writeBufferFill << ") is not close to end frame (" << m_lastModelEndFrame << ")" << endl;
+                    cout << "AudioCallbackPlaySource::unifyRingBuffers: Not unifying: write buffer has less (" << wb->getReadSpace() << ") than " << m_blockSize*2 << " to read and write buffer fill (" << m_writeBufferFill << ") is not close to end frame (" << m_lastModelEndFrame << ")" << endl;
 #endif
 		    return;
 		}
@@ -1749,7 +1694,7 @@
     }
     
 #ifdef DEBUG_AUDIO_PLAY_SOURCE_PLAYING
-    SVDEBUG << "AudioCallbackPlaySource::unifyRingBuffers: m_readBufferFill = " << m_readBufferFill << ", rf = " << rf << ", m_writeBufferFill = " << m_writeBufferFill << endl;
+    cout << "AudioCallbackPlaySource::unifyRingBuffers: m_readBufferFill = " << m_readBufferFill << ", rf = " << rf << ", m_writeBufferFill = " << m_writeBufferFill << endl;
 #endif
 
     sv_frame_t wf = m_writeBufferFill;
@@ -1779,7 +1724,7 @@
     m_readBuffers = m_writeBuffers;
     m_readBufferFill = m_writeBufferFill;
 #ifdef DEBUG_AUDIO_PLAY_SOURCE_PLAYING
-    cerr << "unified" << endl;
+    cout << "unified" << endl;
 #endif
 }
 
--- a/audio/AudioCallbackPlaySource.h	Mon Dec 05 17:03:09 2016 +0000
+++ b/audio/AudioCallbackPlaySource.h	Wed Dec 14 14:28:41 2016 +0000
@@ -39,6 +39,10 @@
     class RubberBandStretcher;
 }
 
+namespace breakfastquay {
+    class ResamplerWrapper;
+}
+
 class Model;
 class ViewManagerBase;
 class AudioGenerator;
@@ -86,23 +90,23 @@
      * from the given frame.  If playback is already under way, reseek
      * to the given frame and continue.
      */
-    virtual void play(sv_frame_t startFrame);
+    virtual void play(sv_frame_t startFrame) override;
 
     /**
      * Stop playback and ensure that no more data is returned.
      */
-    virtual void stop();
+    virtual void stop() override;
 
     /**
      * Return whether playback is currently supposed to be happening.
      */
-    virtual bool isPlaying() const { return m_playing; }
+    virtual bool isPlaying() const override { return m_playing; }
 
     /**
      * Return the frame number that is currently expected to be coming
      * out of the speakers.  (i.e. compensating for playback latency.)
      */
-    virtual sv_frame_t getCurrentPlayingFrame();
+    virtual sv_frame_t getCurrentPlayingFrame() override;
     
     /** 
      * Return the last frame that would come out of the speakers if we
@@ -121,10 +125,15 @@
     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
      * called by the target class.
      */
-    virtual void setSystemPlaybackBlockSize(int blockSize);
+    virtual void setSystemPlaybackBlockSize(int blockSize) override;
 
     /**
      * Get the block size of the target audio device.  This may be an
@@ -132,16 +141,16 @@
      * size; the source should behave itself even if this value turns
      * out to be inaccurate.
      */
-    int getTargetBlockSize() const;
+    virtual int getTargetBlockSize() const override;
 
     /**
      * Set the playback latency of the target audio device, in frames
-     * at the target sample rate.  This is the difference between the
+     * at the device sample rate.  This is the difference between the
      * frame currently "leaving the speakers" and the last frame (or
      * highest last frame across all channels) requested via
      * getSamples().  The default is zero.
      */
-    void setSystemPlaybackLatency(int);
+    virtual void setSystemPlaybackLatency(int) override;
 
     /**
      * Get the playback latency of the target audio device.
@@ -155,25 +164,34 @@
      * source sample rate, this class will resample automatically to
      * fit.
      */
-    void setSystemPlaybackSampleRate(int);
+    virtual void setSystemPlaybackSampleRate(int) override;
 
     /**
      * Return the sample rate set by the target audio device (or the
      * source sample rate if the target hasn't set one).
      */
-    virtual sv_samplerate_t getTargetSampleRate() const;
+    virtual sv_samplerate_t getDeviceSampleRate() const override;
 
     /**
+     * Indicate how many channels the target audio device was opened
+     * with. Note that the target device does channel mixing in the
+     * case where our requested channel count does not match its, so
+     * long as we provide the number of channels we specified when the
+     * target was started in getApplicationChannelCount().
+     */
+    virtual void setSystemPlaybackChannelCount(int) override;
+    
+    /**
      * Set the current output levels for metering (for call from the
      * target)
      */
-    void setOutputLevels(float left, float right);
+    virtual void setOutputLevels(float left, float right) override;
 
     /**
      * Return the current (or thereabouts) output levels in the range
      * 0.0 -> 1.0, for metering purposes.
      */
-    virtual bool getOutputLevels(float &left, float &right);
+    virtual bool getOutputLevels(float &left, float &right) override;
 
     /**
      * Get the number of channels of audio that in the source models.
@@ -187,30 +205,52 @@
      * to the play target.  This may be more than the source channel
      * count: for example, a mono source will provide 2 channels
      * after pan.
+     *
      * This may safely be called from a realtime thread.  Returns 0 if
      * there is no source yet available.
+     *
+     * override from AudioPlaySource
      */
-    int getTargetChannelCount() const;
+    virtual int getTargetChannelCount() const override;
 
     /**
+     * Get the number of channels of audio the device is
+     * expecting. Equal to whatever getTargetChannelCount() was
+     * returning at the time the device was initialised.
+     */
+    int getDeviceChannelCount() const;
+    
+    /**
      * ApplicationPlaybackSource equivalent of the above.
+     *
+     * override from breakfastquay::ApplicationPlaybackSource
      */
-    virtual int getApplicationChannelCount() const {
+    virtual int getApplicationChannelCount() const override {
         return getTargetChannelCount();
     }
     
     /**
-     * Get the actual sample rate of the source material.  This may
-     * safely be called from a realtime thread.  Returns 0 if there is
-     * no source yet available.
+     * Get the actual sample rate of the source material (the main
+     * model).  This may safely be called from a realtime thread.
+     * Returns 0 if there is no source yet available.
+     *
+     * When this changes, the AudioCallbackPlaySource notifies its
+     * ResamplerWrapper of the new sample rate so that it can resample
+     * correctly on the way to the device (which is opened at a fixed
+     * rate, see getApplicationSampleRate).
      */
-    virtual sv_samplerate_t getSourceSampleRate() const;
+    virtual sv_samplerate_t getSourceSampleRate() const override;
 
     /**
-     * ApplicationPlaybackSource equivalent of the above.
+     * ApplicationPlaybackSource interface method: get the sample rate
+     * at which the application wants the device to be opened. We
+     * always allow the device to open at its default rate, and then
+     * we resample if the audio is at a different rate. This avoids
+     * having to close and re-open the device to obtain consistent
+     * behaviour for consecutive sessions with different source rates.
      */
-    virtual int getApplicationSampleRate() const {
-        return int(round(getSourceSampleRate()));
+    virtual int getApplicationSampleRate() const override {
+        return 0;
     }
 
     /**
@@ -218,7 +258,7 @@
      * audio data, in all channels.  This may safely be called from a
      * realtime thread.
      */
-    virtual int getSourceSamples(int count, float **buffer);
+    virtual int getSourceSamples(float *const *buffer, int nchannels, int count) override;
 
     /**
      * Set the time stretcher factor (i.e. playback speed).
@@ -226,12 +266,6 @@
     void setTimeStretch(double factor);
 
     /**
-     * Set the resampler quality, 0 - 2 where 0 is fastest and 2 is
-     * highest quality.
-     */
-    void setResampleQuality(int q);
-
-    /**
      * Set a single real-time plugin as a processing effect for
      * auditioning during playback.
      *
@@ -246,7 +280,7 @@
      * Pass a null pointer to remove the current auditioning plugin,
      * if any.
      */
-    void setAuditioningEffect(Auditionable *plugin);
+    virtual void setAuditioningEffect(Auditionable *plugin) override;
 
     /**
      * Specify that only the given set of models should be played.
@@ -259,24 +293,26 @@
      */
     void clearSoloModelSet();
 
-    std::string getClientName() const { return m_clientName; }
+    virtual std::string getClientName() const override {
+        return m_clientName;
+    }
 
 signals:
-    void modelReplaced();
-
     void playStatusChanged(bool isPlaying);
 
     void sampleRateMismatch(sv_samplerate_t requested,
                             sv_samplerate_t available,
                             bool willResample);
 
+    void channelCountIncreased();
+
     void audioOverloadPluginDisabled();
     void audioTimeStretchMultiChannelDisabled();
 
     void activity(QString);
 
 public slots:
-    void audioProcessingOverload();
+    void audioProcessingOverload() override;
 
 protected slots:
     void selectionChanged();
@@ -310,7 +346,8 @@
     int                               m_sourceChannelCount;
     sv_frame_t                        m_blockSize;
     sv_samplerate_t                   m_sourceSampleRate;
-    sv_samplerate_t                   m_targetSampleRate;
+    sv_samplerate_t                   m_deviceSampleRate;
+    int                               m_deviceChannelCount;
     sv_frame_t                        m_playLatency;
     breakfastquay::SystemPlaybackTarget *m_target;
     double                            m_lastRetrievalTimestamp;
@@ -370,7 +407,7 @@
     sv_frame_t mixModels(sv_frame_t &frame, sv_frame_t count, float **buffers);
 
     // Called from getSourceSamples.
-    void applyAuditioningEffect(sv_frame_t count, float **buffers);
+    void applyAuditioningEffect(sv_frame_t count, float *const *buffers);
 
     // Ranges of current selections, if play selection is active
     std::vector<RealTime> m_rangeStarts;
@@ -395,9 +432,7 @@
     QMutex m_mutex;
     QWaitCondition m_condition;
     FillThread *m_fillThread;
-    SRC_STATE *m_converter;
-    int m_resampleQuality;
-    void initialiseConverter();
+    breakfastquay::ResamplerWrapper *m_resamplerWrapper; // I don't own this
 };
 
 #endif
--- a/audio/AudioRecordTarget.cpp	Mon Dec 05 17:03:09 2016 +0000
+++ b/audio/AudioRecordTarget.cpp	Wed Dec 14 14:28:41 2016 +0000
@@ -27,6 +27,7 @@
     m_clientName(clientName.toUtf8().data()),
     m_recording(false),
     m_recordSampleRate(44100),
+    m_recordChannelCount(2),
     m_frameCount(0),
     m_model(0)
 {
@@ -37,6 +38,18 @@
     QMutexLocker locker(&m_mutex);
 }
 
+int
+AudioRecordTarget::getApplicationSampleRate() const
+{
+    return 0; // don't care
+}
+
+int
+AudioRecordTarget::getApplicationChannelCount() const
+{
+    return m_recordChannelCount;
+}
+
 void
 AudioRecordTarget::setSystemRecordBlockSize(int)
 {
@@ -54,7 +67,13 @@
 }
 
 void
-AudioRecordTarget::putSamples(int nframes, float **samples)
+AudioRecordTarget::setSystemRecordChannelCount(int c)
+{
+    m_recordChannelCount = c;
+}
+
+void
+AudioRecordTarget::putSamples(const float *const *samples, int, int nframes)
 {
     bool secChanged = false;
     sv_frame_t frameToEmit = 0;
@@ -153,7 +172,9 @@
 
     m_audioFileName = recordedDir.filePath(filename);
 
-    m_model = new WritableWaveFileModel(m_recordSampleRate, 2, m_audioFileName);
+    m_model = new WritableWaveFileModel(m_recordSampleRate,
+                                        m_recordChannelCount,
+                                        m_audioFileName);
 
     if (!m_model->isOK()) {
         cerr << "ERROR: AudioRecordTarget::startRecording: Recording failed"
--- a/audio/AudioRecordTarget.h	Mon Dec 05 17:03:09 2016 +0000
+++ b/audio/AudioRecordTarget.h	Wed Dec 14 14:28:41 2016 +0000
@@ -36,20 +36,21 @@
     AudioRecordTarget(ViewManagerBase *, QString clientName);
     virtual ~AudioRecordTarget();
 
-    virtual std::string getClientName() const { return m_clientName; }
+    virtual std::string getClientName() const override { return m_clientName; }
     
-    virtual int getApplicationSampleRate() const { return 0; } // don't care
-    virtual int getApplicationChannelCount() const { return 2; }
+    virtual int getApplicationSampleRate() const override;
+    virtual int getApplicationChannelCount() const override;
 
-    virtual void setSystemRecordBlockSize(int);
-    virtual void setSystemRecordSampleRate(int);
-    virtual void setSystemRecordLatency(int);
+    virtual void setSystemRecordBlockSize(int) override;
+    virtual void setSystemRecordSampleRate(int) override;
+    virtual void setSystemRecordLatency(int) override;
+    virtual void setSystemRecordChannelCount(int) override;
 
-    virtual void putSamples(int nframes, float **samples);
+    virtual void putSamples(const float *const *samples, int nchannels, int nframes) override;
     
-    virtual void setInputLevels(float peakLeft, float peakRight);
+    virtual void setInputLevels(float peakLeft, float peakRight) override;
 
-    virtual void audioProcessingOverload() { }
+    virtual void audioProcessingOverload() override { }
 
     QString getRecordContainerFolder();
     QString getRecordFolder();
@@ -71,6 +72,7 @@
     std::string m_clientName;
     bool m_recording;
     sv_samplerate_t m_recordSampleRate;
+    int m_recordChannelCount;
     sv_frame_t m_frameCount;
     QString m_audioFileName;
     WritableWaveFileModel *m_model;
--- a/framework/MainWindowBase.cpp	Mon Dec 05 17:03:09 2016 +0000
+++ b/framework/MainWindowBase.cpp	Wed Dec 14 14:28:41 2016 +0000
@@ -76,6 +76,7 @@
 #include <bqaudioio/SystemPlaybackTarget.h>
 #include <bqaudioio/SystemAudioIO.h>
 #include <bqaudioio/AudioFactory.h>
+#include <bqaudioio/ResamplerWrapper.h>
 
 #include <QApplication>
 #include <QMessageBox>
@@ -141,6 +142,7 @@
     m_soundOptions(options),
     m_playSource(0),
     m_recordTarget(0),
+    m_resamplerWrapper(0),
     m_playTarget(0),
     m_audioIO(0),
     m_oscQueue(0),
@@ -240,6 +242,8 @@
 
     connect(m_playSource, SIGNAL(sampleRateMismatch(sv_samplerate_t, sv_samplerate_t, bool)),
 	    this,           SLOT(sampleRateMismatch(sv_samplerate_t, sv_samplerate_t, bool)));
+    connect(m_playSource, SIGNAL(channelCountIncreased()),
+            this,           SLOT(recreateAudioIO()));
     connect(m_playSource, SIGNAL(audioOverloadPluginDisabled()),
             this,           SLOT(audioOverloadPluginDisabled()));
     connect(m_playSource, SIGNAL(audioTimeStretchMultiChannelDisabled()),
@@ -293,14 +297,8 @@
     // depends on whether we handle recording or not) before we delete
     // the ApplicationPlaybackSource and ApplicationRecordTarget that
     // they refer to.
-    
-    // First prevent this trying to call target.
-    if (m_playSource) m_playSource->setSystemPlaybackTarget(0);
-
-    // Then delete the breakfastquay::System object.
-    // Only one of these two exists!
-    delete m_audioIO;
-    delete m_playTarget;
+
+    deleteAudioIO();
 
     // Then delete the Application objects.
     delete m_playSource;
@@ -1399,7 +1397,9 @@
     if (Preferences::getInstance()->getFixedSampleRate() != 0) {
         rate = Preferences::getInstance()->getFixedSampleRate();
     } else if (Preferences::getInstance()->getResampleOnLoad()) {
-        rate = m_playSource->getSourceSampleRate();
+        if (getMainModel()) {
+            rate = getMainModel()->getSampleRate();
+        }
     }
 
     ReadOnlyWaveFileModel *newModel = new ReadOnlyWaveFileModel(source, rate);
@@ -2158,7 +2158,9 @@
     if (getMainModel()) {
         rate = getMainModel()->getSampleRate();
     } else if (Preferences::getInstance()->getResampleOnLoad()) {
-        rate = m_playSource->getSourceSampleRate();
+        if (getMainModel()) {
+            rate = getMainModel()->getSampleRate();
+        }
     }
 
     RDFImporter importer
@@ -2300,27 +2302,49 @@
 
     if (!(m_soundOptions & WithAudioOutput)) return;
 
-    //!!! how to handle preferences
-/*    
     QSettings settings;
     settings.beginGroup("Preferences");
-    QString targetName = settings.value("audio-target", "").toString();
+    QString implementation = settings.value
+        ("audio-target", "").toString();
+    QString suffix;
+    if (implementation != "") suffix = "-" + implementation;
+    QString recordDevice = settings.value
+        ("audio-record-device" + suffix, "").toString();
+    QString playbackDevice = settings.value
+        ("audio-playback-device" + suffix, "").toString();
     settings.endGroup();
-    AudioTargetFactory *factory = AudioTargetFactory::getInstance();
-
-    factory->setDefaultCallbackTarget(targetName);
-*/
-
+
+    if (implementation == "auto") {
+        implementation = "";
+    }
+    
+    breakfastquay::AudioFactory::Preference preference;
+    preference.implementation = implementation.toStdString();
+    preference.recordDevice = recordDevice.toStdString();
+    preference.playbackDevice = playbackDevice.toStdString();
+
+    SVCERR << "createAudioIO: Preferred implementation = \""
+            << preference.implementation << "\"" << endl;
+    SVCERR << "createAudioIO: Preferred playback device = \""
+            << preference.playbackDevice << "\"" << endl;
+    SVCERR << "createAudioIO: Preferred record device = \""
+            << preference.recordDevice << "\"" << endl;
+
+    if (!m_resamplerWrapper) {
+        m_resamplerWrapper = new breakfastquay::ResamplerWrapper(m_playSource);
+        m_playSource->setResamplerWrapper(m_resamplerWrapper);
+    }
+    
     if (m_soundOptions & WithAudioInput) {
         m_audioIO = breakfastquay::AudioFactory::
-            createCallbackIO(m_recordTarget, m_playSource);
+            createCallbackIO(m_recordTarget, m_resamplerWrapper, preference);
         if (m_audioIO) {
             m_audioIO->suspend(); // start in suspended state
             m_playSource->setSystemPlaybackTarget(m_audioIO);
         }
     } else {
         m_playTarget = breakfastquay::AudioFactory::
-            createCallbackPlayTarget(m_playSource);
+            createCallbackPlayTarget(m_resamplerWrapper, preference);
         if (m_playTarget) {
             m_playTarget->suspend(); // start in suspended state
             m_playSource->setSystemPlaybackTarget(m_playTarget);
@@ -2329,25 +2353,54 @@
 
     if (!m_playTarget && !m_audioIO) {
         emit hideSplash();
-
-//        if (factory->isAutoCallbackTarget(targetName)) {
+        if (implementation == "") {
             QMessageBox::warning
 	    (this, tr("Couldn't open audio device"),
 	     tr("<b>No audio available</b><p>Could not open an audio device for playback.<p>Automatic audio device detection failed. Audio playback will not be available during this session.</p>"),
 	     QMessageBox::Ok);
-/*
         } else {
             QMessageBox::warning
                 (this, tr("Couldn't open audio device"),
                  tr("<b>No audio available</b><p>Failed to open your preferred audio device (\"%1\").<p>Audio playback will not be available during this session.</p>")
-                 .arg(factory->getCallbackTargetDescription(targetName)),
+                 .arg(breakfastquay::AudioFactory::
+                      getImplementationDescription(implementation.toStdString())
+                      .c_str()),
                  QMessageBox::Ok);
         }
-*/
-            return;
     }
 }
 
+void
+MainWindowBase::deleteAudioIO()
+{
+    // First prevent this trying to call target.
+    if (m_playSource) {
+        m_playSource->setSystemPlaybackTarget(0);
+        m_playSource->setResamplerWrapper(0);
+    }
+
+    // Then delete the breakfastquay::System object.
+    // Only one of these two exists!
+    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 = 0;
+    m_playTarget = 0;
+    m_resamplerWrapper = 0;
+}
+
+void
+MainWindowBase::recreateAudioIO()
+{
+    deleteAudioIO();
+    createAudioIO();
+}
+
 WaveFileModel *
 MainWindowBase::getMainModel()
 {
@@ -2826,6 +2879,8 @@
         }
     }
 
+    if (m_viewManager) m_viewManager->setGlobalCentreFrame(0);
+    
     m_audioIO->resume();
 
     WritableWaveFileModel *model = m_recordTarget->startRecording();
--- a/framework/MainWindowBase.h	Mon Dec 05 17:03:09 2016 +0000
+++ b/framework/MainWindowBase.h	Wed Dec 14 14:28:41 2016 +0000
@@ -66,8 +66,9 @@
 class AlignmentModel;
 
 namespace breakfastquay {
-class SystemPlaybackTarget;
-class SystemAudioIO;
+    class SystemPlaybackTarget;
+    class SystemAudioIO;
+    class ResamplerWrapper;
 }
 
 /**
@@ -198,6 +199,7 @@
 public slots:
     virtual void preferenceChanged(PropertyContainer::PropertyName);
     virtual void resizeConstrained(QSize);
+    virtual void recreateAudioIO();
 
 protected slots:
     virtual void zoomIn();
@@ -344,6 +346,7 @@
 
     AudioCallbackPlaySource *m_playSource;
     AudioRecordTarget       *m_recordTarget;
+    breakfastquay::ResamplerWrapper *m_resamplerWrapper;
     breakfastquay::SystemPlaybackTarget *m_playTarget; // only one of this...
     breakfastquay::SystemAudioIO *m_audioIO;           // ... and this exists
 
@@ -463,6 +466,8 @@
     virtual void setDefaultSessionTemplate(QString);
 
     virtual void createAudioIO();
+    virtual void deleteAudioIO();
+    
     virtual void openHelpUrl(QString url);
     virtual void openLocalFolder(QString path);