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 {