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<const SampleBuffer> * 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 commit1d452331d1. 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<float> 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 inc5f7ccba49We 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<Sample> 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 commit1769ed517dsince 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<SampleBuffer> 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 <messmer.dalton@gmail.com> * Update src/gui/clips/SampleClipView.cpp Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com> * Update plugins/SlicerT/SlicerT.cpp Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com> * Update plugins/SlicerT/SlicerT.cpp Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com> * 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 <mrdomclark@gmail.com> * 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<const SampleBuffer> * 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 <daniel.kauss.serna@gmail.com> Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com> Co-authored-by: DomClark <mrdomclark@gmail.com>
This commit is contained in:
64
src/core/AudioResampler.cpp
Normal file
64
src/core/AudioResampler.cpp
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* AudioResampler.cpp - wrapper for libsamplerate
|
||||
*
|
||||
* Copyright (c) 2023 saker <sakertooth@gmail.com>
|
||||
*
|
||||
* 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 <samplerate.h>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
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
|
||||
@@ -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
|
||||
|
||||
@@ -22,13 +22,17 @@
|
||||
*
|
||||
*/
|
||||
|
||||
#include <QDomElement>
|
||||
|
||||
#include "EnvelopeAndLfoParameters.h"
|
||||
|
||||
#include <QDomElement>
|
||||
#include <QFileInfo>
|
||||
|
||||
#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();
|
||||
}
|
||||
|
||||
@@ -23,13 +23,15 @@
|
||||
*
|
||||
*/
|
||||
|
||||
#include <QDomElement>
|
||||
|
||||
|
||||
#include "LfoController.h"
|
||||
#include "AudioEngine.h"
|
||||
#include "Song.h"
|
||||
|
||||
#include <QDomElement>
|
||||
#include <QFileInfo>
|
||||
|
||||
#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<SampleBuffer>())
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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<OscillatorConstants::waveform_t> Oscillator::generateAntiAliasUserWaveTable(const SampleBuffer* sampleBuffer)
|
||||
{
|
||||
if (sampleBuffer->m_userAntiAliasWaveTable == nullptr) {return;}
|
||||
|
||||
auto userAntiAliasWaveTable = std::make_unique<OscillatorConstants::waveform_t>();
|
||||
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<float>(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<Oscillator::WaveShape::UserDefined>(
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
230
src/core/Sample.cpp
Normal file
230
src/core/Sample.cpp
Normal file
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
* Sample.cpp - State for container-class SampleBuffer
|
||||
*
|
||||
* Copyright (c) 2023 saker <sakertooth@gmail.com>
|
||||
*
|
||||
* 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 <QPainter>
|
||||
#include <QRect>
|
||||
|
||||
namespace lmms {
|
||||
|
||||
Sample::Sample(const QString& audioFile)
|
||||
: m_buffer(std::make_shared<SampleBuffer>(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<SampleBuffer>(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<SampleBuffer>(data, numFrames, sampleRate))
|
||||
, m_startFrame(0)
|
||||
, m_endFrame(m_buffer->size())
|
||||
, m_loopStartFrame(0)
|
||||
, m_loopEndFrame(m_buffer->size())
|
||||
{
|
||||
}
|
||||
|
||||
Sample::Sample(std::shared_ptr<const SampleBuffer> 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<float>(Engine::audioEngine()->processingSampleRate()) / m_buffer->sampleRate();
|
||||
resampleRatio *= frequency() / desiredFrequency;
|
||||
|
||||
auto playBuffer = std::vector<sampleFrame>(numFrames / resampleRatio);
|
||||
if (!typeInfo<float>::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<float>(m_buffer->sampleRate()) * 1000;
|
||||
return std::chrono::milliseconds{static_cast<int>(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<int>(state->m_frameIndex - startFrame(), numFrames);
|
||||
copyBufferBackward(dst, state->m_frameIndex, framesToCopy);
|
||||
}
|
||||
else
|
||||
{
|
||||
framesToCopy = std::min<int>(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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,21 +25,22 @@
|
||||
#include "SampleClip.h"
|
||||
|
||||
#include <QDomElement>
|
||||
#include <QFileInfo>
|
||||
|
||||
#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<const SampleBuffer> 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<int>(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
|
||||
}
|
||||
}
|
||||
|
||||
184
src/core/SampleDecoder.cpp
Normal file
184
src/core/SampleDecoder.cpp
Normal file
@@ -0,0 +1,184 @@
|
||||
#include "SampleDecoder.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QString>
|
||||
#include <memory>
|
||||
#include <sndfile.h>
|
||||
|
||||
#ifdef LMMS_HAVE_OGGVORBIS
|
||||
#include <vorbis/vorbisfile.h>
|
||||
#endif
|
||||
|
||||
#include "AudioEngine.h"
|
||||
#include "DrumSynth.h"
|
||||
#include "Engine.h"
|
||||
#include "lmms_basics.h"
|
||||
|
||||
namespace lmms {
|
||||
|
||||
namespace {
|
||||
|
||||
using Decoder = std::optional<SampleDecoder::Result>(*)(const QString&);
|
||||
|
||||
auto decodeSampleSF(const QString& audioFile) -> std::optional<SampleDecoder::Result>;
|
||||
auto decodeSampleDS(const QString& audioFile) -> std::optional<SampleDecoder::Result>;
|
||||
#ifdef LMMS_HAVE_OGGVORBIS
|
||||
auto decodeSampleOggVorbis(const QString& audioFile) -> std::optional<SampleDecoder::Result>;
|
||||
#endif
|
||||
|
||||
static constexpr std::array<Decoder, 3> decoders = {&decodeSampleSF,
|
||||
#ifdef LMMS_HAVE_OGGVORBIS
|
||||
&decodeSampleOggVorbis,
|
||||
#endif
|
||||
&decodeSampleDS};
|
||||
|
||||
auto decodeSampleSF(const QString& audioFile) -> std::optional<SampleDecoder::Result>
|
||||
{
|
||||
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<sample_t>(sfInfo.channels * sfInfo.frames);
|
||||
sf_read_float(sndFile, buf.data(), buf.size());
|
||||
|
||||
sf_close(sndFile);
|
||||
file.close();
|
||||
|
||||
auto result = std::vector<sampleFrame>(sfInfo.frames);
|
||||
for (int i = 0; i < static_cast<int>(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<int>(sfInfo.samplerate)};
|
||||
}
|
||||
|
||||
auto decodeSampleDS(const QString& audioFile) -> std::optional<SampleDecoder::Result>
|
||||
{
|
||||
// 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<int_sample_t[]>{dataPtr}; // NOLINT, we have to use a C-style array here
|
||||
|
||||
if (frames <= 0 || !data) { return std::nullopt; }
|
||||
|
||||
auto result = std::vector<sampleFrame>(frames);
|
||||
src_short_to_float_array(data.get(), &result[0][0], frames * DEFAULT_CHANNELS);
|
||||
|
||||
return SampleDecoder::Result{std::move(result), static_cast<int>(engineRate)};
|
||||
}
|
||||
|
||||
#ifdef LMMS_HAVE_OGGVORBIS
|
||||
auto decodeSampleOggVorbis(const QString& audioFile) -> std::optional<SampleDecoder::Result>
|
||||
{
|
||||
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<float>(numSamples);
|
||||
auto output = static_cast<float**>(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<sampleFrame>(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<int>(sampleRate)};
|
||||
}
|
||||
#endif // LMMS_HAVE_OGGVORBIS
|
||||
} // namespace
|
||||
|
||||
auto SampleDecoder::supportedAudioTypes() -> const std::vector<AudioType>&
|
||||
{
|
||||
static const auto s_audioTypes = []
|
||||
{
|
||||
auto types = std::vector<AudioType>();
|
||||
|
||||
// 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<Result>
|
||||
{
|
||||
auto result = std::optional<Result>{};
|
||||
for (const auto& decoder : decoders)
|
||||
{
|
||||
result = decoder(audioFile);
|
||||
if (result) { break; }
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace lmms
|
||||
@@ -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<float>(Engine::audioEngine()->processingSampleRate()) / m_sample->sampleRate());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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<const SampleBuffer> 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<sampleFrame>(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<const SampleBuffer>(std::move(bigBuffer), Engine::audioEngine()->inputSampleRate());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -67,32 +67,22 @@ f_cnt_t AudioSampleRecorder::framesRecorded() const
|
||||
return frames;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void AudioSampleRecorder::createSampleBuffer( SampleBuffer** sampleBuf )
|
||||
std::shared_ptr<const SampleBuffer> 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<sampleFrame>(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<const SampleBuffer>(std::move(bigBuffer), sampleRate());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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<LfoController*>(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<LfoController*>(model());
|
||||
auto& buffer = lfoModel->m_userDefSampleBuffer;
|
||||
buffer = SampleLoader::createBufferFromFile(fileName);
|
||||
|
||||
m_userWaveBtn->setToolTip(buffer->audioFile());
|
||||
}
|
||||
|
||||
|
||||
|
||||
126
src/gui/SampleLoader.cpp
Normal file
126
src/gui/SampleLoader.cpp
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* SampleLoader.cpp - Static functions that open audio files
|
||||
*
|
||||
* Copyright (c) 2023 saker <sakertooth@gmail.com>
|
||||
*
|
||||
* 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 <QFileInfo>
|
||||
#include <QMessageBox>
|
||||
#include <memory>
|
||||
|
||||
#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<const SampleBuffer> SampleLoader::createBufferFromFile(const QString& filePath)
|
||||
{
|
||||
if (filePath.isEmpty()) { return SampleBuffer::emptyBuffer(); }
|
||||
|
||||
try
|
||||
{
|
||||
return std::make_shared<SampleBuffer>(filePath);
|
||||
}
|
||||
catch (const std::runtime_error& error)
|
||||
{
|
||||
if (getGUI()) { displayError(QString::fromStdString(error.what())); }
|
||||
return SampleBuffer::emptyBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<const SampleBuffer> SampleLoader::createBufferFromBase64(const QString& base64, int sampleRate)
|
||||
{
|
||||
if (base64.isEmpty()) { return SampleBuffer::emptyBuffer(); }
|
||||
|
||||
try
|
||||
{
|
||||
return std::make_shared<SampleBuffer>(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
|
||||
94
src/gui/SampleWaveform.cpp
Normal file
94
src/gui/SampleWaveform.cpp
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* SampleWaveform.cpp
|
||||
*
|
||||
* Copyright (c) 2023 saker <sakertooth@gmail.com>
|
||||
*
|
||||
* 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<float>(numPixels, 1);
|
||||
auto max = std::vector<float>(numPixels, -1);
|
||||
auto squared = std::vector<float>(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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<int>(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<int>( 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();
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
#include <cmath>
|
||||
|
||||
#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<float>(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
|
||||
|
||||
@@ -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<int>(EnvelopeAndLfoParameters::LfoShape::UserDefinedWave));
|
||||
_de->accept();
|
||||
@@ -316,9 +316,10 @@ void EnvelopeAndLfoView::dropEvent( QDropEvent * _de )
|
||||
else if( type == QString( "clip_%1" ).arg( static_cast<int>(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<int>(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<f_cnt_t>( 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<int>( SECS_PER_LFO_OSCILLATION *
|
||||
@@ -499,7 +496,7 @@ void EnvelopeAndLfoView::lfoUserWaveChanged()
|
||||
if( static_cast<EnvelopeAndLfoParameters::LfoShape>(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." ),
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <QPainter>
|
||||
|
||||
#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<float>( length() ) );
|
||||
m_samples[i] = Oscillator::userWaveSample(sampleBuffer.get(), i / static_cast<float>(length()));
|
||||
}
|
||||
sampleBuffer->dataUnlock();
|
||||
}
|
||||
|
||||
sharedObject::unref( sampleBuffer );
|
||||
|
||||
emit samplesChanged( 0, length() - 1 );
|
||||
return fileName;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user