diff --git a/include/AudioResampler.h b/include/AudioResampler.h new file mode 100644 index 00000000000..379146962a8 --- /dev/null +++ b/include/AudioResampler.h @@ -0,0 +1,64 @@ +/* + * AudioResampler.h - wrapper around libsamplerate + * + * Copyright (c) 2023 saker + * + * 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_AUDIO_RESAMPLER_H +#define LMMS_AUDIO_RESAMPLER_H + +#include + +#include "lmms_export.h" + +namespace lmms { + +class LMMS_EXPORT AudioResampler +{ +public: + struct ProcessResult + { + int error; + long inputFramesUsed; + long outputFramesGenerated; + }; + + AudioResampler(int interpolationMode, int channels); + AudioResampler(const AudioResampler&) = delete; + AudioResampler(AudioResampler&&) = delete; + ~AudioResampler(); + + AudioResampler& operator=(const AudioResampler&) = delete; + AudioResampler& operator=(AudioResampler&&) = delete; + + auto resample(const float* in, long inputFrames, float* out, long outputFrames, double ratio) -> ProcessResult; + auto interpolationMode() const -> int { return m_interpolationMode; } + auto channels() const -> int { return m_channels; } + +private: + int m_interpolationMode = -1; + int m_channels = 0; + int m_error = 0; + SRC_STATE* m_state = nullptr; +}; +} // namespace lmms + +#endif // LMMS_AUDIO_RESAMPLER_H diff --git a/include/AudioSampleRecorder.h b/include/AudioSampleRecorder.h index 8937ceb5efd..d481cc16cf4 100644 --- a/include/AudioSampleRecorder.h +++ b/include/AudioSampleRecorder.h @@ -28,6 +28,7 @@ #include #include +#include #include "AudioDevice.h" @@ -44,8 +45,7 @@ class AudioSampleRecorder : public AudioDevice ~AudioSampleRecorder() override; f_cnt_t framesRecorded() const; - void createSampleBuffer( SampleBuffer** sampleBuffer ); - + std::shared_ptr createSampleBuffer(); private: void writeBuffer( const surroundSampleFrame * _ab, diff --git a/include/EnvelopeAndLfoParameters.h b/include/EnvelopeAndLfoParameters.h index 7abc3910e96..2a8d3a685e6 100644 --- a/include/EnvelopeAndLfoParameters.h +++ b/include/EnvelopeAndLfoParameters.h @@ -25,6 +25,7 @@ #ifndef LMMS_ENVELOPE_AND_LFO_PARAMETERS_H #define LMMS_ENVELOPE_AND_LFO_PARAMETERS_H +#include #include #include "JournallingObject.h" @@ -167,7 +168,7 @@ public slots: sample_t * m_lfoShapeData; sample_t m_random; bool m_bad_lfoShapeData; - SampleBuffer m_userWave; + std::shared_ptr m_userWave = SampleBuffer::emptyBuffer(); enum class LfoShape { diff --git a/include/LfoController.h b/include/LfoController.h index 109edbd3fd8..01b4b1862f7 100644 --- a/include/LfoController.h +++ b/include/LfoController.h @@ -87,7 +87,7 @@ public slots: private: float m_heldSample; - SampleBuffer * m_userDefSampleBuffer; + std::shared_ptr m_userDefSampleBuffer = SampleBuffer::emptyBuffer(); protected slots: void updatePhase(); diff --git a/include/Oscillator.h b/include/Oscillator.h index dab0b948d08..a480bf52424 100644 --- a/include/Oscillator.h +++ b/include/Oscillator.h @@ -28,7 +28,9 @@ #include #include +#include #include +#include "interpolation.h" #include "Engine.h" #include "lmms_constants.h" @@ -46,7 +48,6 @@ class IntModel; class LMMS_EXPORT Oscillator { - MM_OPERATORS public: enum class WaveShape { @@ -91,18 +92,23 @@ class LMMS_EXPORT Oscillator static void waveTableInit(); static void destroyFFTPlans(); - static void generateAntiAliasUserWaveTable(SampleBuffer* sampleBuffer); + static std::unique_ptr generateAntiAliasUserWaveTable(const SampleBuffer* sampleBuffer); inline void setUseWaveTable(bool n) { m_useWaveTable = n; } - inline void setUserWave( const SampleBuffer * _wave ) + void setUserWave(std::shared_ptr _wave) { m_userWave = _wave; } + void setUserAntiAliasWaveTable(std::shared_ptr waveform) + { + m_userAntiAliasWaveTable = waveform; + } + void update(sampleFrame* ab, const fpp_t frames, const ch_cnt_t chnl, bool modulator = false); // now follow the wave-shape-routines... @@ -164,9 +170,18 @@ class LMMS_EXPORT Oscillator return 1.0f - fast_rand() * 2.0f / FAST_RAND_MAX; } - inline sample_t userWaveSample( const float _sample ) const + static sample_t userWaveSample(const SampleBuffer* buffer, const float sample) { - return m_userWave->userWaveSample( _sample ); + if (buffer == nullptr || buffer->size() == 0) { return 0; } + const auto frames = buffer->size(); + const auto frame = sample * frames; + auto f1 = static_cast(frame) % frames; + if (f1 < 0) + { + f1 += frames; + } + + return linearInterpolate(buffer->data()[f1][0], buffer->data()[(f1 + 1) % frames][0], fraction(frame)); } struct wtSampleControl { @@ -203,7 +218,7 @@ class LMMS_EXPORT Oscillator table[control.band][control.f2], fraction(control.frame)); } - inline sample_t wtSample(const std::unique_ptr& table, const float sample) const + sample_t wtSample(const OscillatorConstants::waveform_t* table, const float sample) const { assert(table != nullptr); wtSampleControl control = getWtSampleControl(sample); @@ -247,7 +262,8 @@ class LMMS_EXPORT Oscillator Oscillator * m_subOsc; float m_phaseOffset; float m_phase; - const SampleBuffer * m_userWave; + std::shared_ptr m_userWave = SampleBuffer::emptyBuffer(); + std::shared_ptr m_userAntiAliasWaveTable; bool m_useWaveTable; // There are many update*() variants; the modulator flag is stored as a member variable to avoid // adding more explicit parameters to all of them. Can be converted to a parameter if needed. diff --git a/include/Sample.h b/include/Sample.h new file mode 100644 index 00000000000..2ccb78b19a0 --- /dev/null +++ b/include/Sample.h @@ -0,0 +1,139 @@ +/* + * Sample.h - State for container-class SampleBuffer + * + * Copyright (c) 2023 saker + * + * 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_H +#define LMMS_SAMPLE_H + +#include +#include + +#include "AudioResampler.h" +#include "Note.h" +#include "SampleBuffer.h" +#include "lmms_export.h" + +class QPainter; +class QRect; + +namespace lmms { +class LMMS_EXPORT Sample +{ +public: + // values for buffer margins, used for various libsamplerate interpolation modes + // the array positions correspond to the converter_type parameter values in libsamplerate + // if there appears problems with playback on some interpolation mode, then the value for that mode + // may need to be higher - conversely, to optimize, some may work with lower values + static constexpr auto s_interpolationMargins = std::array{64, 64, 64, 4, 4}; + + enum class Loop + { + Off, + On, + PingPong + }; + + class LMMS_EXPORT PlaybackState + { + public: + PlaybackState(bool varyingPitch = false, int interpolationMode = SRC_LINEAR) + : m_resampler(interpolationMode, DEFAULT_CHANNELS) + , m_varyingPitch(varyingPitch) + { + } + + auto resampler() -> AudioResampler& { return m_resampler; } + auto frameIndex() const -> f_cnt_t { return m_frameIndex; } + auto varyingPitch() const -> bool { return m_varyingPitch; } + auto backwards() const -> bool { return m_backwards; } + + void setFrameIndex(f_cnt_t frameIndex) { m_frameIndex = frameIndex; } + void setVaryingPitch(bool varyingPitch) { m_varyingPitch = varyingPitch; } + void setBackwards(bool backwards) { m_backwards = backwards; } + + private: + AudioResampler m_resampler; + f_cnt_t m_frameIndex = 0; + bool m_varyingPitch = false; + bool m_backwards = false; + friend class Sample; + }; + + Sample() = default; + Sample(const QByteArray& base64, int sampleRate = Engine::audioEngine()->processingSampleRate()); + Sample(const sampleFrame* data, int numFrames, int sampleRate = Engine::audioEngine()->processingSampleRate()); + Sample(const Sample& other); + Sample(Sample&& other); + explicit Sample(const QString& audioFile); + explicit Sample(std::shared_ptr buffer); + + auto operator=(const Sample&) -> Sample&; + auto operator=(Sample&&) -> Sample&; + + auto play(sampleFrame* dst, PlaybackState* state, int numFrames, float desiredFrequency = DefaultBaseFreq, + Loop loopMode = Loop::Off) -> 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 -> int { return m_buffer->size(); } + + auto toBase64() const -> QString { return m_buffer->toBase64(); } + + auto data() const -> const sampleFrame* { return m_buffer->data(); } + auto buffer() const -> std::shared_ptr { 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); } + auto loopEndFrame() const -> int { return m_loopEndFrame.load(std::memory_order_relaxed); } + auto amplification() const -> float { return m_amplification.load(std::memory_order_relaxed); } + auto frequency() const -> float { return m_frequency.load(std::memory_order_relaxed); } + auto reversed() const -> bool { return m_reversed.load(std::memory_order_relaxed); } + + void setStartFrame(int startFrame) { m_startFrame.store(startFrame, std::memory_order_relaxed); } + void setEndFrame(int endFrame) { m_endFrame.store(endFrame, std::memory_order_relaxed); } + void setLoopStartFrame(int loopStartFrame) { m_loopStartFrame.store(loopStartFrame, std::memory_order_relaxed); } + void setLoopEndFrame(int loopEndFrame) { m_loopEndFrame.store(loopEndFrame, std::memory_order_relaxed); } + void setAllPointFrames(int startFrame, int endFrame, int loopStartFrame, int loopEndFrame); + void setAmplification(float amplification) { m_amplification.store(amplification, std::memory_order_relaxed); } + void setFrequency(float frequency) { m_frequency.store(frequency, std::memory_order_relaxed); } + void setReversed(bool reversed) { m_reversed.store(reversed, std::memory_order_relaxed); } + +private: + void playSampleRange(PlaybackState* state, sampleFrame* dst, size_t numFrames) const; + void amplifySampleRange(sampleFrame* src, int numFrames) const; + void copyBufferForward(sampleFrame* dst, int initialPosition, int advanceAmount) const; + void copyBufferBackward(sampleFrame* dst, int initialPosition, int advanceAmount) const; + +private: + std::shared_ptr m_buffer = SampleBuffer::emptyBuffer(); + std::atomic m_startFrame = 0; + std::atomic m_endFrame = 0; + std::atomic m_loopStartFrame = 0; + std::atomic m_loopEndFrame = 0; + std::atomic m_amplification = 1.0f; + std::atomic m_frequency = DefaultBaseFreq; + std::atomic m_reversed = false; +}; +} // namespace lmms +#endif diff --git a/include/SampleBuffer.h b/include/SampleBuffer.h index 3d1013baa01..0db8aa4d35c 100644 --- a/include/SampleBuffer.h +++ b/include/SampleBuffer.h @@ -25,333 +25,74 @@ #ifndef LMMS_SAMPLE_BUFFER_H #define LMMS_SAMPLE_BUFFER_H +#include +#include #include -#include -#include - +#include #include +#include -#include "lmms_export.h" -#include "interpolation.h" +#include "AudioEngine.h" +#include "Engine.h" #include "lmms_basics.h" -#include "lmms_math.h" -#include "shared_object.h" -#include "OscillatorConstants.h" -#include "MemoryManager.h" - - -class QPainter; -class QRect; - -namespace lmms -{ - -// values for buffer margins, used for various libsamplerate interpolation modes -// the array positions correspond to the converter_type parameter values in libsamplerate -// if there appears problems with playback on some interpolation mode, then the value for that mode -// may need to be higher - conversely, to optimize, some may work with lower values -const f_cnt_t MARGIN[] = { 64, 64, 64, 4, 4 }; +#include "lmms_export.h" -class LMMS_EXPORT SampleBuffer : public QObject, public sharedObject +namespace lmms { +class LMMS_EXPORT SampleBuffer { - Q_OBJECT - MM_OPERATORS public: - enum class LoopMode { - Off = 0, - On, - PingPong - }; - class LMMS_EXPORT handleState - { - MM_OPERATORS - public: - handleState(bool varyingPitch = false, int interpolationMode = SRC_LINEAR); - virtual ~handleState(); - - const f_cnt_t frameIndex() const - { - return m_frameIndex; - } - - void setFrameIndex(f_cnt_t index) - { - m_frameIndex = index; - } - - bool isBackwards() const - { - return m_isBackwards; - } - - void setBackwards(bool backwards) - { - m_isBackwards = backwards; - } - - int interpolationMode() const - { - return m_interpolationMode; - } - - - private: - f_cnt_t m_frameIndex; - const bool m_varyingPitch; - bool m_isBackwards; - SRC_STATE * m_resamplingData; - int m_interpolationMode; - - friend class SampleBuffer; - - } ; - - - SampleBuffer(); - // constructor which either loads sample _audio_file or decodes - // base64-data out of string - SampleBuffer(const QString & audioFile, bool isBase64Data = false); - SampleBuffer(const sampleFrame * data, const f_cnt_t frames); - explicit SampleBuffer(const f_cnt_t frames); - SampleBuffer(const SampleBuffer & orig); - - friend void swap(SampleBuffer & first, SampleBuffer & second) noexcept; - SampleBuffer& operator= (const SampleBuffer that); - - ~SampleBuffer() override; - - bool play( - sampleFrame * ab, - handleState * state, - const fpp_t frames, - const float freq, - const LoopMode loopMode = LoopMode::Off - ); - - void visualize( - QPainter & p, - const QRect & dr, - const QRect & clip, - f_cnt_t fromFrame = 0, - f_cnt_t toFrame = 0 - ); - inline void visualize( - QPainter & p, - const QRect & dr, - f_cnt_t fromFrame = 0, - f_cnt_t toFrame = 0 - ) - { - visualize(p, dr, dr, fromFrame, toFrame); - } - - inline const QString & audioFile() const - { - return m_audioFile; - } - - inline f_cnt_t startFrame() const - { - return m_startFrame; - } + using value_type = sampleFrame; + using reference = sampleFrame&; + using const_reference = const sampleFrame&; + using iterator = std::vector::iterator; + using const_iterator = std::vector::const_iterator; + using difference_type = std::vector::difference_type; + using size_type = std::vector::size_type; + using reverse_iterator = std::vector::reverse_iterator; + using const_reverse_iterator = std::vector::const_reverse_iterator; - inline f_cnt_t endFrame() const - { - return m_endFrame; - } + SampleBuffer() = default; + explicit SampleBuffer(const QString& audioFile); + SampleBuffer(const QString& base64, int sampleRate); + SampleBuffer(std::vector data, int sampleRate); + SampleBuffer( + const sampleFrame* data, int numFrames, int sampleRate = Engine::audioEngine()->processingSampleRate()); - inline f_cnt_t loopStartFrame() const - { - return m_loopStartFrame; - } + friend void swap(SampleBuffer& first, SampleBuffer& second) noexcept; + auto toBase64() const -> QString; - inline f_cnt_t loopEndFrame() const - { - return m_loopEndFrame; - } + auto audioFile() const -> const QString& { return m_audioFile; } + auto sampleRate() const -> sample_rate_t { return m_sampleRate; } - void setLoopStartFrame(f_cnt_t start) - { - m_loopStartFrame = start; - } + auto begin() -> iterator { return m_data.begin(); } + auto end() -> iterator { return m_data.end(); } - void setLoopEndFrame(f_cnt_t end) - { - m_loopEndFrame = end; - } + auto begin() const -> const_iterator { return m_data.begin(); } + auto end() const -> const_iterator { return m_data.end(); } - void setAllPointFrames( - f_cnt_t start, - f_cnt_t end, - f_cnt_t loopStart, - f_cnt_t loopEnd - ) - { - m_startFrame = start; - m_endFrame = end; - m_loopStartFrame = loopStart; - m_loopEndFrame = loopEnd; - } + auto cbegin() const -> const_iterator { return m_data.cbegin(); } + auto cend() const -> const_iterator { return m_data.cend(); } - inline f_cnt_t frames() const - { - return m_frames; - } + auto rbegin() -> reverse_iterator { return m_data.rbegin(); } + auto rend() -> reverse_iterator { return m_data.rend(); } - inline float amplification() const - { - return m_amplification; - } + auto rbegin() const -> const_reverse_iterator { return m_data.rbegin(); } + auto rend() const -> const_reverse_iterator { return m_data.rend(); } - inline bool reversed() const - { - return m_reversed; - } + auto crbegin() const -> const_reverse_iterator { return m_data.crbegin(); } + auto crend() const -> const_reverse_iterator { return m_data.crend(); } - inline float frequency() const - { - return m_frequency; - } + 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(); } - sample_rate_t sampleRate() const - { - return m_sampleRate; - } - - int sampleLength() const - { - return double(m_endFrame - m_startFrame) / m_sampleRate * 1000; - } - - inline void setFrequency(float freq) - { - m_frequency = freq; - } - - inline void setSampleRate(sample_rate_t rate) - { - m_sampleRate = rate; - } - - inline const sampleFrame * data() const - { - return m_data; - } - - QString openAudioFile() const; - QString openAndSetAudioFile(); - QString openAndSetWaveformFile(); - - QString & toBase64(QString & dst) const; - - - // protect calls from the GUI to this function with dataReadLock() and - // dataUnlock() - SampleBuffer * resample(const sample_rate_t srcSR, const sample_rate_t dstSR); - - void normalizeSampleRate(const sample_rate_t srcSR, bool keepSettings = false); - - // protect calls from the GUI to this function with dataReadLock() and - // dataUnlock(), out of loops for efficiency - inline sample_t userWaveSample(const float sample) const - { - f_cnt_t frames = m_frames; - sampleFrame * data = m_data; - const float frame = sample * frames; - f_cnt_t f1 = static_cast(frame) % frames; - if (f1 < 0) - { - f1 += frames; - } - return linearInterpolate(data[f1][0], data[(f1 + 1) % frames][0], fraction(frame)); - } - - void dataReadLock() - { - m_varLock.lockForRead(); - } - - void dataUnlock() - { - m_varLock.unlock(); - } - - - std::unique_ptr m_userAntiAliasWaveTable; - - -public slots: - void setAudioFile(const QString & audioFile); - void loadFromBase64(const QString & data); - void setStartFrame(const lmms::f_cnt_t s); - void setEndFrame(const lmms::f_cnt_t e); - void setAmplification(float a); - void setReversed(bool on); - void sampleRateChanged(); + static auto emptyBuffer() -> std::shared_ptr; private: - static sample_rate_t audioEngineSampleRate(); - - void update(bool keepSettings = false); - - void convertIntToFloat(int_sample_t * & ibuf, f_cnt_t frames, int channels); - void directFloatWrite(sample_t * & fbuf, f_cnt_t frames, int channels); - - f_cnt_t decodeSampleSF( - QString fileName, - sample_t * & buf, - ch_cnt_t & channels, - sample_rate_t & samplerate - ); -#ifdef LMMS_HAVE_OGGVORBIS - f_cnt_t decodeSampleOGGVorbis( - QString fileName, - int_sample_t * & buf, - ch_cnt_t & channels, - sample_rate_t & samplerate - ); -#endif - f_cnt_t decodeSampleDS( - QString fileName, - int_sample_t * & buf, - ch_cnt_t & channels, - sample_rate_t & samplerate - ); - + std::vector m_data; QString m_audioFile; - sampleFrame * m_origData; - f_cnt_t m_origFrames; - sampleFrame * m_data; - mutable QReadWriteLock m_varLock; - f_cnt_t m_frames; - f_cnt_t m_startFrame; - f_cnt_t m_endFrame; - f_cnt_t m_loopStartFrame; - f_cnt_t m_loopEndFrame; - float m_amplification; - bool m_reversed; - float m_frequency; - sample_rate_t m_sampleRate; - - sampleFrame * getSampleFragment( - f_cnt_t index, - f_cnt_t frames, - LoopMode loopMode, - sampleFrame * * tmp, - bool * backwards, - f_cnt_t loopStart, - f_cnt_t loopEnd, - f_cnt_t end - ) const; - - f_cnt_t getLoopedIndex(f_cnt_t index, f_cnt_t startf, f_cnt_t endf) const; - f_cnt_t getPingPongIndex(f_cnt_t index, f_cnt_t startf, f_cnt_t endf) const; - - -signals: - void sampleUpdated(); - -} ; + sample_rate_t m_sampleRate = Engine::audioEngine()->processingSampleRate(); +}; } // namespace lmms diff --git a/include/SampleClip.h b/include/SampleClip.h index 5246787bdc6..da11996b14c 100644 --- a/include/SampleClip.h +++ b/include/SampleClip.h @@ -25,7 +25,9 @@ #ifndef LMMS_SAMPLE_CLIP_H #define LMMS_SAMPLE_CLIP_H +#include #include "Clip.h" +#include "Sample.h" namespace lmms { @@ -45,14 +47,15 @@ class SampleClip : public Clip Q_OBJECT mapPropertyFromModel(bool,isRecord,setRecord,m_recordModel); public: - SampleClip( Track * _track ); + SampleClip(Track* track, Sample sample, bool isPlaying); + SampleClip(Track* track); SampleClip( const SampleClip& orig ); ~SampleClip() override; SampleClip& operator=( const SampleClip& that ) = delete; void changeLength( const TimePos & _length ) override; - const QString & sampleFile() const; + const QString& sampleFile() const; void saveSettings( QDomDocument & _doc, QDomElement & _parent ) override; void loadSettings( const QDomElement & _this ) override; @@ -61,9 +64,9 @@ class SampleClip : public Clip return "sampleclip"; } - SampleBuffer* sampleBuffer() + Sample& sample() { - return m_sampleBuffer; + return m_sample; } TimePos sampleLength() const; @@ -74,10 +77,10 @@ class SampleClip : public Clip bool isPlaying() const; void setIsPlaying(bool isPlaying); + void setSampleBuffer(std::shared_ptr sb); public slots: - void setSampleBuffer( lmms::SampleBuffer* sb ); - void setSampleFile( const QString & sf ); + void setSampleFile(const QString& sf); void updateLength(); void toggleRecord(); void playbackPositionChanged(); @@ -85,7 +88,7 @@ public slots: private: - SampleBuffer* m_sampleBuffer; + Sample m_sample; BoolModel m_recordModel; bool m_isPlaying; diff --git a/include/SampleDecoder.h b/include/SampleDecoder.h new file mode 100644 index 00000000000..d7ce076ddd3 --- /dev/null +++ b/include/SampleDecoder.h @@ -0,0 +1,57 @@ +/* + * SampleDecoder.h - Decodes audio files in various formats + * + * Copyright (c) 2023 saker + * + * 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_DECODER_H +#define LMMS_SAMPLE_DECODER_H + +#include +#include +#include +#include +#include + +#include "lmms_basics.h" + +namespace lmms { +class SampleDecoder +{ +public: + struct Result + { + std::vector data; + int sampleRate; + }; + + struct AudioType + { + std::string name; + std::string extension; + }; + + static auto decode(const QString& audioFile) -> std::optional; + static auto supportedAudioTypes() -> const std::vector&; +}; +} // namespace lmms + +#endif // LMMS_SAMPLE_DECODER_H diff --git a/include/SampleLoader.h b/include/SampleLoader.h new file mode 100644 index 00000000000..7dbdbdc3312 --- /dev/null +++ b/include/SampleLoader.h @@ -0,0 +1,48 @@ +/* + * SampleLoader.h - Load audio and waveform files + * + * Copyright (c) 2023 saker + * + * 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_GUI_SAMPLE_LOADER_H +#define LMMS_GUI_SAMPLE_LOADER_H + +#include +#include + +#include "SampleBuffer.h" +#include "lmms_export.h" + +namespace lmms::gui { +class LMMS_EXPORT SampleLoader +{ +public: + static QString openAudioFile(const QString& previousFile = ""); + static QString openWaveformFile(const QString& previousFile = ""); + static std::shared_ptr createBufferFromFile(const QString& filePath); + static std::shared_ptr createBufferFromBase64( + const QString& base64, int sampleRate = Engine::audioEngine()->processingSampleRate()); +private: + static void displayError(const QString& message); +}; +} // namespace lmms::gui + +#endif // LMMS_GUI_SAMPLE_LOADER_H diff --git a/include/SamplePlayHandle.h b/include/SamplePlayHandle.h index 31b4f0bd59e..280010b066d 100644 --- a/include/SamplePlayHandle.h +++ b/include/SamplePlayHandle.h @@ -26,6 +26,7 @@ #ifndef LMMS_SAMPLE_PLAY_HANDLE_H #define LMMS_SAMPLE_PLAY_HANDLE_H +#include "Sample.h" #include "SampleBuffer.h" #include "AutomatableModel.h" #include "PlayHandle.h" @@ -43,7 +44,7 @@ class AudioPort; class LMMS_EXPORT SamplePlayHandle : public PlayHandle { public: - SamplePlayHandle( SampleBuffer* sampleBuffer , bool ownAudioPort = true ); + SamplePlayHandle(Sample* sample, bool ownAudioPort = true); SamplePlayHandle( const QString& sampleFile ); SamplePlayHandle( SampleClip* clip ); ~SamplePlayHandle() override; @@ -81,11 +82,11 @@ class LMMS_EXPORT SamplePlayHandle : public PlayHandle private: - SampleBuffer * m_sampleBuffer; + Sample* m_sample; bool m_doneMayReturnTrue; f_cnt_t m_frame; - SampleBuffer::handleState m_state; + Sample::PlaybackState m_state; const bool m_ownAudioPort; diff --git a/include/SampleRecordHandle.h b/include/SampleRecordHandle.h index de1ca19ba1a..df2d7c772c3 100644 --- a/include/SampleRecordHandle.h +++ b/include/SampleRecordHandle.h @@ -27,6 +27,7 @@ #include #include +#include #include "PlayHandle.h" #include "TimePos.h" @@ -53,7 +54,7 @@ class SampleRecordHandle : public PlayHandle bool isFromTrack( const Track * _track ) const override; f_cnt_t framesRecorded() const; - void createSampleBuffer( SampleBuffer * * _sample_buf ); + std::shared_ptr createSampleBuffer(); private: diff --git a/include/SampleWaveform.h b/include/SampleWaveform.h new file mode 100644 index 00000000000..692c1f9cb37 --- /dev/null +++ b/include/SampleWaveform.h @@ -0,0 +1,41 @@ +/* + * SampleWaveform.h + * + * Copyright (c) 2023 saker + * + * 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_GUI_SAMPLE_WAVEFORM_H +#define LMMS_GUI_SAMPLE_WAVEFORM_H + +#include + +#include "Sample.h" +#include "lmms_export.h" + +namespace lmms::gui { +class LMMS_EXPORT SampleWaveform +{ +public: + static void visualize(const Sample& sample, QPainter& p, const QRect& dr, int fromFrame = 0, int toFrame = 0); +}; +} // namespace lmms::gui + +#endif // LMMS_GUI_SAMPLE_WAVEFORM_H diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.cpp b/plugins/AudioFileProcessor/AudioFileProcessor.cpp index 459ff566c4f..ce3c9c07b90 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessor.cpp @@ -29,7 +29,6 @@ #include #include #include - #include #include "AudioEngine.h" @@ -42,6 +41,8 @@ #include "NotePlayHandle.h" #include "PathUtil.h" #include "PixmapButton.h" +#include "SampleLoader.h" +#include "SampleWaveform.h" #include "Song.h" #include "StringPairDrag.h" #include "Clipboard.h" @@ -83,7 +84,6 @@ Plugin::Descriptor PLUGIN_EXPORT audiofileprocessor_plugin_descriptor = AudioFileProcessor::AudioFileProcessor( InstrumentTrack * _instrument_track ) : Instrument( _instrument_track, &audiofileprocessor_plugin_descriptor ), - m_sampleBuffer(), m_ampModel( 100, 0, 500, 1, this, tr( "Amplify" ) ), m_startPointModel( 0, 0, 1, 0.0000001f, this, tr( "Start of sample" ) ), m_endPointModel( 1, 0, 1, 0.0000001f, this, tr( "End of sample" ) ), @@ -131,18 +131,18 @@ void AudioFileProcessor::playNote( NotePlayHandle * _n, // played. if( m_stutterModel.value() == true && _n->frequency() < 20.0 ) { - m_nextPlayStartPoint = m_sampleBuffer.startFrame(); + m_nextPlayStartPoint = m_sample.startFrame(); m_nextPlayBackwards = false; return; } if( !_n->m_pluginData ) { - if( m_stutterModel.value() == true && m_nextPlayStartPoint >= m_sampleBuffer.endFrame() ) + if (m_stutterModel.value() == true && m_nextPlayStartPoint >= m_sample.endFrame()) { // Restart playing the note if in stutter mode, not in loop mode, // and we're at the end of the sample. - m_nextPlayStartPoint = m_sampleBuffer.startFrame(); + m_nextPlayStartPoint = m_sample.startFrame(); m_nextPlayBackwards = false; } // set interpolation mode for libsamplerate @@ -159,25 +159,25 @@ void AudioFileProcessor::playNote( NotePlayHandle * _n, srcmode = SRC_SINC_MEDIUM_QUALITY; break; } - _n->m_pluginData = new handleState( _n->hasDetuningInfo(), srcmode ); - ((handleState *)_n->m_pluginData)->setFrameIndex( m_nextPlayStartPoint ); - ((handleState *)_n->m_pluginData)->setBackwards( m_nextPlayBackwards ); + _n->m_pluginData = new Sample::PlaybackState(_n->hasDetuningInfo(), srcmode); + static_cast(_n->m_pluginData)->setFrameIndex(m_nextPlayStartPoint); + static_cast(_n->m_pluginData)->setBackwards(m_nextPlayBackwards); // debug code -/* qDebug( "frames %d", m_sampleBuffer.frames() ); - qDebug( "startframe %d", m_sampleBuffer.startFrame() ); +/* qDebug( "frames %d", m_sample->frames() ); + qDebug( "startframe %d", m_sample->startFrame() ); qDebug( "nextPlayStartPoint %d", m_nextPlayStartPoint );*/ } if( ! _n->isFinished() ) { - if( m_sampleBuffer.play( _working_buffer + offset, - (handleState *)_n->m_pluginData, + if (m_sample.play(_working_buffer + offset, + static_cast(_n->m_pluginData), frames, _n->frequency(), - static_cast( m_loopModel.value() ) ) ) + static_cast(m_loopModel.value()))) { applyRelease( _working_buffer, _n ); - emit isPlaying( ((handleState *)_n->m_pluginData)->frameIndex() ); + emit isPlaying(static_cast(_n->m_pluginData)->frameIndex()); } else { @@ -191,8 +191,8 @@ void AudioFileProcessor::playNote( NotePlayHandle * _n, } if( m_stutterModel.value() == true ) { - m_nextPlayStartPoint = ((handleState *)_n->m_pluginData)->frameIndex(); - m_nextPlayBackwards = ((handleState *)_n->m_pluginData)->isBackwards(); + m_nextPlayStartPoint = static_cast(_n->m_pluginData)->frameIndex(); + m_nextPlayBackwards = static_cast(_n->m_pluginData)->backwards(); } } @@ -201,7 +201,7 @@ void AudioFileProcessor::playNote( NotePlayHandle * _n, void AudioFileProcessor::deleteNotePluginData( NotePlayHandle * _n ) { - delete (handleState *)_n->m_pluginData; + delete static_cast(_n->m_pluginData); } @@ -209,11 +209,10 @@ void AudioFileProcessor::deleteNotePluginData( NotePlayHandle * _n ) void AudioFileProcessor::saveSettings(QDomDocument& doc, QDomElement& elem) { - elem.setAttribute("src", m_sampleBuffer.audioFile()); - if (m_sampleBuffer.audioFile().isEmpty()) + elem.setAttribute("src", m_sample.sampleFile()); + if (m_sample.sampleFile().isEmpty()) { - QString s; - elem.setAttribute("sampledata", m_sampleBuffer.toBase64(s)); + elem.setAttribute("sampledata", m_sample.toBase64()); } m_reverseModel.saveSettings(doc, elem, "reversed"); m_loopModel.saveSettings(doc, elem, "looped"); @@ -230,20 +229,17 @@ void AudioFileProcessor::saveSettings(QDomDocument& doc, QDomElement& elem) void AudioFileProcessor::loadSettings(const QDomElement& elem) { - if (!elem.attribute("src").isEmpty()) + if (auto srcFile = elem.attribute("src"); !srcFile.isEmpty()) { - setAudioFile(elem.attribute("src"), false); - - QString absolutePath = PathUtil::toAbsolute(m_sampleBuffer.audioFile()); - if (!QFileInfo(absolutePath).exists()) + if (QFileInfo(PathUtil::toAbsolute(srcFile)).exists()) { - QString message = tr("Sample not found: %1").arg(m_sampleBuffer.audioFile()); - Engine::getSong()->collectError(message); + setAudioFile(srcFile, false); } + else { Engine::getSong()->collectError(QString("%1: %2").arg(tr("Sample not found"), srcFile)); } } - else if (!elem.attribute("sampledata").isEmpty()) + else if (auto sampleData = elem.attribute("sampledata"); !sampleData.isEmpty()) { - m_sampleBuffer.loadFromBase64(elem.attribute("srcdata")); + m_sample = Sample(gui::SampleLoader::createBufferFromBase64(sampleData)); } m_loopModel.loadSettings(elem, "looped"); @@ -274,6 +270,7 @@ void AudioFileProcessor::loadSettings(const QDomElement& elem) } pointChanged(); + emit sampleUpdated(); } @@ -298,7 +295,7 @@ QString AudioFileProcessor::nodeName() const auto AudioFileProcessor::beatLen(NotePlayHandle* note) const -> int { // If we can play indefinitely, use the default beat note duration - if (static_cast(m_loopModel.value()) != SampleBuffer::LoopMode::Off) { return 0; } + if (static_cast(m_loopModel.value()) != Sample::Loop::Off) { return 0; } // Otherwise, use the remaining sample duration const auto baseFreq = instrumentTrack()->baseFreq(); @@ -306,10 +303,10 @@ auto AudioFileProcessor::beatLen(NotePlayHandle* note) const -> int * Engine::audioEngine()->processingSampleRate() / Engine::audioEngine()->baseSampleRate(); - const auto startFrame = m_nextPlayStartPoint >= m_sampleBuffer.endFrame() - ? m_sampleBuffer.startFrame() + const auto startFrame = m_nextPlayStartPoint >= m_sample.endFrame() + ? m_sample.startFrame() : m_nextPlayStartPoint; - const auto duration = m_sampleBuffer.endFrame() - startFrame; + const auto duration = m_sample.endFrame() - startFrame; return static_cast(std::floor(duration * freqFactor)); } @@ -322,25 +319,22 @@ gui::PluginView* AudioFileProcessor::instantiateView( QWidget * _parent ) return new gui::AudioFileProcessorView( this, _parent ); } - - - -void AudioFileProcessor::setAudioFile( const QString & _audio_file, - bool _rename ) +void AudioFileProcessor::setAudioFile(const QString& _audio_file, bool _rename) { // is current channel-name equal to previous-filename?? if( _rename && ( instrumentTrack()->name() == - QFileInfo( m_sampleBuffer.audioFile() ).fileName() || - m_sampleBuffer.audioFile().isEmpty() ) ) + QFileInfo(m_sample.sampleFile()).fileName() || + m_sample.sampleFile().isEmpty())) { // then set it to new one instrumentTrack()->setName( PathUtil::cleanName( _audio_file ) ); } // else we don't touch the track-name, because the user named it self - m_sampleBuffer.setAudioFile( _audio_file ); + m_sample = Sample(gui::SampleLoader::createBufferFromFile(_audio_file)); loopPointChanged(); + emit sampleUpdated(); } @@ -348,9 +342,10 @@ void AudioFileProcessor::setAudioFile( const QString & _audio_file, void AudioFileProcessor::reverseModelChanged() { - m_sampleBuffer.setReversed( m_reverseModel.value() ); - m_nextPlayStartPoint = m_sampleBuffer.startFrame(); + m_sample.setReversed(m_reverseModel.value()); + m_nextPlayStartPoint = m_sample.startFrame(); m_nextPlayBackwards = false; + emit sampleUpdated(); } @@ -358,13 +353,14 @@ void AudioFileProcessor::reverseModelChanged() void AudioFileProcessor::ampModelChanged() { - m_sampleBuffer.setAmplification( m_ampModel.value() / 100.0f ); + m_sample.setAmplification(m_ampModel.value() / 100.0f); + emit sampleUpdated(); } void AudioFileProcessor::stutterModelChanged() { - m_nextPlayStartPoint = m_sampleBuffer.startFrame(); + m_nextPlayStartPoint = m_sample.startFrame(); m_nextPlayBackwards = false; } @@ -433,14 +429,14 @@ void AudioFileProcessor::loopPointChanged() void AudioFileProcessor::pointChanged() { - const auto f_start = static_cast(m_startPointModel.value() * m_sampleBuffer.frames()); - const auto f_end = static_cast(m_endPointModel.value() * m_sampleBuffer.frames()); - const auto f_loop = static_cast(m_loopPointModel.value() * m_sampleBuffer.frames()); + const auto f_start = static_cast(m_startPointModel.value() * m_sample.sampleSize()); + const auto f_end = static_cast(m_endPointModel.value() * m_sample.sampleSize()); + const auto f_loop = static_cast(m_loopPointModel.value() * m_sample.sampleSize()); m_nextPlayStartPoint = f_start; m_nextPlayBackwards = false; - m_sampleBuffer.setAllPointFrames( f_start, f_end, f_loop, f_end ); + m_sample.setAllPointFrames(f_start, f_end, f_loop, f_end); emit dataChanged(); } @@ -601,7 +597,7 @@ void AudioFileProcessorView::newWaveView() delete m_waveView; m_waveView = 0; } - m_waveView = new AudioFileProcessorWaveView( this, 245, 75, castModel()->m_sampleBuffer ); + m_waveView = new AudioFileProcessorWaveView(this, 245, 75, &castModel()->m_sample); m_waveView->move( 2, 172 ); m_waveView->setKnobs( dynamic_cast( m_startKnob ), @@ -648,7 +644,8 @@ void AudioFileProcessorView::paintEvent( QPaintEvent * ) auto a = castModel(); QString file_name = ""; - int idx = a->m_sampleBuffer.audioFile().length(); + + int idx = a->m_sample.sampleFile().length(); p.setFont( pointSize<8>( font() ) ); @@ -659,7 +656,7 @@ void AudioFileProcessorView::paintEvent( QPaintEvent * ) while( idx > 0 && fm.size( Qt::TextSingleLine, file_name + "..." ).width() < 210 ) { - file_name = a->m_sampleBuffer.audioFile()[--idx] + file_name; + file_name = a->m_sample.sampleFile()[--idx] + file_name; } if( idx > 0 ) @@ -687,7 +684,7 @@ void AudioFileProcessorView::sampleUpdated() void AudioFileProcessorView::openAudioFile() { - QString af = castModel()->m_sampleBuffer.openAudioFile(); + QString af = SampleLoader::openAudioFile(); if (af.isEmpty()) { return; } castModel()->setAudioFile(af); @@ -701,8 +698,7 @@ void AudioFileProcessorView::openAudioFile() void AudioFileProcessorView::modelChanged() { auto a = castModel(); - connect( &a->m_sampleBuffer, SIGNAL( sampleUpdated() ), - this, SLOT( sampleUpdated() ) ); + connect(a, &AudioFileProcessor::sampleUpdated, this, &AudioFileProcessorView::sampleUpdated); m_ampKnob->setModel( &a->m_ampModel ); m_startKnob->setModel( &a->m_startPointModel ); m_endKnob->setModel( &a->m_endPointModel ); @@ -719,20 +715,20 @@ void AudioFileProcessorView::modelChanged() void AudioFileProcessorWaveView::updateSampleRange() { - if( m_sampleBuffer.frames() > 1 ) + if (m_sample->sampleSize() > 1) { - const f_cnt_t marging = ( m_sampleBuffer.endFrame() - m_sampleBuffer.startFrame() ) * 0.1; - m_from = qMax( 0, m_sampleBuffer.startFrame() - marging ); - m_to = qMin( m_sampleBuffer.endFrame() + marging, m_sampleBuffer.frames() ); + const f_cnt_t marging = (m_sample->endFrame() - m_sample->startFrame()) * 0.1; + m_from = qMax(0, m_sample->startFrame() - marging); + m_to = qMin(m_sample->endFrame() + marging, m_sample->sampleSize()); } } -AudioFileProcessorWaveView::AudioFileProcessorWaveView( QWidget * _parent, int _w, int _h, SampleBuffer& buf ) : +AudioFileProcessorWaveView::AudioFileProcessorWaveView(QWidget * _parent, int _w, int _h, Sample* buf) : QWidget( _parent ), - m_sampleBuffer( buf ), + m_sample(buf), m_graph( QPixmap( _w - 2 * s_padding, _h - 2 * s_padding ) ), m_from( 0 ), - m_to( m_sampleBuffer.frames() ), + m_to(m_sample->sampleSize()), m_last_from( 0 ), m_last_to( 0 ), m_last_amp( 0 ), @@ -880,11 +876,11 @@ void AudioFileProcessorWaveView::paintEvent( QPaintEvent * _pe ) const QRect graph_rect( s_padding, s_padding, width() - 2 * s_padding, height() - 2 * s_padding ); const f_cnt_t frames = m_to - m_from; - m_startFrameX = graph_rect.x() + ( m_sampleBuffer.startFrame() - m_from ) * + m_startFrameX = graph_rect.x() + (m_sample->startFrame() - m_from) * double( graph_rect.width() ) / frames; - m_endFrameX = graph_rect.x() + ( m_sampleBuffer.endFrame() - m_from ) * + m_endFrameX = graph_rect.x() + (m_sample->endFrame() - m_from) * double( graph_rect.width() ) / frames; - m_loopFrameX = graph_rect.x() + ( m_sampleBuffer.loopStartFrame() - m_from ) * + m_loopFrameX = graph_rect.x() + (m_sample->loopStartFrame() - m_from) * double( graph_rect.width() ) / frames; const int played_width_px = ( m_framesPlayed - m_from ) * double( graph_rect.width() ) / frames; @@ -959,7 +955,7 @@ void AudioFileProcessorWaveView::paintEvent( QPaintEvent * _pe ) p.setFont( pointSize<8>( font() ) ); QString length_text; - const int length = m_sampleBuffer.sampleLength(); + const int length = m_sample->sampleDuration().count(); if( length > 20000 ) { @@ -988,42 +984,37 @@ void AudioFileProcessorWaveView::updateGraph() { if( m_to == 1 ) { - m_to = m_sampleBuffer.frames() * 0.7; + m_to = m_sample->sampleSize() * 0.7; slideSamplePointToFrames( Point::End, m_to * 0.7 ); } - if( m_from > m_sampleBuffer.startFrame() ) + if (m_from > m_sample->startFrame()) { - m_from = m_sampleBuffer.startFrame(); + m_from = m_sample->startFrame(); } - if( m_to < m_sampleBuffer.endFrame() ) + if (m_to < m_sample->endFrame()) { - m_to = m_sampleBuffer.endFrame(); + m_to = m_sample->endFrame(); } - if( m_sampleBuffer.reversed() != m_reversed ) + if (m_sample->reversed() != m_reversed) { reverse(); } - else if( m_last_from == m_from && m_last_to == m_to && m_sampleBuffer.amplification() == m_last_amp ) + else if (m_last_from == m_from && m_last_to == m_to && m_sample->amplification() == m_last_amp) { return; } m_last_from = m_from; m_last_to = m_to; - m_last_amp = m_sampleBuffer.amplification(); + m_last_amp = m_sample->amplification(); m_graph.fill( Qt::transparent ); QPainter p( &m_graph ); p.setPen( QColor( 255, 255, 255 ) ); - - m_sampleBuffer.visualize( - p, - QRect( 0, 0, m_graph.width(), m_graph.height() ), - m_from, m_to - ); + SampleWaveform::visualize(*m_sample, p, QRect(0, 0, m_graph.width(), m_graph.height()), m_from, m_to); } @@ -1031,9 +1022,9 @@ void AudioFileProcessorWaveView::updateGraph() void AudioFileProcessorWaveView::zoom( const bool _out ) { - const f_cnt_t start = m_sampleBuffer.startFrame(); - const f_cnt_t end = m_sampleBuffer.endFrame(); - const f_cnt_t frames = m_sampleBuffer.frames(); + const f_cnt_t start = m_sample->startFrame(); + const f_cnt_t end = m_sample->endFrame(); + const f_cnt_t frames = m_sample->sampleSize(); const f_cnt_t d_from = start - m_from; const f_cnt_t d_to = m_to - end; @@ -1066,7 +1057,7 @@ void AudioFileProcessorWaveView::zoom( const bool _out ) ); } - if( double( new_to - new_from ) / m_sampleBuffer.sampleRate() > 0.05 ) + if (static_cast(new_to - new_from) / m_sample->sampleRate() > 0.05) { m_from = new_from; m_to = new_to; @@ -1085,8 +1076,8 @@ void AudioFileProcessorWaveView::slide( int _px ) step = -step; } - f_cnt_t step_from = qBound( 0, m_from + step, m_sampleBuffer.frames() ) - m_from; - f_cnt_t step_to = qBound( m_from + 1, m_to + step, m_sampleBuffer.frames() ) - m_to; + f_cnt_t step_from = qBound(0, m_from + step, m_sample->sampleSize()) - m_from; + f_cnt_t step_to = qBound(m_from + 1, m_to + step, m_sample->sampleSize()) - m_to; step = qAbs( step_from ) < qAbs( step_to ) ? step_from : step_to; @@ -1147,7 +1138,7 @@ void AudioFileProcessorWaveView::slideSamplePointByFrames( Point _point, f_cnt_t } else { - const double v = static_cast( _frames ) / m_sampleBuffer.frames(); + const double v = static_cast(_frames) / m_sample->sampleSize(); if( _slide_to ) { a_knob->slideTo( v ); @@ -1164,11 +1155,11 @@ void AudioFileProcessorWaveView::slideSamplePointByFrames( Point _point, f_cnt_t void AudioFileProcessorWaveView::slideSampleByFrames( f_cnt_t _frames ) { - if( m_sampleBuffer.frames() <= 1 ) + if (m_sample->sampleSize() <= 1) { return; } - const double v = static_cast( _frames ) / m_sampleBuffer.frames(); + const double v = static_cast( _frames ) / m_sample->sampleSize(); // update knobs in the right order // to avoid them clamping each other if (v < 0) @@ -1191,14 +1182,14 @@ void AudioFileProcessorWaveView::slideSampleByFrames( f_cnt_t _frames ) void AudioFileProcessorWaveView::reverse() { slideSampleByFrames( - m_sampleBuffer.frames() - - m_sampleBuffer.endFrame() - - m_sampleBuffer.startFrame() + m_sample->sampleSize() + - m_sample->endFrame() + - m_sample->startFrame() ); const f_cnt_t from = m_from; - m_from = m_sampleBuffer.frames() - m_to; - m_to = m_sampleBuffer.frames() - from; + m_from = m_sample->sampleSize() - m_to; + m_to = m_sample->sampleSize() - from; m_reversed = ! m_reversed; } @@ -1240,8 +1231,7 @@ void AudioFileProcessorWaveView::knob::slideTo( double _v, bool _check_bound ) float AudioFileProcessorWaveView::knob::getValue( const QPoint & _p ) { const double dec_fact = ! m_waveView ? 1 : - double( m_waveView->m_to - m_waveView->m_from ) - / m_waveView->m_sampleBuffer.frames(); + static_cast(m_waveView->m_to - m_waveView->m_from) / m_waveView->m_sample->sampleSize(); const float inc = Knob::getValue( _p ) * dec_fact; return inc; @@ -1262,12 +1252,12 @@ bool AudioFileProcessorWaveView::knob::checkBound( double _v ) const return false; const double d1 = qAbs( m_relatedKnob->model()->value() - model()->value() ) - * ( m_waveView->m_sampleBuffer.frames() ) - / m_waveView->m_sampleBuffer.sampleRate(); + * (m_waveView->m_sample->sampleSize()) + / m_waveView->m_sample->sampleRate(); const double d2 = qAbs( m_relatedKnob->model()->value() - _v ) - * ( m_waveView->m_sampleBuffer.frames() ) - / m_waveView->m_sampleBuffer.sampleRate(); + * (m_waveView->m_sample->sampleSize()) + / m_waveView->m_sample->sampleRate(); return d1 < d2 || d2 > 0.005; } diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.h b/plugins/AudioFileProcessor/AudioFileProcessor.h index 4a5f21cc09a..80a40c56f5e 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.h +++ b/plugins/AudioFileProcessor/AudioFileProcessor.h @@ -31,6 +31,7 @@ #include "ComboBoxModel.h" #include "Instrument.h" #include "InstrumentView.h" +#include "Sample.h" #include "SampleBuffer.h" #include "Knob.h" @@ -78,8 +79,7 @@ class AudioFileProcessor : public Instrument public slots: - void setAudioFile( const QString & _audio_file, bool _rename = true ); - + void setAudioFile(const QString& _audio_file, bool _rename = true); private slots: void reverseModelChanged(); @@ -93,12 +93,10 @@ private slots: signals: void isPlaying( lmms::f_cnt_t _current_frame ); - + void sampleUpdated(); private: - using handleState = SampleBuffer::handleState; - - SampleBuffer m_sampleBuffer; + Sample m_sample; FloatModel m_ampModel; FloatModel m_startPointModel; @@ -246,7 +244,7 @@ public slots: SampleLoop } ; - SampleBuffer& m_sampleBuffer; + Sample* m_sample; QPixmap m_graph; f_cnt_t m_from; f_cnt_t m_to; @@ -266,8 +264,10 @@ public slots: f_cnt_t m_framesPlayed; bool m_animation; + friend class AudioFileProcessorView; + public: - AudioFileProcessorWaveView( QWidget * _parent, int _w, int _h, SampleBuffer& buf ); + AudioFileProcessorWaveView(QWidget * _parent, int _w, int _h, Sample* buf); void setKnobs(knob *_start, knob *_end, knob *_loop ); diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index 0713d310038..2d67f0ddff3 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -46,7 +46,7 @@ #include "Knob.h" #include "NotePlayHandle.h" #include "PathUtil.h" -#include "SampleBuffer.h" +#include "Sample.h" #include "Song.h" #include "PatchesDialog.h" @@ -437,7 +437,7 @@ void GigInstrument::play( sampleFrame * _working_buffer ) if (sample.region->PitchTrack == true) { freq_factor *= sample.freqFactor; } // We need a bit of margin so we don't get glitching - samples = frames / freq_factor + MARGIN[m_interpolation]; + samples = frames / freq_factor + Sample::s_interpolationMargins[m_interpolation]; } // Load this note's data diff --git a/plugins/Patman/Patman.cpp b/plugins/Patman/Patman.cpp index 24c54d66be5..e525498a5ee 100644 --- a/plugins/Patman/Patman.cpp +++ b/plugins/Patman/Patman.cpp @@ -153,8 +153,8 @@ void PatmanInstrument::playNote( NotePlayHandle * _n, float play_freq = hdata->tuned ? _n->frequency() : hdata->sample->frequency(); - if( hdata->sample->play( _working_buffer + offset, hdata->state, frames, - play_freq, m_loopedModel.value() ? SampleBuffer::LoopMode::On : SampleBuffer::LoopMode::Off ) ) + if (hdata->sample->play(_working_buffer + offset, hdata->state, frames, + play_freq, m_loopedModel.value() ? Sample::Loop::On : Sample::Loop::Off)) { applyRelease( _working_buffer, _n ); } @@ -170,7 +170,6 @@ void PatmanInstrument::playNote( NotePlayHandle * _n, void PatmanInstrument::deleteNotePluginData( NotePlayHandle * _n ) { auto hdata = (handle_data*)_n->m_pluginData; - sharedObject::unref( hdata->sample ); delete hdata->state; delete hdata; } @@ -356,9 +355,8 @@ PatmanInstrument::LoadError PatmanInstrument::loadPatch( } } - auto psample = new SampleBuffer(data, frames); - psample->setFrequency( root_freq / 1000.0f ); - psample->setSampleRate( sample_rate ); + auto psample = std::make_shared(data, frames, sample_rate); + psample->setFrequency(root_freq / 1000.0f); if( modes & MODES_LOOPING ) { @@ -366,7 +364,7 @@ PatmanInstrument::LoadError PatmanInstrument::loadPatch( psample->setLoopEndFrame( loop_end ); } - m_patchSamples.push_back( psample ); + m_patchSamples.push_back(psample); delete[] wave_samples; delete[] data; @@ -382,7 +380,6 @@ void PatmanInstrument::unloadCurrentPatch() { while( !m_patchSamples.empty() ) { - sharedObject::unref( m_patchSamples.back() ); m_patchSamples.pop_back(); } } @@ -395,7 +392,7 @@ void PatmanInstrument::selectSample( NotePlayHandle * _n ) const float freq = _n->frequency(); float min_dist = HUGE_VALF; - SampleBuffer* sample = nullptr; + std::shared_ptr sample = nullptr; for (const auto& patchSample : m_patchSamples) { @@ -412,15 +409,8 @@ void PatmanInstrument::selectSample( NotePlayHandle * _n ) auto hdata = new handle_data; hdata->tuned = m_tunedModel.value(); - if( sample ) - { - hdata->sample = sharedObject::ref( sample ); - } - else - { - hdata->sample = new SampleBuffer( nullptr, 0 ); - } - hdata->state = new SampleBuffer::handleState( _n->hasDetuningInfo() ); + hdata->sample = sample ? sample : std::make_shared(); + hdata->state = new Sample::PlaybackState(_n->hasDetuningInfo()); _n->m_pluginData = hdata; } diff --git a/plugins/Patman/Patman.h b/plugins/Patman/Patman.h index 3a15db5f362..8d2c8c6577b 100644 --- a/plugins/Patman/Patman.h +++ b/plugins/Patman/Patman.h @@ -28,6 +28,7 @@ #include "Instrument.h" #include "InstrumentView.h" +#include "Sample.h" #include "SampleBuffer.h" #include "AutomatableModel.h" #include "MemoryManager.h" @@ -87,13 +88,13 @@ public slots: struct handle_data { MM_OPERATORS - SampleBuffer::handleState* state; + Sample::PlaybackState* state; bool tuned; - SampleBuffer* sample; + std::shared_ptr sample; }; QString m_patchFile; - QVector m_patchSamples; + QVector> m_patchSamples; BoolModel m_loopedModel; BoolModel m_tunedModel; diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 2918265cead..c9e75df601e 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -31,6 +31,7 @@ #include "Engine.h" #include "InstrumentTrack.h" #include "PathUtil.h" +#include "SampleLoader.h" #include "Song.h" #include "embed.h" #include "lmms_constants.h" @@ -76,7 +77,7 @@ SlicerT::SlicerT(InstrumentTrack* instrumentTrack) void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) { - if (m_originalSample.frames() <= 1) { return; } + if (m_originalSample.sampleSize() <= 1) { return; } int noteIndex = handle->key() - m_parentTrack->baseNote(); const fpp_t frames = handle->framesLeftForCurrentPeriod(); @@ -115,24 +116,24 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) if (noteLeft > 0) { - int noteFrame = noteDone * m_originalSample.frames(); + int noteFrame = noteDone * m_originalSample.sampleSize(); SRC_STATE* resampleState = playbackState->resamplingState(); SRC_DATA resampleData; resampleData.data_in = (m_originalSample.data() + noteFrame)->data(); resampleData.data_out = (workingBuffer + offset)->data(); - resampleData.input_frames = noteLeft * m_originalSample.frames(); + resampleData.input_frames = noteLeft * m_originalSample.sampleSize(); resampleData.output_frames = frames; resampleData.src_ratio = speedRatio; src_process(resampleState, &resampleData); - float nextNoteDone = noteDone + frames * (1.0f / speedRatio) / m_originalSample.frames(); + float nextNoteDone = noteDone + frames * (1.0f / speedRatio) / m_originalSample.sampleSize(); playbackState->setNoteDone(nextNoteDone); // exponential fade out, applyRelease() not used since it extends the note length int fadeOutFrames = m_fadeOutFrames.value() / 1000.0f * Engine::audioEngine()->processingSampleRate(); - int noteFramesLeft = noteLeft * m_originalSample.frames() * speedRatio; + int noteFramesLeft = noteLeft * m_originalSample.sampleSize() * speedRatio; for (int i = 0; i < frames; i++) { float fadeValue = static_cast(noteFramesLeft - i) / fadeOutFrames; @@ -160,7 +161,7 @@ void SlicerT::deleteNotePluginData(NotePlayHandle* handle) // http://www.iro.umontreal.ca/~pift6080/H09/documents/papers/bello_onset_tutorial.pdf void SlicerT::findSlices() { - if (m_originalSample.frames() <= 1) { return; } + if (m_originalSample.sampleSize() <= 1) { return; } m_slicePoints = {}; const int windowSize = 512; @@ -170,8 +171,8 @@ void SlicerT::findSlices() int minDist = sampleRate * minBeatLength; float maxMag = -1; - std::vector singleChannel(m_originalSample.frames(), 0); - for (int i = 0; i < m_originalSample.frames(); i++) + std::vector singleChannel(m_originalSample.sampleSize(), 0); + for (int i = 0; i < m_originalSample.sampleSize(); i++) { singleChannel[i] = (m_originalSample.data()[i][0] + m_originalSample.data()[i][1]) / 2; maxMag = std::max(maxMag, singleChannel[i]); @@ -232,7 +233,7 @@ void SlicerT::findSlices() spectralFlux = 1E-10; // again for no divison by zero } - m_slicePoints.push_back(m_originalSample.frames()); + m_slicePoints.push_back(m_originalSample.sampleSize()); for (float& sliceValue : m_slicePoints) { @@ -255,7 +256,7 @@ void SlicerT::findSlices() for (float& sliceIndex : m_slicePoints) { - sliceIndex /= m_originalSample.frames(); + sliceIndex /= m_originalSample.sampleSize(); } m_slicePoints[0] = 0; @@ -268,10 +269,10 @@ void SlicerT::findSlices() // and lies in the 100 - 200 bpm range void SlicerT::findBPM() { - if (m_originalSample.frames() <= 1) { return; } + if (m_originalSample.sampleSize() <= 1) { return; } float sampleRate = m_originalSample.sampleRate(); - float totalFrames = m_originalSample.frames(); + float totalFrames = m_originalSample.sampleSize(); float sampleLength = totalFrames / sampleRate; float bpmEstimate = 240.0f / sampleLength; @@ -295,7 +296,7 @@ std::vector SlicerT::getMidi() std::vector outputNotes; float speedRatio = static_cast(m_originalBPM.value()) / Engine::getSong()->getTempo(); - float outFrames = m_originalSample.frames() * speedRatio; + float outFrames = m_originalSample.sampleSize() * speedRatio; float framesPerTick = Engine::framesPerTick(); float totalTicks = outFrames / framesPerTick; @@ -320,7 +321,7 @@ std::vector SlicerT::getMidi() void SlicerT::updateFile(QString file) { - m_originalSample.setAudioFile(file); + if (auto buffer = gui::SampleLoader::createBufferFromFile(file)) { m_originalSample = Sample(std::move(buffer)); } findBPM(); findSlices(); @@ -336,11 +337,10 @@ void SlicerT::updateSlices() void SlicerT::saveSettings(QDomDocument& document, QDomElement& element) { element.setAttribute("version", "1"); - element.setAttribute("src", m_originalSample.audioFile()); - if (m_originalSample.audioFile().isEmpty()) + element.setAttribute("src", m_originalSample.sampleFile()); + if (m_originalSample.sampleFile().isEmpty()) { - QString s; - element.setAttribute("sampledata", m_originalSample.toBase64(s)); + element.setAttribute("sampledata", m_originalSample.toBase64()); } element.setAttribute("totalSlices", static_cast(m_slicePoints.size())); @@ -357,20 +357,23 @@ void SlicerT::saveSettings(QDomDocument& document, QDomElement& element) void SlicerT::loadSettings(const QDomElement& element) { - if (!element.attribute("src").isEmpty()) + if (auto srcFile = element.attribute("src"); !srcFile.isEmpty()) { - m_originalSample.setAudioFile(element.attribute("src")); - - QString absolutePath = PathUtil::toAbsolute(m_originalSample.audioFile()); - if (!QFileInfo(absolutePath).exists()) + if (QFileInfo(PathUtil::toAbsolute(srcFile)).exists()) + { + auto buffer = gui::SampleLoader::createBufferFromFile(srcFile); + m_originalSample = Sample(std::move(buffer)); + } + else { - QString message = tr("Sample not found: %1").arg(m_originalSample.audioFile()); + QString message = tr("Sample not found: %1").arg(srcFile); Engine::getSong()->collectError(message); } } - else if (!element.attribute("sampledata").isEmpty()) + else if (auto sampleData = element.attribute("sampledata"); !sampleData.isEmpty()) { - m_originalSample.loadFromBase64(element.attribute("srcdata")); + auto buffer = gui::SampleLoader::createBufferFromBase64(sampleData); + m_originalSample = Sample(std::move(buffer)); } if (!element.attribute("totalSlices").isEmpty()) diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 8671eecd1f8..010985dfce1 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -33,6 +33,7 @@ #include "Instrument.h" #include "InstrumentView.h" #include "Note.h" +#include "Sample.h" #include "SampleBuffer.h" #include "SlicerTView.h" #include "lmms_basics.h" @@ -95,7 +96,7 @@ public slots: ComboBoxModel m_sliceSnap; BoolModel m_enableSync; - SampleBuffer m_originalSample; + Sample m_originalSample; std::vector m_slicePoints; diff --git a/plugins/SlicerT/SlicerTView.cpp b/plugins/SlicerT/SlicerTView.cpp index 833d4b434af..bbdb53ccbc2 100644 --- a/plugins/SlicerT/SlicerTView.cpp +++ b/plugins/SlicerT/SlicerTView.cpp @@ -31,6 +31,7 @@ #include "DataFile.h" #include "Engine.h" #include "InstrumentTrack.h" +#include "SampleLoader.h" #include "SlicerT.h" #include "Song.h" #include "StringPairDrag.h" @@ -108,7 +109,7 @@ Knob* SlicerTView::createStyledKnob() void SlicerTView::exportMidi() { using namespace Clipboard; - if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } + if (m_slicerTParent->m_originalSample.sampleSize() <= 1) { return; } DataFile dataFile(DataFile::Type::ClipboardData); QDomElement noteList = dataFile.createElement("note-list"); @@ -129,7 +130,7 @@ void SlicerTView::exportMidi() void SlicerTView::openFiles() { - QString audioFile = m_slicerTParent->m_originalSample.openAudioFile(); + const auto audioFile = SampleLoader::openAudioFile(); if (audioFile.isEmpty()) { return; } m_slicerTParent->updateFile(audioFile); } diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index 6685f4f8cec..66747036a1f 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -26,6 +26,7 @@ #include +#include "SampleWaveform.h" #include "SlicerT.h" #include "SlicerTView.h" #include "embed.h" @@ -84,12 +85,13 @@ SlicerTWaveform::SlicerTWaveform(int totalWidth, int totalHeight, SlicerT* instr void SlicerTWaveform::drawSeekerWaveform() { m_seekerWaveform.fill(s_waveformBgColor); - if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } + if (m_slicerTParent->m_originalSample.sampleSize() <= 1) { return; } QPainter brush(&m_seekerWaveform); brush.setPen(s_waveformColor); - m_slicerTParent->m_originalSample.visualize(brush, QRect(0, 0, m_seekerWaveform.width(), m_seekerWaveform.height()), - 0, m_slicerTParent->m_originalSample.frames()); + SampleWaveform::visualize(m_slicerTParent->m_originalSample, brush, + QRect(0, 0, m_seekerWaveform.width(), m_seekerWaveform.height()), 0, + m_slicerTParent->m_originalSample.sampleSize()); // increase brightness in inner color QBitmap innerMask = m_seekerWaveform.createMaskFromColor(s_waveformMaskColor, Qt::MaskMode::MaskOutColor); @@ -100,7 +102,7 @@ void SlicerTWaveform::drawSeekerWaveform() void SlicerTWaveform::drawSeeker() { m_seeker.fill(s_emptyColor); - if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } + if (m_slicerTParent->m_originalSample.sampleSize() <= 1) { return; } QPainter brush(&m_seeker); brush.setPen(s_sliceColor); @@ -134,16 +136,17 @@ void SlicerTWaveform::drawSeeker() void SlicerTWaveform::drawEditorWaveform() { m_editorWaveform.fill(s_emptyColor); - if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } + if (m_slicerTParent->m_originalSample.sampleSize() <= 1) { return; } QPainter brush(&m_editorWaveform); - float startFrame = m_seekerStart * m_slicerTParent->m_originalSample.frames(); - float endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.frames(); + float startFrame = m_seekerStart * m_slicerTParent->m_originalSample.sampleSize(); + float endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.sampleSize(); brush.setPen(s_waveformColor); float zoomOffset = (m_editorHeight - m_zoomLevel * m_editorHeight) / 2; - m_slicerTParent->m_originalSample.visualize( - brush, QRect(0, zoomOffset, m_editorWidth, m_zoomLevel * m_editorHeight), startFrame, endFrame); + + SampleWaveform::visualize(m_slicerTParent->m_originalSample, brush, + QRect(0, zoomOffset, m_editorWidth, m_zoomLevel * m_editorHeight), startFrame, endFrame); // increase brightness in inner color QBitmap innerMask = m_editorWaveform.createMaskFromColor(s_waveformMaskColor, Qt::MaskMode::MaskOutColor); @@ -157,7 +160,7 @@ void SlicerTWaveform::drawEditor() QPainter brush(&m_sliceEditor); // No sample loaded - if (m_slicerTParent->m_originalSample.frames() <= 1) + if (m_slicerTParent->m_originalSample.sampleSize() <= 1) { brush.setPen(s_playHighlightColor); brush.setFont(QFont(brush.font().family(), 9.0f, -1, false)); @@ -306,7 +309,7 @@ void SlicerTWaveform::mousePressEvent(QMouseEvent* me) drawEditorWaveform(); break; case Qt::MouseButton::LeftButton: - if (m_slicerTParent->m_originalSample.frames() <= 1) { static_cast(parent())->openFiles(); } + if (m_slicerTParent->m_originalSample.sampleSize() <= 1) { static_cast(parent())->openFiles(); } // update seeker middle for correct movement m_seekerMiddle = static_cast(me->x() - s_seekerHorMargin) / m_seekerWidth; break; diff --git a/plugins/TripleOscillator/TripleOscillator.cpp b/plugins/TripleOscillator/TripleOscillator.cpp index 5b8f6e8ade6..d5f2e905fd0 100644 --- a/plugins/TripleOscillator/TripleOscillator.cpp +++ b/plugins/TripleOscillator/TripleOscillator.cpp @@ -23,8 +23,8 @@ */ - #include +#include #include "TripleOscillator.h" #include "AudioEngine.h" @@ -35,9 +35,11 @@ #include "Knob.h" #include "NotePlayHandle.h" #include "Oscillator.h" +#include "PathUtil.h" #include "PixmapButton.h" #include "SampleBuffer.h" - +#include "SampleLoader.h" +#include "Song.h" #include "embed.h" #include "plugin_export.h" @@ -133,22 +135,13 @@ OscillatorObject::OscillatorObject( Model * _parent, int _idx ) : } - - - -OscillatorObject::~OscillatorObject() -{ - sharedObject::unref( m_sampleBuffer ); -} - - - - void OscillatorObject::oscUserDefWaveDblClick() { - QString af = m_sampleBuffer->openAndSetWaveformFile(); + auto af = gui::SampleLoader::openWaveformFile(); if( af != "" ) { + m_sampleBuffer = gui::SampleLoader::createBufferFromFile(af); + m_userAntiAliasWaveTable = Oscillator::generateAntiAliasUserWaveTable(m_sampleBuffer.get()); // TODO: //m_usrWaveBtn->setToolTip(m_sampleBuffer->audioFile()); } @@ -289,8 +282,16 @@ void TripleOscillator::loadSettings( const QDomElement & _this ) "modalgo" + QString::number( i+1 ) ); m_osc[i]->m_useWaveTableModel.loadSettings( _this, "useWaveTable" + QString::number (i+1 ) ); - m_osc[i]->m_sampleBuffer->setAudioFile( _this.attribute( - "userwavefile" + is ) ); + + if (auto userWaveFile = _this.attribute("userwavefile" + is); !userWaveFile.isEmpty()) + { + if (QFileInfo(PathUtil::toAbsolute(userWaveFile)).exists()) + { + m_osc[i]->m_sampleBuffer = gui::SampleLoader::createBufferFromFile(userWaveFile); + m_osc[i]->m_userAntiAliasWaveTable = Oscillator::generateAntiAliasUserWaveTable(m_osc[i]->m_sampleBuffer.get()); + } + else { Engine::getSong()->collectError(QString("%1: %2").arg(tr("Sample not found"), userWaveFile)); } + } } } @@ -360,7 +361,8 @@ void TripleOscillator::playNote( NotePlayHandle * _n, oscs_l[i]->setUserWave( m_osc[i]->m_sampleBuffer ); oscs_r[i]->setUserWave( m_osc[i]->m_sampleBuffer ); - + oscs_l[i]->setUserAntiAliasWaveTable(m_osc[i]->m_userAntiAliasWaveTable); + oscs_r[i]->setUserAntiAliasWaveTable(m_osc[i]->m_userAntiAliasWaveTable); } _n->m_pluginData = new oscPtr; diff --git a/plugins/TripleOscillator/TripleOscillator.h b/plugins/TripleOscillator/TripleOscillator.h index f3290153b4c..c0258b5448b 100644 --- a/plugins/TripleOscillator/TripleOscillator.h +++ b/plugins/TripleOscillator/TripleOscillator.h @@ -26,9 +26,13 @@ #ifndef _TRIPLE_OSCILLATOR_H #define _TRIPLE_OSCILLATOR_H +#include + #include "Instrument.h" #include "InstrumentView.h" #include "AutomatableModel.h" +#include "OscillatorConstants.h" +#include "SampleBuffer.h" namespace lmms { @@ -57,9 +61,6 @@ class OscillatorObject : public Model Q_OBJECT public: OscillatorObject( Model * _parent, int _idx ); - ~OscillatorObject() override; - - private: FloatModel m_volumeModel; FloatModel m_panModel; @@ -71,7 +72,8 @@ class OscillatorObject : public Model IntModel m_waveShapeModel; IntModel m_modulationAlgoModel; BoolModel m_useWaveTableModel; - SampleBuffer* m_sampleBuffer; + std::shared_ptr m_sampleBuffer = SampleBuffer::emptyBuffer(); + std::shared_ptr m_userAntiAliasWaveTable; float m_volumeLeft; float m_volumeRight; diff --git a/src/core/AudioResampler.cpp b/src/core/AudioResampler.cpp new file mode 100644 index 00000000000..5f6b6a23955 --- /dev/null +++ b/src/core/AudioResampler.cpp @@ -0,0 +1,64 @@ +/* + * AudioResampler.cpp - wrapper for libsamplerate + * + * Copyright (c) 2023 saker + * + * 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 "AudioResampler.h" + +#include +#include +#include + +namespace lmms { + +AudioResampler::AudioResampler(int interpolationMode, int channels) + : m_interpolationMode(interpolationMode) + , m_channels(channels) + , m_state(src_new(interpolationMode, channels, &m_error)) +{ + if (!m_state) + { + const auto errorMessage = std::string{src_strerror(m_error)}; + const auto fullMessage = std::string{"Failed to create an AudioResampler: "} + errorMessage; + throw std::runtime_error{fullMessage}; + } +} + +AudioResampler::~AudioResampler() +{ + src_delete(m_state); +} + +auto AudioResampler::resample(const float* in, long inputFrames, float* out, long outputFrames, double ratio) + -> ProcessResult +{ + auto data = SRC_DATA{}; + data.data_in = in; + data.input_frames = inputFrames; + data.data_out = out; + data.output_frames = outputFrames; + data.src_ratio = ratio; + data.end_of_input = 0; + return {src_process(m_state, &data), data.input_frames_used, data.output_frames_gen}; +} + +} // namespace lmms diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 1155f5e0d22..a7f8243b983 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -4,6 +4,7 @@ set(LMMS_SRCS core/AudioEngine.cpp core/AudioEngineProfiler.cpp core/AudioEngineWorkerThread.cpp + core/AudioResampler.cpp core/AutomatableModel.cpp core/AutomationClip.cpp core/AutomationNode.cpp @@ -65,8 +66,10 @@ set(LMMS_SRCS core/RemotePlugin.cpp core/RenderManager.cpp core/RingBuffer.cpp + core/Sample.cpp core/SampleBuffer.cpp core/SampleClip.cpp + core/SampleDecoder.cpp core/SamplePlayHandle.cpp core/SampleRecordHandle.cpp core/Scale.cpp diff --git a/src/core/EnvelopeAndLfoParameters.cpp b/src/core/EnvelopeAndLfoParameters.cpp index 0a9673c8e2c..611700c5197 100644 --- a/src/core/EnvelopeAndLfoParameters.cpp +++ b/src/core/EnvelopeAndLfoParameters.cpp @@ -22,13 +22,17 @@ * */ +#include "EnvelopeAndLfoParameters.h" + #include +#include -#include "EnvelopeAndLfoParameters.h" #include "AudioEngine.h" #include "Engine.h" #include "Oscillator.h" - +#include "PathUtil.h" +#include "SampleLoader.h" +#include "Song.h" namespace lmms { @@ -118,7 +122,7 @@ EnvelopeAndLfoParameters::EnvelopeAndLfoParameters( m_controlEnvAmountModel( false, this, tr( "Modulate env amount" ) ), m_lfoFrame( 0 ), m_lfoAmountIsZero( false ), - m_lfoShapeData( nullptr ) + m_lfoShapeData(nullptr) { m_amountModel.setCenterValue( 0 ); m_lfoAmountModel.setCenterValue( 0 ); @@ -221,7 +225,7 @@ inline sample_t EnvelopeAndLfoParameters::lfoShapeSample( fpp_t _frame_offset ) shape_sample = Oscillator::sawSample( phase ); break; case LfoShape::UserDefinedWave: - shape_sample = m_userWave.userWaveSample( phase ); + shape_sample = Oscillator::userWaveSample(m_userWave.get(), phase); break; case LfoShape::RandomWave: if( frame == 0 ) @@ -354,7 +358,7 @@ void EnvelopeAndLfoParameters::saveSettings( QDomDocument & _doc, m_lfoAmountModel.saveSettings( _doc, _parent, "lamt" ); m_x100Model.saveSettings( _doc, _parent, "x100" ); m_controlEnvAmountModel.saveSettings( _doc, _parent, "ctlenvamt" ); - _parent.setAttribute( "userwavefile", m_userWave.audioFile() ); + _parent.setAttribute("userwavefile", m_userWave->audioFile()); } @@ -386,7 +390,14 @@ void EnvelopeAndLfoParameters::loadSettings( const QDomElement & _this ) m_sustainModel.setValue( 1.0 - m_sustainModel.value() ); } - m_userWave.setAudioFile( _this.attribute( "userwavefile" ) ); + if (const auto userWaveFile = _this.attribute("userwavefile"); !userWaveFile.isEmpty()) + { + if (QFileInfo(PathUtil::toAbsolute(userWaveFile)).exists()) + { + m_userWave = gui::SampleLoader::createBufferFromFile(_this.attribute("userwavefile")); + } + else { Engine::getSong()->collectError(QString("%1: %2").arg(tr("Sample not found"), userWaveFile)); } + } updateSampleVars(); } diff --git a/src/core/LfoController.cpp b/src/core/LfoController.cpp index 88f64803c2b..152e0ad8b78 100644 --- a/src/core/LfoController.cpp +++ b/src/core/LfoController.cpp @@ -23,14 +23,16 @@ * */ -#include +#include "LfoController.h" +#include +#include -#include "LfoController.h" #include "AudioEngine.h" +#include "PathUtil.h" +#include "SampleLoader.h" #include "Song.h" - namespace lmms { @@ -48,7 +50,7 @@ LfoController::LfoController( Model * _parent ) : m_phaseOffset( 0 ), m_currentPhase( 0 ), m_sampleFunction( &Oscillator::sinSample ), - m_userDefSampleBuffer( new SampleBuffer ) + m_userDefSampleBuffer(std::make_shared()) { setSampleExact( true ); connect( &m_waveModel, SIGNAL(dataChanged()), @@ -74,7 +76,6 @@ LfoController::LfoController( Model * _parent ) : LfoController::~LfoController() { - sharedObject::unref( m_userDefSampleBuffer ); m_baseModel.disconnect( this ); m_speedModel.disconnect( this ); m_amountModel.disconnect( this ); @@ -122,7 +123,7 @@ void LfoController::updateValueBuffer() } case Oscillator::WaveShape::UserDefined: { - currentSample = m_userDefSampleBuffer->userWaveSample(phase); + currentSample = Oscillator::userWaveSample(m_userDefSampleBuffer.get(), phase); break; } default: @@ -222,7 +223,7 @@ void LfoController::saveSettings( QDomDocument & _doc, QDomElement & _this ) m_phaseModel.saveSettings( _doc, _this, "phase" ); m_waveModel.saveSettings( _doc, _this, "wave" ); m_multiplierModel.saveSettings( _doc, _this, "multiplier" ); - _this.setAttribute( "userwavefile" , m_userDefSampleBuffer->audioFile() ); + _this.setAttribute("userwavefile", m_userDefSampleBuffer->audioFile()); } @@ -237,7 +238,15 @@ void LfoController::loadSettings( const QDomElement & _this ) m_phaseModel.loadSettings( _this, "phase" ); m_waveModel.loadSettings( _this, "wave" ); m_multiplierModel.loadSettings( _this, "multiplier" ); - m_userDefSampleBuffer->setAudioFile( _this.attribute("userwavefile" ) ); + + if (const auto userWaveFile = _this.attribute("userwavefile"); !userWaveFile.isEmpty()) + { + if (QFileInfo(PathUtil::toAbsolute(userWaveFile)).exists()) + { + m_userDefSampleBuffer = gui::SampleLoader::createBufferFromFile(_this.attribute("userwavefile")); + } + else { Engine::getSong()->collectError(QString("%1: %2").arg(tr("Sample not found"), userWaveFile)); } + } updateSampleFunction(); } diff --git a/src/core/Oscillator.cpp b/src/core/Oscillator.cpp index 06033b63edb..0330fad584a 100644 --- a/src/core/Oscillator.cpp +++ b/src/core/Oscillator.cpp @@ -182,19 +182,23 @@ void Oscillator::generateFromFFT(int bands, sample_t* table) normalize(s_sampleBuffer.data(), table, OscillatorConstants::WAVETABLE_LENGTH, 2*OscillatorConstants::WAVETABLE_LENGTH + 1); } -void Oscillator::generateAntiAliasUserWaveTable(SampleBuffer *sampleBuffer) +std::unique_ptr Oscillator::generateAntiAliasUserWaveTable(const SampleBuffer* sampleBuffer) { - if (sampleBuffer->m_userAntiAliasWaveTable == nullptr) {return;} - + auto userAntiAliasWaveTable = std::make_unique(); for (int i = 0; i < OscillatorConstants::WAVE_TABLES_PER_WAVEFORM_COUNT; ++i) { - for (int i = 0; i < OscillatorConstants::WAVETABLE_LENGTH; ++i) + // TODO: This loop seems to be doing the same thing for each iteration of the outer loop, + // and could probably be moved out of it + for (int j = 0; j < OscillatorConstants::WAVETABLE_LENGTH; ++j) { - s_sampleBuffer[i] = sampleBuffer->userWaveSample((float)i / (float)OscillatorConstants::WAVETABLE_LENGTH); + s_sampleBuffer[j] = Oscillator::userWaveSample( + sampleBuffer, static_cast(j) / OscillatorConstants::WAVETABLE_LENGTH); } fftwf_execute(s_fftPlan); - Oscillator::generateFromFFT(OscillatorConstants::MAX_FREQ / freqFromWaveTableBand(i), (*(sampleBuffer->m_userAntiAliasWaveTable))[i].data()); + Oscillator::generateFromFFT(OscillatorConstants::MAX_FREQ / freqFromWaveTableBand(i), (*userAntiAliasWaveTable)[i].data()); } + + return userAntiAliasWaveTable; } @@ -807,13 +811,13 @@ template<> inline sample_t Oscillator::getSample( const float _sample ) { - if (m_useWaveTable && !m_isModulator) + if (m_useWaveTable && m_userAntiAliasWaveTable && !m_isModulator) { - return wtSample(m_userWave->m_userAntiAliasWaveTable, _sample); + return wtSample(m_userAntiAliasWaveTable.get(), _sample); } else { - return userWaveSample(_sample); + return userWaveSample(m_userWave.get(), _sample); } } diff --git a/src/core/Sample.cpp b/src/core/Sample.cpp new file mode 100644 index 00000000000..cdb12851fa7 --- /dev/null +++ b/src/core/Sample.cpp @@ -0,0 +1,230 @@ +/* + * Sample.cpp - State for container-class SampleBuffer + * + * Copyright (c) 2023 saker + * + * 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 "Sample.h" + +#include +#include + +namespace lmms { + +Sample::Sample(const QString& audioFile) + : m_buffer(std::make_shared(audioFile)) + , m_startFrame(0) + , m_endFrame(m_buffer->size()) + , m_loopStartFrame(0) + , m_loopEndFrame(m_buffer->size()) +{ +} + +Sample::Sample(const QByteArray& base64, int sampleRate) + : m_buffer(std::make_shared(base64, sampleRate)) + , m_startFrame(0) + , m_endFrame(m_buffer->size()) + , m_loopStartFrame(0) + , m_loopEndFrame(m_buffer->size()) +{ +} + +Sample::Sample(const sampleFrame* data, int numFrames, int sampleRate) + : m_buffer(std::make_shared(data, numFrames, sampleRate)) + , m_startFrame(0) + , m_endFrame(m_buffer->size()) + , m_loopStartFrame(0) + , m_loopEndFrame(m_buffer->size()) +{ +} + +Sample::Sample(std::shared_ptr buffer) + : m_buffer(buffer) + , m_startFrame(0) + , m_endFrame(m_buffer->size()) + , m_loopStartFrame(0) + , m_loopEndFrame(m_buffer->size()) +{ +} + +Sample::Sample(const Sample& other) + : m_buffer(other.m_buffer) + , m_startFrame(other.startFrame()) + , m_endFrame(other.endFrame()) + , m_loopStartFrame(other.loopStartFrame()) + , m_loopEndFrame(other.loopEndFrame()) + , m_amplification(other.amplification()) + , m_frequency(other.frequency()) + , m_reversed(other.reversed()) +{ +} + +Sample::Sample(Sample&& other) + : m_buffer(std::move(other.m_buffer)) + , m_startFrame(other.startFrame()) + , m_endFrame(other.endFrame()) + , m_loopStartFrame(other.loopStartFrame()) + , m_loopEndFrame(other.loopEndFrame()) + , m_amplification(other.amplification()) + , m_frequency(other.frequency()) + , m_reversed(other.reversed()) +{ +} + +auto Sample::operator=(const Sample& other) -> Sample& +{ + m_buffer = other.m_buffer; + m_startFrame = other.startFrame(); + m_endFrame = other.endFrame(); + m_loopStartFrame = other.loopStartFrame(); + m_loopEndFrame = other.loopEndFrame(); + m_amplification = other.amplification(); + m_frequency = other.frequency(); + m_reversed = other.reversed(); + + return *this; +} + +auto Sample::operator=(Sample&& other) -> Sample& +{ + m_buffer = std::move(other.m_buffer); + m_startFrame = other.startFrame(); + m_endFrame = other.endFrame(); + m_loopStartFrame = other.loopStartFrame(); + m_loopEndFrame = other.loopEndFrame(); + m_amplification = other.amplification(); + m_frequency = other.frequency(); + m_reversed = other.reversed(); + + return *this; +} + +bool Sample::play(sampleFrame* dst, PlaybackState* state, int numFrames, float desiredFrequency, Loop loopMode) +{ + if (numFrames <= 0 || desiredFrequency <= 0) { return false; } + + auto resampleRatio = static_cast(Engine::audioEngine()->processingSampleRate()) / m_buffer->sampleRate(); + resampleRatio *= frequency() / desiredFrequency; + + auto playBuffer = std::vector(numFrames / resampleRatio); + if (!typeInfo::isEqual(resampleRatio, 1.0f)) + { + playBuffer.resize(playBuffer.size() + s_interpolationMargins[state->resampler().interpolationMode()]); + } + + const auto start = startFrame(); + const auto end = endFrame(); + const auto loopStart = loopStartFrame(); + const auto loopEnd = loopEndFrame(); + + switch (loopMode) + { + case Loop::Off: + state->m_frameIndex = std::clamp(state->m_frameIndex, start, end); + if (state->m_frameIndex == end) { return false; } + break; + case Loop::On: + state->m_frameIndex = std::clamp(state->m_frameIndex, start, loopEnd); + if (state->m_frameIndex == loopEnd) { state->m_frameIndex = loopStart; } + break; + case Loop::PingPong: + state->m_frameIndex = std::clamp(state->m_frameIndex, start, loopEnd); + if (state->m_frameIndex == loopEnd) + { + state->m_frameIndex = loopEnd - 1; + state->m_backwards = true; + } + else if (state->m_frameIndex <= loopStart && state->m_backwards) + { + state->m_frameIndex = loopStart; + state->m_backwards = false; + } + break; + } + + playSampleRange(state, playBuffer.data(), playBuffer.size()); + + const auto result + = state->resampler().resample(&playBuffer[0][0], playBuffer.size(), &dst[0][0], numFrames, resampleRatio); + if (result.error != 0) { return false; } + + state->m_frameIndex += (state->m_backwards ? -1 : 1) * result.inputFramesUsed; + amplifySampleRange(dst, result.outputFramesGenerated); + + return true; +} + +auto Sample::sampleDuration() const -> std::chrono::milliseconds +{ + const auto numFrames = endFrame() - startFrame(); + const auto duration = numFrames / static_cast(m_buffer->sampleRate()) * 1000; + return std::chrono::milliseconds{static_cast(duration)}; +} + +void Sample::setAllPointFrames(int startFrame, int endFrame, int loopStartFrame, int loopEndFrame) +{ + setStartFrame(startFrame); + setEndFrame(endFrame); + setLoopStartFrame(loopStartFrame); + setLoopEndFrame(loopEndFrame); +} + +void Sample::playSampleRange(PlaybackState* state, sampleFrame* dst, size_t numFrames) const +{ + auto framesToCopy = 0; + if (state->m_backwards) + { + framesToCopy = std::min(state->m_frameIndex - startFrame(), numFrames); + copyBufferBackward(dst, state->m_frameIndex, framesToCopy); + } + else + { + framesToCopy = std::min(endFrame() - state->m_frameIndex, numFrames); + copyBufferForward(dst, state->m_frameIndex, framesToCopy); + } + + if (framesToCopy < numFrames) { std::fill_n(dst + framesToCopy, numFrames - framesToCopy, sampleFrame{0, 0}); } +} + +void Sample::copyBufferForward(sampleFrame* dst, int initialPosition, int advanceAmount) const +{ + reversed() ? std::copy_n(m_buffer->rbegin() + initialPosition, advanceAmount, dst) + : std::copy_n(m_buffer->begin() + initialPosition, advanceAmount, dst); +} + +void Sample::copyBufferBackward(sampleFrame* dst, int initialPosition, int advanceAmount) const +{ + reversed() ? std::reverse_copy( + m_buffer->rbegin() + initialPosition - advanceAmount, m_buffer->rbegin() + initialPosition, dst) + : std::reverse_copy( + m_buffer->begin() + initialPosition - advanceAmount, m_buffer->begin() + initialPosition, dst); +} + +void Sample::amplifySampleRange(sampleFrame* src, int numFrames) const +{ + const auto amplification = m_amplification.load(std::memory_order_relaxed); + for (int i = 0; i < numFrames; ++i) + { + src[i][0] *= amplification; + src[i][1] *= amplification; + } +} +} // namespace lmms diff --git a/src/core/SampleBuffer.cpp b/src/core/SampleBuffer.cpp index 2a0076a283c..550a9d7bc10 100644 --- a/src/core/SampleBuffer.cpp +++ b/src/core/SampleBuffer.cpp @@ -23,1612 +23,74 @@ */ #include "SampleBuffer.h" -#include "Oscillator.h" +#include -#include - -#include -#include -#include -#include - - -#include - -#define OV_EXCLUDE_STATIC_CALLBACKS -#ifdef LMMS_HAVE_OGGVORBIS -#include -#endif - -#ifdef LMMS_HAVE_FLAC_STREAM_ENCODER_H -#include -#endif - -#ifdef LMMS_HAVE_FLAC_STREAM_DECODER_H -#include -#endif - - -#include "AudioEngine.h" -#include "base64.h" -#include "ConfigManager.h" -#include "DrumSynth.h" -#include "endian_handling.h" -#include "Engine.h" -#include "GuiApplication.h" -#include "Note.h" #include "PathUtil.h" +#include "SampleDecoder.h" +#include "lmms_basics.h" -#include "FileDialog.h" +namespace lmms { -namespace lmms +SampleBuffer::SampleBuffer(const sampleFrame* data, int numFrames, int sampleRate) + : m_data(data, data + numFrames) + , m_sampleRate(sampleRate) { - -SampleBuffer::SampleBuffer() : - m_userAntiAliasWaveTable(nullptr), - m_audioFile(""), - m_origData(nullptr), - m_origFrames(0), - m_data(nullptr), - m_frames(0), - m_startFrame(0), - m_endFrame(0), - m_loopStartFrame(0), - m_loopEndFrame(0), - m_amplification(1.0f), - m_reversed(false), - m_frequency(DefaultBaseFreq), - m_sampleRate(audioEngineSampleRate()) -{ - - connect(Engine::audioEngine(), SIGNAL(sampleRateChanged()), this, SLOT(sampleRateChanged())); - update(); } - - -SampleBuffer::SampleBuffer(const QString & audioFile, bool isBase64Data) - : SampleBuffer() +SampleBuffer::SampleBuffer(const QString& audioFile) { - if (isBase64Data) - { - loadFromBase64(audioFile); - } - else - { - m_audioFile = audioFile; - update(); - } -} - - + if (audioFile.isEmpty()) { throw std::runtime_error{"Failure loading audio file: Audio file path is empty."}; } + const auto absolutePath = PathUtil::toAbsolute(audioFile); - -SampleBuffer::SampleBuffer(const sampleFrame * data, const f_cnt_t frames) - : SampleBuffer() -{ - if (frames > 0) + if (auto decodedResult = SampleDecoder::decode(absolutePath)) { - m_origData = MM_ALLOC( frames); - memcpy(m_origData, data, frames * BYTES_PER_FRAME); - m_origFrames = frames; - update(); + auto& [data, sampleRate] = *decodedResult; + m_data = std::move(data); + m_sampleRate = sampleRate; + m_audioFile = PathUtil::toShortestRelative(audioFile); + return; } -} - - + throw std::runtime_error{ + "Failed to decode audio file: Either the audio codec is unsupported, or the file is corrupted."}; +} -SampleBuffer::SampleBuffer(const f_cnt_t frames) - : SampleBuffer() +SampleBuffer::SampleBuffer(const QString& base64, int sampleRate) + : m_sampleRate(sampleRate) { - if (frames > 0) - { - m_origData = MM_ALLOC( frames); - memset(m_origData, 0, frames * BYTES_PER_FRAME); - m_origFrames = frames; - update(); - } + // TODO: Replace with non-Qt equivalent + const auto bytes = QByteArray::fromBase64(base64.toUtf8()); + m_data.resize(bytes.size() / sizeof(sampleFrame)); + std::memcpy(reinterpret_cast(m_data.data()), bytes, m_data.size() * sizeof(sampleFrame)); } - - - -SampleBuffer::SampleBuffer(const SampleBuffer& orig) +SampleBuffer::SampleBuffer(std::vector data, int sampleRate) + : m_data(std::move(data)) + , m_sampleRate(sampleRate) { - orig.m_varLock.lockForRead(); - - m_audioFile = orig.m_audioFile; - m_origFrames = orig.m_origFrames; - m_origData = (m_origFrames > 0) ? MM_ALLOC( m_origFrames) : nullptr; - m_frames = orig.m_frames; - m_data = (m_frames > 0) ? MM_ALLOC( m_frames) : nullptr; - m_startFrame = orig.m_startFrame; - m_endFrame = orig.m_endFrame; - m_loopStartFrame = orig.m_loopStartFrame; - m_loopEndFrame = orig.m_loopEndFrame; - m_amplification = orig.m_amplification; - m_reversed = orig.m_reversed; - m_frequency = orig.m_frequency; - m_sampleRate = orig.m_sampleRate; - - //Deep copy m_origData and m_data from original - const auto origFrameBytes = m_origFrames * BYTES_PER_FRAME; - const auto frameBytes = m_frames * BYTES_PER_FRAME; - if (orig.m_origData != nullptr && origFrameBytes > 0) - { memcpy(m_origData, orig.m_origData, origFrameBytes); } - if (orig.m_data != nullptr && frameBytes > 0) - { memcpy(m_data, orig.m_data, frameBytes); } - - orig.m_varLock.unlock(); } - - - void swap(SampleBuffer& first, SampleBuffer& second) noexcept { using std::swap; - - // Lock both buffers for writing, with address as lock ordering - if (&first == &second) { return; } - else if (&first > &second) - { - first.m_varLock.lockForWrite(); - second.m_varLock.lockForWrite(); - } - else - { - second.m_varLock.lockForWrite(); - first.m_varLock.lockForWrite(); - } - - first.m_audioFile.swap(second.m_audioFile); - swap(first.m_origData, second.m_origData); swap(first.m_data, second.m_data); - swap(first.m_origFrames, second.m_origFrames); - swap(first.m_frames, second.m_frames); - swap(first.m_startFrame, second.m_startFrame); - swap(first.m_endFrame, second.m_endFrame); - swap(first.m_loopStartFrame, second.m_loopStartFrame); - swap(first.m_loopEndFrame, second.m_loopEndFrame); - swap(first.m_amplification, second.m_amplification); - swap(first.m_frequency, second.m_frequency); - swap(first.m_reversed, second.m_reversed); + swap(first.m_audioFile, second.m_audioFile); swap(first.m_sampleRate, second.m_sampleRate); - - // Unlock again - first.m_varLock.unlock(); - second.m_varLock.unlock(); -} - - - - -SampleBuffer& SampleBuffer::operator=(SampleBuffer that) -{ - swap(*this, that); - return *this; -} - - - - -SampleBuffer::~SampleBuffer() -{ - MM_FREE(m_origData); - MM_FREE(m_data); -} - - - -void SampleBuffer::sampleRateChanged() -{ - update(true); -} - -sample_rate_t SampleBuffer::audioEngineSampleRate() -{ - return Engine::audioEngine()->processingSampleRate(); -} - - -void SampleBuffer::update(bool keepSettings) -{ - const bool lock = (m_data != nullptr); - if (lock) - { - Engine::audioEngine()->requestChangeInModel(); - m_varLock.lockForWrite(); - MM_FREE(m_data); - } - - // File size and sample length limits - const int fileSizeMax = 300; // MB - const int sampleLengthMax = 90; // Minutes - - enum class FileLoadError - { - None, - ReadPermissionDenied, - TooLarge, - Invalid - }; - FileLoadError fileLoadError = FileLoadError::None; - - if (m_audioFile.isEmpty() && m_origData != nullptr && m_origFrames > 0) - { - // TODO: reverse- and amplification-property is not covered - // by following code... - m_data = MM_ALLOC( m_origFrames); - memcpy(m_data, m_origData, m_origFrames * BYTES_PER_FRAME); - if (keepSettings == false) - { - m_frames = m_origFrames; - m_loopStartFrame = m_startFrame = 0; - m_loopEndFrame = m_endFrame = m_frames; - } - } - else if (!m_audioFile.isEmpty()) - { - QString file = PathUtil::toAbsolute(m_audioFile); - int_sample_t * buf = nullptr; - sample_t * fbuf = nullptr; - ch_cnt_t channels = DEFAULT_CHANNELS; - sample_rate_t samplerate = audioEngineSampleRate(); - m_frames = 0; - - const QFileInfo fileInfo(file); - if (!fileInfo.isReadable()) - { - fileLoadError = FileLoadError::ReadPermissionDenied; - } - else if (fileInfo.size() > fileSizeMax * 1024 * 1024) - { - fileLoadError = FileLoadError::TooLarge; - } - else - { - // Use QFile to handle unicode file names on Windows - QFile f(file); - SNDFILE * sndFile = nullptr; - SF_INFO sfInfo; - sfInfo.format = 0; - - if (f.open(QIODevice::ReadOnly) && (sndFile = sf_open_fd(f.handle(), SFM_READ, &sfInfo, false))) - { - f_cnt_t frames = sfInfo.frames; - int rate = sfInfo.samplerate; - if (frames / rate > sampleLengthMax * 60) - { - fileLoadError = FileLoadError::TooLarge; - } - sf_close(sndFile); - } - f.close(); - } - - if (fileLoadError == FileLoadError::None) - { -#ifdef LMMS_HAVE_OGGVORBIS - // workaround for a bug in libsndfile or our libsndfile decoder - // causing some OGG files to be distorted -> try with OGG Vorbis - // decoder first if filename extension matches "ogg" - if (m_frames == 0 && fileInfo.suffix() == "ogg") - { - m_frames = decodeSampleOGGVorbis(file, buf, channels, samplerate); - } -#endif - if (m_frames == 0) - { - m_frames = decodeSampleSF(file, fbuf, channels, samplerate); - } -#ifdef LMMS_HAVE_OGGVORBIS - if (m_frames == 0) - { - m_frames = decodeSampleOGGVorbis(file, buf, channels, samplerate); - } -#endif - if (m_frames == 0) - { - m_frames = decodeSampleDS(file, buf, channels, samplerate); - } - - if (m_frames == 0) - { - fileLoadError = FileLoadError::Invalid; - } - } - - if (m_frames == 0 || fileLoadError != FileLoadError::None) // if still no frames, bail - { - // sample couldn't be decoded, create buffer containing - // one sample-frame - m_data = MM_ALLOC( 1); - memset(m_data, 0, sizeof(*m_data)); - m_frames = 1; - m_loopStartFrame = m_startFrame = 0; - m_loopEndFrame = m_endFrame = 1; - } - else // otherwise normalize sample rate - { - normalizeSampleRate(samplerate, keepSettings); - } - } - else - { - // neither an audio-file nor a buffer to copy from, so create - // buffer containing one sample-frame - m_data = MM_ALLOC( 1); - memset(m_data, 0, sizeof(*m_data)); - m_frames = 1; - m_loopStartFrame = m_startFrame = 0; - m_loopEndFrame = m_endFrame = 1; - } - - if (lock) - { - m_varLock.unlock(); - Engine::audioEngine()->doneChangeInModel(); - } - - emit sampleUpdated(); - - // allocate space for anti-aliased wave table - if (m_userAntiAliasWaveTable == nullptr) - { - m_userAntiAliasWaveTable = std::make_unique(); - } - Oscillator::generateAntiAliasUserWaveTable(this); - - if (fileLoadError != FileLoadError::None) - { - QString title = tr("Fail to open file"); - QString message; - - switch (fileLoadError) - { - case FileLoadError::None: - // present just to avoid a compiler warning - break; - - case FileLoadError::ReadPermissionDenied: - message = tr("Read permission denied"); - break; - - case FileLoadError::TooLarge: - message = tr("Audio files are limited to %1 MB " - "in size and %2 minutes of playing time" - ).arg(fileSizeMax).arg(sampleLengthMax); - break; - - case FileLoadError::Invalid: - message = tr("Invalid audio file"); - break; - } - - if (gui::getGUI() != nullptr) - { - QMessageBox::information(nullptr, title, message, QMessageBox::Ok); - } - else - { - fprintf(stderr, "%s\n", message.toUtf8().constData()); - } - } -} - - -void SampleBuffer::convertIntToFloat( - int_sample_t * & ibuf, - f_cnt_t frames, - int channels -) -{ - // following code transforms int-samples into float-samples and does amplifying & reversing - const float fac = 1 / OUTPUT_SAMPLE_MULTIPLIER; - m_data = MM_ALLOC( frames); - const int ch = (channels > 1) ? 1 : 0; - - // if reversing is on, we also reverse when scaling - bool isReversed = m_reversed; - int idx = isReversed ? (frames - 1) * channels : 0; - for (f_cnt_t frame = 0; frame < frames; ++frame) - { - m_data[frame][0] = ibuf[idx+0] * fac; - m_data[frame][1] = ibuf[idx+ch] * fac; - idx += isReversed ? -channels : channels; - } - - delete[] ibuf; -} - -void SampleBuffer::directFloatWrite( - sample_t * & fbuf, - f_cnt_t frames, - int channels -) -{ - - m_data = MM_ALLOC( frames); - const int ch = (channels > 1) ? 1 : 0; - - // if reversing is on, we also reverse when scaling - bool isReversed = m_reversed; - int idx = isReversed ? (frames - 1) * channels : 0; - for (f_cnt_t frame = 0; frame < frames; ++frame) - { - m_data[frame][0] = fbuf[idx+0]; - m_data[frame][1] = fbuf[idx+ch]; - idx += isReversed ? -channels : channels; - } - - delete[] fbuf; -} - - -void SampleBuffer::normalizeSampleRate(const sample_rate_t srcSR, bool keepSettings) -{ - const sample_rate_t oldRate = m_sampleRate; - // do samplerate-conversion to our default-samplerate - if (srcSR != audioEngineSampleRate()) - { - SampleBuffer * resampled = resample(srcSR, audioEngineSampleRate()); - - m_sampleRate = audioEngineSampleRate(); - MM_FREE(m_data); - m_frames = resampled->frames(); - m_data = MM_ALLOC( m_frames); - memcpy(m_data, resampled->data(), m_frames * sizeof(sampleFrame)); - delete resampled; - } - - if (keepSettings == false) - { - // update frame-variables - m_loopStartFrame = m_startFrame = 0; - m_loopEndFrame = m_endFrame = m_frames; - } - else if (oldRate != audioEngineSampleRate()) - { - auto oldRateToNewRateRatio = static_cast(audioEngineSampleRate()) / oldRate; - - m_startFrame = std::clamp(f_cnt_t(m_startFrame * oldRateToNewRateRatio), 0, m_frames); - m_endFrame = std::clamp(f_cnt_t(m_endFrame * oldRateToNewRateRatio), m_startFrame, m_frames); - m_loopStartFrame = std::clamp(f_cnt_t(m_loopStartFrame * oldRateToNewRateRatio), 0, m_frames); - m_loopEndFrame = std::clamp(f_cnt_t(m_loopEndFrame * oldRateToNewRateRatio), m_loopStartFrame, m_frames); - m_sampleRate = audioEngineSampleRate(); - } -} - - - - -f_cnt_t SampleBuffer::decodeSampleSF( - QString fileName, - sample_t * & buf, - ch_cnt_t & channels, - sample_rate_t & samplerate -) -{ - SNDFILE * sndFile; - SF_INFO sfInfo; - sfInfo.format = 0; - f_cnt_t frames = 0; - sf_count_t sfFramesRead; - - - // Use QFile to handle unicode file names on Windows - QFile f(fileName); - if (f.open(QIODevice::ReadOnly) && (sndFile = sf_open_fd(f.handle(), SFM_READ, &sfInfo, false))) - { - frames = sfInfo.frames; - - buf = new sample_t[sfInfo.channels * frames]; - sfFramesRead = sf_read_float(sndFile, buf, sfInfo.channels * frames); - - if (sfFramesRead < sfInfo.channels * frames) - { -#ifdef DEBUG_LMMS - qDebug("SampleBuffer::decodeSampleSF(): could not read" - " sample %s: %s", fileName, sf_strerror(nullptr)); -#endif - } - channels = sfInfo.channels; - samplerate = sfInfo.samplerate; - - sf_close(sndFile); - } - else - { -#ifdef DEBUG_LMMS - qDebug("SampleBuffer::decodeSampleSF(): could not load " - "sample %s: %s", fileName, sf_strerror(nullptr)); -#endif - } - f.close(); - - //write down either directly or convert i->f depending on file type - - if (frames > 0 && buf != nullptr) - { - directFloatWrite(buf, frames, channels); - } - - return frames; -} - - - - -#ifdef LMMS_HAVE_OGGVORBIS - -// callback-functions for reading ogg-file - -size_t qfileReadCallback(void * ptr, size_t size, size_t n, void * udata ) -{ - return static_cast(udata)->read((char*) ptr, size * n); -} - - - - -int qfileSeekCallback(void * udata, ogg_int64_t offset, int whence) -{ - auto f = static_cast(udata); - - if (whence == SEEK_CUR) - { - f->seek(f->pos() + offset); - } - else if (whence == SEEK_END) - { - f->seek(f->size() + offset); - } - else - { - f->seek(offset); - } - return 0; -} - - - - -int qfileCloseCallback(void * udata) -{ - delete static_cast(udata); - return 0; } - - - -long qfileTellCallback(void * udata) +QString SampleBuffer::toBase64() const { - return static_cast(udata)->pos(); + // 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(); } - - - -f_cnt_t SampleBuffer::decodeSampleOGGVorbis( - QString fileName, - int_sample_t * & buf, - ch_cnt_t & channels, - sample_rate_t & samplerate -) -{ - static ov_callbacks callbacks = - { - qfileReadCallback, - qfileSeekCallback, - qfileCloseCallback, - qfileTellCallback - } ; - - OggVorbis_File vf; - - f_cnt_t frames = 0; - - auto f = new QFile(fileName); - if (f->open(QFile::ReadOnly) == false) - { - delete f; - return 0; - } - - int err = ov_open_callbacks(f, &vf, nullptr, 0, callbacks); - - if (err < 0) - { - switch (err) - { - case OV_EREAD: - printf("SampleBuffer::decodeSampleOGGVorbis():" - " media read error\n"); - break; - case OV_ENOTVORBIS: - printf("SampleBuffer::decodeSampleOGGVorbis():" - " not an Ogg Vorbis file\n"); - break; - case OV_EVERSION: - printf("SampleBuffer::decodeSampleOGGVorbis():" - " vorbis version mismatch\n"); - break; - case OV_EBADHEADER: - printf("SampleBuffer::decodeSampleOGGVorbis():" - " invalid Vorbis bitstream header\n"); - break; - case OV_EFAULT: - printf("SampleBuffer::decodeSampleOgg(): " - "internal logic fault\n"); - break; - } - delete f; - return 0; - } - - ov_pcm_seek(&vf, 0); - - channels = ov_info(&vf, -1)->channels; - samplerate = ov_info(&vf, -1)->rate; - - ogg_int64_t total = ov_pcm_total(&vf, -1); - - buf = new int_sample_t[total * channels]; - int bitstream = 0; - long bytesRead = 0; - - do - { - bytesRead = ov_read(&vf, - (char *) &buf[frames * channels], - (total - frames) * channels * BYTES_PER_INT_SAMPLE, - isLittleEndian() ? 0 : 1, - BYTES_PER_INT_SAMPLE, - 1, - &bitstream - ); - - if (bytesRead < 0) - { - break; - } - frames += bytesRead / (channels * BYTES_PER_INT_SAMPLE); - } - while (bytesRead != 0 && bitstream == 0); - - ov_clear(&vf); - - // if buffer isn't empty, convert it to float and write it down - if (frames > 0 && buf != nullptr) - { - convertIntToFloat(buf, frames, channels); - } - - return frames; -} -#endif // LMMS_HAVE_OGGVORBIS - - - - -f_cnt_t SampleBuffer::decodeSampleDS( - QString fileName, - int_sample_t * & buf, - ch_cnt_t & channels, - sample_rate_t & samplerate -) -{ - DrumSynth ds; - f_cnt_t frames = ds.GetDSFileSamples(fileName, buf, channels, samplerate); - - if (frames > 0 && buf != nullptr) - { - convertIntToFloat(buf, frames, channels); - } - - return frames; - -} - - - - -bool SampleBuffer::play( - sampleFrame * ab, - handleState * state, - const fpp_t frames, - const float freq, - const LoopMode loopMode -) -{ - f_cnt_t startFrame = m_startFrame; - f_cnt_t endFrame = m_endFrame; - f_cnt_t loopStartFrame = m_loopStartFrame; - f_cnt_t loopEndFrame = m_loopEndFrame; - - if (endFrame == 0 || frames == 0) - { - return false; - } - - // variable for determining if we should currently be playing backwards in a ping-pong loop - bool isBackwards = state->isBackwards(); - - // The SampleBuffer can play a given sample with increased or decreased pitch. However, only - // samples that contain a tone that matches the default base note frequency of 440 Hz will - // produce the exact requested pitch in [Hz]. - const double freqFactor = (double) freq / (double) m_frequency * - m_sampleRate / Engine::audioEngine()->processingSampleRate(); - - // calculate how many frames we have in requested pitch - const auto totalFramesForCurrentPitch = static_cast((endFrame - startFrame) / freqFactor); - - if (totalFramesForCurrentPitch == 0) - { - return false; - } - - - // this holds the index of the first frame to play - f_cnt_t playFrame = std::max(state->m_frameIndex, startFrame); - - if (loopMode == LoopMode::Off) - { - if (playFrame >= endFrame || (endFrame - playFrame) / freqFactor == 0) - { - // the sample is done being played - return false; - } - } - else if (loopMode == LoopMode::On) - { - playFrame = getLoopedIndex(playFrame, loopStartFrame, loopEndFrame); - } - else - { - playFrame = getPingPongIndex(playFrame, loopStartFrame, loopEndFrame); - } - - f_cnt_t fragmentSize = (f_cnt_t)(frames * freqFactor) + MARGIN[state->interpolationMode()]; - - sampleFrame * tmp = nullptr; - - // check whether we have to change pitch... - if (freqFactor != 1.0 || state->m_varyingPitch) - { - SRC_DATA srcData; - // Generate output - srcData.data_in = - getSampleFragment(playFrame, fragmentSize, loopMode, &tmp, &isBackwards, - loopStartFrame, loopEndFrame, endFrame )->data(); - srcData.data_out = ab->data(); - srcData.input_frames = fragmentSize; - srcData.output_frames = frames; - srcData.src_ratio = 1.0 / freqFactor; - srcData.end_of_input = 0; - int error = src_process(state->m_resamplingData, &srcData); - if (error) - { - printf("SampleBuffer: error while resampling: %s\n", - src_strerror(error)); - } - if (srcData.output_frames_gen > frames) - { - printf("SampleBuffer: not enough frames: %ld / %d\n", - srcData.output_frames_gen, frames); - } - // Advance - switch (loopMode) - { - case LoopMode::Off: - playFrame += srcData.input_frames_used; - break; - case LoopMode::On: - playFrame += srcData.input_frames_used; - playFrame = getLoopedIndex(playFrame, loopStartFrame, loopEndFrame); - break; - case LoopMode::PingPong: - { - f_cnt_t left = srcData.input_frames_used; - if (state->isBackwards()) - { - playFrame -= srcData.input_frames_used; - if (playFrame < loopStartFrame) - { - left -= (loopStartFrame - playFrame); - playFrame = loopStartFrame; - } - else left = 0; - } - playFrame += left; - playFrame = getPingPongIndex(playFrame, loopStartFrame, loopEndFrame); - break; - } - } - } - else - { - // we don't have to pitch, so we just copy the sample-data - // as is into pitched-copy-buffer - - // Generate output - memcpy(ab, - getSampleFragment(playFrame, frames, loopMode, &tmp, &isBackwards, - loopStartFrame, loopEndFrame, endFrame), - frames * BYTES_PER_FRAME); - // Advance - switch (loopMode) - { - case LoopMode::Off: - playFrame += frames; - break; - case LoopMode::On: - playFrame += frames; - playFrame = getLoopedIndex(playFrame, loopStartFrame, loopEndFrame); - break; - case LoopMode::PingPong: - { - f_cnt_t left = frames; - if (state->isBackwards()) - { - playFrame -= frames; - if (playFrame < loopStartFrame) - { - left -= (loopStartFrame - playFrame); - playFrame = loopStartFrame; - } - else left = 0; - } - playFrame += left; - playFrame = getPingPongIndex(playFrame, loopStartFrame, loopEndFrame); - break; - } - } - } - - if (tmp != nullptr) - { - MM_FREE(tmp); - } - - state->setBackwards(isBackwards); - state->setFrameIndex(playFrame); - - for (fpp_t i = 0; i < frames; ++i) - { - ab[i][0] *= m_amplification; - ab[i][1] *= m_amplification; - } - - return true; -} - - - - -sampleFrame * SampleBuffer::getSampleFragment( - f_cnt_t index, - f_cnt_t frames, - LoopMode loopMode, - sampleFrame * * tmp, - bool * backwards, - f_cnt_t loopStart, - f_cnt_t loopEnd, - f_cnt_t end -) const -{ - if (loopMode == LoopMode::Off) - { - if (index + frames <= end) - { - return m_data + index; - } - } - else if (loopMode == LoopMode::On) - { - if (index + frames <= loopEnd) - { - return m_data + index; - } - } - else - { - if (!*backwards && index + frames < loopEnd) - { - return m_data + index; - } - } - - *tmp = MM_ALLOC( frames); - - if (loopMode == LoopMode::Off) - { - f_cnt_t available = end - index; - memcpy(*tmp, m_data + index, available * BYTES_PER_FRAME); - memset(*tmp + available, 0, (frames - available) * BYTES_PER_FRAME); - } - else if (loopMode == LoopMode::On) - { - f_cnt_t copied = std::min(frames, loopEnd - index); - memcpy(*tmp, m_data + index, copied * BYTES_PER_FRAME); - f_cnt_t loopFrames = loopEnd - loopStart; - while (copied < frames) - { - f_cnt_t todo = std::min(frames - copied, loopFrames); - memcpy(*tmp + copied, m_data + loopStart, todo * BYTES_PER_FRAME); - copied += todo; - } - } - else - { - f_cnt_t pos = index; - bool currentBackwards = pos < loopStart - ? false - : *backwards; - f_cnt_t copied = 0; - - - if (currentBackwards) - { - copied = std::min(frames, pos - loopStart); - for (int i = 0; i < copied; i++) - { - (*tmp)[i][0] = m_data[pos - i][0]; - (*tmp)[i][1] = m_data[pos - i][1]; - } - pos -= copied; - if (pos == loopStart) { currentBackwards = false; } - } - else - { - copied = std::min(frames, loopEnd - pos); - memcpy(*tmp, m_data + pos, copied * BYTES_PER_FRAME); - pos += copied; - if (pos == loopEnd) { currentBackwards = true; } - } - - while (copied < frames) - { - if (currentBackwards) - { - f_cnt_t todo = std::min(frames - copied, pos - loopStart); - for (int i = 0; i < todo; i++) - { - (*tmp)[copied + i][0] = m_data[pos - i][0]; - (*tmp)[copied + i][1] = m_data[pos - i][1]; - } - pos -= todo; - copied += todo; - if (pos <= loopStart) { currentBackwards = false; } - } - else - { - f_cnt_t todo = std::min(frames - copied, loopEnd - pos); - memcpy(*tmp + copied, m_data + pos, todo * BYTES_PER_FRAME); - pos += todo; - copied += todo; - if (pos >= loopEnd) { currentBackwards = true; } - } - } - *backwards = currentBackwards; - } - - return *tmp; -} - - - - -f_cnt_t SampleBuffer::getLoopedIndex(f_cnt_t index, f_cnt_t startf, f_cnt_t endf) const -{ - if (index < endf) - { - return index; - } - return startf + (index - startf) % (endf - startf); -} - - -f_cnt_t SampleBuffer::getPingPongIndex(f_cnt_t index, f_cnt_t startf, f_cnt_t endf) const -{ - if (index < endf) - { - return index; - } - const f_cnt_t loopLen = endf - startf; - const f_cnt_t loopPos = (index - endf) % (loopLen * 2); - - return (loopPos < loopLen) - ? endf - loopPos - : startf + (loopPos - loopLen); -} - - -/* @brief Draws a sample buffer on the QRect given in the range [fromFrame, toFrame) - * @param QPainter p: Painter object for the painting operations - * @param QRect dr: QRect where the buffer will be drawn in - * @param QRect clip: QRect used for clipping - * @param f_cnt_t fromFrame: First frame of the range - * @param f_cnt_t toFrame: Last frame of the range non-inclusive - */ -void SampleBuffer::visualize( - QPainter & p, - const QRect & dr, - const QRect & clip, - f_cnt_t fromFrame, - f_cnt_t toFrame -) -{ - if (m_frames == 0) { return; } - - const bool focusOnRange = toFrame <= m_frames && 0 <= fromFrame && fromFrame < toFrame; - //TODO: If the clip QRect is not being used we should remove it - //p.setClipRect(clip); - const int w = dr.width(); - const int h = dr.height(); - - const int yb = h / 2 + dr.y(); - const float ySpace = h * 0.5f; - const int nbFrames = focusOnRange ? toFrame - fromFrame : m_frames; - - const double fpp = std::max(1., static_cast(nbFrames) / w); - // There are 2 possibilities: Either nbFrames is bigger than - // the width, so we will have width points, or nbFrames is - // smaller than the width (fpp = 1) and we will have nbFrames - // points - const int totalPoints = nbFrames > w - ? w - : nbFrames; - std::vector fEdgeMax(totalPoints); - std::vector fEdgeMin(totalPoints); - std::vector fRmsMax(totalPoints); - std::vector fRmsMin(totalPoints); - int curPixel = 0; - const int xb = dr.x(); - const int first = focusOnRange ? fromFrame : 0; - const int last = focusOnRange ? toFrame - 1 : m_frames - 1; - // When the number of frames isn't perfectly divisible by the - // width, the remaining frames don't fit the last pixel and are - // past the visible area. lastVisibleFrame is the index number of - // the last visible frame. - const int visibleFrames = (fpp * w); - const int lastVisibleFrame = focusOnRange - ? fromFrame + visibleFrames - 1 - : visibleFrames - 1; - - for (double frame = first; frame <= last && frame <= lastVisibleFrame; frame += fpp) - { - float maxData = -1; - float minData = 1; - - auto rmsData = std::array{}; - - // Find maximum and minimum samples within range - for (int i = 0; i < fpp && frame + i <= last; ++i) - { - for (int j = 0; j < 2; ++j) - { - auto curData = m_data[static_cast(frame) + i][j]; - - if (curData > maxData) { maxData = curData; } - if (curData < minData) { minData = curData; } - - rmsData[j] += curData * curData; - } - } - - const float trueRmsData = (rmsData[0] + rmsData[1]) / 2 / fpp; - const float sqrtRmsData = sqrt(trueRmsData); - const float maxRmsData = std::clamp(sqrtRmsData, minData, maxData); - const float minRmsData = std::clamp(-sqrtRmsData, minData, maxData); - - // If nbFrames >= w, we can use curPixel to calculate X - // but if nbFrames < w, we need to calculate it proportionally - // to the total number of points - auto x = nbFrames >= w - ? xb + curPixel - : xb + ((static_cast(curPixel) / nbFrames) * w); - // Partial Y calculation - auto py = ySpace * m_amplification; - fEdgeMax[curPixel] = QPointF(x, (yb - (maxData * py))); - fEdgeMin[curPixel] = QPointF(x, (yb - (minData * py))); - fRmsMax[curPixel] = QPointF(x, (yb - (maxRmsData * py))); - fRmsMin[curPixel] = QPointF(x, (yb - (minRmsData * py))); - ++curPixel; - } - - for (int i = 0; i < totalPoints; ++i) - { - p.drawLine(fEdgeMax[i], fEdgeMin[i]); - } - - p.setPen(p.pen().color().lighter(123)); - - for (int i = 0; i < totalPoints; ++i) - { - p.drawLine(fRmsMax[i], fRmsMin[i]); - } -} - - - - -QString SampleBuffer::openAudioFile() const -{ - gui::FileDialog ofd(nullptr, tr("Open audio file")); - - QString dir; - if (!m_audioFile.isEmpty()) - { - QString f = m_audioFile; - if (QFileInfo(f).isRelative()) - { - f = ConfigManager::inst()->userSamplesDir() + f; - if (QFileInfo(f).exists() == false) - { - f = ConfigManager::inst()->factorySamplesDir() + - m_audioFile; - } - } - dir = QFileInfo(f).absolutePath(); - } - else - { - dir = ConfigManager::inst()->userSamplesDir(); - } - // change dir to position of previously opened file - ofd.setDirectory(dir); - ofd.setFileMode(gui::FileDialog::ExistingFiles); - - // set filters - QStringList types; - types << tr("All Audio-Files (*.wav *.ogg " -#ifdef LMMS_HAVE_SNDFILE_MP3 - "*.mp3 " -#endif - "*.ds *.flac *.spx *.voc " - "*.aif *.aiff *.au *.raw)") - << tr("Wave-Files (*.wav)") - << tr("OGG-Files (*.ogg)") -#ifdef LMMS_HAVE_SNDFILE_MP3 - << tr("MP3-Files (*.mp3)") -#endif - << tr("DrumSynth-Files (*.ds)") - << tr("FLAC-Files (*.flac)") - << tr("SPEEX-Files (*.spx)") - //<< tr("MIDI-Files (*.mid)") - << tr("VOC-Files (*.voc)") - << tr("AIFF-Files (*.aif *.aiff)") - << tr("AU-Files (*.au)") - << tr("RAW-Files (*.raw)") - //<< tr("MOD-Files (*.mod)") - ; - ofd.setNameFilters(types); - if (!m_audioFile.isEmpty()) - { - // select previously opened file - ofd.selectFile(QFileInfo(m_audioFile).fileName()); - } - - if (ofd.exec () == QDialog::Accepted) - { - if (ofd.selectedFiles().isEmpty()) - { - return QString(); - } - return PathUtil::toShortestRelative(ofd.selectedFiles()[0]); - } - - return QString(); -} - - - - -QString SampleBuffer::openAndSetAudioFile() -{ - QString fileName = this->openAudioFile(); - - if(!fileName.isEmpty()) - { - this->setAudioFile(fileName); - } - - return fileName; -} - - -QString SampleBuffer::openAndSetWaveformFile() -{ - if (m_audioFile.isEmpty()) - { - m_audioFile = ConfigManager::inst()->factorySamplesDir() + "waveforms/10saw.flac"; - } - - QString fileName = this->openAudioFile(); - - if (!fileName.isEmpty()) - { - this->setAudioFile(fileName); - } - else - { - m_audioFile = ""; - } - - return fileName; -} - - - -#undef LMMS_HAVE_FLAC_STREAM_ENCODER_H /* not yet... */ -#undef LMMS_HAVE_FLAC_STREAM_DECODER_H - -#ifdef LMMS_HAVE_FLAC_STREAM_ENCODER_H -FLAC__StreamEncoderWriteStatus flacStreamEncoderWriteCallback( - const FLAC__StreamEncoder * /*encoder*/, - const FLAC__byte buffer[], - unsigned int /*samples*/, - unsigned int bytes, - unsigned int /*currentFrame*/, - void * clientData -) -{ -/* if (bytes == 0) - { - return FLAC__STREAM_ENCODER_WRITE_STATUS_OK; - }*/ - return (static_cast(clientData)->write( - (const char *) buffer, bytes) == (int) bytes) - ? FLAC__STREAM_ENCODER_WRITE_STATUS_OK - : FLAC__STREAM_ENCODER_WRITE_STATUS_FATAL_ERROR; -} - - -void flacStreamEncoderMetadataCallback( - const FLAC__StreamEncoder *, - const FLAC__StreamMetadata * metadata, - void * clientData -) -{ - QBuffer * b = static_cast(clientData); - b->seek(0); - b->write((const char *) metadata, sizeof(*metadata)); -} - -#endif // LMMS_HAVE_FLAC_STREAM_ENCODER_H - - - -QString & SampleBuffer::toBase64(QString & dst) const -{ -#ifdef LMMS_HAVE_FLAC_STREAM_ENCODER_H - const f_cnt_t FRAMES_PER_BUF = 1152; - - FLAC__StreamEncoder * flacEnc = FLAC__stream_encoder_new(); - FLAC__stream_encoder_set_channels(flacEnc, DEFAULT_CHANNELS); - FLAC__stream_encoder_set_blocksize(flacEnc, FRAMES_PER_BUF); -/* FLAC__stream_encoder_set_do_exhaustive_model_search(flacEnc, true); - FLAC__stream_encoder_set_do_mid_side_stereo(flacEnc, true);*/ - FLAC__stream_encoder_set_sample_rate(flacEnc, - Engine::audioEngine()->sampleRate()); - - QBuffer baWriter; - baWriter.open(QBuffer::WriteOnly); - - FLAC__stream_encoder_set_write_callback(flacEnc, - flacStreamEncoderWriteCallback); - FLAC__stream_encoder_set_metadata_callback(flacEnc, - flacStreamEncoderMetadataCallback); - FLAC__stream_encoder_set_client_data(flacEnc, &baWriter); - - if (FLAC__stream_encoder_init(flacEnc) != FLAC__STREAM_ENCODER_OK) - { - printf("Error within FLAC__stream_encoder_init()!\n"); - } - - f_cnt_t frameCnt = 0; - - while (frameCnt < m_frames) - { - f_cnt_t remaining = std::min(FRAMES_PER_BUF, m_frames - frameCnt); - FLAC__int32 buf[FRAMES_PER_BUF * DEFAULT_CHANNELS]; - for (f_cnt_t f = 0; f < remaining; ++f) - { - for (ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ++ch) - { - buf[f*DEFAULT_CHANNELS+ch] = (FLAC__int32)( - AudioEngine::clip(m_data[f+frameCnt][ch]) * - OUTPUT_SAMPLE_MULTIPLIER); - } - } - FLAC__stream_encoder_process_interleaved(flacEnc, buf, remaining); - frameCnt += remaining; - } - FLAC__stream_encoder_finish(flacEnc); - FLAC__stream_encoder_delete(flacEnc); - printf("%d %d\n", frameCnt, (int)baWriter.size()); - baWriter.close(); - - base64::encode(baWriter.buffer().data(), baWriter.buffer().size(), dst); - -#else // LMMS_HAVE_FLAC_STREAM_ENCODER_H - - base64::encode((const char *) m_data, - m_frames * sizeof(sampleFrame), dst); - -#endif // LMMS_HAVE_FLAC_STREAM_ENCODER_H - - return dst; -} - - - - -SampleBuffer * SampleBuffer::resample(const sample_rate_t srcSR, const sample_rate_t dstSR ) -{ - sampleFrame * data = m_data; - const f_cnt_t frames = m_frames; - const auto dstFrames = static_cast((frames / (float)srcSR) * (float)dstSR); - auto dstSB = new SampleBuffer(dstFrames); - sampleFrame * dstBuf = dstSB->m_origData; - - // yeah, libsamplerate, let's rock with sinc-interpolation! - int error; - SRC_STATE * state; - if ((state = src_new(SRC_SINC_MEDIUM_QUALITY, DEFAULT_CHANNELS, &error)) != nullptr) - { - SRC_DATA srcData; - srcData.end_of_input = 1; - srcData.data_in = data->data(); - srcData.data_out = dstBuf->data(); - srcData.input_frames = frames; - srcData.output_frames = dstFrames; - srcData.src_ratio = (double) dstSR / srcSR; - if ((error = src_process(state, &srcData))) - { - printf("SampleBuffer: error while resampling: %s\n", src_strerror(error)); - } - src_delete(state); - } - else - { - printf("Error: src_new() failed in SampleBuffer.cpp!\n"); - } - dstSB->update(); - return dstSB; -} - - - - -void SampleBuffer::setAudioFile(const QString & audioFile) -{ - m_audioFile = PathUtil::toShortestRelative(audioFile); - update(); -} - - - -#ifdef LMMS_HAVE_FLAC_STREAM_DECODER_H - -struct flacStreamDecoderClientData -{ - QBuffer * readBuffer; - QBuffer * writeBuffer; -} ; - - - -FLAC__StreamDecoderReadStatus flacStreamDecoderReadCallback( - const FLAC__StreamDecoder * /*decoder*/, - FLAC__byte * buffer, - unsigned int * bytes, - void * clientData -) -{ - int res = static_cast( - clientData)->readBuffer->read((char *) buffer, *bytes); - - if (res > 0) - { - *bytes = res; - return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE; - } - - *bytes = 0; - return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM; -} - - - - -FLAC__StreamDecoderWriteStatus flacStreamDecoderWriteCallback( - const FLAC__StreamDecoder * /*decoder*/, - const FLAC__Frame * frame, - const FLAC__int32 * const buffer[], - void * clientData -) -{ - if (frame->header.channels != 2) - { - printf("channels != 2 in flacStreamDecoderWriteCallback()\n"); - return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; - } - - if (frame->header.bits_per_sample != 16) - { - printf("bits_per_sample != 16 in flacStreamDecoderWriteCallback()\n"); - return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; - } - - const f_cnt_t numberOfFrames = frame->header.blocksize; - for (f_cnt_t f = 0; f < numberOfFrames; ++f) - { - sampleFrame sframe = { buffer[0][f] / OUTPUT_SAMPLE_MULTIPLIER, - buffer[1][f] / OUTPUT_SAMPLE_MULTIPLIER - } ; - static_cast( - clientData )->writeBuffer->write( - (const char *) sframe, sizeof(sframe)); - } - return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; -} - - -void flacStreamDecoderMetadataCallback( - const FLAC__StreamDecoder *, - const FLAC__StreamMetadata *, - void * /*clientData*/ -) -{ - printf("stream decoder metadata callback\n"); -/* QBuffer * b = static_cast(clientData); - b->seek(0); - b->write((const char *) metadata, sizeof(*metadata));*/ -} - - -void flacStreamDecoderErrorCallback( - const FLAC__StreamDecoder *, - FLAC__StreamDecoderErrorStatus status, - void * /*clientData*/ -) -{ - printf("error callback! %d\n", status); - // what to do now?? -} - -#endif // LMMS_HAVE_FLAC_STREAM_DECODER_H - - -void SampleBuffer::loadFromBase64(const QString & data) -{ - char * dst = nullptr; - int dsize = 0; - base64::decode(data, &dst, &dsize); - -#ifdef LMMS_HAVE_FLAC_STREAM_DECODER_H - - QByteArray origData = QByteArray::fromRawData(dst, dsize); - QBuffer baReader(&origData); - baReader.open(QBuffer::ReadOnly); - - QBuffer baWriter; - baWriter.open(QBuffer::WriteOnly); - - flacStreamDecoderClientData cdata = { &baReader, &baWriter } ; - - FLAC__StreamDecoder * flacDec = FLAC__stream_decoder_new(); - - FLAC__stream_decoder_set_read_callback(flacDec, - flacStreamDecoderReadCallback); - FLAC__stream_decoder_set_write_callback(flacDec, - flacStreamDecoderWriteCallback); - FLAC__stream_decoder_set_error_callback(flacDec, - flacStreamDecoderErrorCallback); - FLAC__stream_decoder_set_metadata_callback(flacDec, - flacStreamDecoderMetadataCallback); - FLAC__stream_decoder_set_client_data(flacDec, &cdata); - - FLAC__stream_decoder_init(flacDec); - - FLAC__stream_decoder_process_until_end_of_stream(flacDec); - - FLAC__stream_decoder_finish(flacDec); - FLAC__stream_decoder_delete(flacDec); - - baReader.close(); - - origData = baWriter.buffer(); - printf("%d\n", (int) origData.size()); - - m_origFrames = origData.size() / sizeof(sampleFrame); - MM_FREE(m_origData); - m_origData = MM_ALLOC( m_origFrames); - memcpy(m_origData, origData.data(), origData.size()); - -#else /* LMMS_HAVE_FLAC_STREAM_DECODER_H */ - - m_origFrames = dsize / sizeof(sampleFrame); - MM_FREE(m_origData); - m_origData = MM_ALLOC( m_origFrames); - memcpy(m_origData, dst, dsize); - -#endif // LMMS_HAVE_FLAC_STREAM_DECODER_H - - delete[] dst; - - m_audioFile = QString(); - update(); -} - - - - -void SampleBuffer::setStartFrame(const f_cnt_t s) -{ - m_startFrame = s; -} - - - - -void SampleBuffer::setEndFrame(const f_cnt_t e) -{ - m_endFrame = e; -} - - - - -void SampleBuffer::setAmplification(float a) -{ - m_amplification = a; - emit sampleUpdated(); -} - - - - -void SampleBuffer::setReversed(bool on) -{ - Engine::audioEngine()->requestChangeInModel(); - m_varLock.lockForWrite(); - if (m_reversed != on) { std::reverse(m_data, m_data + m_frames); } - m_reversed = on; - m_varLock.unlock(); - Engine::audioEngine()->doneChangeInModel(); - emit sampleUpdated(); -} - - - - - -SampleBuffer::handleState::handleState(bool varyingPitch, int interpolationMode) : - m_frameIndex(0), - m_varyingPitch(varyingPitch), - m_isBackwards(false) -{ - int error; - m_interpolationMode = interpolationMode; - - if ((m_resamplingData = src_new(interpolationMode, DEFAULT_CHANNELS, &error)) == nullptr) - { - qDebug("Error: src_new() failed in SampleBuffer.cpp!\n"); - } -} - - - - -SampleBuffer::handleState::~handleState() +auto SampleBuffer::emptyBuffer() -> std::shared_ptr { - src_delete(m_resamplingData); + static auto s_buffer = std::make_shared(); + return s_buffer; } } // namespace lmms diff --git a/src/core/SampleClip.cpp b/src/core/SampleClip.cpp index 2febaee2e7f..1a493900d03 100644 --- a/src/core/SampleClip.cpp +++ b/src/core/SampleClip.cpp @@ -25,21 +25,22 @@ #include "SampleClip.h" #include +#include +#include "PathUtil.h" #include "SampleBuffer.h" #include "SampleClipView.h" +#include "SampleLoader.h" #include "SampleTrack.h" #include "TimeLineWidget.h" - namespace lmms { - -SampleClip::SampleClip( Track * _track ) : - Clip( _track ), - m_sampleBuffer( new SampleBuffer ), - m_isPlaying( false ) +SampleClip::SampleClip(Track* _track, Sample sample, bool isPlaying) + : Clip(_track) + , m_sample(std::move(sample)) + , m_isPlaying(false) { saveJournallingState( false ); setSampleFile( "" ); @@ -87,14 +88,14 @@ SampleClip::SampleClip( Track * _track ) : updateTrackClips(); } +SampleClip::SampleClip(Track* track) + : SampleClip(track, Sample(), false) +{ +} + SampleClip::SampleClip(const SampleClip& orig) : - SampleClip(orig.getTrack()) + SampleClip(orig.getTrack(), orig.m_sample, orig.m_isPlaying) { - // TODO: This creates a new SampleBuffer for the new Clip, eating up memory - // & eventually causing performance issues. Letting tracks share buffers - // when they're identical would fix this, but isn't possible right now. - *m_sampleBuffer = *orig.m_sampleBuffer; - m_isPlaying = orig.m_isPlaying; } @@ -107,9 +108,6 @@ SampleClip::~SampleClip() { sampletrack->updateClips(); } - Engine::audioEngine()->requestChangeInModel(); - sharedObject::unref( m_sampleBuffer ); - Engine::audioEngine()->doneChangeInModel(); } @@ -123,33 +121,30 @@ void SampleClip::changeLength( const TimePos & _length ) -const QString & SampleClip::sampleFile() const +const QString& SampleClip::sampleFile() const { - return m_sampleBuffer->audioFile(); + return m_sample.sampleFile(); } - - -void SampleClip::setSampleBuffer( SampleBuffer* sb ) +void SampleClip::setSampleBuffer(std::shared_ptr sb) { - Engine::audioEngine()->requestChangeInModel(); - sharedObject::unref( m_sampleBuffer ); - Engine::audioEngine()->doneChangeInModel(); - m_sampleBuffer = sb; + { + const auto guard = Engine::audioEngine()->requestChangesGuard(); + m_sample = Sample(std::move(sb)); + } updateLength(); emit sampleChanged(); } - - -void SampleClip::setSampleFile(const QString & sf) +void SampleClip::setSampleFile(const QString& sf) { int length = 0; if (!sf.isEmpty()) { - m_sampleBuffer->setAudioFile(sf); + //Otherwise set it to the sample's length + m_sample = Sample(gui::SampleLoader::createBufferFromFile(sf)); length = sampleLength(); } @@ -228,7 +223,7 @@ void SampleClip::updateLength() TimePos SampleClip::sampleLength() const { - return (int)( m_sampleBuffer->frames() / Engine::framesPerTick() ); + return static_cast(m_sample.sampleSize() / Engine::framesPerTick(m_sample.sampleRate())); } @@ -236,7 +231,7 @@ TimePos SampleClip::sampleLength() const void SampleClip::setSampleStartFrame(f_cnt_t startFrame) { - m_sampleBuffer->setStartFrame( startFrame ); + m_sample.setStartFrame(startFrame); } @@ -244,7 +239,7 @@ void SampleClip::setSampleStartFrame(f_cnt_t startFrame) void SampleClip::setSamplePlayLength(f_cnt_t length) { - m_sampleBuffer->setEndFrame( length ); + m_sample.setEndFrame(length); } @@ -267,15 +262,15 @@ void SampleClip::saveSettings( QDomDocument & _doc, QDomElement & _this ) if( sampleFile() == "" ) { QString s; - _this.setAttribute( "data", m_sampleBuffer->toBase64( s ) ); + _this.setAttribute("data", m_sample.toBase64()); } - _this.setAttribute( "sample_rate", m_sampleBuffer->sampleRate()); + _this.setAttribute( "sample_rate", m_sample.sampleRate()); if (const auto& c = color()) { _this.setAttribute("color", c->name()); } - if (m_sampleBuffer->reversed()) + if (m_sample.reversed()) { _this.setAttribute("reversed", "true"); } @@ -291,14 +286,23 @@ void SampleClip::loadSettings( const QDomElement & _this ) { movePosition( _this.attribute( "pos" ).toInt() ); } - setSampleFile( _this.attribute( "src" ) ); - if( sampleFile().isEmpty() && _this.hasAttribute( "data" ) ) + + if (const auto srcFile = _this.attribute("src"); !srcFile.isEmpty()) { - m_sampleBuffer->loadFromBase64( _this.attribute( "data" ) ); - if (_this.hasAttribute("sample_rate")) + if (QFileInfo(PathUtil::toAbsolute(srcFile)).exists()) { - m_sampleBuffer->setSampleRate(_this.attribute("sample_rate").toInt()); + setSampleFile(srcFile); } + else { Engine::getSong()->collectError(QString("%1: %2").arg(tr("Sample not found"), srcFile)); } + } + + if( sampleFile().isEmpty() && _this.hasAttribute( "data" ) ) + { + auto sampleRate = _this.hasAttribute("sample_rate") ? _this.attribute("sample_rate").toInt() : + Engine::audioEngine()->processingSampleRate(); + + auto buffer = gui::SampleLoader::createBufferFromBase64(_this.attribute("data"), sampleRate); + m_sample = Sample(std::move(buffer)); } changeLength( _this.attribute( "len" ).toInt() ); setMuted( _this.attribute( "muted" ).toInt() ); @@ -311,7 +315,7 @@ void SampleClip::loadSettings( const QDomElement & _this ) if(_this.hasAttribute("reversed")) { - m_sampleBuffer->setReversed(true); + m_sample.setReversed(true); emit wasReversed(); // tell SampleClipView to update the view } } diff --git a/src/core/SampleDecoder.cpp b/src/core/SampleDecoder.cpp new file mode 100644 index 00000000000..4db0d438a85 --- /dev/null +++ b/src/core/SampleDecoder.cpp @@ -0,0 +1,184 @@ +#include "SampleDecoder.h" + +#include +#include +#include +#include +#include + +#ifdef LMMS_HAVE_OGGVORBIS +#include +#endif + +#include "AudioEngine.h" +#include "DrumSynth.h" +#include "Engine.h" +#include "lmms_basics.h" + +namespace lmms { + +namespace { + +using Decoder = std::optional(*)(const QString&); + +auto decodeSampleSF(const QString& audioFile) -> std::optional; +auto decodeSampleDS(const QString& audioFile) -> std::optional; +#ifdef LMMS_HAVE_OGGVORBIS +auto decodeSampleOggVorbis(const QString& audioFile) -> std::optional; +#endif + +static constexpr std::array decoders = {&decodeSampleSF, +#ifdef LMMS_HAVE_OGGVORBIS + &decodeSampleOggVorbis, +#endif + &decodeSampleDS}; + +auto decodeSampleSF(const QString& audioFile) -> std::optional +{ + 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)) { return std::nullopt; } + + sndFile = sf_open_fd(file.handle(), SFM_READ, &sfInfo, false); + if (sf_error(sndFile) != 0) { return std::nullopt; } + + 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]}; + } + } + + return SampleDecoder::Result{std::move(result), static_cast(sfInfo.samplerate)}; +} + +auto decodeSampleDS(const QString& audioFile) -> std::optional +{ + // Populated by DrumSynth::GetDSFileSamples + int_sample_t* dataPtr = nullptr; + + auto ds = DrumSynth{}; + const auto engineRate = Engine::audioEngine()->processingSampleRate(); + const auto frames = ds.GetDSFileSamples(audioFile, dataPtr, DEFAULT_CHANNELS, engineRate); + const auto data = std::unique_ptr{dataPtr}; // NOLINT, we have to use a C-style array here + + if (frames <= 0 || !data) { return std::nullopt; } + + auto result = std::vector(frames); + src_short_to_float_array(data.get(), &result[0][0], frames * DEFAULT_CHANNELS); + + return SampleDecoder::Result{std::move(result), static_cast(engineRate)}; +} + +#ifdef LMMS_HAVE_OGGVORBIS +auto decodeSampleOggVorbis(const QString& audioFile) -> std::optional +{ + auto vorbisFile = OggVorbis_File{}; + const auto openError = ov_fopen(audioFile.toLocal8Bit(), &vorbisFile); + + if (openError != 0) { return std::nullopt; } + + const auto vorbisInfo = ov_info(&vorbisFile, -1); + const auto numChannels = vorbisInfo->channels; + const auto sampleRate = vorbisInfo->rate; + const auto numSamples = ov_pcm_total(&vorbisFile, -1); + + auto buffer = std::vector(numSamples); + auto output = static_cast(nullptr); + + auto totalSamplesRead = 0; + while (true) + { + auto samplesRead = ov_read_float(&vorbisFile, &output, numSamples, 0); + + if (samplesRead < 0) { return std::nullopt; } + else if (samplesRead == 0) { break; } + + std::copy_n(*output, samplesRead, buffer.begin() + totalSamplesRead); + totalSamplesRead += samplesRead; + } + + ov_clear(&vorbisFile); + auto result = std::vector(numSamples / numChannels); + for (int i = 0; i < buffer.size(); ++i) + { + if (numChannels == 1) { result[i] = {buffer[i], buffer[i]}; } + else if (numChannels > 1) { result[i] = {buffer[i * numChannels], buffer[i * numChannels + 1]}; } + } + + return SampleDecoder::Result{std::move(result), static_cast(sampleRate)}; +} +#endif // LMMS_HAVE_OGGVORBIS +} // namespace + +auto SampleDecoder::supportedAudioTypes() -> const std::vector& +{ + static const auto s_audioTypes = [] + { + auto types = std::vector(); + + // Add DrumSynth by default since that support comes from us + types.push_back(AudioType{"DrumSynth", "ds"}); + + auto sfFormatInfo = SF_FORMAT_INFO{}; + auto simpleTypeCount = 0; + sf_command(nullptr, SFC_GET_SIMPLE_FORMAT_COUNT, &simpleTypeCount, sizeof(int)); + + // TODO: Ideally, this code should be iterating over the major formats, but some important extensions such as *.ogg + // are not included. This is planned for future versions of sndfile. + for (int simple = 0; simple < simpleTypeCount; ++simple) + { + sfFormatInfo.format = simple; + sf_command(nullptr, SFC_GET_SIMPLE_FORMAT, &sfFormatInfo, sizeof(sfFormatInfo)); + + auto it = std::find_if(types.begin(), types.end(), + [&](const AudioType& type) { return sfFormatInfo.extension == type.extension; }); + if (it != types.end()) { continue; } + + auto name = std::string{sfFormatInfo.extension}; + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char ch) { return std::toupper(ch); }); + + types.push_back(AudioType{std::move(name), sfFormatInfo.extension}); + + return types; + } + + std::sort(types.begin(), types.end(), + [&](const AudioType& a, const AudioType& b) { return a.name < b.name; }); + return types; + }(); + return s_audioTypes; +} + +auto SampleDecoder::decode(const QString& audioFile) -> std::optional +{ + auto result = std::optional{}; + for (const auto& decoder : decoders) + { + result = decoder(audioFile); + if (result) { break; } + } + + return result; +} + +} // namespace lmms diff --git a/src/core/SamplePlayHandle.cpp b/src/core/SamplePlayHandle.cpp index ea27146cb07..61ded132aca 100644 --- a/src/core/SamplePlayHandle.cpp +++ b/src/core/SamplePlayHandle.cpp @@ -35,9 +35,9 @@ namespace lmms { -SamplePlayHandle::SamplePlayHandle( SampleBuffer* sampleBuffer , bool ownAudioPort ) : +SamplePlayHandle::SamplePlayHandle(Sample* sample, bool ownAudioPort) : PlayHandle( Type::SamplePlayHandle ), - m_sampleBuffer( sharedObject::ref( sampleBuffer ) ), + m_sample(sample), m_doneMayReturnTrue( true ), m_frame( 0 ), m_ownAudioPort( ownAudioPort ), @@ -56,16 +56,15 @@ SamplePlayHandle::SamplePlayHandle( SampleBuffer* sampleBuffer , bool ownAudioPo SamplePlayHandle::SamplePlayHandle( const QString& sampleFile ) : - SamplePlayHandle( new SampleBuffer( sampleFile ) , true) + SamplePlayHandle(new Sample(sampleFile), true) { - sharedObject::unref( m_sampleBuffer ); } SamplePlayHandle::SamplePlayHandle( SampleClip* clip ) : - SamplePlayHandle( clip->sampleBuffer() , false) + SamplePlayHandle(&clip->sample(), false) { m_track = clip->getTrack(); setAudioPort( ( (SampleTrack *)clip->getTrack() )->audioPort() ); @@ -76,10 +75,10 @@ SamplePlayHandle::SamplePlayHandle( SampleClip* clip ) : SamplePlayHandle::~SamplePlayHandle() { - sharedObject::unref( m_sampleBuffer ); if( m_ownAudioPort ) { delete audioPort(); + delete m_sample; } } @@ -115,7 +114,7 @@ void SamplePlayHandle::play( sampleFrame * buffer ) m_volumeModel->value() / DefaultVolume } };*/ // SamplePlayHandle always plays the sample at its original pitch; // it is used only for previews, SampleTracks and the metronome. - if (!m_sampleBuffer->play(workingBuffer, &m_state, frames, DefaultBaseFreq)) + if (!m_sample->play(workingBuffer, &m_state, frames, DefaultBaseFreq)) { memset(workingBuffer, 0, frames * sizeof(sampleFrame)); } @@ -145,8 +144,8 @@ bool SamplePlayHandle::isFromTrack( const Track * _track ) const f_cnt_t SamplePlayHandle::totalFrames() const { - return ( m_sampleBuffer->endFrame() - m_sampleBuffer->startFrame() ) * - ( Engine::audioEngine()->processingSampleRate() / m_sampleBuffer->sampleRate() ); + return (m_sample->endFrame() - m_sample->startFrame()) * + (static_cast(Engine::audioEngine()->processingSampleRate()) / m_sample->sampleRate()); } diff --git a/src/core/SampleRecordHandle.cpp b/src/core/SampleRecordHandle.cpp index 10e970b8f3e..6857efa83d5 100644 --- a/src/core/SampleRecordHandle.cpp +++ b/src/core/SampleRecordHandle.cpp @@ -51,13 +51,8 @@ SampleRecordHandle::SampleRecordHandle( SampleClip* clip ) : SampleRecordHandle::~SampleRecordHandle() { - if( !m_buffers.empty() ) - { - SampleBuffer* sb; - createSampleBuffer( &sb ); - m_clip->setSampleBuffer( sb ); - } - + if (!m_buffers.empty()) { m_clip->setSampleBuffer(createSampleBuffer()); } + while( !m_buffers.empty() ) { delete[] m_buffers.front().first; @@ -111,28 +106,22 @@ f_cnt_t SampleRecordHandle::framesRecorded() const -void SampleRecordHandle::createSampleBuffer( SampleBuffer** sampleBuf ) +std::shared_ptr SampleRecordHandle::createSampleBuffer() { const f_cnt_t frames = framesRecorded(); // create buffer to store all recorded buffers in - auto data = new sampleFrame[frames]; - // make sure buffer is cleaned up properly at the end... - sampleFrame * data_ptr = data; - - - assert( data != nullptr ); + auto bigBuffer = std::vector(frames); // now copy all buffers into big buffer - for( bufferList::const_iterator it = m_buffers.begin(); it != m_buffers.end(); ++it ) + auto framesCopied = 0; + for (const auto& [buf, numFrames] : m_buffers) { - memcpy( data_ptr, ( *it ).first, ( *it ).second * - sizeof( sampleFrame ) ); - data_ptr += ( *it ).second; + std::copy_n(buf, numFrames, bigBuffer.begin() + framesCopied); + framesCopied += numFrames; } + // create according sample-buffer out of big buffer - *sampleBuf = new SampleBuffer( data, frames ); - ( *sampleBuf)->setSampleRate( Engine::audioEngine()->inputSampleRate() ); - delete[] data; + return std::make_shared(std::move(bigBuffer), Engine::audioEngine()->inputSampleRate()); } diff --git a/src/core/Track.cpp b/src/core/Track.cpp index 7a664a11e63..13c1d69882d 100644 --- a/src/core/Track.cpp +++ b/src/core/Track.cpp @@ -283,10 +283,9 @@ void Track::loadSettings( const QDomElement & element ) return; } - while( !m_clips.empty() ) { - delete m_clips.front(); -// m_clips.erase( m_clips.begin() ); + auto guard = Engine::audioEngine()->requestChangesGuard(); + deleteClips(); } QDomNode node = element.firstChild(); diff --git a/src/core/audio/AudioSampleRecorder.cpp b/src/core/audio/AudioSampleRecorder.cpp index f60248c5014..b5bbf5a8fe0 100644 --- a/src/core/audio/AudioSampleRecorder.cpp +++ b/src/core/audio/AudioSampleRecorder.cpp @@ -67,32 +67,22 @@ f_cnt_t AudioSampleRecorder::framesRecorded() const return frames; } - - - -void AudioSampleRecorder::createSampleBuffer( SampleBuffer** sampleBuf ) +std::shared_ptr AudioSampleRecorder::createSampleBuffer() { const f_cnt_t frames = framesRecorded(); // create buffer to store all recorded buffers in - auto data = new sampleFrame[frames]; - // make sure buffer is cleaned up properly at the end... - sampleFrame * data_ptr = data; - - - assert( data != nullptr ); + auto bigBuffer = std::vector(frames); // now copy all buffers into big buffer - for( BufferList::ConstIterator it = m_buffers.begin(); - it != m_buffers.end(); ++it ) + auto framesCopied = 0; + for (const auto& [buf, numFrames] : m_buffers) { - memcpy( data_ptr, ( *it ).first, ( *it ).second * - sizeof( sampleFrame ) ); - data_ptr += ( *it ).second; + std::copy_n(buf, numFrames, bigBuffer.begin() + framesCopied); + framesCopied += numFrames; } + // create according sample-buffer out of big buffer - *sampleBuf = new SampleBuffer( data, frames ); - ( *sampleBuf )->setSampleRate( sampleRate() ); - delete[] data; + return std::make_shared(std::move(bigBuffer), sampleRate()); } diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index e050d14bda6..ac010f4f4fa 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -34,7 +34,9 @@ SET(LMMS_SRCS gui/PluginBrowser.cpp gui/ProjectNotes.cpp gui/RowTableView.cpp + gui/SampleLoader.cpp gui/SampleTrackWindow.cpp + gui/SampleWaveform.cpp gui/SendButtonIndicator.cpp gui/SideBar.cpp gui/SideBarWidget.cpp diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index 32f29988b83..89201cb044e 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -57,7 +57,9 @@ #include "PatternStore.h" #include "PluginFactory.h" #include "PresetPreviewPlayHandle.h" +#include "Sample.h" #include "SampleClip.h" +#include "SampleLoader.h" #include "SamplePlayHandle.h" #include "SampleTrack.h" #include "Song.h" @@ -715,9 +717,12 @@ void FileBrowserTreeWidget::previewFileItem(FileItem* file) embed::getIconPixmap("sample_file", 24, 24), 0); // TODO: this can be removed once we do this outside the event thread qApp->processEvents(QEventLoop::ExcludeUserInputEvents); - auto s = new SamplePlayHandle(fileName); - s->setDoneMayReturnTrue(false); - newPPH = s; + if (auto buffer = SampleLoader::createBufferFromFile(fileName)) + { + auto s = new SamplePlayHandle(new lmms::Sample{std::move(buffer)}); + s->setDoneMayReturnTrue(false); + newPPH = s; + } delete tf; } else if ( diff --git a/src/gui/LfoControllerDialog.cpp b/src/gui/LfoControllerDialog.cpp index 77362b169aa..559ac13360c 100644 --- a/src/gui/LfoControllerDialog.cpp +++ b/src/gui/LfoControllerDialog.cpp @@ -31,6 +31,7 @@ #include "Knob.h" #include "TempoSyncKnob.h" #include "PixmapButton.h" +#include "SampleLoader.h" namespace lmms::gui { @@ -210,14 +211,14 @@ LfoControllerDialog::~LfoControllerDialog() void LfoControllerDialog::askUserDefWave() { - SampleBuffer * sampleBuffer = dynamic_cast(this->model())-> - m_userDefSampleBuffer; - QString fileName = sampleBuffer->openAndSetWaveformFile(); - if( fileName.isEmpty() == false ) - { - // TODO: - m_userWaveBtn->setToolTip(sampleBuffer->audioFile()); - } + const auto fileName = SampleLoader::openWaveformFile(); + if (fileName.isEmpty()) { return; } + + auto lfoModel = dynamic_cast(model()); + auto& buffer = lfoModel->m_userDefSampleBuffer; + buffer = SampleLoader::createBufferFromFile(fileName); + + m_userWaveBtn->setToolTip(buffer->audioFile()); } diff --git a/src/gui/SampleLoader.cpp b/src/gui/SampleLoader.cpp new file mode 100644 index 00000000000..f2340852d77 --- /dev/null +++ b/src/gui/SampleLoader.cpp @@ -0,0 +1,126 @@ +/* + * SampleLoader.cpp - Static functions that open audio files + * + * Copyright (c) 2023 saker + * + * 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 "SampleLoader.h" + +#include +#include +#include + +#include "ConfigManager.h" +#include "FileDialog.h" +#include "GuiApplication.h" +#include "PathUtil.h" +#include "SampleDecoder.h" +#include "Song.h" + +namespace lmms::gui { +QString SampleLoader::openAudioFile(const QString& previousFile) +{ + auto openFileDialog = FileDialog(nullptr, QObject::tr("Open audio file")); + auto dir = !previousFile.isEmpty() ? PathUtil::toAbsolute(previousFile) : ConfigManager::inst()->userSamplesDir(); + + // change dir to position of previously opened file + openFileDialog.setDirectory(dir); + openFileDialog.setFileMode(FileDialog::ExistingFiles); + + // set filters + auto fileTypes = QStringList{}; + auto allFileTypes = QStringList{}; + auto nameFilters = QStringList{}; + const auto& supportedAudioTypes = SampleDecoder::supportedAudioTypes(); + + for (const auto& audioType : supportedAudioTypes) + { + const auto name = QString::fromStdString(audioType.name); + const auto extension = QString::fromStdString(audioType.extension); + const auto displayExtension = QString{"*.%1"}.arg(extension); + fileTypes.append(QString{"%1 (%2)"}.arg(FileDialog::tr("%1 files").arg(name), displayExtension)); + allFileTypes.append(displayExtension); + } + + nameFilters.append(QString{"%1 (%2)"}.arg(FileDialog::tr("All audio files"), allFileTypes.join(" "))); + nameFilters.append(fileTypes); + nameFilters.append(QString("%1 (*)").arg(FileDialog::tr("Other files"))); + + openFileDialog.setNameFilters(nameFilters); + + if (!previousFile.isEmpty()) + { + // select previously opened file + openFileDialog.selectFile(QFileInfo{previousFile}.fileName()); + } + + if (openFileDialog.exec() == QDialog::Accepted) + { + if (openFileDialog.selectedFiles().isEmpty()) { return ""; } + + return PathUtil::toShortestRelative(openFileDialog.selectedFiles()[0]); + } + + return ""; +} + +QString SampleLoader::openWaveformFile(const QString& previousFile) +{ + return openAudioFile( + previousFile.isEmpty() ? ConfigManager::inst()->factorySamplesDir() + "waveforms/10saw.flac" : previousFile); +} + +std::shared_ptr SampleLoader::createBufferFromFile(const QString& filePath) +{ + if (filePath.isEmpty()) { return SampleBuffer::emptyBuffer(); } + + try + { + return std::make_shared(filePath); + } + catch (const std::runtime_error& error) + { + if (getGUI()) { displayError(QString::fromStdString(error.what())); } + return SampleBuffer::emptyBuffer(); + } +} + +std::shared_ptr SampleLoader::createBufferFromBase64(const QString& base64, int sampleRate) +{ + if (base64.isEmpty()) { return SampleBuffer::emptyBuffer(); } + + try + { + return std::make_shared(base64, sampleRate); + } + catch (const std::runtime_error& error) + { + if (getGUI()) { displayError(QString::fromStdString(error.what())); } + return SampleBuffer::emptyBuffer(); + } +} + +void SampleLoader::displayError(const QString& message) +{ + QMessageBox::critical(nullptr, QObject::tr("Error loading sample"), message); +} + +} // namespace lmms::gui diff --git a/src/gui/SampleWaveform.cpp b/src/gui/SampleWaveform.cpp new file mode 100644 index 00000000000..5d3afdee318 --- /dev/null +++ b/src/gui/SampleWaveform.cpp @@ -0,0 +1,94 @@ +/* + * SampleWaveform.cpp + * + * Copyright (c) 2023 saker + * + * 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 "SampleWaveform.h" + +namespace lmms::gui { + +void SampleWaveform::visualize(const Sample& sample, QPainter& p, const QRect& dr, int fromFrame, int toFrame) +{ + if (sample.sampleSize() == 0) { return; } + + const auto x = dr.x(); + const auto height = dr.height(); + const auto width = dr.width(); + const auto centerY = dr.center().y(); + + const auto halfHeight = height / 2; + const auto buffer = sample.data() + fromFrame; + + const auto color = p.pen().color(); + const auto rmsColor = color.lighter(123); + + auto numFrames = toFrame - fromFrame; + if (numFrames == 0) { numFrames = sample.sampleSize(); } + + const auto framesPerPixel = std::max(1, numFrames / width); + + constexpr auto maxFramesPerPixel = 512; + const auto resolution = std::max(1, framesPerPixel / maxFramesPerPixel); + const auto framesPerResolution = framesPerPixel / resolution; + + const auto numPixels = std::min(numFrames, width); + auto min = std::vector(numPixels, 1); + auto max = std::vector(numPixels, -1); + auto squared = std::vector(numPixels); + + const auto maxFrames = numPixels * framesPerPixel; + for (int i = 0; i < maxFrames; i += resolution) + { + const auto pixelIndex = i / framesPerPixel; + const auto value = std::accumulate(buffer[i].begin(), buffer[i].end(), 0.0f) / buffer[i].size(); + if (value > max[pixelIndex]) { max[pixelIndex] = value; } + if (value < min[pixelIndex]) { min[pixelIndex] = value; } + squared[pixelIndex] += value * value; + } + + const auto amplification = sample.amplification(); + const auto reversed = sample.reversed(); + + for (int i = 0; i < numPixels; i++) + { + const auto lineY1 = centerY - max[i] * halfHeight * amplification; + const auto lineY2 = centerY - min[i] * halfHeight * amplification; + + auto lineX = i + x; + if (reversed) { lineX = width - lineX; } + + p.drawLine(lineX, lineY1, lineX, lineY2); + + const auto rms = std::sqrt(squared[i] / framesPerResolution); + const auto maxRMS = std::clamp(rms, min[i], max[i]); + const auto minRMS = std::clamp(-rms, min[i], max[i]); + + const auto rmsLineY1 = centerY - maxRMS * halfHeight * amplification; + const auto rmsLineY2 = centerY - minRMS * halfHeight * amplification; + + p.setPen(rmsColor); + p.drawLine(lineX, rmsLineY1, lineX, rmsLineY2); + p.setPen(color); + } +} + +} // namespace lmms::gui diff --git a/src/gui/clips/ClipView.cpp b/src/gui/clips/ClipView.cpp index b2ad5c99cc7..5c8a12b91e3 100644 --- a/src/gui/clips/ClipView.cpp +++ b/src/gui/clips/ClipView.cpp @@ -294,6 +294,17 @@ void ClipView::remove() // delete ourself close(); + + if (m_clip->getTrack()) + { + auto guard = Engine::audioEngine()->requestChangesGuard(); + m_clip->getTrack()->removeClip(m_clip); + } + + // TODO: Clip::~Clip should not be responsible for removing the Clip from the Track. + // One would expect that a call to Track::removeClip would already do that for you, as well + // as actually deleting the Clip with the deleteLater function. That being said, it shouldn't + // be possible to make a Clip without a Track (i.e., Clip::getTrack is never nullptr). m_clip->deleteLater(); } diff --git a/src/gui/clips/SampleClipView.cpp b/src/gui/clips/SampleClipView.cpp index 81bbd271d54..8f31633856f 100644 --- a/src/gui/clips/SampleClipView.cpp +++ b/src/gui/clips/SampleClipView.cpp @@ -32,8 +32,9 @@ #include "AutomationEditor.h" #include "embed.h" #include "PathUtil.h" -#include "SampleBuffer.h" #include "SampleClip.h" +#include "SampleLoader.h" +#include "SampleWaveform.h" #include "Song.h" #include "StringPairDrag.h" @@ -62,9 +63,11 @@ void SampleClipView::updateSample() update(); // set tooltip to filename so that user can see what sample this // sample-clip contains - setToolTip(m_clip->m_sampleBuffer->audioFile() != "" ? - PathUtil::toAbsolute(m_clip->m_sampleBuffer->audioFile()) : - tr( "Double-click to open sample" ) ); + setToolTip( + !m_clip->m_sample.sampleFile().isEmpty() + ? PathUtil::toAbsolute(m_clip->m_sample.sampleFile()) + : tr("Double-click to open sample") + ); } @@ -120,8 +123,7 @@ void SampleClipView::dropEvent( QDropEvent * _de ) } else if( StringPairDrag::decodeKey( _de ) == "sampledata" ) { - m_clip->m_sampleBuffer->loadFromBase64( - StringPairDrag::decodeValue( _de ) ); + m_clip->setSampleBuffer(SampleLoader::createBufferFromBase64(StringPairDrag::decodeValue(_de))); m_clip->updateLength(); update(); _de->accept(); @@ -179,12 +181,12 @@ void SampleClipView::mouseReleaseEvent(QMouseEvent *_me) void SampleClipView::mouseDoubleClickEvent( QMouseEvent * ) { - QString af = m_clip->m_sampleBuffer->openAudioFile(); + QString af = SampleLoader::openAudioFile(); if ( af.isEmpty() ) {} //Don't do anything if no file is loaded - else if ( af == m_clip->m_sampleBuffer->audioFile() ) + else if (af == m_clip->m_sample.sampleFile()) { //Instead of reloading the existing file, just reset the size - int length = (int) ( m_clip->m_sampleBuffer->frames() / Engine::framesPerTick() ); + int length = static_cast(m_clip->m_sample.sampleSize() / Engine::framesPerTick()); m_clip->changeLength(length); } else @@ -267,9 +269,9 @@ void SampleClipView::paintEvent( QPaintEvent * pe ) float offset = m_clip->startTimeOffset() / ticksPerBar * pixelsPerBar(); QRect r = QRect( offset, spacing, qMax( static_cast( m_clip->sampleLength() * ppb / ticksPerBar ), 1 ), rect().bottom() - 2 * spacing ); - m_clip->m_sampleBuffer->visualize( p, r, pe->rect() ); + SampleWaveform::visualize(m_clip->m_sample, p, r); - QString name = PathUtil::cleanName(m_clip->m_sampleBuffer->audioFile()); + QString name = PathUtil::cleanName(m_clip->m_sample.sampleFile()); paintTextLabel(name, p); // disable antialiasing for borders, since its not needed @@ -322,7 +324,7 @@ void SampleClipView::paintEvent( QPaintEvent * pe ) void SampleClipView::reverseSample() { - m_clip->sampleBuffer()->setReversed(!m_clip->sampleBuffer()->reversed()); + m_clip->m_sample.setReversed(!m_clip->m_sample.reversed()); Engine::getSong()->setModified(); update(); } diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index c8ef19b79bd..e6917db9151 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -39,6 +39,7 @@ #include #include "SampleClip.h" +#include "SampleWaveform.h" #ifndef __USE_XOPEN #define __USE_XOPEN @@ -1236,9 +1237,9 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) } // draw ghost sample - if (m_ghostSample != nullptr && m_ghostSample->sampleBuffer()->frames() > 1 && m_renderSample) + if (m_ghostSample != nullptr && m_ghostSample->sample().sampleSize() > 1 && m_renderSample) { - int sampleFrames = m_ghostSample->sampleBuffer()->frames(); + int sampleFrames = m_ghostSample->sample().sampleSize(); int length = static_cast(sampleFrames) / Engine::framesPerTick(); int editorHeight = grid_bottom - TOP_MARGIN; @@ -1248,7 +1249,7 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) int yOffset = (editorHeight - sampleHeight) / 2.0f + TOP_MARGIN; p.setPen(m_ghostSampleColor); - m_ghostSample->sampleBuffer()->visualize(p, QRect(startPos, yOffset, sampleWidth, sampleHeight), 0, sampleFrames); + SampleWaveform::visualize(m_ghostSample->sample(), p, QRect(startPos, yOffset, sampleWidth, sampleHeight), 0, sampleFrames); } // draw ghost notes diff --git a/src/gui/instrument/EnvelopeAndLfoView.cpp b/src/gui/instrument/EnvelopeAndLfoView.cpp index 2a192832601..c3bf53b39a4 100644 --- a/src/gui/instrument/EnvelopeAndLfoView.cpp +++ b/src/gui/instrument/EnvelopeAndLfoView.cpp @@ -28,6 +28,7 @@ #include "EnvelopeAndLfoView.h" #include "EnvelopeAndLfoParameters.h" +#include "SampleLoader.h" #include "embed.h" #include "Engine.h" #include "gui_templates.h" @@ -306,8 +307,7 @@ void EnvelopeAndLfoView::dropEvent( QDropEvent * _de ) QString value = StringPairDrag::decodeValue( _de ); if( type == "samplefile" ) { - m_params->m_userWave.setAudioFile( - StringPairDrag::decodeValue( _de ) ); + m_params->m_userWave = SampleLoader::createBufferFromFile(value); m_userLfoBtn->model()->setValue( true ); m_params->m_lfoWaveModel.setValue(static_cast(EnvelopeAndLfoParameters::LfoShape::UserDefinedWave)); _de->accept(); @@ -316,9 +316,10 @@ void EnvelopeAndLfoView::dropEvent( QDropEvent * _de ) else if( type == QString( "clip_%1" ).arg( static_cast(Track::Type::Sample) ) ) { DataFile dataFile( value.toUtf8() ); - m_params->m_userWave.setAudioFile( dataFile.content(). + auto file = dataFile.content(). firstChildElement().firstChildElement(). - firstChildElement().attribute( "src" ) ); + firstChildElement().attribute("src"); + m_params->m_userWave = SampleLoader::createBufferFromFile(file); m_userLfoBtn->model()->setValue( true ); m_params->m_lfoWaveModel.setValue(static_cast(EnvelopeAndLfoParameters::LfoShape::UserDefinedWave)); _de->accept(); @@ -428,8 +429,6 @@ void EnvelopeAndLfoView::paintEvent( QPaintEvent * ) osc_frames *= 100.0f; } - // userWaveSample() may be used, called out of loop for efficiency - m_params->m_userWave.dataReadLock(); float old_y = 0; for( int x = 0; x <= LFO_GRAPH_W; ++x ) { @@ -465,8 +464,7 @@ void EnvelopeAndLfoView::paintEvent( QPaintEvent * ) val = m_randomGraph; break; case EnvelopeAndLfoParameters::LfoShape::UserDefinedWave: - val = m_params->m_userWave. - userWaveSample( phase ); + val = Oscillator::userWaveSample(m_params->m_userWave.get(), phase); break; } if( static_cast( cur_sample ) <= @@ -481,7 +479,6 @@ void EnvelopeAndLfoView::paintEvent( QPaintEvent * ) graph_y_base + cur_y ) ); old_y = cur_y; } - m_params->m_userWave.dataUnlock(); p.setPen( QColor( 201, 201, 225 ) ); int ms_per_osc = static_cast( SECS_PER_LFO_OSCILLATION * @@ -499,7 +496,7 @@ void EnvelopeAndLfoView::lfoUserWaveChanged() if( static_cast(m_params->m_lfoWaveModel.value()) == EnvelopeAndLfoParameters::LfoShape::UserDefinedWave ) { - if( m_params->m_userWave.frames() <= 1 ) + if (m_params->m_userWave->size() <= 1) { TextFloat::displayMessage( tr( "Hint" ), tr( "Drag and drop a sample into this window." ), diff --git a/src/gui/widgets/Graph.cpp b/src/gui/widgets/Graph.cpp index 9972209a842..922b98668fb 100644 --- a/src/gui/widgets/Graph.cpp +++ b/src/gui/widgets/Graph.cpp @@ -26,6 +26,7 @@ #include #include "Graph.h" +#include "SampleLoader.h" #include "StringPairDrag.h" #include "SampleBuffer.h" #include "Oscillator.h" @@ -588,21 +589,16 @@ void graphModel::setWaveToNoise() QString graphModel::setWaveToUser() { - auto sampleBuffer = new SampleBuffer; - QString fileName = sampleBuffer->openAndSetWaveformFile(); + QString fileName = gui::SampleLoader::openWaveformFile(); if( fileName.isEmpty() == false ) { - sampleBuffer->dataReadLock(); + auto sampleBuffer = gui::SampleLoader::createBufferFromFile(fileName); for( int i = 0; i < length(); i++ ) { - m_samples[i] = sampleBuffer->userWaveSample( - i / static_cast( length() ) ); + m_samples[i] = Oscillator::userWaveSample(sampleBuffer.get(), i / static_cast(length())); } - sampleBuffer->dataUnlock(); } - sharedObject::unref( sampleBuffer ); - emit samplesChanged( 0, length() - 1 ); return fileName; }; diff --git a/src/tracks/SampleTrack.cpp b/src/tracks/SampleTrack.cpp index 876cb307fdd..13050285668 100644 --- a/src/tracks/SampleTrack.cpp +++ b/src/tracks/SampleTrack.cpp @@ -108,10 +108,10 @@ bool SampleTrack::play( const TimePos & _start, const fpp_t _frames, { if( sClip->isPlaying() == false && _start >= (sClip->startPosition() + sClip->startTimeOffset()) ) { - auto bufferFramesPerTick = Engine::framesPerTick (sClip->sampleBuffer ()->sampleRate ()); + auto bufferFramesPerTick = Engine::framesPerTick(sClip->sample().sampleRate()); f_cnt_t sampleStart = bufferFramesPerTick * ( _start - sClip->startPosition() - sClip->startTimeOffset() ); f_cnt_t clipFrameLength = bufferFramesPerTick * ( sClip->endPosition() - sClip->startPosition() - sClip->startTimeOffset() ); - f_cnt_t sampleBufferLength = sClip->sampleBuffer()->frames(); + f_cnt_t sampleBufferLength = sClip->sample().sampleSize(); //if the Clip smaller than the sample length we play only until Clip end //else we play the sample to the end but nothing more f_cnt_t samplePlayLength = clipFrameLength > sampleBufferLength ? sampleBufferLength : clipFrameLength;