From ce722dd6b6cdfa5c7378161ce7d1fa98bfe5fec2 Mon Sep 17 00:00:00 2001 From: saker Date: Mon, 25 Dec 2023 07:07:11 -0500 Subject: [PATCH] Refactor ``SampleBuffer`` (#6610) * Add refactored SampleBuffer * Add Sample * Add SampleLoader * Integrate changes into AudioSampleRecorder * Integrate changes into Oscillator * Integrate changes into SampleClip/SamplePlayHandle * Integrate changes into Graph * Remove SampleBuffer include from SampleClipView * Integrate changes into Patman * Reduce indirection to sample buffer from Sample * Integrate changes into AudioFileProcessor * Remove old SampleBuffer * Include memory header in TripleOscillator * Include memory header in Oscillator * Use atomic_load within SampleClip::sample * Include memory header in EnvelopeAndLfoParameters * Use std::atomic_load for most calls to Oscillator::userWaveSample * Revert accidental change on SamplePlayHandle L.111 * Check if audio file is empty before loading * Add asserts to Sample * Add cassert include within Sample * Adjust assert expressions in Sample * Remove use of shared ownership for Sample Sample does not need to be wrapped around a std::shared_ptr. This was to work with the audio thread, but the audio thread can instead have their own Sample separate from the UI's Sample, so changes to the UI's Sample would not leave the audio worker thread using freed data if it had pointed to it. * Use ArrayVector in Sample * Enforce std::atomic_load for users of std::shared_ptr * Use requestChangesGuard in ClipView::remove Fixes data race when deleting SampleClip * Revert only formatting changes * Update ClipView::remove comment * Revert "Remove use of shared ownership for Sample" This reverts commit 1d452331d16626ac2f3c42bbd25f1267a61cc116. In some cases, you can infact do away with shared ownership on Sample if there are no writes being made to either of them, but to make sure changes are reflected to the object in cases where writes do happen, they should work with the same one. * Fix heap-use-after-free in Track::loadSettings * Remove m_buffer asserts * Refactor play functionality (again) The responsibility of resampling the buffer and moving the frame index is now in Sample::play, allowing the removal of both playSampleRangeLoop and playSampleRangePingPong. * Change copyright * Cast processingSampleRate to float Fixes division by zero error * Update include/SampleLoader.h Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Update include/SampleLoader.h Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Format SampleLoader.h * Remove SampleBuffer.h include in SampleRecordHandle.h * Update src/core/Oscillator.cpp Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Use typeInfo for float equality comparison Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Use std::min in Sample::visualize Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Move in result to m_data * Use if block in playSampleRange * Pass in unique_ptr to SampleClip::setSampleBuffer * Return const QString& from SampleBuffer::audioFile * Do not pass in unique_ptr by r-value reference * Use isEmpty() within SampleClipView::updateSample * Remove use of atomic_store and atomic_load * Remove ArrayVector comment * Use array specialization for unique_ptr when managing DrumSynth data Also made it so that we don't create result before checking if we failed to decode the file, potentially saving us an allocation. * Don't manually delete Clip if it has a Track * Clean up generateAntiAliasUserWaveTable function Also, make it so that we actually call this function when necessary in TripleOscillator. * Set user wave, even when value is empty If the value or file is empty, I think showing a error popup here is ideal. * Remove whitespace in EnvelopeAndLfoParameters.cpp L#121 * Fix error in c5f7ccba492dd867524156afa652c4eff99f9b40 We still have to delete the Clip's, or else we would just be eating up memory. But we should first make sure that the Track's no longer see this Clip in their m_clips vector. This has to happen as it's own operation because we have to wait for the audio thread(s) first. This would ensure that Track's do not create PlayHandle's that would refer to a Clip that is currently being destroyed. After that, then we call deleteLater on the Clip. * Convert std::shared_ptr to Sample This conversion does not apply to Patman as there seems to be issues with it causing heap-use-after-free issues, such as with PatmanInstrument::unloadCurrentPatch * Fix segfault when closing LMMS Song should be deleted before AudioEngine. * Construct buffer through SampleLoader in FileBrowser's previewFileItem function + Remove const qualification in SamplePlayHandle(const QString&) constructor for m_sample * Move guard out of removeClip and deleteClips + Revert commit 1769ed517da389997b40568f26dd54b800a5d62c since this would fix it anyway (we don't try to lock the engine to delete the global automation track when closing LMMS now) * Simplify the switch in play function for loopMode * Add SampleDecoder * Add LMMS_HAVE_OGGVORBIS comment * Fix unused variable error * Include unordered_map * Simplify SampleDecoder Instead of using the extension (which could be wrong) for the file, we simply loop through all the decoders available. First sndfile because it covers a lot of formats, then the ogg decoder for the few cases where sndfile would not work for certain audio codecs, and then the DrumSynth decoder. * Attempt to fix Mac builds * Attempt to fix Mac builds take 2 * Add vector include to SampleDecoder * Add TODO comment about shared ownership with clips Calls to ClipView::remove may occur at any point, which can cause a problem when the Track is using the clip about to be removed. A suitable solution would be to use shared ownership between the Track and ClipView for the clip. Track's can then simply remove the shared pointer in their m_clips vector, and ClipView can call reset on the shared pointer on calls to ClipView::remove. * Adjust TODO comment Disregard the shared ownership idea. Since we would be modifying the collection of Clip's in Track when removing the Clip, the Track could be iterating said collection while this happens, causing a bug. In this case, we do actually want a synchronization mechanism. However, I didn't mention another separate issue in the TODO comment that should've been addressed: ~Clip should not be responsible for actually removing the itself from it's Track. With calls to removeClip, one would expect that to already occur. * Remove Sample::playbackSize Inside SampleClip::sampleLength, we should be using Sample::sampleSize instead. * Fix issues involving length of Sample's SampleClip::sampleLength should be passing the Sample's sample rate to Engine::framesPerTick. I also changed sampleDuration to return a std::chrono::milliseconds instead of an int so that the callers know what time interval is being used. * Simplify if condition in src/gui/FileBrowser.cpp Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Simplify if condition in src/core/SampleBuffer.cpp Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Update style in include/Oscillator.h Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Format src/core/SampleDecoder.cpp Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Set the sample rate to be that of the AudioEngine by default I also removed some checks involving the state of the SampleBuffer. These functions should expect a valid SampleBuffer each time. This helps to simplify things since we don't have to validate it in each function. * Set single-argument constructors in Sample and SampleBuffer to be explicit * Do not make a copy when reading result from the decoder * Add constructor to pass in vector of sampleFrame's directly * Do a pass by value and move in SampleBuffer.cpp Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Pass vector by value in SampleBuffer.h Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Make Sample(std::shared_ptr) constructor explicit * Properly draw sample waveform when reversed * Collect sample not found errors when loading project Also return empty buffers when trying to load either an empty file or empty Base64 string * Use std::make_unique in SampleLoader * Fix loop modes * Limit sample duration to [start, end] and not the entire buffer * Use structured binding to access buffer * Check if GUI exists before displaying error * Make Base64 constructor pass in the string instead * Remove use of QByteArray::fromBase64Encoding * Inline simple functions in SampleBuffer * Dynamically include supported audio file types * Remove redundant inline specifier Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Translate file types * Cache calls to SampleDecoder::supportedAudioTypes * Fix translations in SampleLoader (again) Also ensure that all the file types are listed first. Also simplified the generation of the list a bit. * Store static local variable for supported audio types instead of in the header Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Clamp frame index depending on loop mode * Inline member functions of PlaybackState * Do not collect errors in SampleLoader when loading projects Also fix conflicts with surrounding codebase * Default construct shared pointers to SampleBuffer * Simplify and optimize Sample::visulaize() * Remove redundant gui:: prefix * Rearrange Sample::visualize after optimizations by DanielKauss * Apply amplification when visualizing sample waveforms * Set default min and max values to 1 and -1 * Treat waveform as mono signal when visualizing * Ensure visualization works when framesPerPixel < 1 * Simplify Sample::visualize a bit more * Fix CPU lag in Sample by using atomics (with relaxed ordering) Changing any of the frame markers originally took a writer lock on a mutex. The problem is that Sample::play took a reader lock first before executing. Because Sample::play has to wait on the writer, this created a lot of lag and raised the CPU meter. The solution would to be to use atomics instead. * Fix errors from merge * Fix broken LFO controller functionality The shared_ptr should have been taken by reference. * Remove TODO * Update EnvelopeAndLfoView.cpp Co-authored-by: Dalton Messmer * Update src/gui/clips/SampleClipView.cpp Co-authored-by: Dalton Messmer * Update plugins/SlicerT/SlicerT.cpp Co-authored-by: Dalton Messmer * Update plugins/SlicerT/SlicerT.cpp Co-authored-by: Dalton Messmer * Store shortest relative path in SampleBuffer * Tie up a few loose ends * Use sample_rate_t when storing sample rate in SampleBuffer * Add missing named requirement functions and aliases * Use sampledata attribute when loading from Base64 in AFP * Remove initializer for m_userWave in the constructor * Do not use trailing return syntax when return is void * Move decoder functionality into unnamed namespace * Remove redundant gui:: prefix * Use PathUtil::toAbsolute to simplify code in SampleLoader::openAudioFile * Fix translations in SampleLoader::openAudioFile Co-authored-by: DomClark * Fix formatting for ternary operator * Remove redundant inlines * Resolve UB when decoding from Base64 data in SampleBuffer * Fix up SampleClip constructors * Add AudioResampler, a wrapper class around libsamplerate The wrapper has only been applied to Sample::PlaybackState for now. AudioResampler should be used by other classes in the future that do resampling with libsamplerate. * Move buffer when moving and simplify assignment functions in Sample * Move Sample::visualize out of Sample and into the GUI namespace * Initialize supportedAudioTypes in static lambda * Return shared pointer from SampleLoader * Create and use static empty SampleBuffer by default * Fix header guard in SampleWaveform.h * Remove use of src_clone CI seems to have an old version of libsamplerate and does not have this method. * Include memory header in SampleBuffer.h * Remove mutex and shared_mutex includes in Sample.h * Attempt to fix string operand error within AudioResampler * Include string header in AudioResampler.cpp * Add LMMS_EXPORT for SampleWaveform class declaration * Add LMMS_EXPORT for AudioResampler class declaration * Enforce returning std::shared_ptr * Restrict the size of the memcpy to the destination size, not the source size * Do not make resample const AudioResampler::resample, while seemingly not changing the data of the resampler, still alters its internal state and therefore should not be const. This is because libsamplerate manages state when resampling. * Initialize data.end_of_input * Add trailing new lines * Simplify AudioResampler interface * Fix header guard prefix to LMMS_GUI instead of LMMS * Remove Sample::resampleSampleRange --------- Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> Co-authored-by: Daniel Kauss Co-authored-by: Dalton Messmer Co-authored-by: DomClark --- include/AudioResampler.h | 64 + include/AudioSampleRecorder.h | 4 +- include/EnvelopeAndLfoParameters.h | 3 +- include/LfoController.h | 2 +- include/Oscillator.h | 30 +- include/Sample.h | 139 ++ include/SampleBuffer.h | 353 +--- include/SampleClip.h | 17 +- include/SampleDecoder.h | 57 + include/SampleLoader.h | 48 + include/SamplePlayHandle.h | 7 +- include/SampleRecordHandle.h | 3 +- include/SampleWaveform.h | 41 + .../AudioFileProcessor/AudioFileProcessor.cpp | 192 +- .../AudioFileProcessor/AudioFileProcessor.h | 16 +- plugins/GigPlayer/GigPlayer.cpp | 4 +- plugins/Patman/Patman.cpp | 26 +- plugins/Patman/Patman.h | 7 +- plugins/SlicerT/SlicerT.cpp | 55 +- plugins/SlicerT/SlicerT.h | 3 +- plugins/SlicerT/SlicerTView.cpp | 5 +- plugins/SlicerT/SlicerTWaveform.cpp | 25 +- plugins/TripleOscillator/TripleOscillator.cpp | 36 +- plugins/TripleOscillator/TripleOscillator.h | 10 +- src/core/AudioResampler.cpp | 64 + src/core/CMakeLists.txt | 3 + src/core/EnvelopeAndLfoParameters.cpp | 25 +- src/core/LfoController.cpp | 29 +- src/core/Oscillator.cpp | 22 +- src/core/Sample.cpp | 230 +++ src/core/SampleBuffer.cpp | 1616 +---------------- src/core/SampleClip.cpp | 88 +- src/core/SampleDecoder.cpp | 184 ++ src/core/SamplePlayHandle.cpp | 17 +- src/core/SampleRecordHandle.cpp | 31 +- src/core/Track.cpp | 5 +- src/core/audio/AudioSampleRecorder.cpp | 26 +- src/gui/CMakeLists.txt | 2 + src/gui/FileBrowser.cpp | 11 +- src/gui/LfoControllerDialog.cpp | 17 +- src/gui/SampleLoader.cpp | 126 ++ src/gui/SampleWaveform.cpp | 94 + src/gui/clips/ClipView.cpp | 11 + src/gui/clips/SampleClipView.cpp | 26 +- src/gui/editors/AutomationEditor.cpp | 7 +- src/gui/instrument/EnvelopeAndLfoView.cpp | 17 +- src/gui/widgets/Graph.cpp | 12 +- src/tracks/SampleTrack.cpp | 4 +- 48 files changed, 1551 insertions(+), 2263 deletions(-) create mode 100644 include/AudioResampler.h create mode 100644 include/Sample.h create mode 100644 include/SampleDecoder.h create mode 100644 include/SampleLoader.h create mode 100644 include/SampleWaveform.h create mode 100644 src/core/AudioResampler.cpp create mode 100644 src/core/Sample.cpp create mode 100644 src/core/SampleDecoder.cpp create mode 100644 src/gui/SampleLoader.cpp create mode 100644 src/gui/SampleWaveform.cpp diff --git a/include/AudioResampler.h b/include/AudioResampler.h new file mode 100644 index 000000000..379146962 --- /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 8937ceb5e..d481cc16c 100644 --- a/include/AudioSampleRecorder.h +++ b/include/AudioSampleRecorder.h @@ -28,6 +28,7 @@ #include #include +#include #include "AudioDevice.h" @@ -44,8 +45,7 @@ public: ~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 7abc3910e..2a8d3a685 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 @@ private: 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 109edbd3f..01b4b1862 100644 --- a/include/LfoController.h +++ b/include/LfoController.h @@ -87,7 +87,7 @@ protected: 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 dab0b948d..a480bf524 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 @@ public: 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 @@ public: 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 @@ public: 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 @@ private: 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 000000000..2ccb78b19 --- /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 3d1013baa..0db8aa4d3 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" +#include "lmms_export.h" - -class QPainter; -class QRect; - -namespace lmms +namespace lmms { +class LMMS_EXPORT SampleBuffer { - -// 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 }; - -class LMMS_EXPORT SampleBuffer : public QObject, public sharedObject -{ - 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(); + 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; - const f_cnt_t frameIndex() const - { - return m_frameIndex; - } + 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()); - void setFrameIndex(f_cnt_t index) - { - m_frameIndex = index; - } + friend void swap(SampleBuffer& first, SampleBuffer& second) noexcept; + auto toBase64() const -> QString; - bool isBackwards() const - { - return m_isBackwards; - } + auto audioFile() const -> const QString& { return m_audioFile; } + auto sampleRate() const -> sample_rate_t { return m_sampleRate; } - void setBackwards(bool backwards) - { - m_isBackwards = backwards; - } + auto begin() -> iterator { return m_data.begin(); } + auto end() -> iterator { return m_data.end(); } - int interpolationMode() const - { - return m_interpolationMode; - } + auto begin() const -> const_iterator { return m_data.begin(); } + auto end() const -> const_iterator { return m_data.end(); } + auto cbegin() const -> const_iterator { return m_data.cbegin(); } + auto cend() const -> const_iterator { return m_data.cend(); } - private: - f_cnt_t m_frameIndex; - const bool m_varyingPitch; - bool m_isBackwards; - SRC_STATE * m_resamplingData; - int m_interpolationMode; + auto rbegin() -> reverse_iterator { return m_data.rbegin(); } + auto rend() -> reverse_iterator { return m_data.rend(); } - friend class SampleBuffer; + auto rbegin() const -> const_reverse_iterator { return m_data.rbegin(); } + auto rend() const -> const_reverse_iterator { return m_data.rend(); } - } ; + auto crbegin() const -> const_reverse_iterator { return m_data.crbegin(); } + auto crend() const -> const_reverse_iterator { return m_data.crend(); } + 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(); } - 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; - } - - inline f_cnt_t endFrame() const - { - return m_endFrame; - } - - inline f_cnt_t loopStartFrame() const - { - return m_loopStartFrame; - } - - inline f_cnt_t loopEndFrame() const - { - return m_loopEndFrame; - } - - void setLoopStartFrame(f_cnt_t start) - { - m_loopStartFrame = start; - } - - void setLoopEndFrame(f_cnt_t end) - { - m_loopEndFrame = 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; - } - - inline f_cnt_t frames() const - { - return m_frames; - } - - inline float amplification() const - { - return m_amplification; - } - - inline bool reversed() const - { - return m_reversed; - } - - inline float frequency() const - { - return m_frequency; - } - - 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 5246787bd..da11996b1 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 @@ public: return "sampleclip"; } - SampleBuffer* sampleBuffer() + Sample& sample() { - return m_sampleBuffer; + return m_sample; } TimePos sampleLength() const; @@ -74,10 +77,10 @@ public: 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 000000000..d7ce076dd --- /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 000000000..7dbdbdc33 --- /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 31b4f0bd5..280010b06 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 @@ public: 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 de1ca19ba..df2d7c772 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 @@ public: 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 000000000..692c1f9cb --- /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 459ff566c..ce3c9c07b 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 4a5f21cc0..80a40c56f 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 @@ public: 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 @@ private: SampleLoop } ; - SampleBuffer& m_sampleBuffer; + Sample* m_sample; QPixmap m_graph; f_cnt_t m_from; f_cnt_t m_to; @@ -266,8 +264,10 @@ private: 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 0713d3100..2d67f0ddf 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 24c54d66b..e525498a5 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 3a15db5f3..8d2c8c657 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 @@ private: 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 2918265ce..c9e75df60 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()) { - QString message = tr("Sample not found: %1").arg(m_originalSample.audioFile()); + auto buffer = gui::SampleLoader::createBufferFromFile(srcFile); + m_originalSample = Sample(std::move(buffer)); + } + else + { + 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 8671eecd1..010985dfc 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 @@ private: 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 833d4b434..bbdb53ccb 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 6685f4f8c..66747036a 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 5b8f6e8ad..d5f2e905f 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 f3290153b..c0258b544 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 @@ private: 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 000000000..5f6b6a239 --- /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 c2dc0bf78..26d458f9e 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 0a9673c8e..611700c51 100644 --- a/src/core/EnvelopeAndLfoParameters.cpp +++ b/src/core/EnvelopeAndLfoParameters.cpp @@ -22,13 +22,17 @@ * */ -#include - #include "EnvelopeAndLfoParameters.h" + +#include +#include + #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 88f64803c..152e0ad8b 100644 --- a/src/core/LfoController.cpp +++ b/src/core/LfoController.cpp @@ -23,13 +23,15 @@ * */ -#include - - #include "LfoController.h" -#include "AudioEngine.h" -#include "Song.h" +#include +#include + +#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 06033b63e..0330fad58 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 000000000..cdb12851f --- /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 2a0076a28..550a9d7bc 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) + if (audioFile.isEmpty()) { throw std::runtime_error{"Failure loading audio file: Audio file path is empty."}; } + const auto absolutePath = PathUtil::toAbsolute(audioFile); + + if (auto decodedResult = SampleDecoder::decode(absolutePath)) { - loadFromBase64(audioFile); - } - else - { - m_audioFile = audioFile; - 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 sampleFrame * data, const f_cnt_t frames) - : SampleBuffer() +SampleBuffer::SampleBuffer(const QString& base64, int sampleRate) + : m_sampleRate(sampleRate) { - if (frames > 0) - { - m_origData = MM_ALLOC( frames); - memcpy(m_origData, data, 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 f_cnt_t frames) - : SampleBuffer() +SampleBuffer::SampleBuffer(std::vector data, int sampleRate) + : m_data(std::move(data)) + , m_sampleRate(sampleRate) { - if (frames > 0) - { - m_origData = MM_ALLOC( frames); - memset(m_origData, 0, frames * BYTES_PER_FRAME); - m_origFrames = frames; - update(); - } } - - - -SampleBuffer::SampleBuffer(const SampleBuffer& orig) -{ - 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) +QString SampleBuffer::toBase64() const { - swap(*this, that); - return *this; + // 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(); } - - - -SampleBuffer::~SampleBuffer() +auto SampleBuffer::emptyBuffer() -> std::shared_ptr { - 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) -{ - return static_cast(udata)->pos(); -} - - - - -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() -{ - 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 ba8420a41..42d4f6441 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( "" ); @@ -81,14 +82,14 @@ SampleClip::SampleClip( Track * _track ) : updateTrackClips(); } -SampleClip::SampleClip(const SampleClip& orig) : - SampleClip(orig.getTrack()) +SampleClip::SampleClip(Track* track) + : SampleClip(track, Sample(), false) +{ +} + +SampleClip::SampleClip(const SampleClip& orig) : + 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; } @@ -101,9 +102,6 @@ SampleClip::~SampleClip() { sampletrack->updateClips(); } - Engine::audioEngine()->requestChangeInModel(); - sharedObject::unref( m_sampleBuffer ); - Engine::audioEngine()->doneChangeInModel(); } @@ -117,33 +115,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(); } @@ -222,7 +217,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())); } @@ -230,7 +225,7 @@ TimePos SampleClip::sampleLength() const void SampleClip::setSampleStartFrame(f_cnt_t startFrame) { - m_sampleBuffer->setStartFrame( startFrame ); + m_sample.setStartFrame(startFrame); } @@ -238,7 +233,7 @@ void SampleClip::setSampleStartFrame(f_cnt_t startFrame) void SampleClip::setSamplePlayLength(f_cnt_t length) { - m_sampleBuffer->setEndFrame( length ); + m_sample.setEndFrame(length); } @@ -261,15 +256,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"); } @@ -285,14 +280,23 @@ void SampleClip::loadSettings( const QDomElement & _this ) { movePosition( _this.attribute( "pos" ).toInt() ); } - setSampleFile( _this.attribute( "src" ) ); + + if (const auto srcFile = _this.attribute("src"); !srcFile.isEmpty()) + { + if (QFileInfo(PathUtil::toAbsolute(srcFile)).exists()) + { + setSampleFile(srcFile); + } + else { Engine::getSong()->collectError(QString("%1: %2").arg(tr("Sample not found"), srcFile)); } + } + if( sampleFile().isEmpty() && _this.hasAttribute( "data" ) ) { - m_sampleBuffer->loadFromBase64( _this.attribute( "data" ) ); - if (_this.hasAttribute("sample_rate")) - { - m_sampleBuffer->setSampleRate(_this.attribute("sample_rate").toInt()); - } + 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() ); @@ -305,7 +309,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 000000000..4db0d438a --- /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 ea27146cb..61ded132a 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 10e970b8f..6857efa83 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 7a664a11e..13c1d6988 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 f60248c50..b5bbf5a8f 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 e050d14bd..ac010f4f4 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 32f29988b..89201cb04 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 77362b169..559ac1336 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 000000000..f2340852d --- /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 000000000..5d3afdee3 --- /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 b2ad5c99c..5c8a12b91 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 81bbd271d..8f3163385 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 e7153bfa3..90881c7ef 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 @@ -1235,9 +1236,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; @@ -1247,7 +1248,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 2a1928326..c3bf53b39 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 9972209a8..922b98668 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 876cb307f..130502856 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;