faf1c8e68a3e — Chris Cannam 9 months ago
Merge from incremental branch
M bqaudiostream/AudioReadStream.h +39 -5
@@ 83,13 83,20 @@ public:
     bool isSeekable() const;
 
     /**
-     * Return an estimate of the number of frames in the stream (at
-     * its native sample rate) or zero if the stream can't provide
+     * Return an estimate of the number of frames in the stream, at
+     * its native sample rate, or zero if the stream can't provide
      * that information.
      *
-     * For seekable streams (see isSeekable()) this is guaranteed to
-     * return a true frame count. For other streams it may be
-     * approximate, hence the name.
+     * There is no way to distinguish between a stream that can't
+     * provide this estimate and a stream of truly zero
+     * duration. Although unsatisfactory, this is at least consistent
+     * with the treatment of WAV files of zero data size, which are
+     * usually understood as files that are still being written and
+     * may have a true duration that is so far unknown.
+     *
+     * For seekable streams (see isSeekable()), any non-zero return
+     * value is guaranteed to be a true frame count. For other streams
+     * it may be approximate, hence the name.
      */
     size_t getEstimatedFrameCount() const;
 

          
@@ 150,6 157,31 @@ public:
      * empty string otherwise.
      */
     virtual std::string getArtistName() const = 0;
+
+    /**
+     * Return true if this reader has explicit support for synchronous
+     * incremental reading of its file (i.e. reading while the audio
+     * file is still being written without returning early on
+     * EOF). Few readers do, and it may depend on the file as well as
+     * the reader.
+     *
+     * If so, then setIncrementalTimeouts can be used to tell it that
+     * when the end of file is reached, if the file has apparently not
+     * been finalised, it should wait a certain amount of time for
+     * more data rather than giving up directly.
+     */
+    virtual bool hasIncrementalSupport() const;
+
+    /**
+     * Set timeouts for incremental reading. If hasIncrementalSupport
+     * returns true and retryTimeoutMs is greater than zero, then if
+     * EOF is reached during a read and the file has not yet
+     * detectably been finalised by its writer, the reader will wait
+     * retryTimeoutMs milliseconds and try again. The totalTimeoutMs
+     * value, which will usually be larger, places a limit on the
+     * total duration of retries. Both are zero by default.
+     */
+    void setIncrementalTimeouts(int retryTimeoutMs, int totalTimeoutMs);
     
 protected:
     AudioReadStream();

          
@@ 159,6 191,8 @@ protected:
     size_t m_sampleRate;
     size_t m_estimatedFrameCount;
     bool m_seekable;
+    int m_retryTimeoutMs;
+    int m_totalTimeoutMs;
 
 private:
     int getResampledChunk(int count, float *frames);

          
M build/Makefile.inc +1 -1
@@ 4,7 4,7 @@ HEADERS	:= $(wildcard src/*.h) $(wildcar
 OBJECTS	:= $(patsubst %.cpp,%.o,$(SOURCES))
 LIBRARY	:= libbqaudiostream.a
 
-CXXFLAGS := -std=c++98 -Wall $(AUDIOSTREAM_DEFINES) -I../bqvec -I../bqthingfactory -I../bqresample -I./bqaudiostream -fpic $(THIRD_PARTY_INCLUDES)
+CXXFLAGS := -std=c++11 -Wall $(AUDIOSTREAM_DEFINES) -I../bqvec -I../bqthingfactory -I../bqresample -I./bqaudiostream -fpic $(THIRD_PARTY_INCLUDES)
 
 all:	$(LIBRARY)
 

          
M src/AudioReadStream.cpp +15 -0
@@ 48,6 48,8 @@ AudioReadStream::AudioReadStream() :
     m_sampleRate(0),
     m_estimatedFrameCount(0),
     m_seekable(false),
+    m_retryTimeoutMs(0),
+    m_totalTimeoutMs(0),
     m_retrievalRate(0),
     m_totalFileFrames(0),
     m_totalRetrievedFrames(0),

          
@@ 114,6 116,19 @@ AudioReadStream::seek(size_t frame)
     return performSeek(frame);
 }
 
+bool
+AudioReadStream::hasIncrementalSupport() const
+{
+    return false;
+}
+
+void
+AudioReadStream::setIncrementalTimeouts(int retryTimeoutMs, int totalTimeoutMs)
+{
+    m_retryTimeoutMs = retryTimeoutMs;
+    m_totalTimeoutMs = totalTimeoutMs;
+}
+
 size_t
 AudioReadStream::getInterleavedFrames(size_t count, float *frames)
 {

          
M src/AudioReadStreamFactory.cpp +9 -6
@@ 134,15 134,18 @@ AudioReadStreamFactory::getFileFilter()
 // builders are guaranteed to be registered in lexical order. So we
 // should put the desirable readers first and the iffy ones after.
 
+// SimpleWavFileReadStream reads most WAV files. It's much more
+// limited than the libsndfile-based WavFileReadStream, but it doesn't
+// lack any features we actually use, and it includes optional
+// incremental reading (read-during-write) which the other doesn't, so
+// we have it first. One of these two must also come before the other
+// general platform frameworks because we don't currently have seek
+// support in those
+#include "SimpleWavFileReadStream.cpp"
+
 // WavFileReadStream uses libsndfile, which is mostly trustworthy
 #include "WavFileReadStream.cpp"
 
-// SimpleWavFileReadStream reads most WAV files. The dedicated
-// WavFileReadStream using libsndfile is better and goes first, but
-// this must come before the other general platform libraries because
-// we don't currently have seek support in those
-#include "SimpleWavFileReadStream.cpp"
-
 // OggVorbisReadStream uses the official libraries, which ought to be good
 #include "OggVorbisReadStream.cpp"
 

          
M src/AudioWriteStreamFactory.cpp +7 -0
@@ 141,8 141,15 @@ AudioWriteStreamFactory::isExtensionSupp
 // #ifdef'd out if the implementation is not selected, so there is no
 // overhead.
 
+// WavFileWriteStream uses libsndfile, which is mostly trustworthy
 #include "WavFileWriteStream.cpp"
+
+// SimpleWavFileWriteStream writes only 24-bit WAV files. The
+// dedicated WavFileReadStream using libsndfile is generally much
+// better and goes first
 #include "SimpleWavFileWriteStream.cpp"
+
 #include "CoreAudioWriteStream.cpp"
+
 #include "OpusWriteStream.cpp"
 

          
M src/SimpleWavFileReadStream.cpp +91 -8
@@ 34,16 34,18 @@ 
 
 #include "SimpleWavFileReadStream.h"
 
-#if ! (defined(HAVE_LIBSNDFILE) || defined(HAVE_SNDFILE))
+#include <iostream>
 
-#include <iostream>
+#include <chrono>
+#include <thread>
 
 //#define DEBUG_SIMPLE_WAV_FILE_READ_STREAM 1
 
 namespace breakfastquay
 {
 
-static std::vector<std::string> extensions() {
+static std::vector<std::string>
+getSimpleWavReaderExtensions() {
     std::vector<std::string> ee;
     ee.push_back("wav");
     return ee;

          
@@ 53,16 55,18 @@ static
 AudioReadStreamBuilder<SimpleWavFileReadStream>
 simplewavbuilder(
     std::string("http://breakfastquay.com/rdf/turbot/audiostream/SimpleWavFileReadStream"),
-    extensions()
+    getSimpleWavReaderExtensions()
     );
 
 SimpleWavFileReadStream::SimpleWavFileReadStream(std::string filename) :
     m_path(filename),
     m_file(0),
     m_bitDepth(0),
+    m_dataChunkOffset(0),
     m_dataChunkSize(0),
     m_dataReadOffset(0),
-    m_dataReadStart(0)
+    m_dataReadStart(0),
+    m_retryCount(0)
 {
 #ifdef _MSC_VER
     // This is behind _MSC_VER not _WIN32 because the fstream

          
@@ 88,6 92,8 @@ SimpleWavFileReadStream::SimpleWavFileRe
         throw FileNotFound(m_path);
     }
 
+    m_seekable = true;
+
     readHeader();
 }
 

          
@@ 151,7 157,6 @@ SimpleWavFileReadStream::readHeader()
     m_channelCount = channels;
     m_sampleRate = sampleRate;
     m_bitDepth = bitsPerSample;
-    m_seekable = true;
 
     // we don't use
     (void)byteRate;

          
@@ 161,12 166,15 @@ SimpleWavFileReadStream::readHeader()
         m_file->ignore(fmtSize - 16);
     }
 
+    m_dataChunkOffset = m_file->tellg();
     m_dataChunkSize = readExpectedChunkSize("data");
+
     if (bytesPerFrame > 0) {
         m_estimatedFrameCount = m_dataChunkSize / bytesPerFrame;
     } else {
         m_estimatedFrameCount = 0;
     }
+
     m_dataReadOffset = 0;
     m_dataReadStart = m_file->tellg();
 }

          
@@ 285,7 293,7 @@ SimpleWavFileReadStream::performSeek(siz
         return false;
     }
     
-    std::ifstream::pos_type actual = m_file->tellg();
+    std::streampos actual = m_file->tellg();
     // (In fact I think tellg() always reports whatever you passed to seekg())
     if (actual != std::ifstream::pos_type(target)) {
 #ifdef DEBUG_SIMPLE_WAV_FILE_READ_STREAM

          
@@ 316,6 324,73 @@ SimpleWavFileReadStream::performSeek(siz
     return true;
 }
 
+bool
+SimpleWavFileReadStream::shouldRetry(int justReadBytes)
+{
+#ifdef DEBUG_SIMPLE_WAV_FILE_READ_STREAM
+    std::cerr << "SimpleWavFileReadStream::shouldRetry: m_dataReadOffset = "
+              << m_dataReadOffset << ", m_dataChunkSize = " << m_dataChunkSize
+              << ", justReadBytes = " << justReadBytes
+              << ", m_retryTimeoutMs = " << m_retryTimeoutMs
+              << ", m_totalTimeoutMs = " << m_totalTimeoutMs
+              << std::endl;
+#endif
+
+    if (m_dataChunkSize > 0) {
+        return false;
+    }
+    if (m_retryTimeoutMs == 0 || m_totalTimeoutMs == 0) {
+        return false;
+    }
+    if (m_file->bad()) {
+#ifdef DEBUG_SIMPLE_WAV_FILE_READ_STREAM
+        std::cerr << "SimpleWavFileReadStream::shouldRetry: file is bad"
+                  << std::endl;
+#endif
+        return false;
+    }
+
+    int permittedRetryCount = m_totalTimeoutMs / m_retryTimeoutMs;
+
+    if (m_file->eof()) {
+        if (m_retryCount > permittedRetryCount) {
+#ifdef DEBUG_SIMPLE_WAV_FILE_READ_STREAM
+            std::cerr << "SimpleWavFileReadStream::shouldRetry: permitted retry limit of " << permittedRetryCount << " exceeded" << std::endl;
+#endif
+            return false;
+        }
+        std::this_thread::sleep_for(std::chrono::milliseconds(m_retryTimeoutMs));
+        m_file->clear();
+        std::streampos location = m_file->tellg();
+        m_file->seekg(m_dataChunkOffset, std::ios::beg);
+        m_dataChunkSize = readExpectedChunkSize("data");
+        std::streamoff target = location - std::streamoff(justReadBytes);
+        m_file->seekg(target, std::ios::beg);
+        if (m_file->fail()) {
+#ifdef DEBUG_SIMPLE_WAV_FILE_READ_STREAM
+            std::cerr << "SimpleWavFileReadStream::shouldRetry: seek to "
+                      << target << " failed" << std::endl;
+#endif
+            return false;
+        }
+#ifdef DEBUG_SIMPLE_WAV_FILE_READ_STREAM
+        std::cerr << "SimpleWavFileReadStream::shouldRetry: re-seek to "
+                  << target << " succeeded, returning true" << std::endl;
+#endif
+        ++m_retryCount;
+        return true;
+    } else {
+#ifdef DEBUG_SIMPLE_WAV_FILE_READ_STREAM
+        std::cerr << "SimpleWavFileReadStream::shouldRetry: file is not bad or at eof, something else must be wrong" << std::endl;
+#endif
+    }
+    
+#ifdef DEBUG_SIMPLE_WAV_FILE_READ_STREAM
+    std::cerr << "SimpleWavFileReadStream::shouldRetry: returning false" << std::endl;
+#endif        
+    return false;
+}
+
 size_t
 SimpleWavFileReadStream::getFrames(size_t count, float *frames)
 {

          
@@ 325,6 400,11 @@ SimpleWavFileReadStream::getFrames(size_
     size_t requested = count * m_channelCount;
     size_t got = 0;
 
+#ifdef DEBUG_SIMPLE_WAV_FILE_READ_STREAM
+    std::cerr << "SimpleWavFileReadStream::getFrames: count = " << count
+              << ", requested = " << requested << std::endl;
+#endif
+
     while (got < requested) {
         if (m_dataChunkSize > 0 && m_dataReadOffset >= m_dataChunkSize) {
             break;

          
@@ 332,6 412,9 @@ SimpleWavFileReadStream::getFrames(size_
         int gotHere = getBytes(sampleSize, buf);
         m_dataReadOffset += gotHere;
         if (gotHere < sampleSize) {
+            if (m_dataChunkSize == 0 && shouldRetry(gotHere)) {
+                continue;
+            }
             break;
         }
         switch (m_bitDepth) {

          
@@ 341,6 424,7 @@ SimpleWavFileReadStream::getFrames(size_
         case 32: frames[got] = convertSampleFloat(buf); break;
         }
         ++got;
+        m_retryCount = 0;
     }
 
     if (got < requested) {

          
@@ 434,4 518,3 @@ SimpleWavFileReadStream::le2int(const st
 
 }
 
-#endif

          
M src/SimpleWavFileReadStream.h +6 -4
@@ 37,9 37,6 @@ 
 
 #include "../bqaudiostream/AudioReadStream.h"
 
-// If we have libsndfile, we shouldn't be using this class
-#if ! (defined(HAVE_LIBSNDFILE) || defined(HAVE_SNDFILE))
-
 #ifdef _MSC_VER
 #include <windows.h>
 #endif

          
@@ 63,6 60,8 @@ public:
 
     virtual std::string getError() const { return m_error; }
 
+    virtual bool hasIncrementalSupport() const { return true; }
+    
 protected:
     virtual size_t getFrames(size_t count, float *frames);
     virtual bool performSeek(size_t frame);

          
@@ 76,6 75,7 @@ private:
     std::ifstream *m_file;
     int m_bitDepth;
     bool m_floatSwap;
+    uint32_t m_dataChunkOffset;
     uint32_t m_dataChunkSize;
     uint32_t m_dataReadOffset;
     uint32_t m_dataReadStart;

          
@@ 87,6 87,9 @@ private:
     uint32_t readChunkSizeAfterTag();
     uint32_t readMandatoryNumber(int length);
 
+    int m_retryCount;
+    bool shouldRetry(int justRead);
+    
     float convertSample8(const std::vector<uint8_t> &);
     float convertSample16(const std::vector<uint8_t> &);
     float convertSample24(const std::vector<uint8_t> &);

          
@@ 100,5 103,4 @@ private:
 
 #endif
 
-#endif
 

          
M src/SimpleWavFileWriteStream.cpp +3 -5
@@ 34,8 34,6 @@ 
 
 #include "SimpleWavFileWriteStream.h"
 
-#if ! (defined(HAVE_LIBSNDFILE) || defined(HAVE_SNDFILE))
-
 #include "../bqaudiostream/Exceptions.h"
 #include <iostream>
 #include <stdint.h>

          
@@ 45,7 43,8 @@ using namespace std;
 namespace breakfastquay
 {
 
-static std::vector<std::string> extensions() {
+static std::vector<std::string>
+getSimpleWavWriterExtensions() {
     std::vector<std::string> ee;
     ee.push_back("wav");
     return ee;

          
@@ 55,7 54,7 @@ static
 AudioWriteStreamBuilder<SimpleWavFileWriteStream>
 simplewavbuilder(
     std::string("http://breakfastquay.com/rdf/turbot/audiostream/SimpleWavFileWriteStream"),
-    extensions()
+    getSimpleWavWriterExtensions()
     );
 
 SimpleWavFileWriteStream::SimpleWavFileWriteStream(Target target) :

          
@@ 236,4 235,3 @@ SimpleWavFileWriteStream::putInterleaved
 
 }
 
-#endif

          
M src/SimpleWavFileWriteStream.h +0 -4
@@ 37,9 37,6 @@ 
 
 #include "../bqaudiostream/AudioWriteStream.h"
 
-// If we have libsndfile, we shouldn't be using this class
-#if ! (defined(HAVE_LIBSNDFILE) || defined(HAVE_SNDFILE))
-
 #ifdef _MSC_VER
 #include <windows.h>
 #endif

          
@@ 74,4 71,3 @@ protected:
 
 #endif
 
-#endif

          
M src/WavFileWriteStream.cpp +5 -2
@@ 42,7 42,8 @@ 
 namespace breakfastquay
 {
 
-static std::vector<std::string> extensions() {
+static std::vector<std::string>
+getWavWriterExtensions() {
     std::vector<std::string> ee;
     ee.push_back("wav");
     ee.push_back("aiff");

          
@@ 53,7 54,7 @@ static
 AudioWriteStreamBuilder<WavFileWriteStream>
 wavbuilder(
     std::string("http://breakfastquay.com/rdf/turbot/audiostream/WavFileWriteStream"),
-    extensions()
+    getWavWriterExtensions()
     );
 
 WavFileWriteStream::WavFileWriteStream(Target target) :

          
@@ 103,6 104,8 @@ WavFileWriteStream::putInterleavedFrames
     if (written != sf_count_t(count)) {
         throw FileOperationFailed(getPath(), "write sf data");
     }
+
+    sf_write_sync(m_file);
 }
 
 }

          
A => test/TestWavReadWhileWriting.h +142 -0
@@ 0,0 1,142 @@ 
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+/* Copyright Chris Cannam - All Rights Reserved */
+
+#ifndef TEST_WAV_READ_WHILE_WRITING_H
+#define TEST_WAV_READ_WHILE_WRITING_H
+
+#include <QObject>
+#include <QtTest>
+
+#include "bqaudiostream/AudioReadStreamFactory.h"
+#include "bqaudiostream/AudioReadStream.h"
+#include "bqaudiostream/AudioWriteStreamFactory.h"
+#include "bqaudiostream/AudioWriteStream.h"
+
+#include <thread>
+#include <chrono>
+
+namespace breakfastquay {
+
+class TestWavReadWhileWriting : public QObject
+{
+    Q_OBJECT
+
+    void initBuf(std::vector<float> &buffer, int start, int n) {
+        for (int i = 0; i < n; ++i) {
+            float value = float(start + i) / 32767.f;
+            buffer[i] = value;
+        }
+    }
+
+    void zeroBuf(std::vector<float> &buffer) {
+        for (int i = 0; i < int(buffer.size()); ++i) {
+            buffer[i] = 0.f;
+        }
+    }
+    
+    bool checkBuf(const std::vector<float> &buffer, int start, int n) {
+        for (int i = 0; i < n; ++i) {
+            float value = float(start + i) / 32767.f;
+            float diff = fabsf(buffer[i] - value);
+            float threshold = 1.f / 65534.f;
+            if (diff > threshold) {
+                std::cerr << "checkBuf: at index " << i << " (start = "
+                          << start << ", n = " << n << "), expected "
+                          << value << ", found " << buffer[i]
+                          << " (diff = " << diff << ", above threshold "
+                          << threshold << ")" << std::endl;
+                return false;
+            }
+        }
+        return true;
+    }
+    
+private slots:
+    void readWhileWritingNoWait() {
+
+        int bs = 1024;
+        int channels = 2;
+        int rate = 44100;
+        std::vector<float> buffer(bs * channels, 0.f);
+        std::string file = "test-audiostream-readwhilewriting.wav";
+        
+        auto ws = AudioWriteStreamFactory::createWriteStream(file, channels, rate);
+        QVERIFY(ws->getError() == std::string());
+
+        auto rs = AudioReadStreamFactory::createReadStream(file);
+        QVERIFY(rs->getError() == std::string());
+
+        QCOMPARE(rs->getChannelCount(), size_t(channels));
+        QCOMPARE(rs->getSampleRate(), size_t(44100));
+        QVERIFY(rs->hasIncrementalSupport());
+        QVERIFY(!rs->isSeekable());
+        QCOMPARE(rs->getEstimatedFrameCount(), size_t(0));
+
+        QCOMPARE(rs->getInterleavedFrames(bs, buffer.data()), size_t(0));
+
+        initBuf(buffer, 0, bs * channels);
+
+        ws->putInterleavedFrames(bs, buffer.data());
+
+        zeroBuf(buffer);
+
+        QCOMPARE(rs->getInterleavedFrames(bs, buffer.data()), size_t(bs));
+
+        QVERIFY(checkBuf(buffer, 0, bs * channels));
+        
+        delete rs;
+        delete ws;
+    }
+
+    void readWhileWritingWithWait() {
+
+        int bs = 1024;
+        int channels = 2;
+        int rate = 44100;
+        std::vector<float> readbuf(bs * channels, 0.f);
+        std::vector<float> writebuf(bs * channels, 0.f);
+        std::string file = "test-audiostream-readwhilewriting.wav";
+        
+        auto ws = AudioWriteStreamFactory::createWriteStream(file, channels, rate);
+        QVERIFY(ws->getError() == std::string());
+
+        auto rs = AudioReadStreamFactory::createReadStream(file);
+        QVERIFY(rs->getError() == std::string());
+
+        QCOMPARE(rs->getChannelCount(), size_t(channels));
+        QCOMPARE(rs->getSampleRate(), size_t(44100));
+        QVERIFY(rs->hasIncrementalSupport());
+
+        rs->setIncrementalTimeouts(20, 200);
+
+        initBuf(writebuf, 0, bs * channels);
+        
+        auto writer = [&]() {
+            std::this_thread::sleep_for(std::chrono::milliseconds(150));
+            ws->putInterleavedFrames(bs/2, writebuf.data());
+            std::this_thread::sleep_for(std::chrono::milliseconds(150));
+            ws->putInterleavedFrames(bs/2, writebuf.data() + (bs/2) * channels);
+        };
+        
+        QCOMPARE(rs->getInterleavedFrames(bs, readbuf.data()), size_t(0));
+
+        rs->setIncrementalTimeouts(20, 1000);
+        
+        std::thread writeThread(writer);
+        
+        QCOMPARE(rs->getInterleavedFrames(bs, readbuf.data()), size_t(bs));
+
+        QVERIFY(checkBuf(readbuf, 0, bs * channels));
+
+        writeThread.join();
+        
+        delete rs;
+        delete ws;
+    }
+
+};
+
+
+}
+
+#endif

          
M test/main.cpp +7 -0
@@ 5,6 5,7 @@ 
 #include "TestWavSeek.h"
 #include "TestAudioStreamRead.h"
 #include "TestWavReadWrite.h"
+#include "TestWavReadWhileWriting.h"
 #include <QtTest>
 
 #include <iostream>

          
@@ 41,6 42,12 @@ int main(int argc, char *argv[])
 	else ++bad;
     }
 
+    {
+	breakfastquay::TestWavReadWhileWriting t;
+	if (QTest::qExec(&t, argc, argv) == 0) ++good;
+	else ++bad;
+    }
+
     if (bad > 0) {
 	std::cerr << "\n********* " << bad << " test suite(s) failed!\n" << std::endl;
 	return 1;

          
M test/test.pro +2 -1
@@ 15,7 15,8 @@ LIBS += -L.. -lbqaudiostream -L../../bqr
 INCLUDEPATH += . .. ../../bqvec ../../bqresample ../../bqthingfactory
 DEPENDPATH += . .. ../../bqvec ../../bqresample ../../bqthingfactory
 
-HEADERS += AudioStreamTestData.h TestAudioStreamRead.h TestSimpleWavRead.h TestWavReadWrite.h TestWavSeek.h
+HEADERS += AudioStreamTestData.h TestAudioStreamRead.h TestSimpleWavRead.h TestWavReadWrite.h TestWavSeek.h TestWavReadWhileWriting.h
+
 SOURCES += main.cpp
 
 !win32 {