From 49e8852c1c81499530051cebca14ab4c1e70c3d8 Mon Sep 17 00:00:00 2001 From: sakertooth Date: Sun, 27 Aug 2023 17:12:52 -0400 Subject: [PATCH] Add refactored SampleBuffer --- include/SampleBuffer2.h | 83 ++++++++++++++++ src/core/CMakeLists.txt | 1 + src/core/SampleBuffer2.cpp | 199 +++++++++++++++++++++++++++++++++++++ 3 files changed, 283 insertions(+) create mode 100644 include/SampleBuffer2.h create mode 100644 src/core/SampleBuffer2.cpp diff --git a/include/SampleBuffer2.h b/include/SampleBuffer2.h new file mode 100644 index 000000000..f5869f02f --- /dev/null +++ b/include/SampleBuffer2.h @@ -0,0 +1,83 @@ +/* + * SampleBuffer2.h - container-class SampleBuffer2 + * + * Copyright (c) 2005-2014 Tobias Doerffel + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_SAMPLE_BUFFER_H +#define LMMS_SAMPLE_BUFFER_H + +#include +#include +#include +#include +#include + +#include "AudioEngine.h" +#include "Engine.h" +#include "lmms_basics.h" +#include "lmms_export.h" + +namespace lmms { +class LMMS_EXPORT SampleBuffer2 +{ +public: + using value_type = sampleFrame; + using reference = sampleFrame&; + using const_iterator = std::vector::const_iterator; + using const_reverse_iterator = std::vector::const_reverse_iterator; + using difference_type = std::vector::difference_type; + using size_type = std::vector::size_type; + + SampleBuffer2() = default; + SampleBuffer2(const QString& audioFile); + SampleBuffer2(const QByteArray& base64Data, int sampleRate); + SampleBuffer2( + const sampleFrame* data, int numFrames, int sampleRate = Engine::audioEngine()->processingSampleRate()); + + friend void swap(SampleBuffer2& first, SampleBuffer2& second) noexcept; + auto toBase64() const -> QString; + + auto audioFile() const -> QString; + auto sampleRate() const -> sample_rate_t; + + auto begin() const -> const_iterator; + auto end() const -> const_iterator; + auto rbegin() const -> const_reverse_iterator; + auto rend() const -> const_reverse_iterator; + + auto data() const -> const sampleFrame*; + auto size() const -> size_type; + bool empty() const; + +private: + void decodeSampleSF(const QString& fileName); + void decodeSampleDS(const QString& fileName); + +private: + std::vector m_data; + std::optional m_audioFile; + int m_sampleRate = 0; +}; + +} // namespace lmms + +#endif // LMMS_SAMPLE_BUFFER_H diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 319882af2..bbc5e8597 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -66,6 +66,7 @@ set(LMMS_SRCS core/RenderManager.cpp core/RingBuffer.cpp core/SampleBuffer.cpp + core/SampleBuffer2.cpp core/SampleClip.cpp core/SamplePlayHandle.cpp core/SampleRecordHandle.cpp diff --git a/src/core/SampleBuffer2.cpp b/src/core/SampleBuffer2.cpp new file mode 100644 index 000000000..5788b0f5f --- /dev/null +++ b/src/core/SampleBuffer2.cpp @@ -0,0 +1,199 @@ +/* + * SampleBuffer2.cpp - container-class SampleBuffer2 + * + * Copyright (c) 2005-2014 Tobias Doerffel + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "SampleBuffer2.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AudioEngine.h" +#include "DrumSynth.h" +#include "Engine.h" +#include "PathUtil.h" + +namespace lmms { + +SampleBuffer2::SampleBuffer2(const sampleFrame* data, int numFrames, int sampleRate) + : m_data(data, data + numFrames) + , m_sampleRate(sampleRate) +{ +} + +SampleBuffer2::SampleBuffer2(const QString& audioFile) +{ + if (audioFile.isEmpty()) { throw std::runtime_error{"Failure loading audio file: Audio file path is empty."}; } + + auto resolvedFileName = PathUtil::toAbsolute(PathUtil::toShortestRelative(audioFile)); + QFileInfo{resolvedFileName}.suffix() == "ds" ? decodeSampleDS(resolvedFileName) : decodeSampleSF(resolvedFileName); +} + +SampleBuffer2::SampleBuffer2(const QByteArray& base64Data, int sampleRate) + : m_data(reinterpret_cast(base64Data.data()), + reinterpret_cast(base64Data.data()) + base64Data.size() / sizeof(sampleFrame)) + , m_sampleRate(sampleRate) +{ +} + +void swap(SampleBuffer2& first, SampleBuffer2& second) noexcept +{ + using std::swap; + swap(first.m_data, second.m_data); + swap(first.m_audioFile, second.m_audioFile); + swap(first.m_sampleRate, second.m_sampleRate); +} + +void SampleBuffer2::decodeSampleSF(const QString& audioFile) +{ + SNDFILE* sndFile = nullptr; + auto sfInfo = SF_INFO{}; + + // Use QFile to handle unicode file names on Windows + auto file = QFile{audioFile}; + if (!file.open(QIODevice::ReadOnly)) + { + throw std::runtime_error{ + "Failed to open sample " + audioFile.toStdString() + ": " + file.errorString().toStdString()}; + } + + sndFile = sf_open_fd(file.handle(), SFM_READ, &sfInfo, false); + if (sf_error(sndFile) != 0) + { + throw std::runtime_error{"Failure opening audio handle: " + std::string{sf_strerror(sndFile)}}; + } + + auto buf = std::vector(sfInfo.channels * sfInfo.frames); + sf_read_float(sndFile, buf.data(), buf.size()); + + sf_close(sndFile); + file.close(); + + auto result = std::vector(sfInfo.frames); + for (int i = 0; i < static_cast(result.size()); ++i) + { + if (sfInfo.channels == 1) + { + // Upmix from mono to stereo + result[i] = {buf[i], buf[i]}; + } + else if (sfInfo.channels > 1) + { + // TODO: Add support for higher number of channels (i.e., 5.1 channel systems) + // The current behavior assumes stereo in all cases excluding mono. + // This may not be the expected behavior, given some audio files with a higher number of channels. + result[i] = {buf[i * sfInfo.channels], buf[i * sfInfo.channels + 1]}; + } + } + + m_data = result; + m_audioFile = audioFile; + m_sampleRate = sfInfo.samplerate; +} + +void SampleBuffer2::decodeSampleDS(const QString& audioFile) +{ + auto data = std::unique_ptr{}; + int_sample_t* dataPtr = nullptr; + + auto ds = DrumSynth{}; + const auto engineRate = Engine::audioEngine()->processingSampleRate(); + const auto frames = ds.GetDSFileSamples(audioFile, dataPtr, DEFAULT_CHANNELS, engineRate); + data.reset(dataPtr); + + auto result = std::vector(frames); + if (frames > 0 && data != nullptr) + { + src_short_to_float_array(data.get(), &result[0][0], frames * DEFAULT_CHANNELS); + } + else { throw std::runtime_error{"Decoding failure: failed to decode DrumSynth file."}; } + + m_data = result; + m_audioFile = audioFile; + m_sampleRate = engineRate; +} + +QString SampleBuffer2::toBase64() const +{ + // TODO: Replace with non-Qt equivalent + const auto data = reinterpret_cast(m_data.data()); + const auto size = static_cast(m_data.size() * sizeof(sampleFrame)); + const auto byteArray = QByteArray{data, size}; + return byteArray.toBase64(); +} + +auto SampleBuffer2::audioFile() const -> QString +{ + return m_audioFile.value_or(""); +} + +auto SampleBuffer2::sampleRate() const -> sample_rate_t +{ + return m_sampleRate; +} + +auto SampleBuffer2::begin() const -> const_iterator +{ + return m_data.begin(); +} + +auto SampleBuffer2::end() const -> const_iterator +{ + return m_data.end(); +} + +auto SampleBuffer2::rbegin() const -> const_reverse_iterator +{ + return m_data.rbegin(); +} + +auto SampleBuffer2::rend() const -> const_reverse_iterator +{ + return m_data.rend(); +} + +auto SampleBuffer2::data() const -> const sampleFrame* +{ + return m_data.data(); +} + +auto SampleBuffer2::size() const -> size_type +{ + return m_data.size(); +} + +auto SampleBuffer2::empty() const -> bool +{ + return m_data.empty(); +} + +} // namespace lmms \ No newline at end of file