From 2873c970e81fd3a35dcbfbd4048dcf8a02a94ea5 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 24 Jun 2020 05:51:20 +0200 Subject: [PATCH 1/2] Audio file incremental reading --- dpf.mk | 1 + src/CMakeLists.txt | 1 + src/sfizz/AudioReader.cpp | 367 ++++++++++++++++++++++++++++++++++++++ src/sfizz/AudioReader.h | 59 ++++++ src/sfizz/FilePool.cpp | 93 ++++------ src/sfizz/Oversampler.cpp | 130 ++++++++++++-- src/sfizz/Oversampler.h | 12 ++ 7 files changed, 594 insertions(+), 69 deletions(-) create mode 100644 src/sfizz/AudioReader.cpp create mode 100644 src/sfizz/AudioReader.h diff --git a/dpf.mk b/dpf.mk index 772dd364..7dd64e45 100644 --- a/dpf.mk +++ b/dpf.mk @@ -56,6 +56,7 @@ sfizz-clean: SFIZZ_SOURCES = \ src/sfizz/ADSREnvelope.cpp \ + src/sfizz/AudioReader.cpp \ src/sfizz/Curve.cpp \ src/sfizz/effects/Apan.cpp \ src/sfizz/Effects.cpp \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 67c3b36f..bc2ce9b2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,6 +8,7 @@ set (SFIZZ_SOURCES sfizz/FileId.cpp sfizz/FilePool.cpp sfizz/FileInstrument.cpp + sfizz/AudioReader.cpp sfizz/FilterPool.cpp sfizz/EQPool.cpp sfizz/Region.cpp diff --git a/src/sfizz/AudioReader.cpp b/src/sfizz/AudioReader.cpp new file mode 100644 index 00000000..bd397bc1 --- /dev/null +++ b/src/sfizz/AudioReader.cpp @@ -0,0 +1,367 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "AudioReader.h" +#include +#include + +namespace sfz { + +class BasicSndfileReader : public AudioReader { +public: + explicit BasicSndfileReader(SndfileHandle handle) : handle_(handle) {} + virtual ~BasicSndfileReader() {} + + int format() const override; + int64_t frames() const override; + unsigned channels() const override; + unsigned sampleRate() const override; + bool getInstrument(SF_INSTRUMENT* instrument) override; + +protected: + SndfileHandle handle_; +}; + +int BasicSndfileReader::format() const +{ + return handle_.format(); +} + +int64_t BasicSndfileReader::frames() const +{ + return handle_.frames(); +} + +unsigned BasicSndfileReader::channels() const +{ + return handle_.channels(); +} + +unsigned BasicSndfileReader::sampleRate() const +{ + return handle_.samplerate(); +} + +bool BasicSndfileReader::getInstrument(SF_INSTRUMENT* instrument) +{ + if (handle_.command(SFC_GET_INSTRUMENT, instrument, sizeof(SF_INSTRUMENT)) == SF_FALSE) + return false; + return true; +} + +//------------------------------------------------------------------------------ + +/** + * @brief Audio file reader in forward direction + */ +class ForwardReader : public BasicSndfileReader { +public: + explicit ForwardReader(SndfileHandle handle); + AudioReaderType type() const override; + size_t readNextBlock(float* buffer, size_t frames) override; +}; + +ForwardReader::ForwardReader(SndfileHandle handle) + : BasicSndfileReader(handle) +{ +} + +AudioReaderType ForwardReader::type() const +{ + return AudioReaderType::Forward; +} + +size_t ForwardReader::readNextBlock(float* buffer, size_t frames) +{ + sf_count_t readFrames = handle_.readf(buffer, frames); + if (frames <= 0) + return 0; + + return readFrames; +} + +//------------------------------------------------------------------------------ + +template +struct AudioFrame { + T samples[N]; +}; + +/** + * @brief Reorder a sequence of frames in reverse + */ +static void reverse_frames(float* data, sf_count_t frames, unsigned channels) +{ + switch (channels) { + +#define SPECIALIZE_FOR(N) \ + case N: \ + std::reverse( \ + reinterpret_cast *>(data), \ + reinterpret_cast *>(data) + frames); \ + break + + SPECIALIZE_FOR(1); + SPECIALIZE_FOR(2); + + default: + for (sf_count_t i = 0; i < frames / 2; ++i) { + sf_count_t j = frames - 1 - i; + float* frame1 = &data[i * channels]; + float* frame2 = &data[j * channels]; + for (unsigned c = 0; c < channels; ++c) + std::swap(frame1[c], frame2[c]); + } + break; + +#undef SPECIALIZE_FOR + } +} + +//------------------------------------------------------------------------------ + +/** + * @brief Audio file reader in reverse direction, for fast-seeking formats + */ +class ReverseReader : public BasicSndfileReader { +public: + explicit ReverseReader(SndfileHandle handle); + AudioReaderType type() const override; + size_t readNextBlock(float* buffer, size_t frames) override; + +private: + sf_count_t position_ {}; +}; + +ReverseReader::ReverseReader(SndfileHandle handle) + : BasicSndfileReader(handle) +{ + position_ = handle.seek(0, SF_SEEK_END); +} + +AudioReaderType ReverseReader::type() const +{ + return AudioReaderType::Reverse; +} + +size_t ReverseReader::readNextBlock(float* buffer, size_t frames) +{ + sf_count_t position = position_; + const unsigned channels = handle_.channels(); + + const sf_count_t readFrames = std::min(frames, position); + if (readFrames <= 0) + return false; + + position -= readFrames; + if (handle_.seek(position, SEEK_SET) != position || + handle_.readf(buffer, readFrames) != readFrames) + return false; + + position_ = position; + reverse_frames(buffer, readFrames, channels); + return readFrames; +} + +//------------------------------------------------------------------------------ + +/** + * @brief Audio file reader in reverse direction, for slow-seeking formats + */ +class NoSeekReverseReader : public BasicSndfileReader { +public: + explicit NoSeekReverseReader(SndfileHandle handle); + AudioReaderType type() const override; + size_t readNextBlock(float* buffer, size_t frames) override; + +private: + void readWholeFile(); + +private: + std::unique_ptr fileBuffer_; + sf_count_t fileFramesLeft_ { 0 }; +}; + +NoSeekReverseReader::NoSeekReverseReader(SndfileHandle handle) + : BasicSndfileReader(handle) +{ +} + +AudioReaderType NoSeekReverseReader::type() const +{ + return AudioReaderType::NoSeekReverse; +} + +size_t NoSeekReverseReader::readNextBlock(float* buffer, size_t frames) +{ + float* fileBuffer = fileBuffer_.get(); + if (!fileBuffer) { + readWholeFile(); + fileBuffer = fileBuffer_.get(); + } + + const unsigned channels = handle_.channels(); + const sf_count_t fileFramesLeft = fileFramesLeft_; + sf_count_t readFrames = std::min(frames, fileFramesLeft); + if (readFrames <= 0) + return 0; + + std::copy( + &fileBuffer[channels * (fileFramesLeft - readFrames)], + &fileBuffer[channels * fileFramesLeft], buffer); + reverse_frames(buffer, readFrames, channels); + + fileFramesLeft_ = fileFramesLeft - readFrames; + return readFrames; +} + +void NoSeekReverseReader::readWholeFile() +{ + const sf_count_t frames = handle_.frames(); + const unsigned channels = handle_.channels(); + float* fileBuffer = new float[channels * frames]; + fileBuffer_.reset(fileBuffer); + fileFramesLeft_ = handle_.readf(fileBuffer, frames); +} + +//------------------------------------------------------------------------------ + +const std::error_category& sndfile_category() +{ + class sndfile_category : public std::error_category { + public: + const char* name() const noexcept override + { + return "sndfile"; + } + + std::string message(int condition) const override + { + const char* str = sf_error_number(condition); + return str ? str : ""; + } + }; + + static const sndfile_category cat; + return cat; +} + +//------------------------------------------------------------------------------ + +class DummyAudioReader : public AudioReader { +public: + explicit DummyAudioReader(AudioReaderType type) : type_(type) {} + AudioReaderType type() const override { return type_; } + int format() const override { return 0; } + int64_t frames() const override { return 0; } + unsigned channels() const override { return 1; } + unsigned sampleRate() const override { return 44100; } + size_t readNextBlock(float*, size_t) override { return 0; } + bool getInstrument(SF_INSTRUMENT* ) override { return false; } + +private: + AudioReaderType type_ {}; +}; + +//------------------------------------------------------------------------------ + +static bool formatHasFastSeeking(int format) +{ + bool fast; + + const int type = format & SF_FORMAT_TYPEMASK; + const int subtype = format & SF_FORMAT_SUBMASK; + + switch (type) { + case SF_FORMAT_WAV: + case SF_FORMAT_AIFF: + case SF_FORMAT_AU: + case SF_FORMAT_RAW: + case SF_FORMAT_WAVEX: + // TODO: list more PCM formats that support fast seeking + fast = subtype >= SF_FORMAT_PCM_S8 && subtype <= SF_FORMAT_DOUBLE; + break; + case SF_FORMAT_FLAC: + // seeking has acceptable overhead + fast = true; + break; + case SF_FORMAT_OGG: + // ogg is prohibitively slow at seeking (possibly others) + // cf. https://github.com/erikd/libsndfile/issues/491 + fast = false; + break; + default: + fast = false; + break; + } + + return fast; +} + +AudioReaderPtr createAudioReader(const fs::path& path, bool reverse, std::error_code* ec) +{ + AudioReaderPtr reader; + + if (ec) + ec->clear(); + +#if defined(_WIN32) + SndfileHandle handle(path.wstring().c_str()); +#else + SndfileHandle handle(path.c_str()); +#endif + + if (!handle) { + if (ec) + *ec = std::error_code(handle.error(), sndfile_category()); + reader.reset(new DummyAudioReader(reverse ? AudioReaderType::Reverse : AudioReaderType::Forward)); + } + else if (!reverse) + reader.reset(new ForwardReader(handle)); + else if (formatHasFastSeeking(handle.format())) + reader.reset(new ReverseReader(handle)); + else + reader.reset(new NoSeekReverseReader(handle)); + + return reader; +} + +AudioReaderPtr createExplicitAudioReader(const fs::path& path, AudioReaderType type, std::error_code* ec) +{ + AudioReaderPtr reader; + + if (ec) + ec->clear(); + +#if defined(_WIN32) + SndfileHandle handle(path.wstring().c_str()); +#else + SndfileHandle handle(path.c_str()); +#endif + + if (!handle) { + if (ec) + *ec = std::error_code(handle.error(), sndfile_category()); + reader.reset(new DummyAudioReader(type)); + } + else { + switch (type) { + case AudioReaderType::Forward: + reader.reset(new ForwardReader(handle)); + break; + case AudioReaderType::Reverse: + reader.reset(new ReverseReader(handle)); + break; + case AudioReaderType::NoSeekReverse: + reader.reset(new NoSeekReverseReader(handle)); + break; + } + } + + return reader; +} + +} // namespace sfz diff --git a/src/sfizz/AudioReader.h b/src/sfizz/AudioReader.h new file mode 100644 index 00000000..653abce3 --- /dev/null +++ b/src/sfizz/AudioReader.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "absl/types/span.h" +#include "ghc/fs_std.hpp" +#include +#include +#if defined(_WIN32) +#define ENABLE_SNDFILE_WINDOWS_PROTOTYPES 1 +#include +#endif +#include + +namespace sfz { + +/** + * @brief Designation of a particular kind of audio reader + */ +enum class AudioReaderType { + //! Reader in forward direction + Forward, + //! Reader in reverse direction + Reverse, + //! Reader in reverse direction, operating on a whole file instead of seeking + NoSeekReverse, +}; + +/** + * @brief Reader of audio file data + */ +class AudioReader { +public: + virtual ~AudioReader() {} + virtual AudioReaderType type() const = 0; + virtual int format() const = 0; + virtual int64_t frames() const = 0; + virtual unsigned channels() const = 0; + virtual unsigned sampleRate() const = 0; + virtual size_t readNextBlock(float* buffer, size_t frames) = 0; + virtual bool getInstrument(SF_INSTRUMENT* instrument) = 0; +}; + +typedef std::unique_ptr AudioReaderPtr; + +/** + * @brief Create a file reader of detected type. + */ +AudioReaderPtr createAudioReader(const fs::path& path, bool reverse, std::error_code* ec = nullptr); + +/** + * @brief Create a file reader of explicit type. (for testing purposes) + */ +AudioReaderPtr createExplicitAudioReader(const fs::path& path, AudioReaderType type, std::error_code* ec = nullptr); + +} // namespace sfz diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 95519ac6..af9f50c7 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -24,6 +24,7 @@ // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "FilePool.h" +#include "AudioReader.h" #include "FileInstrument.h" #include "Buffer.h" #include "AudioBuffer.h" @@ -40,69 +41,50 @@ #include #include -void readBaseFile(SndfileHandle& sndFile, sfz::FileAudioBuffer& output, uint32_t numFrames, bool reverse) +void readBaseFile(sfz::AudioReader& reader, sfz::FileAudioBuffer& output, uint32_t numFrames) { output.reset(); output.resize(numFrames); - if (reverse) - sndFile.seek(-static_cast(numFrames), SEEK_END); - - const unsigned channels = sndFile.channels(); + const unsigned channels = reader.channels(); if (channels == 1) { output.addChannel(); output.clear(); - sndFile.readf(output.channelWriter(0), numFrames); + reader.readNextBlock(output.channelWriter(0), numFrames); } else if (channels == 2) { output.addChannel(); output.addChannel(); output.clear(); sfz::Buffer tempReadBuffer { 2 * numFrames }; - sndFile.readf(tempReadBuffer.data(), numFrames); + reader.readNextBlock(tempReadBuffer.data(), numFrames); sfz::readInterleaved(tempReadBuffer, output.getSpan(0), output.getSpan(1)); } - - if (reverse) { - for (unsigned c = 0; c < channels; ++c) { - // TODO: consider optimizing with SIMD - absl::Span channel = output.getSpan(c); - std::reverse(channel.begin(), channel.end()); - } - } } -std::unique_ptr readFromFile(SndfileHandle& sndFile, uint32_t numFrames, sfz::Oversampling factor, bool reverse) +std::unique_ptr readFromFile(sfz::AudioReader& reader, uint32_t numFrames, sfz::Oversampling factor) { auto baseBuffer = absl::make_unique(); - readBaseFile(sndFile, *baseBuffer, numFrames, reverse); + readBaseFile(reader, *baseBuffer, numFrames); if (factor == sfz::Oversampling::x1) return baseBuffer; - auto outputBuffer = absl::make_unique(sndFile.channels(), numFrames * static_cast(factor)); + auto outputBuffer = absl::make_unique(reader.channels(), numFrames * static_cast(factor)); outputBuffer->clear(); sfz::Oversampler oversampler { factor }; oversampler.stream(*baseBuffer, *outputBuffer); return outputBuffer; } -void streamFromFile(SndfileHandle& sndFile, uint32_t numFrames, sfz::Oversampling factor, bool reverse, sfz::FileAudioBuffer& output, std::atomic* filledFrames = nullptr) +void streamFromFile(sfz::AudioReader& reader, uint32_t numFrames, sfz::Oversampling factor, sfz::FileAudioBuffer& output, std::atomic* filledFrames = nullptr) { - if (factor == sfz::Oversampling::x1) { - readBaseFile(sndFile, output, numFrames, reverse); - if (filledFrames != nullptr) - filledFrames->store(numFrames); - return; - } - - auto baseBuffer = readFromFile(sndFile, numFrames, sfz::Oversampling::x1, reverse); output.reset(); - output.addChannels(baseBuffer->getNumChannels()); + output.addChannels(reader.channels()); output.resize(numFrames * static_cast(factor)); output.clear(); sfz::Oversampler oversampler { factor }; - oversampler.stream(*baseBuffer, output, filledFrames); + oversampler.stream(reader, output, filledFrames); } sfz::FilePool::FilePool(sfz::Logger& logger) @@ -210,24 +192,26 @@ absl::optional sfz::FilePool::getFileInformation(const Fil if (!fs::exists(file)) return {}; - SndfileHandle sndFile(file.string().c_str()); - if (sndFile.channels() != 1 && sndFile.channels() != 2) { + AudioReaderPtr reader = createAudioReader(file, fileId.isReverse()); + const unsigned channels = reader->channels(); + + if (channels != 1 && channels != 2) { DBG("[sfizz] Missing logic for " << sndFile.channels() << " channels, discarding sample " << fileId); return {}; } FileInformation returnedValue; - returnedValue.end = static_cast(sndFile.frames()) - 1; - returnedValue.sampleRate = static_cast(sndFile.samplerate()); - returnedValue.numChannels = sndFile.channels(); + returnedValue.end = static_cast(reader->frames()) - 1; + returnedValue.sampleRate = static_cast(reader->sampleRate()); + returnedValue.numChannels = reader->channels(); SF_INSTRUMENT instrumentInfo {}; - const int sndFormat = sndFile.format(); + const int sndFormat = reader->format(); if ((sndFormat & SF_FORMAT_TYPEMASK) == SF_FORMAT_FLAC) sfz::FileInstruments::extractFromFlac(file, instrumentInfo); else - sndFile.command(SFC_GET_INSTRUMENT, &instrumentInfo, sizeof(instrumentInfo)); + reader->getInstrument(&instrumentInfo); if (!fileId.isReverse()) { if (instrumentInfo.loop_count > 0) { @@ -249,10 +233,10 @@ bool sfz::FilePool::preloadFile(const FileId& fileId, uint32_t maxOffset) noexce return false; const fs::path file { rootDirectory / fileId.filename() }; - SndfileHandle sndFile(file.string().c_str()); + AudioReaderPtr reader = createAudioReader(file, fileId.isReverse()); // FIXME: Large offsets will require large preloading; is this OK in practice? Apparently sforzando does the same - const auto frames = static_cast(sndFile.frames()); + const auto frames = static_cast(reader->frames()); const auto framesToLoad = [&]() { if (preloadSize == 0) return frames; @@ -263,12 +247,12 @@ bool sfz::FilePool::preloadFile(const FileId& fileId, uint32_t maxOffset) noexce const auto existingFile = preloadedFiles.find(fileId); if (existingFile != preloadedFiles.end()) { if (framesToLoad > existingFile->second.preloadedData->getNumFrames()) { - preloadedFiles[fileId].preloadedData = readFromFile(sndFile, framesToLoad, oversamplingFactor, fileId.isReverse()); + preloadedFiles[fileId].preloadedData = readFromFile(*reader, framesToLoad, oversamplingFactor); } } else { - fileInformation->sampleRate = static_cast(oversamplingFactor) * static_cast(sndFile.samplerate()); + fileInformation->sampleRate = static_cast(oversamplingFactor) * static_cast(reader->sampleRate()); FileDataHandle handle { - readFromFile(sndFile, framesToLoad, oversamplingFactor, fileId.isReverse()), + readFromFile(*reader, framesToLoad, oversamplingFactor), *fileInformation }; preloadedFiles.insert_or_assign(fileId, handle); @@ -283,17 +267,17 @@ absl::optional sfz::FilePool::loadFile(const FileId& fileId return {}; const fs::path file { rootDirectory / fileId.filename() }; - SndfileHandle sndFile(file.string().c_str()); + AudioReaderPtr reader = createAudioReader(file, fileId.isReverse()); // FIXME: Large offsets will require large preloading; is this OK in practice? Apparently sforzando does the same - const auto frames = static_cast(sndFile.frames()); + const auto frames = static_cast(reader->frames()); const auto existingFile = loadedFiles.find(fileId); if (existingFile != loadedFiles.end()) { return existingFile->second; } else { - fileInformation->sampleRate = static_cast(oversamplingFactor) * static_cast(sndFile.samplerate()); + fileInformation->sampleRate = static_cast(oversamplingFactor) * static_cast(reader->sampleRate()); FileDataHandle handle { - readFromFile(sndFile, frames, oversamplingFactor, fileId.isReverse()), + readFromFile(*reader, frames, oversamplingFactor), *fileInformation }; loadedFiles.insert_or_assign(fileId, handle); @@ -342,8 +326,8 @@ void sfz::FilePool::setPreloadSize(uint32_t preloadSize) noexcept const auto numFrames = preloadedFile.second.preloadedData->getNumFrames() / static_cast(oversamplingFactor); const auto maxOffset = numFrames > this->preloadSize ? static_cast(numFrames) - this->preloadSize : 0; fs::path file { rootDirectory / preloadedFile.first.filename() }; - SndfileHandle sndFile(file.string().c_str()); - preloadedFile.second.preloadedData = readFromFile(sndFile, preloadSize + maxOffset, oversamplingFactor, preloadedFile.first.isReverse()); + AudioReaderPtr reader = createAudioReader(file, preloadedFile.first.isReverse()); + preloadedFile.second.preloadedData = readFromFile(*reader, preloadSize + maxOffset, oversamplingFactor); } this->preloadSize = preloadSize; } @@ -392,14 +376,15 @@ void sfz::FilePool::loadingThread() noexcept const auto waitDuration = loadStartTime - promise->creationTime; const fs::path file { rootDirectory / promise->fileId.filename() }; - SndfileHandle sndFile(file.string().c_str()); - if (sndFile.error() != 0) { - DBG("[sfizz] libsndfile errored for " << promise->fileId << " with message " << sndFile.strError()); + std::error_code readError; + AudioReaderPtr reader = createAudioReader(file, promise->fileId.isReverse(), &readError); + if (readError) { + DBG("[sfizz] libsndfile errored for " << promise->fileId << " with message " << readError.what()); promise->dataStatus = FilePromise::DataStatus::Error; continue; } - const auto frames = static_cast(sndFile.frames()); - streamFromFile(sndFile, frames, oversamplingFactor, promise->fileId.isReverse(), promise->fileData, &promise->availableFrames); + const auto frames = static_cast(reader->frames()); + streamFromFile(*reader, frames, oversamplingFactor, promise->fileData, &promise->availableFrames); promise->dataStatus = FilePromise::DataStatus::Ready; const auto loadDuration = std::chrono::high_resolution_clock::now() - loadStartTime; logger.logFileTime(waitDuration, loadDuration, frames, promise->fileId.filename()); @@ -453,8 +438,8 @@ void sfz::FilePool::setOversamplingFactor(sfz::Oversampling factor) noexcept const auto numFrames = preloadedFile.second.preloadedData->getNumFrames() / static_cast(this->oversamplingFactor); const uint32_t maxOffset = numFrames > this->preloadSize ? static_cast(numFrames) - this->preloadSize : 0; fs::path file { rootDirectory / preloadedFile.first.filename() }; - SndfileHandle sndFile(file.string().c_str()); - preloadedFile.second.preloadedData = readFromFile(sndFile, preloadSize + maxOffset, factor, preloadedFile.first.isReverse()); + AudioReaderPtr reader = createAudioReader(file, preloadedFile.first.isReverse()); + preloadedFile.second.preloadedData = readFromFile(*reader, preloadSize + maxOffset, factor); preloadedFile.second.information.sampleRate *= samplerateChange; } diff --git a/src/sfizz/Oversampler.cpp b/src/sfizz/Oversampler.cpp index 1027b8ba..67de1c30 100644 --- a/src/sfizz/Oversampler.cpp +++ b/src/sfizz/Oversampler.cpp @@ -7,6 +7,7 @@ #include "Oversampler.h" #include "Buffer.h" #include "AudioSpan.h" +#include "AudioReader.h" #include "SIMDConfig.h" constexpr std::array coeffsStage2x { @@ -69,28 +70,29 @@ void sfz::Oversampler::stream(AudioSpan input, AudioSpan output, s const auto numFrames = input.getNumFrames(); const auto numChannels = input.getNumChannels(); - std::vector upsampler2x (numChannels); - std::vector upsampler4x (numChannels); - std::vector upsampler8x (numChannels); + std::vector upsampler2x; + std::vector upsampler4x; + std::vector upsampler8x; switch(factor) { case Oversampling::x8: + upsampler8x.resize(numChannels); for (auto& upsampler: upsampler8x) upsampler.set_coefs(coeffsStage8x.data()); // fallthrough case Oversampling::x4: + upsampler4x.resize(numChannels); for (auto& upsampler: upsampler4x) upsampler.set_coefs(coeffsStage4x.data()); // fallthrough case Oversampling::x2: + upsampler2x.resize(numChannels); for (auto& upsampler: upsampler2x) upsampler.set_coefs(coeffsStage2x.data()); break; case Oversampling::x1: - for (size_t i = 0; i < numChannels; ++i) - copy(input.getConstSpan(i), output.getSpan(i).first(numFrames)); - return; + break; } // Intermediate buffers @@ -109,20 +111,119 @@ void sfz::Oversampler::stream(AudioSpan input, AudioSpan output, s for (size_t chanIdx = 0; chanIdx < numChannels; chanIdx++) { const auto inputChunk = input.getSpan(chanIdx).subspan(inputFrameCounter, thisChunkSize); const auto outputChunk = output.getSpan(chanIdx).subspan(outputFrameCounter, outputChunkSize); - if (factor == Oversampling::x2) { + switch (factor) { + case Oversampling::x1: + copy(inputChunk, outputChunk); + break; + case Oversampling::x2: upsampler2x[chanIdx].process_block(outputChunk.data(), inputChunk.data(), static_cast(thisChunkSize)); - continue; - } - if (factor == Oversampling::x4) { + break; + case Oversampling::x4: upsampler2x[chanIdx].process_block(span1.data(), inputChunk.data(), static_cast(thisChunkSize)); upsampler4x[chanIdx].process_block(outputChunk.data(), span1.data(), static_cast(thisChunkSize * 2)); - continue; - } - else if (factor == Oversampling::x8) { + break; + case Oversampling::x8: upsampler2x[chanIdx].process_block(span1.data(), inputChunk.data(), static_cast(thisChunkSize)); upsampler4x[chanIdx].process_block(span2.data(), span1.data(), static_cast(thisChunkSize * 2)); upsampler8x[chanIdx].process_block(outputChunk.data(), span2.data(), static_cast(thisChunkSize * 4)); - continue; + break; + } + } + inputFrameCounter += thisChunkSize; + outputFrameCounter += outputChunkSize; + + if (framesReady != nullptr) + framesReady->fetch_add(outputChunkSize); + } +} + +void sfz::Oversampler::stream(AudioReader& input, AudioSpan output, std::atomic* framesReady) +{ + ASSERT(output.getNumFrames() >= input.getNumFrames() * static_cast(factor)); + ASSERT(output.getNumChannels() == input.getNumChannels()); + + const auto numFrames = static_cast(input.frames()); + const auto numChannels = input.channels(); + + std::vector upsampler2x; + std::vector upsampler4x; + std::vector upsampler8x; + + switch(factor) + { + case Oversampling::x8: + upsampler8x.resize(numChannels); + for (auto& upsampler: upsampler8x) + upsampler.set_coefs(coeffsStage8x.data()); + // fallthrough + case Oversampling::x4: + upsampler4x.resize(numChannels); + for (auto& upsampler: upsampler4x) + upsampler.set_coefs(coeffsStage4x.data()); + // fallthrough + case Oversampling::x2: + upsampler2x.resize(numChannels); + for (auto& upsampler: upsampler2x) + upsampler.set_coefs(coeffsStage2x.data()); + break; + case Oversampling::x1: + break; + } + + // Intermediate buffers + sfz::Buffer fileBlock { chunkSize * numChannels }; + sfz::Buffer buffer1 { chunkSize * 2 }; + sfz::Buffer buffer2 { chunkSize * 4 }; + auto span1 = absl::MakeSpan(buffer1); + auto span2 = absl::MakeSpan(buffer2); + + auto upsample2xFromInterleaved = [numChannels]( + Upsampler2x& upsampler, float* output, const float* input, + size_t numInputFrames, unsigned chanIdx) + { + for (size_t i = 0; i < numInputFrames; ++i) { + float* outp = &output[2 * i]; + const float* inp = &input[i * numChannels + chanIdx]; + upsampler.process_sample(outp[0], outp[1], inp[0]); + } + }; + + size_t inputFrameCounter { 0 }; + size_t outputFrameCounter { 0 }; + bool inputEof = false; + while (!inputEof && inputFrameCounter < numFrames) + { + // std::cout << "Input frames: " << inputFrameCounter << "/" << numFrames << '\n'; + auto thisChunkSize = std::min(chunkSize, numFrames - inputFrameCounter); + const auto numFramesRead = static_cast( + input.readNextBlock(fileBlock.data(), thisChunkSize)); + if (numFramesRead == 0) + break; + if (numFramesRead < thisChunkSize) { + inputEof = true; + thisChunkSize = numFramesRead; + } + const auto outputChunkSize = thisChunkSize * static_cast(factor); + + for (size_t chanIdx = 0; chanIdx < numChannels; chanIdx++) { + const auto outputChunk = output.getSpan(chanIdx).subspan(outputFrameCounter, outputChunkSize); + switch (factor) { + case Oversampling::x1: + for (size_t i = 0; i < thisChunkSize; ++i) + outputChunk[i] = fileBlock[i * numChannels + chanIdx]; + break; + case Oversampling::x2: + upsample2xFromInterleaved(upsampler2x[chanIdx], outputChunk.data(), fileBlock.data(), thisChunkSize, chanIdx); + break; + case Oversampling::x4: + upsample2xFromInterleaved(upsampler2x[chanIdx], span1.data(), fileBlock.data(), thisChunkSize, chanIdx); + upsampler4x[chanIdx].process_block(outputChunk.data(), span1.data(), static_cast(thisChunkSize * 2)); + break; + case Oversampling::x8: + upsample2xFromInterleaved(upsampler2x[chanIdx], span1.data(), fileBlock.data(), thisChunkSize, chanIdx); + upsampler4x[chanIdx].process_block(span2.data(), span1.data(), static_cast(thisChunkSize * 2)); + upsampler8x[chanIdx].process_block(outputChunk.data(), span2.data(), static_cast(thisChunkSize * 4)); + break; } } inputFrameCounter += thisChunkSize; @@ -131,5 +232,4 @@ void sfz::Oversampler::stream(AudioSpan input, AudioSpan output, s if (framesReady != nullptr) framesReady->fetch_add(outputChunkSize); } - } diff --git a/src/sfizz/Oversampler.h b/src/sfizz/Oversampler.h index 25e74eaf..f6890ad2 100644 --- a/src/sfizz/Oversampler.h +++ b/src/sfizz/Oversampler.h @@ -15,6 +15,8 @@ #include "Config.h" namespace sfz { +class AudioReader; + /** * @brief Wraps the internal oversampler in a single function that takes an * AudioBuffer and oversamples it in another pre-allocated one. The @@ -41,6 +43,16 @@ public: * @param framesReady an atomic counter for the ready frames. If null no signaling is done. */ void stream(AudioSpan input, AudioSpan output, std::atomic* framesReady = nullptr); + /** + * @brief Stream the oversampling of an input AudioReader into an output + * one, possibly signaling the caller along the way of the number of + * frames that are written. + * + * @param input + * @param output + * @param framesReady an atomic counter for the ready frames. If null no signaling is done. + */ + void stream(AudioReader& input, AudioSpan output, std::atomic* framesReady = nullptr); Oversampler() = delete; Oversampler(const Oversampler&) = delete; From 320fc2cdaa3e54a3de4dee5f82e643ec1ff892a3 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sun, 26 Jul 2020 00:48:43 +0200 Subject: [PATCH 2/2] Add a benchmark --- benchmarks/BM_audioReaders.cpp | 229 +++++++++++++++++++++++++++++++++ benchmarks/CMakeLists.txt | 3 + src/sfizz/AudioReader.cpp | 46 +++++-- src/sfizz/AudioReader.h | 11 ++ 4 files changed, 276 insertions(+), 13 deletions(-) create mode 100644 benchmarks/BM_audioReaders.cpp diff --git a/benchmarks/BM_audioReaders.cpp b/benchmarks/BM_audioReaders.cpp new file mode 100644 index 00000000..874ff8d2 --- /dev/null +++ b/benchmarks/BM_audioReaders.cpp @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "AudioReader.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifndef _WIN32 +#include +#include +#else +#include +#endif + +/// +struct AutoFD { + AutoFD() {} + ~AutoFD() { reset(); } + + AutoFD(const AutoFD&) = delete; + AutoFD &operator=(const AutoFD&) = delete; + + AutoFD(AutoFD&& other) : fd_(other.fd_) { other.fd_ = -1; } + AutoFD &operator=(AutoFD&& other) + { + if (this == &other) return *this; + reset(other.fd_); + other.fd_ = -1; + return *this; + } + + explicit operator bool() const noexcept { return fd_ != -1; } + int get() const noexcept { return fd_; } + + int release() + { + int fd = fd_; + fd_ = -1; + return fd; + } + + void reset(int fd = -1) noexcept + { + if (fd_ == fd) return; + if (fd_ != -1) close(fd_); + fd_ = fd; + } + +private: + int fd_ = -1; +}; + +/// +class AudioReaderFixture : public benchmark::Fixture { +public: + void SetUp(const ::benchmark::State& state) override + { + workBuffer.resize(2 * static_cast(state.range(0))); + } + + void TearDown(const ::benchmark::State& /* state */) override + { + } + + static AutoFD createAudioFile(int format); + + static AutoFD fileWav; + static AutoFD fileFlac; + static AutoFD fileOgg; + + std::vector workBuffer; +}; + +AutoFD AudioReaderFixture::fileWav = createAudioFile(SF_FORMAT_WAV|SF_FORMAT_PCM_16); +AutoFD AudioReaderFixture::fileFlac = createAudioFile(SF_FORMAT_FLAC|SF_FORMAT_PCM_16); +AutoFD AudioReaderFixture::fileOgg = createAudioFile(SF_FORMAT_OGG|SF_FORMAT_VORBIS); + +AutoFD AudioReaderFixture::createAudioFile(int format) +{ + constexpr unsigned sampleRate = 44100; + constexpr unsigned fileDuration = 10; + constexpr unsigned fileFrames = sampleRate * fileDuration; + + // synth 2 channels of arbitrary waveform + std::unique_ptr sndData { new double[2 * fileFrames] }; + double phase = 0.0; + for (unsigned i = 0; i < fileFrames; ++i) { + sndData[2 * i ] = std::sin(2.0 * M_PI * phase); + sndData[2 * i + 1] = std::cos(2.0 * M_PI * phase); + phase += 440.0 * (1.0 / sampleRate); + phase -= static_cast(phase); + } + + // create anonymous temp file + FILE* file = tmpfile(); + if (!file) + throw std::system_error(errno, std::generic_category()); + + // convert FILE to fd, for sndfile + AutoFD fd; + fd.reset(dup(fileno(file))); + if (!fd) { + fclose(file); + throw std::system_error(errno, std::generic_category()); + } + fclose(file); + + // write to fd + SndfileHandle snd(fd.get(), false, SFM_WRITE, format, 2, sampleRate); + if (snd.error()) + throw std::runtime_error("cannot open sound file for writing"); + snd.writef(sndData.get(), fileFrames); + snd = SndfileHandle(); + + return fd; +} + +static void rewindFd(int fd) +{ +#ifndef _WIN32 + off_t off = lseek(fd, 0, SEEK_SET); +#else + off_t off = _lseek(fd, 0, SEEK_SET); +#endif + if (off == -1) + throw std::system_error(errno, std::generic_category()); +} + +static void doReaderBenchmark(int fd, std::vector &buffer, sfz::AudioReaderType type) +{ + rewindFd(fd); + sfz::AudioReaderPtr reader = sfz::createExplicitAudioReaderWithFd(fd, type); + while (reader->readNextBlock(buffer.data(), buffer.size() / 2) > 0); +} + +static void doEntireRead(int fd) +{ + rewindFd(fd); + + SndfileHandle handle(fd, false); + if (handle.error()) + throw std::runtime_error("cannot open sound file for reading"); + + std::vector buffer(static_cast(2 * handle.frames())); + handle.read(buffer.data(), buffer.size()); +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, EntireWav)(benchmark::State& state) +{ + for (auto _ : state) { + doEntireRead(fileWav.get()); + } +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, ForwardWav)(benchmark::State& state) +{ + for (auto _ : state) { + doReaderBenchmark(fileWav.get(), workBuffer, sfz::AudioReaderType::Forward); + } +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, ReverseWav)(benchmark::State& state) +{ + for (auto _ : state) { + doReaderBenchmark(fileWav.get(), workBuffer, sfz::AudioReaderType::Reverse); + } +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, EntireFlac)(benchmark::State& state) +{ + for (auto _ : state) { + doEntireRead(fileFlac.get()); + } +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, ForwardFlac)(benchmark::State& state) +{ + for (auto _ : state) { + doReaderBenchmark(fileFlac.get(), workBuffer, sfz::AudioReaderType::Forward); + } +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, ReverseFlac)(benchmark::State& state) +{ + for (auto _ : state) { + doReaderBenchmark(fileFlac.get(), workBuffer, sfz::AudioReaderType::Reverse); + } +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, EntireOgg)(benchmark::State& state) +{ + for (auto _ : state) { + doEntireRead(fileOgg.get()); + } +} + +BENCHMARK_DEFINE_F(AudioReaderFixture, ForwardOgg)(benchmark::State& state) +{ + for (auto _ : state) { + doReaderBenchmark(fileOgg.get(), workBuffer, sfz::AudioReaderType::Forward); + } +} + +//BENCHMARK_DEFINE_F(AudioReaderFixture, ReverseOgg)(benchmark::State& state) +//{ +// for (auto _ : state) { +// doReaderBenchmark(fileOgg.get(), workBuffer, sfz::AudioReaderType::Reverse); +// } +//} + +BENCHMARK_REGISTER_F(AudioReaderFixture, ForwardWav)->RangeMultiplier(2)->Range((1 << 6), (1 << 10)); +BENCHMARK_REGISTER_F(AudioReaderFixture, ReverseWav)->RangeMultiplier(2)->Range((1 << 6), (1 << 10)); +BENCHMARK_REGISTER_F(AudioReaderFixture, EntireWav)->Range(1, 1); +BENCHMARK_REGISTER_F(AudioReaderFixture, ForwardFlac)->RangeMultiplier(2)->Range((1 << 6), (1 << 10)); +BENCHMARK_REGISTER_F(AudioReaderFixture, ReverseFlac)->RangeMultiplier(2)->Range((1 << 6), (1 << 10)); +BENCHMARK_REGISTER_F(AudioReaderFixture, EntireFlac)->Range(1, 1); +BENCHMARK_REGISTER_F(AudioReaderFixture, ForwardOgg)->RangeMultiplier(2)->Range((1 << 6), (1 << 10)); +//BENCHMARK_REGISTER_F(AudioReaderFixture, ReverseOgg)->RangeMultiplier(2)->Range((1 << 6), (1 << 10)); +BENCHMARK_REGISTER_F(AudioReaderFixture, EntireOgg)->Range(1, 1); +BENCHMARK_MAIN(); diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 3c0082a1..cb38feb6 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -78,6 +78,9 @@ target_link_libraries(bm_wavfile PRIVATE sfizz-sndfile) sfizz_add_benchmark(bm_flacfile BM_flacfile.cpp) target_link_libraries(bm_flacfile PRIVATE sfizz-sndfile) +sfizz_add_benchmark(bm_audioReaders BM_audioReaders.cpp ../src/sfizz/AudioReader.cpp) +target_link_libraries(bm_audioReaders PRIVATE sfizz-sndfile) + sfizz_add_benchmark(bm_readChunk BM_readChunk.cpp) target_link_libraries(bm_readChunk PRIVATE sfizz-sndfile) sfizz_add_benchmark(bm_readChunkFlac BM_readChunkFlac.cpp) diff --git a/src/sfizz/AudioReader.cpp b/src/sfizz/AudioReader.cpp index bd397bc1..9122ee76 100644 --- a/src/sfizz/AudioReader.cpp +++ b/src/sfizz/AudioReader.cpp @@ -301,19 +301,13 @@ static bool formatHasFastSeeking(int format) return fast; } -AudioReaderPtr createAudioReader(const fs::path& path, bool reverse, std::error_code* ec) +static AudioReaderPtr createAudioReaderWithHandle(SndfileHandle handle, bool reverse, std::error_code* ec) { AudioReaderPtr reader; if (ec) ec->clear(); -#if defined(_WIN32) - SndfileHandle handle(path.wstring().c_str()); -#else - SndfileHandle handle(path.c_str()); -#endif - if (!handle) { if (ec) *ec = std::error_code(handle.error(), sndfile_category()); @@ -329,18 +323,28 @@ AudioReaderPtr createAudioReader(const fs::path& path, bool reverse, std::error_ return reader; } -AudioReaderPtr createExplicitAudioReader(const fs::path& path, AudioReaderType type, std::error_code* ec) +AudioReaderPtr createAudioReader(const fs::path& path, bool reverse, std::error_code* ec) { - AudioReaderPtr reader; - - if (ec) - ec->clear(); - #if defined(_WIN32) SndfileHandle handle(path.wstring().c_str()); #else SndfileHandle handle(path.c_str()); #endif + return createAudioReaderWithHandle(handle, reverse, ec); +} + +AudioReaderPtr createAudioReaderWithFd(int fd, bool reverse, std::error_code* ec) +{ + SndfileHandle handle(fd, false); + return createAudioReaderWithHandle(handle, reverse, ec); +} + +static AudioReaderPtr createExplicitAudioReaderWithHandle(SndfileHandle handle, AudioReaderType type, std::error_code* ec) +{ + AudioReaderPtr reader; + + if (ec) + ec->clear(); if (!handle) { if (ec) @@ -364,4 +368,20 @@ AudioReaderPtr createExplicitAudioReader(const fs::path& path, AudioReaderType t return reader; } +AudioReaderPtr createExplicitAudioReader(const fs::path& path, AudioReaderType type, std::error_code* ec) +{ +#if defined(_WIN32) + SndfileHandle handle(path.wstring().c_str()); +#else + SndfileHandle handle(path.c_str()); +#endif + return createExplicitAudioReaderWithHandle(handle, type, ec); +} + +AudioReaderPtr createExplicitAudioReaderWithFd(int fd, AudioReaderType type, std::error_code* ec) +{ + SndfileHandle handle(fd, false); + return createExplicitAudioReaderWithHandle(handle, type, ec); +} + } // namespace sfz diff --git a/src/sfizz/AudioReader.h b/src/sfizz/AudioReader.h index 653abce3..65a199a3 100644 --- a/src/sfizz/AudioReader.h +++ b/src/sfizz/AudioReader.h @@ -9,6 +9,7 @@ #include "ghc/fs_std.hpp" #include #include +#include #if defined(_WIN32) #define ENABLE_SNDFILE_WINDOWS_PROTOTYPES 1 #include @@ -51,9 +52,19 @@ typedef std::unique_ptr AudioReaderPtr; */ AudioReaderPtr createAudioReader(const fs::path& path, bool reverse, std::error_code* ec = nullptr); +/** + * @brief Create a file reader of detected type. + */ +AudioReaderPtr createAudioReaderWithFd(int fd, bool reverse, std::error_code* ec = nullptr); + /** * @brief Create a file reader of explicit type. (for testing purposes) */ AudioReaderPtr createExplicitAudioReader(const fs::path& path, AudioReaderType type, std::error_code* ec = nullptr); +/** + * @brief Create a file reader of explicit type. (for testing purposes) + */ +AudioReaderPtr createExplicitAudioReaderWithFd(int fd, AudioReaderType type, std::error_code* ec = nullptr); + } // namespace sfz