Skip to content
Draft
2 changes: 1 addition & 1 deletion include/AudioSampleRecorder.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class AudioSampleRecorder : public AudioDevice
~AudioSampleRecorder() override;

f_cnt_t framesRecorded() const;
std::shared_ptr<const SampleBuffer> createSampleBuffer();
SampleBuffer createSampleBuffer();

private:
void writeBuffer(const SampleFrame* _ab, const fpp_t _frames) override;
Expand Down
6 changes: 2 additions & 4 deletions include/EnvelopeAndLfoParameters.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@
#ifndef LMMS_ENVELOPE_AND_LFO_PARAMETERS_H
#define LMMS_ENVELOPE_AND_LFO_PARAMETERS_H

#include <memory>

#include "JournallingObject.h"
#include "AutomatableModel.h"
#include "SampleBuffer.h"
Expand Down Expand Up @@ -145,7 +143,7 @@ class LMMS_EXPORT EnvelopeAndLfoParameters : public Model, public JournallingObj
const TempoSyncKnobModel& getLfoSpeedModel() const { return m_lfoSpeedModel; }
const BoolModel& getX100Model() const { return m_x100Model; }
const IntModel& getLfoWaveModel() const { return m_lfoWaveModel; }
std::shared_ptr<const SampleBuffer> getLfoUserWave() const { return m_userWave; }
const SampleBuffer& getLfoUserWave() const { return m_userWave; }

public slots:
void updateSampleVars();
Expand Down Expand Up @@ -200,7 +198,7 @@ public slots:
sample_t * m_lfoShapeData;
sample_t m_random;
bool m_bad_lfoShapeData;
std::shared_ptr<const SampleBuffer> m_userWave = SampleBuffer::emptyBuffer();
SampleBuffer m_userWave;

constexpr static auto NumLfoShapes = static_cast<std::size_t>(LfoShape::Count);

Expand Down
2 changes: 1 addition & 1 deletion include/LfoController.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public slots:

private:
float m_heldSample;
std::shared_ptr<const SampleBuffer> m_userDefSampleBuffer = SampleBuffer::emptyBuffer();
SampleBuffer m_userDefSampleBuffer;

protected slots:
void updatePhase();
Expand Down
8 changes: 4 additions & 4 deletions include/Oscillator.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class LMMS_EXPORT Oscillator
m_useWaveTable = n;
}

void setUserWave(std::shared_ptr<const SampleBuffer> _wave)
void setUserWave(const SampleBuffer* _wave)
{
m_userWave = _wave;
}
Expand Down Expand Up @@ -167,8 +167,8 @@ class LMMS_EXPORT Oscillator

static sample_t userWaveSample(const SampleBuffer* buffer, const float sample)
{
if (buffer == nullptr || buffer->size() == 0) { return 0; }
const auto frames = buffer->size();
if (buffer == nullptr || buffer->frames() == 0) { return 0; }
const auto frames = buffer->frames();
const auto frame = absFraction(sample) * frames;
const auto f1 = static_cast<f_cnt_t>(frame);

Expand Down Expand Up @@ -250,7 +250,7 @@ class LMMS_EXPORT Oscillator
Oscillator * m_subOsc;
float m_phaseOffset;
float m_phase;
std::shared_ptr<const SampleBuffer> m_userWave = SampleBuffer::emptyBuffer();
const SampleBuffer* m_userWave;
std::shared_ptr<const OscillatorConstants::waveform_t> m_userAntiAliasWaveTable;
bool m_useWaveTable;
// There are many update*() variants; the modulator flag is stored as a member variable to avoid
Expand Down
20 changes: 9 additions & 11 deletions include/Sample.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
#ifndef LMMS_SAMPLE_H
#define LMMS_SAMPLE_H

#include <memory>

#include "AudioEngine.h"
#include "AudioResampler.h"
#include "Note.h"
#include "SampleBuffer.h"
Expand Down Expand Up @@ -68,11 +68,9 @@ class LMMS_EXPORT Sample
};

Sample() = default;

Sample(const SampleFrame* data, size_t numFrames, int sampleRate = Engine::audioEngine()->outputSampleRate());
Sample(const Sample& other);
Sample(Sample&& other) noexcept;
explicit Sample(std::shared_ptr<const SampleBuffer> buffer);
explicit Sample(SampleBuffer buffer);

auto operator=(const Sample&) -> Sample&;
auto operator=(Sample&&) noexcept -> Sample&;
Expand All @@ -81,14 +79,14 @@ class LMMS_EXPORT Sample
double ratio = 1.0) const -> bool;

auto sampleDuration() const -> std::chrono::milliseconds;
auto sampleFile() const -> const QString& { return m_buffer->audioFile(); }
auto sampleRate() const -> int { return m_buffer->sampleRate(); }
auto sampleSize() const -> size_t { return m_buffer->size(); }
auto sampleFile() const -> const QString& { return m_buffer.path(); }
auto sampleRate() const -> int { return m_buffer.sampleRate(); }
auto sampleSize() const -> size_t { return m_buffer.frames(); }

auto toBase64() const -> QString { return m_buffer->toBase64(); }
auto toBase64() const -> QString { return m_buffer.toBase64(); }

auto data() const -> const SampleFrame* { return m_buffer->data(); }
auto buffer() const -> std::shared_ptr<const SampleBuffer> { return m_buffer; }
auto data() const -> const SampleFrame* { return m_buffer.data(); }
auto buffer() const -> const SampleBuffer& { return m_buffer; }
auto startFrame() const -> int { return m_startFrame.load(std::memory_order_relaxed); }
auto endFrame() const -> int { return m_endFrame.load(std::memory_order_relaxed); }
auto loopStartFrame() const -> int { return m_loopStartFrame.load(std::memory_order_relaxed); }
Expand All @@ -108,7 +106,7 @@ class LMMS_EXPORT Sample

private:
f_cnt_t render(SampleFrame* dst, f_cnt_t size, PlaybackState* state, Loop loop) const;
std::shared_ptr<const SampleBuffer> m_buffer = SampleBuffer::emptyBuffer();
SampleBuffer m_buffer;
std::atomic<int> m_startFrame = 0;
std::atomic<int> m_endFrame = 0;
std::atomic<int> m_loopStartFrame = 0;
Expand Down
103 changes: 60 additions & 43 deletions include/SampleBuffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,70 +27,87 @@

#include <QString>
#include <memory>
#include <optional>
#include <vector>

#include "AudioEngine.h"
#include "Engine.h"
#include "LmmsTypes.h"
#include "SampleFrame.h"
#include "lmms_export.h"

namespace lmms {
/**
* @class SampleBuffer
*
* @brief A thread-safe, reference-counted container of immutable PCM audio data.
*
* SampleBuffer is designed for playback of audio files or Base64 assets possibly across multiple threads.
* It enforces shared ownership: copying a SampleBuffer increments a ref count for the shared audio data rather than
* deep copying it.
*
* @warning This class is immutable. If you need a buffer for real-time
* effect processing or mixing, use @ref AudioBuffer instead.
*
* @note Populated via static factory methods @ref fromFile() and @ref fromBase64().
*/
class LMMS_EXPORT SampleBuffer
{
public:
using value_type = SampleFrame;
using reference = SampleFrame&;
using const_reference = const SampleFrame&;
using iterator = std::vector<SampleFrame>::iterator;
using const_iterator = std::vector<SampleFrame>::const_iterator;
using difference_type = std::vector<SampleFrame>::difference_type;
using size_type = std::vector<SampleFrame>::size_type;
using reverse_iterator = std::vector<SampleFrame>::reverse_iterator;
using const_reverse_iterator = std::vector<SampleFrame>::const_reverse_iterator;

/** @brief Creates an empty SampleBuffer. */
SampleBuffer() = default;
SampleBuffer(std::vector<SampleFrame> data, int sampleRate, const QString& audioFile = "");
SampleBuffer(
const SampleFrame* data, size_t numFrames, int sampleRate = Engine::audioEngine()->outputSampleRate());

friend void swap(SampleBuffer& first, SampleBuffer& second) noexcept;
auto toBase64() const -> QString;

auto audioFile() const -> const QString& { return m_audioFile; }
auto sampleRate() const -> sample_rate_t { return m_sampleRate; }
/**
* @brief Constructs a buffer with explicit data.
* @param data A vector of SampleFrame containing the audio data.
* @param sampleRate The recording rate of the source (e.g., 44100).
* @param path The filesystem path the data originated from.
*/
SampleBuffer(std::vector<SampleFrame> data, sample_rate_t sampleRate, const QString& path = "");

auto begin() -> iterator { return m_data.begin(); }
auto end() -> iterator { return m_data.end(); }
/** @returns a const reference to the audio frame at the given frame index. */
auto operator[](f_cnt_t index) const -> const SampleFrame& { return m_data[index]; }

auto begin() const -> const_iterator { return m_data.begin(); }
auto end() const -> const_iterator { return m_data.end(); }

auto cbegin() const -> const_iterator { return m_data.cbegin(); }
auto cend() const -> const_iterator { return m_data.cend(); }
/** @brief Serializes the raw PCM data into a Base64 string for project saving.
@todo This function should be removed once Base64 is migrated to audio files on disk.
*/
auto toBase64() const -> QString;

auto rbegin() -> reverse_iterator { return m_data.rbegin(); }
auto rend() -> reverse_iterator { return m_data.rend(); }
/** @returns true if the buffer contains no audio data. */
auto empty() const -> bool { return m_frames == 0; }

auto rbegin() const -> const_reverse_iterator { return m_data.rbegin(); }
auto rend() const -> const_reverse_iterator { return m_data.rend(); }
/** @returns direct access to the raw audio data. */
auto data() const -> const SampleFrame* { return m_data.get(); }

auto crbegin() const -> const_reverse_iterator { return m_data.crbegin(); }
auto crend() const -> const_reverse_iterator { return m_data.crend(); }
/** @returns the file path associated with this buffer, if any. */
auto path() const -> const QString& { return m_path; }

auto data() const -> const SampleFrame* { return m_data.data(); }
auto size() const -> size_type { return m_data.size(); }
auto empty() const -> bool { return m_data.empty(); }
/** @returns the total number of audio frames. */
auto frames() const -> f_cnt_t { return m_frames; }

static auto emptyBuffer() -> std::shared_ptr<const SampleBuffer>;
/** @returns the original sample rate of the data. */
auto sampleRate() const -> sample_rate_t { return m_sampleRate; }

static std::shared_ptr<const SampleBuffer> fromFile(const QString& path);
static std::shared_ptr<const SampleBuffer> fromBase64(
const QString& str, int sampleRate = Engine::audioEngine()->outputSampleRate());
/**
* @brief Factory method to load and decode an audio file into a SampleBuffer.
* @param path The absolute path to the audio file (WAV, FLAC, OGG, etc.).
* @return std::optional containing the buffer on success, or std::nullopt on failure.
*/
static std::optional<SampleBuffer> fromFile(const QString& path);

/**
* @brief Factory method to reconstruct a buffer from a Base64 string.
* @param str The Base64 encoded audio data.
* @param sampleRate The rate at which the encoded data should be interpreted.
* @return std::optional containing the buffer on success.
* @todo This function should be removed once Base64 is migrated to audio files on disk.
*/
static std::optional<SampleBuffer> fromBase64(const QString& str, sample_rate_t sampleRate);

private:
std::vector<SampleFrame> m_data;
QString m_audioFile;
sample_rate_t m_sampleRate = Engine::audioEngine()->outputSampleRate();
f_cnt_t m_frames = 0;
sample_rate_t m_sampleRate = 0;
QString m_path;
inline static const auto emptyBuffer = std::make_shared<SampleFrame[]>(0);
std::shared_ptr<SampleFrame[]> m_data = emptyBuffer;
Comment on lines +109 to +110
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is intended to be thread-safe, shouldn't these be using atomic shared pointers to prevent a data race on the reference count?

};

} // namespace lmms
Expand Down
2 changes: 1 addition & 1 deletion include/SampleClip.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class SampleClip : public Clip

bool isPlaying() const;
void setIsPlaying(bool isPlaying);
void setSampleBuffer(std::shared_ptr<const SampleBuffer> sb);
void setSampleBuffer(SampleBuffer buffer);

SampleClip* clone() override
{
Expand Down
2 changes: 1 addition & 1 deletion include/SampleDecoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class SampleDecoder
struct Result
{
std::vector<SampleFrame> data;
int sampleRate;
sample_rate_t sampleRate;
};

struct AudioType
Expand Down
2 changes: 1 addition & 1 deletion include/SampleRecordHandle.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class SampleRecordHandle : public PlayHandle
bool isFromTrack( const Track * _track ) const override;

f_cnt_t framesRecorded() const;
std::shared_ptr<const SampleBuffer> createSampleBuffer();
SampleBuffer createSampleBuffer();


private:
Expand Down
4 changes: 2 additions & 2 deletions include/SampleThumbnail.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class LMMS_EXPORT SampleThumbnail
};

SampleThumbnail() = default;
SampleThumbnail(const Sample& sample);
SampleThumbnail(const SampleBuffer& buffer);
void visualize(VisualizeParameters parameters, QPainter& painter) const;

private:
Expand Down Expand Up @@ -137,7 +137,7 @@ class LMMS_EXPORT SampleThumbnail

using ThumbnailCache = std::vector<Thumbnail>;
std::shared_ptr<ThumbnailCache> m_thumbnailCache = std::make_shared<ThumbnailCache>();
std::shared_ptr<const SampleBuffer> m_buffer = SampleBuffer::emptyBuffer();
SampleBuffer m_buffer;
inline static std::unordered_map<SampleThumbnailEntry, std::shared_ptr<ThumbnailCache>, Hash> s_sampleThumbnailCacheMap;
};

Expand Down
16 changes: 11 additions & 5 deletions plugins/AudioFileProcessor/AudioFileProcessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,10 @@ void AudioFileProcessor::loadSettings(const QDomElement& elem)
}
else if (auto sampleData = elem.attribute("sampledata"); !sampleData.isEmpty())
{
m_sample = Sample(SampleBuffer::fromBase64(sampleData));
if (auto buffer = SampleBuffer::fromBase64(sampleData, Engine::audioEngine()->outputSampleRate()))
{
m_sample = Sample{std::move(buffer.value())};
}
}

m_loopModel.loadSettings(elem, "looped");
Expand Down Expand Up @@ -318,10 +321,13 @@ void AudioFileProcessor::setAudioFile(const QString& _audio_file, bool _rename)
}
// else we don't touch the track-name, because the user named it self

m_sample = Sample(SampleBuffer::fromFile(_audio_file));
loopPointChanged();
reverseModelChanged();
emit sampleUpdated();
if (auto buffer = SampleBuffer::fromFile(_audio_file))
{
m_sample = Sample(std::move(buffer.value()));
loopPointChanged();
reverseModelChanged();
emit sampleUpdated();
}
}


Expand Down
6 changes: 2 additions & 4 deletions plugins/Patman/Patman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ PatmanInstrument::LoadError PatmanInstrument::loadPatch(
}
}

auto data = new SampleFrame[frames];
auto data = std::vector<SampleFrame>(frames);

for( f_cnt_t frame = 0; frame < frames; ++frame )
{
Expand All @@ -353,7 +353,7 @@ PatmanInstrument::LoadError PatmanInstrument::loadPatch(
}
}

auto psample = std::make_shared<Sample>(data, frames, sample_rate);
auto psample = std::make_shared<Sample>(SampleBuffer{std::move(data), sample_rate});
psample->setFrequency(root_freq / 1000.0f);

if( modes & MODES_LOOPING )
Expand All @@ -363,8 +363,6 @@ PatmanInstrument::LoadError PatmanInstrument::loadPatch(
}

m_patchSamples.push_back(psample);

delete[] data;
}
fclose( fd );
return( LoadError::OK );
Expand Down
14 changes: 9 additions & 5 deletions plugins/SlicerT/SlicerT.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ std::vector<Note> SlicerT::getMidi()

void SlicerT::updateFile(QString file)
{
if (auto buffer = SampleBuffer::fromFile(file)) { m_originalSample = Sample(std::move(buffer)); }
if (auto buffer = SampleBuffer::fromFile(file)) { m_originalSample = Sample(std::move(buffer.value())); }

findBPM();
findSlices();
Expand Down Expand Up @@ -351,8 +351,10 @@ void SlicerT::loadSettings(const QDomElement& element)
{
if (QFileInfo(PathUtil::toAbsolute(srcFile)).exists())
{
auto buffer = SampleBuffer::fromFile(srcFile);
m_originalSample = Sample(std::move(buffer));
if (auto buffer = SampleBuffer::fromFile(srcFile))
{
m_originalSample = Sample(std::move(buffer.value()));
}
}
else
{
Expand All @@ -362,8 +364,10 @@ void SlicerT::loadSettings(const QDomElement& element)
}
else if (auto sampleData = element.attribute("sampledata"); !sampleData.isEmpty())
{
auto buffer = SampleBuffer::fromBase64(sampleData);
m_originalSample = Sample(std::move(buffer));
if (auto buffer = SampleBuffer::fromBase64(sampleData, Engine::audioEngine()->outputSampleRate()))
{
m_originalSample = Sample{std::move(buffer.value())};
}
}

if (!element.attribute("totalSlices").isEmpty())
Expand Down
Loading