From 21946135375a4cdb019b6227cf98c304e8d8355f Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 3 Sep 2023 17:11:15 +0200 Subject: [PATCH 01/99] extremly basic slicer, note playback and gui works --- plugins/CMakeLists.txt | 1 + plugins/Slicer/CMakeLists.txt | 5 + plugins/Slicer/SlicerT.cpp | 201 ++++++++++++++++++++++++++++++++++ plugins/Slicer/SlicerT.h | 81 ++++++++++++++ plugins/Slicer/SlicerTUI.cpp | 111 +++++++++++++++++++ plugins/Slicer/SlicerTUI.h | 59 ++++++++++ plugins/Slicer/WaveForm.cpp | 57 ++++++++++ plugins/Slicer/WaveForm.h | 52 +++++++++ plugins/Slicer/logo.png | Bin 0 -> 1109 bytes 9 files changed, 567 insertions(+) create mode 100644 plugins/Slicer/CMakeLists.txt create mode 100644 plugins/Slicer/SlicerT.cpp create mode 100644 plugins/Slicer/SlicerT.h create mode 100644 plugins/Slicer/SlicerTUI.cpp create mode 100644 plugins/Slicer/SlicerTUI.h create mode 100644 plugins/Slicer/WaveForm.cpp create mode 100644 plugins/Slicer/WaveForm.h create mode 100644 plugins/Slicer/logo.png diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 9a71be4b823..9d083dfe1ab 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -18,3 +18,4 @@ INCLUDE_DIRECTORIES( FOREACH(PLUGIN ${PLUGIN_LIST}) ADD_SUBDIRECTORY(${PLUGIN}) ENDFOREACH() +ADD_SUBDIRECTORY("Slicer") diff --git a/plugins/Slicer/CMakeLists.txt b/plugins/Slicer/CMakeLists.txt new file mode 100644 index 00000000000..b93021e98f9 --- /dev/null +++ b/plugins/Slicer/CMakeLists.txt @@ -0,0 +1,5 @@ +INCLUDE(BuildPlugin) + +BUILD_PLUGIN(slicert SlicerT.cpp SlicerT.h SlicerTUI.cpp SlicerTUI.h WaveForm.cpp WaveForm.h MOCFILES SlicerT.h SlicerTUI.h WaveForm.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png") + + diff --git a/plugins/Slicer/SlicerT.cpp b/plugins/Slicer/SlicerT.cpp new file mode 100644 index 00000000000..8d832f33259 --- /dev/null +++ b/plugins/Slicer/SlicerT.cpp @@ -0,0 +1,201 @@ +/* + * Slicer.cpp - instrument which uses a usereditable wavetable + * + * Copyright (c) 2006-2008 Andreas Brandmaier + * + * 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 +#include +#include "SlicerT.h" + + +// #include "AudioEngine.h" +// #include "base64.h" +// #include "Engine.h" +// #include "Graph.h" +#include "InstrumentTrack.h" +// #include "Knob.h" +// #include "LedCheckBox.h" +// #include "NotePlayHandle.h" +// #include "PixmapButton.h" +// #include "Song.h" +// #include "interpolation.h" + + +#include "embed.h" +#include "plugin_export.h" + + + + +namespace lmms +{ + + + +extern "C" +{ + +Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = +{ + LMMS_STRINGIFY( PLUGIN_NAME ), + "SlicerT", + QT_TRANSLATE_NOOP( "PluginBrowser", + "A cool testPlugin" ), + "Daniel Kauss Serna>", + 0x0100, + Plugin::Type::Instrument, + new PluginPixmapLoader( "logo" ), + nullptr, + nullptr, +} ; + +} + + + +SlicerT::SlicerT(InstrumentTrack * _instrument_track) : + Instrument( _instrument_track, &slicert_plugin_descriptor ), + m_sampleBuffer(), + noteThreshold( 0.2f, 0.0f, 1.0f, 0.01f, this, tr( "Note Threshold" ) ) +{ + // connect( ¬eThreshold, SIGNAL( dataChanged() ), this, SLOT( updateParams() ) ); + printf("Correctly loaded SlicerT!\n"); +} + + +void SlicerT::findSlices() { + int c = 0; + float lastAvg = 0; + float currentAvg = 0; + slicePoints = {}; + for (int i = 0; i %f : %f\n", i, currentAvg, lastAvg); + if (abs(currentAvg- lastAvg) > noteThreshold.value()) { + c++; + slicePoints.push_back(i); + } + lastAvg = currentAvg; + currentAvg = 0; + } + + } + // for (int i : slicePoints) { + // //printf("%i\n", i); + // } + + printf("Found %i notes\n", c); + emit dataChanged(); +} + +// void SlicerT::updateParams() { + +// } + +void SlicerT::playNote( NotePlayHandle * _n, sampleFrame * _working_buffer ) { + const fpp_t frames = _n->framesLeftForCurrentPeriod(); + const f_cnt_t offset = _n->noteOffset(); + + // init NotePlayHandle data + if( !_n->m_pluginData ) + { + frameCounter = 0; + _n->m_pluginData = new SampleBuffer::handleState( false, SRC_LINEAR ); + + findSlices(); + + } + + // if play returns true (success I guess) + if( m_sampleBuffer.play( _working_buffer + offset, + (SampleBuffer::handleState *)_n->m_pluginData, + frames, _n->frequency(), + static_cast( 0 ) ) ) + { + frameCounter += frames + offset; + // for (int i = 0;i<256;i++) { + // printf("> %f : %f", _working_buffer[i][0], _working_buffer[i][1]); + // printf("\n"); + // } + // printf("%lu : %lu", sizeof(_working_buffer), sizeof(_working_buffer[0])); + // printf("\n"); + // add the buffer, and then process ???? maybe + applyRelease( _working_buffer, _n ); + instrumentTrack()->processAudioBuffer( _working_buffer, + frames + offset, _n ); + + } + + + + // testing + // sampleFrame testFrame; + // printf("starting print"); + // m_sampleBuffer.play(&testFrame, new SampleBuffer::handleState( false, SRC_LINEAR ), 2, _n->frequency(), static_cast( 0 )); + + + +} + +void SlicerT::updateFile(QString file) { + printf("updated audio file"); + m_sampleBuffer.setAudioFile(file); + findSlices(); +} + + +void SlicerT::saveSettings(QDomDocument & _doc, QDomElement & _parent) {} +void SlicerT::loadSettings( const QDomElement & _this ) {} + +QString SlicerT::nodeName() const +{ + return( slicert_plugin_descriptor.name ); +} + +gui::PluginView * SlicerT::instantiateView( QWidget * _parent ) +{ + return( new gui::SlicerTUI( this, _parent ) ); +} + + + +extern "C" +{ + +// necessary for getting instance out of shared lib +PLUGIN_EXPORT Plugin * lmms_plugin_main( Model *m, void * ) +{ + + return( new SlicerT( static_cast( m ) ) ); +} + + +} + + +} // namespace lmms diff --git a/plugins/Slicer/SlicerT.h b/plugins/Slicer/SlicerT.h new file mode 100644 index 00000000000..3c3b9e06ae8 --- /dev/null +++ b/plugins/Slicer/SlicerT.h @@ -0,0 +1,81 @@ +/* + * Slicer.h - declaration of class Slicer and BSynth which + * are a wavetable synthesizer + * + * Copyright (c) 2006-2008 Andreas Brandmaier + * + * 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 TEST_H +#define TEST_H + +#include "AutomatableModel.h" +#include "Instrument.h" +#include "InstrumentView.h" +#include "SampleBuffer.h" +#include "SlicerTUI.h" + +// #include "Graph.h" +// #include "MemoryManager.h" + + +namespace lmms +{ + +class SlicerT : public Instrument{ + Q_OBJECT + public: + SlicerT(InstrumentTrack * _instrument_track); + ~SlicerT() override = default; + + void playNote( NotePlayHandle * _n, + sampleFrame * _working_buffer ) override; + + + void saveSettings( QDomDocument & _doc, + QDomElement & _parent ) override; + void loadSettings( const QDomElement & _this ) override; + + QString nodeName() const override; + + gui::PluginView * instantiateView( QWidget * _parent ) override; + + public slots: + void updateFile(QString file); + void updateParams(); + + private: + int frameCounter = 0; + SampleBuffer m_sampleBuffer; + FloatModel noteThreshold; + + std::vector slicePoints; + + friend class gui::SlicerTUI; + + void findSlices(); + +}; + + +} + +#endif diff --git a/plugins/Slicer/SlicerTUI.cpp b/plugins/Slicer/SlicerTUI.cpp new file mode 100644 index 00000000000..7ce620fc6ff --- /dev/null +++ b/plugins/Slicer/SlicerTUI.cpp @@ -0,0 +1,111 @@ +#include "SlicerTUI.h" +#include "SlicerT.h" +// #include +#include + + +#include "StringPairDrag.h" +#include "Clipboard.h" +#include "Track.h" +#include "DataFile.h" +// #include "AudioEngine.h" +// #include "base64.h" +// #include "Engine.h" +// #include "Graph.h" +// #include "InstrumentTrack.h" +// #include "Knob.h" +// #include "LedCheckBox.h" +// #include "NotePlayHandle.h" +// #include "PixmapButton.h" +// #include "Song.h" +// #include "interpolation.h" + +// #include +#include +#include + +namespace lmms +{ + + +namespace gui +{ +SlicerTUI::SlicerTUI( SlicerT * _instrument, + QWidget * _parent ) : + InstrumentViewFixedSize( _instrument, _parent ), + noteThresholdKnob(KnobType::Bright26, this), + wf(200, 100, (_instrument->slicePoints), _parent) + +{ + setAcceptDrops( true ); + wf.move(30, 30); + noteThresholdKnob.move(30, 200); + noteThresholdKnob.setModel(&_instrument->noteThreshold); + +} + +void SlicerTUI::mousePressEvent( QMouseEvent * _me ) { + update(); +} + +// all the drag stuff is copied from AudioFileProcessor +void SlicerTUI::dragEnterEvent( QDragEnterEvent * _dee ) +{ + // For mimeType() and MimeType enum class + using namespace Clipboard; + + if( _dee->mimeData()->hasFormat( mimeType( MimeType::StringPair ) ) ) + { + QString txt = _dee->mimeData()->data( + mimeType( MimeType::StringPair ) ); + if( txt.section( ':', 0, 0 ) == QString( "clip_%1" ).arg( + static_cast(Track::Type::Sample) ) ) + { + _dee->acceptProposedAction(); + } + else if( txt.section( ':', 0, 0 ) == "samplefile" ) + { + _dee->acceptProposedAction(); + } + else + { + _dee->ignore(); + } + } + else + { + _dee->ignore(); + } + +} + +void SlicerTUI::dropEvent( QDropEvent * _de ) { + QString type = StringPairDrag::decodeKey( _de ); + QString value = StringPairDrag::decodeValue( _de ); + if( type == "samplefile" ) + { + printf("type: samplefile\n"); + castModel()->updateFile( value ); + wf.updateFile( value ); + // castModel()->setAudioFile( value ); + // _de->accept(); + // set wf wave file + return; + } + else if( type == QString( "clip_%1" ).arg( static_cast(Track::Type::Sample) ) ) + { + printf("type: clip file\n"); + DataFile dataFile( value.toUtf8() ); + castModel()->updateFile( dataFile.content().firstChild().toElement().attribute( "src" ) ); + _de->accept(); + return; + } + + _de->ignore(); +} + + +} +} + + diff --git a/plugins/Slicer/SlicerTUI.h b/plugins/Slicer/SlicerTUI.h new file mode 100644 index 00000000000..2e55acf9913 --- /dev/null +++ b/plugins/Slicer/SlicerTUI.h @@ -0,0 +1,59 @@ +#include +#include "WaveForm.h" +// #include "AutomatableModel.h" +#include "Instrument.h" +#include "InstrumentView.h" +#include "Knob.h" +// #include "Graph.h" +// #include "MemoryManager.h" + + + +#ifndef SLICER_UI_H +#define SLICER_UI_H + +namespace lmms +{ + +// forward declaration, to be able to use SlicerT as a parameter +class SlicerT; + +namespace gui +{ + +// class Knob; +// class LedCheckBox; +// class PixmapButton; + + +class SlicerTUI : public InstrumentViewFixedSize +{ + Q_OBJECT +public: + SlicerTUI( SlicerT * _instrument, + QWidget * _parent ); + ~SlicerTUI() override = default; + +// protected slots: + //void sampleSizeChanged( float _new_sample_length ); + +protected: + virtual void dragEnterEvent( QDragEnterEvent * _dee ); + virtual void dropEvent( QDropEvent * _de ); + virtual void mousePressEvent( QMouseEvent * _me ); + + +private: + Knob noteThresholdKnob; + + WaveForm wf; + + +} ; + + +} // namespace gui + +} // namespace lmms + +#endif \ No newline at end of file diff --git a/plugins/Slicer/WaveForm.cpp b/plugins/Slicer/WaveForm.cpp new file mode 100644 index 00000000000..dd58eb44bed --- /dev/null +++ b/plugins/Slicer/WaveForm.cpp @@ -0,0 +1,57 @@ +#include + +#include "WaveForm.h" + +namespace lmms +{ + + +namespace gui +{ + WaveForm::WaveForm(int _w, int _h, std::vector & _slicePoints, QWidget * _parent) : + QWidget(_parent), + m_graph( QPixmap(_w, _h)), + currentSample(), + slicePoints(_slicePoints) + { + width = _w; + height = _h; + setFixedSize( width, height ); + + m_graph.fill(QColor(11, 11, 11)); + } + + void WaveForm::drawWaveForm() { + m_graph.fill(QColor(11, 11, 11)); + QPainter brush(&m_graph); + brush.setPen(QColor(255, 0, 0)); + currentSample.visualize( + brush, + QRect( 0, 0, m_graph.width(), m_graph.height() ), + 0, currentSample.frames()); + brush.setPen(QColor(0, 255, 0)); + + for (int i = 0;i +#include +#include +#include +#include +#include "SampleBuffer.h" + +#ifndef WAVEFORM_H +#define WAVEFORM_H + +namespace lmms +{ + + +namespace gui +{ + + +class WaveForm : public QWidget { + Q_OBJECT + protected: + // virtual void enterEvent( QEvent * _e ); + // virtual void leaveEvent( QEvent * _e ); + // virtual void mousePressEvent( QMouseEvent * _me ); + // virtual void mouseReleaseEvent( QMouseEvent * _me ); + // virtual void mouseMoveEvent( QMouseEvent * _me ); + // virtual void wheelEvent( QWheelEvent * _we ); + virtual void paintEvent( QPaintEvent * _pe ); + + + private: + + QPixmap m_graph; + + SampleBuffer currentSample; + int width; + int height; + + void drawWaveForm(); + + public: + WaveForm(int _w, int _h, std::vector & _slicePoints, QWidget * _parent); + void updateFile(QString file); + + private: + std::vector & slicePoints; + +}; +}} + + +#endif \ No newline at end of file diff --git a/plugins/Slicer/logo.png b/plugins/Slicer/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2e84cba5a1377f9a65f4b1db243846c05b713dab GIT binary patch literal 1109 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sjKx9jP7LeL$-D$|I14-?iy0WW zg+Z8+Vb&Z8pdfpRr>`sf4NgXJQJvX>d|!Zim;!u4T!B&}8T|kM|KEoPYk)xzQWE4B z3=9-z7FITP4o)6kK7IiqVKH$DDQOv5Ie7&|WffI5bxkcDJ$(ZsV>5FbTRUercTX>G zAOC>Bppe+Ogv8|3w9MSR{F2i0iptu$=C+QmNmCXqHI%PT1Dem6$-r|AKVTYt_};w_U!NXB7K(*S6We|JSFoOiXsz8FdcnrTOt2&-)|dIM-^*%*&JceQt?Pec4@e;H|XO%RORm zJGWT29Px>-t+FOzYLvO;^eNcK04<`;f1yG=XO=pJT7uNUR(9=-MyZ|evNJ5+=Dl~Z4JU&4-aUTUP}up-@@>18x9vZm z`?`a3wX0b1{r8`b`8V%pdUGe>(P~!Sf0F`#aQn*cpRcO&&I6RsJzf1=);T3K0RVE& BY2^R_ literal 0 HcmV?d00001 From 5941766b1aa39629710cf097d3c17e15137a1dac Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 3 Sep 2023 20:23:03 +0200 Subject: [PATCH 02/99] very simple peak detection working --- plugins/Slicer/SlicerT.cpp | 34 +++++++++++++++++++--------------- plugins/Slicer/WaveForm.cpp | 2 +- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/plugins/Slicer/SlicerT.cpp b/plugins/Slicer/SlicerT.cpp index 8d832f33259..5696325796b 100644 --- a/plugins/Slicer/SlicerT.cpp +++ b/plugins/Slicer/SlicerT.cpp @@ -76,7 +76,7 @@ Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = SlicerT::SlicerT(InstrumentTrack * _instrument_track) : Instrument( _instrument_track, &slicert_plugin_descriptor ), m_sampleBuffer(), - noteThreshold( 0.2f, 0.0f, 1.0f, 0.01f, this, tr( "Note Threshold" ) ) + noteThreshold( 0.6f, 0.0f, 2.0f, 0.01f, this, tr( "Note Threshold" ) ) { // connect( ¬eThreshold, SIGNAL( dataChanged() ), this, SLOT( updateParams() ) ); printf("Correctly loaded SlicerT!\n"); @@ -85,29 +85,33 @@ SlicerT::SlicerT(InstrumentTrack * _instrument_track) : void SlicerT::findSlices() { int c = 0; - float lastAvg = 0; - float currentAvg = 0; - slicePoints = {}; + const int window = 1024; + int peakIndex = 0; + float lastPeak = 0; + float currentPeak = 0; + slicePoints = {0}; for (int i = 0; i currentPeak) { + currentPeak = sampleValue; + peakIndex = i; + } + + if (i%window==0) { //printf("%i -> %f : %f\n", i, currentAvg, lastAvg); - if (abs(currentAvg- lastAvg) > noteThreshold.value()) { + if (abs(currentPeak / lastPeak) > 1+noteThreshold.value()) { c++; - slicePoints.push_back(i); + slicePoints.push_back(peakIndex); } - lastAvg = currentAvg; - currentAvg = 0; + lastPeak = currentPeak; + currentPeak = 0; } } - // for (int i : slicePoints) { - // //printf("%i\n", i); - // } + for (int i : slicePoints) { + printf("%i\n", i); + } printf("Found %i notes\n", c); emit dataChanged(); diff --git a/plugins/Slicer/WaveForm.cpp b/plugins/Slicer/WaveForm.cpp index dd58eb44bed..a0bc53ce621 100644 --- a/plugins/Slicer/WaveForm.cpp +++ b/plugins/Slicer/WaveForm.cpp @@ -34,7 +34,7 @@ namespace gui for (int i = 0;i Date: Wed, 6 Sep 2023 03:18:36 +0200 Subject: [PATCH 03/99] basic phase vocoder implementation, no effects yet --- plugins/Slicer/SlicerT.cpp | 389 ++++++++++++++++++++++++++++++++--- plugins/Slicer/SlicerT.h | 19 +- plugins/Slicer/SlicerTUI.cpp | 10 +- plugins/Slicer/SlicerTUI.h | 2 + 4 files changed, 382 insertions(+), 38 deletions(-) diff --git a/plugins/Slicer/SlicerT.cpp b/plugins/Slicer/SlicerT.cpp index 5696325796b..83c5b1b4a4d 100644 --- a/plugins/Slicer/SlicerT.cpp +++ b/plugins/Slicer/SlicerT.cpp @@ -22,23 +22,40 @@ * */ +/* +!! always work on the original sample for slicing and whatever, +only use the sped up sample and slices in playNote(). This will keep things organized + + + +load file / slcies get updated +find slices +find bpm / if bpm gets updated +update matchingSpeed sampleBuffer +update matchingSlices vector + +the matchingSpeed buffer will always get initialzed freshly using a buffer, this seems to be the only way +give it the raw data +*/ // #include #include #include "SlicerT.h" - +#include "fft_helpers.h" +#include // #include "AudioEngine.h" // #include "base64.h" -// #include "Engine.h" +#include "Engine.h" // #include "Graph.h" #include "InstrumentTrack.h" // #include "Knob.h" // #include "LedCheckBox.h" // #include "NotePlayHandle.h" // #include "PixmapButton.h" -// #include "Song.h" +#include "Song.h" // #include "interpolation.h" +#include #include "embed.h" @@ -75,11 +92,13 @@ Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = SlicerT::SlicerT(InstrumentTrack * _instrument_track) : Instrument( _instrument_track, &slicert_plugin_descriptor ), - m_sampleBuffer(), + originalSample(), + timeShiftedSample(), noteThreshold( 0.6f, 0.0f, 2.0f, 0.01f, this, tr( "Note Threshold" ) ) { // connect( ¬eThreshold, SIGNAL( dataChanged() ), this, SLOT( updateParams() ) ); printf("Correctly loaded SlicerT!\n"); + warmupFFT(); } @@ -89,31 +108,38 @@ void SlicerT::findSlices() { int peakIndex = 0; float lastPeak = 0; float currentPeak = 0; + int minWindowsPassed = 0; slicePoints = {0}; - for (int i = 0; i currentPeak) { currentPeak = sampleValue; peakIndex = i; } if (i%window==0) { - //printf("%i -> %f : %f\n", i, currentAvg, lastAvg); - if (abs(currentPeak / lastPeak) > 1+noteThreshold.value()) { + //printf("%i -> %f : %f\n", i, currentAvg, lastAvg); + if (abs(currentPeak / lastPeak) > 1+noteThreshold.value() && minWindowsPassed <= 0) { c++; slicePoints.push_back(peakIndex); + minWindowsPassed = 2; // wait at least one window for a new note } lastPeak = currentPeak; currentPeak = 0; + + + minWindowsPassed--; + } } - for (int i : slicePoints) { - printf("%i\n", i); - } + slicePoints.push_back(originalSample.frames()); + // for (int i : slicePoints) { + // printf("%i\n", i); + // } - printf("Found %i notes\n", c); + // printf("Found %i notes\n", c); emit dataChanged(); } @@ -122,33 +148,42 @@ void SlicerT::findSlices() { // } void SlicerT::playNote( NotePlayHandle * _n, sampleFrame * _working_buffer ) { + const fpp_t frames = _n->framesLeftForCurrentPeriod(); const f_cnt_t offset = _n->noteOffset(); + // 0 at 440hz, or where the square thing is on the keyboard + + // init NotePlayHandle data if( !_n->m_pluginData ) { - frameCounter = 0; _n->m_pluginData = new SampleBuffer::handleState( false, SRC_LINEAR ); - findSlices(); + // 0th element is no sound, so play no sound when out of bounds + // int noteIndex = _n->key() - 68; + // if (noteIndex > slicePoints.size()-2 || noteIndex < 0) { + // noteIndex = 0; + // } + + // int sliceStart = slicePoints[noteIndex]; + // int sliceEnd = slicePoints[noteIndex+1]; + + // m_sampleBuffer.setAllPointFrames( sliceStart, sliceEnd, sliceStart, sliceEnd ); + // printf("%i : %i -> %i\n", noteIndex, sliceStart, sliceEnd); + // printf("%i\n", _n->oldKey()); + + } // if play returns true (success I guess) - if( m_sampleBuffer.play( _working_buffer + offset, + if( timeShiftedSample.play( _working_buffer + offset, (SampleBuffer::handleState *)_n->m_pluginData, - frames, _n->frequency(), + frames, 440, static_cast( 0 ) ) ) { - frameCounter += frames + offset; - // for (int i = 0;i<256;i++) { - // printf("> %f : %f", _working_buffer[i][0], _working_buffer[i][1]); - // printf("\n"); - // } - // printf("%lu : %lu", sizeof(_working_buffer), sizeof(_working_buffer[0])); - // printf("\n"); - // add the buffer, and then process ???? maybe + applyRelease( _working_buffer, _n ); instrumentTrack()->processAudioBuffer( _working_buffer, frames + offset, _n ); @@ -156,20 +191,236 @@ void SlicerT::playNote( NotePlayHandle * _n, sampleFrame * _working_buffer ) { } +} + +// create thimeshifted samplebuffer and timeshifted slicePoints +// http://blogs.zynaptiq.com/bernsee/pitch-shifting-using-the-ft/ +void SlicerT::timeShiftSample() { + + printf("starting sample timeshifting\n"); + + using std::vector; + + + + + bpm_t targetBPM = Engine::getSong()->getTempo();; + // nothing to do here + if (targetBPM == originalBPM) { + timeShiftedSample = SampleBuffer(originalSample.data(), originalSample.frames()); + printf("BPM match for sample, finished timeshift\n"); + return; + } + + - // testing - // sampleFrame testFrame; - // printf("starting print"); - // m_sampleBuffer.play(&testFrame, new SampleBuffer::handleState( false, SRC_LINEAR ), 2, _n->frequency(), static_cast( 0 )); + float sampleRate = originalSample.sampleRate(); + float speedRatio = (double)targetBPM / originalBPM; + int originalFrames = originalSample.frames(); + int targetFrames = originalFrames * speedRatio; + + vector rawData(originalFrames, 0); + vector outData(originalFrames, 0); + vector bufferData(originalFrames, sampleFrame()); + + for (int i = 0;i &dataIn, std::vector &dataOut, float sampleRate, float pitchScale) { + using std::vector; + // processing parameters, lower is faster + const int windowSize = 2048; + const int overSampling = 4; + + // values used + const int stepSize = windowSize / overSampling; + const int windowLatency = windowSize - stepSize; + const float freqPerBin = sampleRate/windowSize; + const float expectedPhase = 2.*M_PI*(float)stepSize/(float)windowSize; + + + // audio data + int inFrames = dataIn.size(); + int outFrames = dataOut.size(); + + // initialize buffers + fftwf_complex FFTSpectrum[windowSize]; + vector IFFTReconstruction(windowSize, 0); + vector allMagnitudes(windowSize, 0); + vector allFrequencies(windowSize, 0); + vector lastPhase(windowSize, 0); + vector sumPhase(windowSize, 0); + + // declare vars + float real, imag, phase, prevPhase, magnitude, freq, deltaPhase = 0; + + // fft plans + fftwf_plan fftPlan; + fftwf_plan ifftPlan; + + + + for (int i = 0;i < inFrames - windowSize;i+=stepSize) { + // FFT + fftPlan = fftwf_plan_dft_r2c_1d(windowSize, dataIn.data() + i, FFTSpectrum, FFTW_MEASURE); + fftwf_execute(fftPlan); + + // analysis step + for (int j = 0; j < windowSize; j++) { + + real = FFTSpectrum[j][0]; + imag = FFTSpectrum[j][1]; + + // printf("freq: %3d %+9.5f %+9.5f I original: %+9.5f \n", j, real, imag, dataIn.at(i+j)); + + magnitude = 2.*sqrt(real*real + imag*imag); + phase = atan2(imag,real); + + freq = phase - lastPhase[j]; // subtract prev pahse to get phase diference + lastPhase[j] = phase; + + freq -= (float)j*expectedPhase; // subtract expected phase + + // some black magic to get into +/- PI interval, revise later pls + long qpd = freq/M_PI; + if (qpd >= 0) qpd += qpd&1; + else qpd -= qpd&1; + freq -= M_PI*(float)qpd; + + freq = (float)overSampling*freq/(2.*M_PI); // idk + + freq = (float)j*freqPerBin + freq*freqPerBin; // "compute the k-th partials' true frequency" ok i guess + + allMagnitudes[j] = magnitude; + allFrequencies[j] = freq; + + } + // effects go here + + + // synthesis, all the operations are the reverse of the analysis + for (int j = 0; j < windowSize; j++) { + magnitude = allMagnitudes[j]; + freq = allFrequencies[j]; + + deltaPhase = freq - (float)j*freqPerBin; + + deltaPhase /= freqPerBin; + + deltaPhase = 2.*M_PI*deltaPhase/overSampling;; + + deltaPhase += (float)j*expectedPhase; + + sumPhase[j] += deltaPhase; + deltaPhase = sumPhase[j]; // this is the bin phase + + FFTSpectrum[j][0] = magnitude*cos(deltaPhase); + FFTSpectrum[j][1] = magnitude*sin(deltaPhase); + } + + // inverse fft + ifftPlan = fftwf_plan_dft_c2r_1d(windowSize, FFTSpectrum, IFFTReconstruction.data(), FFTW_MEASURE); + fftwf_execute(ifftPlan); + + // windowing + for (int j = 0; j < windowSize; j++) { + + if (i+j < windowLatency) { + // calculate amount of windows overlapping + float windowIndex = i + j; + int windowsOverlapping = windowIndex / windowSize * overSampling + 1; + + + // since not all windows overlap, just take the average of the ones that do overlap + // this should be improved + // idk why the 4 is there but it works + dataOut[j+i] += 4/windowsOverlapping*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); + } else { + // this computes the weight of the window on the final output + float window = -0.5f*cos(2.*M_PI*(float)j/(float)windowSize)+0.5f; + dataOut[j+i] += 2.0f*window*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); + } + + + } + + + } + + // normalize + float max = -1; + for (int i = 0;isize();i++) { + max = std::max(max, abs(data[i][0])); + max = std::max(max, abs(data[i][1])); + } + + printf("max: %f\n", max); + + for (int i = 0;isize();i++) { + data[i][0] = 0; + data[i][1] = 0; + } + +} + +void SlicerT::warmupFFT() { + const int dataPoints = 512; + + std::vector warmupData(dataPoints, sqrt(2)); + fftwf_complex fftOut[dataPoints]; + fftwf_plan p = fftwf_plan_dft_r2c_1d(dataPoints, warmupData.data(), fftOut, FFTW_MEASURE); + fftwf_execute(p); + + fftwf_plan d = fftwf_plan_dft_c2r_1d(dataPoints, fftOut, warmupData.data(), FFTW_MEASURE); + fftwf_execute(d); + + fftwf_destroy_plan(p); + fftwf_destroy_plan(d); +} + void SlicerT::updateFile(QString file) { - printf("updated audio file"); - m_sampleBuffer.setAudioFile(file); + printf("updated audio file\n"); + originalSample.setAudioFile(file); findSlices(); + // updateBPM() + timeShiftSample(); + } @@ -203,3 +454,81 @@ PLUGIN_EXPORT Plugin * lmms_plugin_main( Model *m, void * ) } // namespace lmms + + + +/* Implementation of Robust peak detection algorithm +Doesnt work that great, it is too sensitive +https://stackoverflow.com/questions/22583391/peak-signal-detection-in-realtime-timeseries-data/46998001#46998001 +void SlicerT::findSlices() { + int c = 0; + const int lag = 64; + const float influence = 1; + + float peak = 0; + float lastMean = 0; + float lastStd = 0; + int noteDuration = 0; + // init vector with the start of the sample + std::vector lastValues = {}; + + + slicePoints = {}; + for (int i = 0; i noteThreshold.value()) { + if (noteDuration <= 0) { + printf("%f : %f : %f\n", peak, lastMean, lastStd); + // lastValues.push_back(influence*peak + (1-influence)* lastValues.back()); + lastValues.push_back(peak); + lastValues.erase(lastValues.begin()); + c++; + slicePoints.push_back(i); + noteDuration = 20; + } + + } else { + + lastValues.push_back(peak); + lastValues.erase(lastValues.begin()); + + } + noteDuration--; + + lastMean = 0; + for (float v : lastValues) { + lastMean += v; + } + lastMean /= lag; + + lastStd = 0; + for (float v : lastValues) { + lastStd += pow(v - lastMean, 2); + } + + lastStd = sqrt(lastStd / lag); + peak = 0; + } + + + + + } + // for (int i : slicePoints) { + // printf("%i\n", i); + // } + + printf("Found %i notes\n", c); + emit dataChanged(); +} +*/ \ No newline at end of file diff --git a/plugins/Slicer/SlicerT.h b/plugins/Slicer/SlicerT.h index 3c3b9e06ae8..1301f7b0258 100644 --- a/plugins/Slicer/SlicerT.h +++ b/plugins/Slicer/SlicerT.h @@ -32,7 +32,7 @@ #include "InstrumentView.h" #include "SampleBuffer.h" #include "SlicerTUI.h" - +#include // #include "Graph.h" // #include "MemoryManager.h" @@ -63,15 +63,24 @@ class SlicerT : public Instrument{ void updateParams(); private: - int frameCounter = 0; - SampleBuffer m_sampleBuffer; + + FloatModel noteThreshold; + SampleBuffer originalSample; + SampleBuffer timeShiftedSample; std::vector slicePoints; - - friend class gui::SlicerTUI; + std::vector timeShiftedSlices; + + int originalBPM = 140; void findSlices(); + void timeShiftSample(); + void phaseVocoder(std::vector &in, std::vector &out, float sampleRate, float pitchScale); + void normalizeSample(sampleFrame * data); + void warmupFFT(); // runs one fft cycle to generate wisdom + + friend class gui::SlicerTUI; }; diff --git a/plugins/Slicer/SlicerTUI.cpp b/plugins/Slicer/SlicerTUI.cpp index 7ce620fc6ff..7137b52754c 100644 --- a/plugins/Slicer/SlicerTUI.cpp +++ b/plugins/Slicer/SlicerTUI.cpp @@ -34,17 +34,21 @@ SlicerTUI::SlicerTUI( SlicerT * _instrument, QWidget * _parent ) : InstrumentViewFixedSize( _instrument, _parent ), noteThresholdKnob(KnobType::Bright26, this), + slicerTParent(_instrument), wf(200, 100, (_instrument->slicePoints), _parent) { setAcceptDrops( true ); wf.move(30, 30); noteThresholdKnob.move(30, 200); - noteThresholdKnob.setModel(&_instrument->noteThreshold); + noteThresholdKnob.setModel(&slicerTParent->noteThreshold); } void SlicerTUI::mousePressEvent( QMouseEvent * _me ) { + slicerTParent->findSlices(); + slicerTParent->timeShiftSample(); + update(); } @@ -85,7 +89,7 @@ void SlicerTUI::dropEvent( QDropEvent * _de ) { if( type == "samplefile" ) { printf("type: samplefile\n"); - castModel()->updateFile( value ); + slicerTParent->updateFile( value ); wf.updateFile( value ); // castModel()->setAudioFile( value ); // _de->accept(); @@ -96,7 +100,7 @@ void SlicerTUI::dropEvent( QDropEvent * _de ) { { printf("type: clip file\n"); DataFile dataFile( value.toUtf8() ); - castModel()->updateFile( dataFile.content().firstChild().toElement().attribute( "src" ) ); + slicerTParent->updateFile( dataFile.content().firstChild().toElement().attribute( "src" ) ); _de->accept(); return; } diff --git a/plugins/Slicer/SlicerTUI.h b/plugins/Slicer/SlicerTUI.h index 2e55acf9913..7965dc32f94 100644 --- a/plugins/Slicer/SlicerTUI.h +++ b/plugins/Slicer/SlicerTUI.h @@ -44,6 +44,8 @@ class SlicerTUI : public InstrumentViewFixedSize private: + SlicerT * slicerTParent; + Knob noteThresholdKnob; WaveForm wf; From 1000e1a3960f5638098ea544a88e74b44bcacaf4 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Wed, 6 Sep 2023 06:03:27 +0200 Subject: [PATCH 04/99] phase vocoder slight rewrite --- plugins/Slicer/SlicerT.cpp | 73 ++++++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 22 deletions(-) diff --git a/plugins/Slicer/SlicerT.cpp b/plugins/Slicer/SlicerT.cpp index 83c5b1b4a4d..8dfab8fdbda 100644 --- a/plugins/Slicer/SlicerT.cpp +++ b/plugins/Slicer/SlicerT.cpp @@ -98,7 +98,7 @@ SlicerT::SlicerT(InstrumentTrack * _instrument_track) : { // connect( ¬eThreshold, SIGNAL( dataChanged() ), this, SLOT( updateParams() ) ); printf("Correctly loaded SlicerT!\n"); - warmupFFT(); + // warmupFFT(); // maybe } @@ -222,8 +222,8 @@ void SlicerT::timeShiftSample() { vector rawData(originalFrames, 0); - vector outData(originalFrames, 0); - vector bufferData(originalFrames, sampleFrame()); + vector outData(originalFrames*2, 0); + vector bufferData(originalFrames*2, sampleFrame()); for (int i = 0;i &dataIn, std::vector &dataO using std::vector; // processing parameters, lower is faster const int windowSize = 2048; - const int overSampling = 4; + const int overSampling = 16; + // audio data + int inFrames = dataIn.size(); + int outFrames = dataIn.size(); + + float lengthRatio = 1;// inFrames / outFrames; + // values used const int stepSize = windowSize / overSampling; - const int windowLatency = windowSize - stepSize; + const int numWindows = inFrames / stepSize; + const int outStepSize = lengthRatio * stepSize; + const int windowLatency = (overSampling-1)*outStepSize; const float freqPerBin = sampleRate/windowSize; const float expectedPhase = 2.*M_PI*(float)stepSize/(float)windowSize; - - // audio data - int inFrames = dataIn.size(); - int outFrames = dataOut.size(); + + printf("frames: %i ,step size: %i , out step size: %i , ratio: %f\n", inFrames, stepSize, outStepSize, lengthRatio); + printf("frames: %i , maxFrames: %i \n", inFrames, (numWindows-overSampling) * stepSize + windowSize); + printf("will drop %i\n", inFrames%(inFrames/numWindows)); + printf("pitch shift: %f", noteThreshold.value()); // initialize buffers + fftwf_complex FFTSpectrum[windowSize]; + vector FFTInput(windowSize, 0); vector IFFTReconstruction(windowSize, 0); vector allMagnitudes(windowSize, 0); vector allFrequencies(windowSize, 0); + vector processedFreq(windowSize, 0); + vector processedMagn(windowSize, 0); vector lastPhase(windowSize, 0); vector sumPhase(windowSize, 0); // declare vars - float real, imag, phase, prevPhase, magnitude, freq, deltaPhase = 0; + float real, imag, phase, magnitude, freq, deltaPhase = 0; + int inWindowIndex = 0; + int outWindowIndex = 0; // fft plans fftwf_plan fftPlan; + fftPlan = fftwf_plan_dft_r2c_1d(windowSize, FFTInput.data(), FFTSpectrum, FFTW_MEASURE); fftwf_plan ifftPlan; + ifftPlan = fftwf_plan_dft_c2r_1d(windowSize, FFTSpectrum, IFFTReconstruction.data(), FFTW_MEASURE); + // remove oversampling, because the actual window is overSampling* bigger than stepsize + for (int i = 0;i < numWindows-overSampling;i++) { + inWindowIndex = i * stepSize; + outWindowIndex = i * outStepSize; - for (int i = 0;i < inFrames - windowSize;i+=stepSize) { // FFT - fftPlan = fftwf_plan_dft_r2c_1d(windowSize, dataIn.data() + i, FFTSpectrum, FFTW_MEASURE); + memcpy(FFTInput.data(), dataIn.data() + inWindowIndex, windowSize*sizeof(float)); + fftwf_execute(fftPlan); // analysis step @@ -312,8 +333,17 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO allFrequencies[j] = freq; } - // effects go here + // pitch shifting + // memset(processedFreq.data(), 0, processedFreq.size()*sizeof(float)); + // memset(processedMagn.data(), 0, processedFreq.size()*sizeof(float)); + // for (int j = 0; j < windowSize/2; j++) { + // int index = j*noteThreshold.value(); + // if (index <= windowSize/2) { + // processedMagn[index] += allMagnitudes[j]; + // processedFreq[index] = allFrequencies[j] * noteThreshold.value(); + // } + // } // synthesis, all the operations are the reverse of the analysis for (int j = 0; j < windowSize; j++) { @@ -336,26 +366,25 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO } // inverse fft - ifftPlan = fftwf_plan_dft_c2r_1d(windowSize, FFTSpectrum, IFFTReconstruction.data(), FFTW_MEASURE); fftwf_execute(ifftPlan); // windowing for (int j = 0; j < windowSize; j++) { - if (i+j < windowLatency) { + float windowIndex = outWindowIndex + j; + if (windowIndex < windowLatency) { // calculate amount of windows overlapping - float windowIndex = i + j; + int windowsOverlapping = windowIndex / windowSize * overSampling + 1; // since not all windows overlap, just take the average of the ones that do overlap - // this should be improved - // idk why the 4 is there but it works - dataOut[j+i] += 4/windowsOverlapping*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); + // this should be improved, at least simplyfiy the expression + dataOut[windowIndex] += overSampling/windowsOverlapping*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); } else { // this computes the weight of the window on the final output float window = -0.5f*cos(2.*M_PI*(float)j/(float)windowSize)+0.5f; - dataOut[j+i] += 2.0f*window*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); + dataOut[windowIndex] += 2.0f*window*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); } @@ -366,7 +395,7 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO // normalize float max = -1; - for (int i = 0;i warmupData(dataPoints, sqrt(2)); fftwf_complex fftOut[dataPoints]; From 85a6e361dc98d731c1f58d4c7e38f27a3ed78a3c Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Wed, 6 Sep 2023 06:37:22 +0200 Subject: [PATCH 05/99] pitch shifting works more or less --- plugins/Slicer/SlicerT.cpp | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/plugins/Slicer/SlicerT.cpp b/plugins/Slicer/SlicerT.cpp index 8dfab8fdbda..347b465c0b6 100644 --- a/plugins/Slicer/SlicerT.cpp +++ b/plugins/Slicer/SlicerT.cpp @@ -236,7 +236,7 @@ void SlicerT::timeShiftSample() { bufferData.data()[i][1] = outData[i]; } - timeShiftedSample = SampleBuffer(bufferData.data(), originalFrames); + timeShiftedSample = *(SampleBuffer(bufferData.data(), originalFrames).resample(sampleRate , sampleRate * 1.1f)); printf("finished sample timeshifting\n"); @@ -247,7 +247,7 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO using std::vector; // processing parameters, lower is faster const int windowSize = 2048; - const int overSampling = 16; + const int overSampling = 32; // audio data int inFrames = dataIn.size(); @@ -334,21 +334,22 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO } // pitch shifting - // memset(processedFreq.data(), 0, processedFreq.size()*sizeof(float)); - // memset(processedMagn.data(), 0, processedFreq.size()*sizeof(float)); - // for (int j = 0; j < windowSize/2; j++) { - // int index = j*noteThreshold.value(); - // if (index <= windowSize/2) { - // processedMagn[index] += allMagnitudes[j]; - // processedFreq[index] = allFrequencies[j] * noteThreshold.value(); - // } + memset(processedFreq.data(), 0, processedFreq.size()*sizeof(float)); + memset(processedMagn.data(), 0, processedFreq.size()*sizeof(float)); + for (int j = 0; j < windowSize/2; j++) { + int index = j*noteThreshold.value(); + if (index <= windowSize/2) { + processedMagn[index] += allMagnitudes[j]; + processedFreq[index] = allFrequencies[j]* noteThreshold.value(); + } + } + - // } // synthesis, all the operations are the reverse of the analysis for (int j = 0; j < windowSize; j++) { - magnitude = allMagnitudes[j]; - freq = allFrequencies[j]; + magnitude = processedMagn[j]; + freq = processedFreq[j]; deltaPhase = freq - (float)j*freqPerBin; From 7ca35af4de10bf077ada97e1e094ebde482a2af4 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Wed, 6 Sep 2023 22:00:46 +0200 Subject: [PATCH 06/99] basic timeshift working --- plugins/Slicer/SlicerT.cpp | 104 +++++++++++++++++++++++++------------ 1 file changed, 70 insertions(+), 34 deletions(-) diff --git a/plugins/Slicer/SlicerT.cpp b/plugins/Slicer/SlicerT.cpp index 347b465c0b6..c45ad42f3a1 100644 --- a/plugins/Slicer/SlicerT.cpp +++ b/plugins/Slicer/SlicerT.cpp @@ -217,10 +217,7 @@ void SlicerT::timeShiftSample() { float sampleRate = originalSample.sampleRate(); float speedRatio = (double)targetBPM / originalBPM; int originalFrames = originalSample.frames(); - int targetFrames = originalFrames * speedRatio; - - vector rawData(originalFrames, 0); vector outData(originalFrames*2, 0); vector bufferData(originalFrames*2, sampleFrame()); @@ -231,12 +228,13 @@ void SlicerT::timeShiftSample() { phaseVocoder(rawData, outData, sampleRate, 1); - for (int i = 0;i &dataIn, std::vector &dataO // audio data int inFrames = dataIn.size(); - int outFrames = dataIn.size(); + int outFrames = dataOut.size(); - float lengthRatio = 1;// inFrames / outFrames; + float lengthRatio = noteThreshold.value();// inFrames / outFrames; // values used const int stepSize = windowSize / overSampling; const int numWindows = inFrames / stepSize; - const int outStepSize = lengthRatio * stepSize; - const int windowLatency = (overSampling-1)*outStepSize; + const int windowLatency = (overSampling-1)*stepSize; const float freqPerBin = sampleRate/windowSize; const float expectedPhase = 2.*M_PI*(float)stepSize/(float)windowSize; - printf("frames: %i ,step size: %i , out step size: %i , ratio: %f\n", inFrames, stepSize, outStepSize, lengthRatio); + printf("frames: %i , out frames: %i , ratio: %f\n", inFrames, outFrames, (float)outFrames / inFrames); printf("frames: %i , maxFrames: %i \n", inFrames, (numWindows-overSampling) * stepSize + windowSize); printf("will drop %i\n", inFrames%(inFrames/numWindows)); - printf("pitch shift: %f", noteThreshold.value()); + printf("pitch shift: %f\n", noteThreshold.value()); // initialize buffers @@ -281,10 +278,11 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO vector lastPhase(windowSize, 0); vector sumPhase(windowSize, 0); + vector outBuffer(inFrames, 0); + // declare vars float real, imag, phase, magnitude, freq, deltaPhase = 0; - int inWindowIndex = 0; - int outWindowIndex = 0; + int windowIndex = 0; // fft plans fftwf_plan fftPlan; @@ -295,11 +293,10 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO // remove oversampling, because the actual window is overSampling* bigger than stepsize for (int i = 0;i < numWindows-overSampling;i++) { - inWindowIndex = i * stepSize; - outWindowIndex = i * outStepSize; + windowIndex = i * stepSize; // FFT - memcpy(FFTInput.data(), dataIn.data() + inWindowIndex, windowSize*sizeof(float)); + memcpy(FFTInput.data(), dataIn.data() + windowIndex, windowSize*sizeof(float)); fftwf_execute(fftPlan); @@ -333,14 +330,20 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO allFrequencies[j] = freq; } + + // pitch shifting + // takes all the values that are below the nyquist frequency (representable with our samplerate) + // nyquist frequency = samplerate / 2 + // and moves them to a different bin + // improve for larger pitch shift memset(processedFreq.data(), 0, processedFreq.size()*sizeof(float)); memset(processedMagn.data(), 0, processedFreq.size()*sizeof(float)); for (int j = 0; j < windowSize/2; j++) { - int index = j*noteThreshold.value(); + int index = (float)j * noteThreshold.value(); if (index <= windowSize/2) { processedMagn[index] += allMagnitudes[j]; - processedFreq[index] = allFrequencies[j]* noteThreshold.value(); + processedFreq[index] = allFrequencies[j] * noteThreshold.value(); } } @@ -370,43 +373,76 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO fftwf_execute(ifftPlan); // windowing + // this is very bad, audible click at the beginning if we take the average, + // but else there is a windowSized delay... + // solution would be to take the average but blend it together better + // pls improve for (int j = 0; j < windowSize; j++) { - float windowIndex = outWindowIndex + j; - if (windowIndex < windowLatency) { + float outIndex = windowIndex + j; + if (outIndex < 0) { // calculate amount of windows overlapping - - int windowsOverlapping = windowIndex / windowSize * overSampling + 1; + float windowsOverlapping = outIndex / windowSize * overSampling + 1; - // since not all windows overlap, just take the average of the ones that do overlap - // this should be improved, at least simplyfiy the expression - dataOut[windowIndex] += overSampling/windowsOverlapping*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); + outBuffer[outIndex] += overSampling/windowsOverlapping*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); + + // no averaging, probably worse + // dataOut[outIndex] = overSampling*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); } else { // this computes the weight of the window on the final output float window = -0.5f*cos(2.*M_PI*(float)j/(float)windowSize)+0.5f; - dataOut[windowIndex] += 2.0f*window*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); - } + outBuffer[outIndex] += 2.0f*window*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); + } } } + fftwf_destroy_plan(fftPlan); + fftwf_destroy_plan(ifftPlan); + // normalize float max = -1; - for (int i = 0;i Date: Thu, 7 Sep 2023 06:15:47 +0200 Subject: [PATCH 07/99] PV timeshift working (no pitch shift) --- plugins/Slicer/SlicerT.cpp | 85 +++++++++++++++----------------------- 1 file changed, 33 insertions(+), 52 deletions(-) diff --git a/plugins/Slicer/SlicerT.cpp b/plugins/Slicer/SlicerT.cpp index c45ad42f3a1..c81951c1f82 100644 --- a/plugins/Slicer/SlicerT.cpp +++ b/plugins/Slicer/SlicerT.cpp @@ -240,25 +240,35 @@ void SlicerT::timeShiftSample() { } - +// basic phase vocoder implementation that time shifts without pitch change +// a lot of stuff needs improvement, mostly the windowing and +// the pitch shifting implemention void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataOut, float sampleRate, float pitchScale) { using std::vector; // processing parameters, lower is faster - const int windowSize = 2048; - const int overSampling = 32; + // lower windows size seems to work better for time scaling, + // this is because the step site is scaled, but not the window size + // this causes slight timing differences between windows + // oversampling is better if higher always (probably) + const int windowSize = 512; + const int overSampling = 64; // audio data int inFrames = dataIn.size(); int outFrames = dataOut.size(); - float lengthRatio = noteThreshold.value();// inFrames / outFrames; + float lengthRatio = noteThreshold.value() + 0.001f;// inFrames / outFrames; // values used const int stepSize = windowSize / overSampling; + const int outStepSize = stepSize * lengthRatio; const int numWindows = inFrames / stepSize; const int windowLatency = (overSampling-1)*stepSize; const float freqPerBin = sampleRate/windowSize; - const float expectedPhase = 2.*M_PI*(float)stepSize/(float)windowSize; + // very important + const float expectedPhaseIn = 2.*M_PI*(float)stepSize/(float)windowSize; + const float expectedPhaseOut = 2.*M_PI*(float)outStepSize/(float)windowSize; + printf("frames: %i , out frames: %i , ratio: %f\n", inFrames, outFrames, (float)outFrames / inFrames); @@ -267,7 +277,6 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO printf("pitch shift: %f\n", noteThreshold.value()); // initialize buffers - fftwf_complex FFTSpectrum[windowSize]; vector FFTInput(windowSize, 0); vector IFFTReconstruction(windowSize, 0); @@ -278,7 +287,7 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO vector lastPhase(windowSize, 0); vector sumPhase(windowSize, 0); - vector outBuffer(inFrames, 0); + vector outBuffer(outFrames, 0); // declare vars float real, imag, phase, magnitude, freq, deltaPhase = 0; @@ -314,7 +323,7 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO freq = phase - lastPhase[j]; // subtract prev pahse to get phase diference lastPhase[j] = phase; - freq -= (float)j*expectedPhase; // subtract expected phase + freq -= (float)j*expectedPhaseIn; // subtract expected phase // some black magic to get into +/- PI interval, revise later pls long qpd = freq/M_PI; @@ -337,22 +346,22 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO // nyquist frequency = samplerate / 2 // and moves them to a different bin // improve for larger pitch shift - memset(processedFreq.data(), 0, processedFreq.size()*sizeof(float)); - memset(processedMagn.data(), 0, processedFreq.size()*sizeof(float)); - for (int j = 0; j < windowSize/2; j++) { - int index = (float)j * noteThreshold.value(); - if (index <= windowSize/2) { - processedMagn[index] += allMagnitudes[j]; - processedFreq[index] = allFrequencies[j] * noteThreshold.value(); - } - } + // memset(processedFreq.data(), 0, processedFreq.size()*sizeof(float)); + // memset(processedMagn.data(), 0, processedFreq.size()*sizeof(float)); + // for (int j = 0; j < windowSize/2; j++) { + // int index = (float)j;// * noteThreshold.value(); + // if (index <= windowSize/2) { + // processedMagn[index] += allMagnitudes[j]; + // processedFreq[index] = allFrequencies[j];// * noteThreshold.value(); + // } + // } // synthesis, all the operations are the reverse of the analysis for (int j = 0; j < windowSize; j++) { - magnitude = processedMagn[j]; - freq = processedFreq[j]; + magnitude = allMagnitudes[j]; + freq = allFrequencies[j]; deltaPhase = freq - (float)j*freqPerBin; @@ -360,7 +369,7 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO deltaPhase = 2.*M_PI*deltaPhase/overSampling;; - deltaPhase += (float)j*expectedPhase; + deltaPhase += (float)j*expectedPhaseOut; sumPhase[j] += deltaPhase; deltaPhase = sumPhase[j]; // this is the bin phase @@ -379,7 +388,7 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO // pls improve for (int j = 0; j < windowSize; j++) { - float outIndex = windowIndex + j; + float outIndex = i * outStepSize + j; if (outIndex < 0) { // calculate amount of windows overlapping float windowsOverlapping = outIndex / windowSize * overSampling + 1; @@ -406,47 +415,19 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO // normalize float max = -1; - for (int i = 0;i Date: Fri, 8 Sep 2023 02:20:01 +0200 Subject: [PATCH 08/99] basic functions work (UI, editing, playback) --- plugins/Slicer/SlicerT.cpp | 372 ++++++++++++++++------------------- plugins/Slicer/SlicerT.h | 19 +- plugins/Slicer/SlicerTUI.cpp | 24 ++- plugins/Slicer/SlicerTUI.h | 9 +- plugins/Slicer/WaveForm.cpp | 194 ++++++++++++++++-- plugins/Slicer/WaveForm.h | 62 ++++-- 6 files changed, 437 insertions(+), 243 deletions(-) diff --git a/plugins/Slicer/SlicerT.cpp b/plugins/Slicer/SlicerT.cpp index c81951c1f82..f3756621896 100644 --- a/plugins/Slicer/SlicerT.cpp +++ b/plugins/Slicer/SlicerT.cpp @@ -38,26 +38,24 @@ the matchingSpeed buffer will always get initialzed freshly using a buffer, this give it the raw data */ +// TODO: add, remove slices; add gui values (fade out, window sizes?); fix timeshift lag (bpm change) +// TODO: midi dragging, open file button +// TODO: fix empty sample, small sample edge cases, general stability stuff + // #include -#include #include "SlicerT.h" +#include + #include "fft_helpers.h" #include // #include "AudioEngine.h" // #include "base64.h" #include "Engine.h" -// #include "Graph.h" #include "InstrumentTrack.h" -// #include "Knob.h" -// #include "LedCheckBox.h" -// #include "NotePlayHandle.h" -// #include "PixmapButton.h" #include "Song.h" -// #include "interpolation.h" #include - #include "embed.h" #include "plugin_export.h" @@ -68,7 +66,6 @@ namespace lmms { - extern "C" { @@ -94,11 +91,99 @@ SlicerT::SlicerT(InstrumentTrack * _instrument_track) : Instrument( _instrument_track, &slicert_plugin_descriptor ), originalSample(), timeShiftedSample(), - noteThreshold( 0.6f, 0.0f, 2.0f, 0.01f, this, tr( "Note Threshold" ) ) + noteThreshold( 0.6f, 0.0f, 2.0f, 0.01f, this, tr( "Note Threshold" ) ), + originalBPM(1, 1, 999, this, tr("original sample bpm")) { - // connect( ¬eThreshold, SIGNAL( dataChanged() ), this, SLOT( updateParams() ) ); + // connect( ¬eThreshold, SIGNAL( dataChanged() ), this, SLOT( updateSlices() ) ); + // TODO: either button to timeshift, threading or generating samples on the fly + connect( &originalBPM, SIGNAL( dataChanged() ), this, SLOT( updateTimeShift() ) ); + printf("Correctly loaded SlicerT!\n"); - // warmupFFT(); // maybe +} + +void SlicerT::playNote( NotePlayHandle * _n, sampleFrame * _working_buffer ) { + const fpp_t frames = _n->framesLeftForCurrentPeriod(); + const int playedFrames = _n->totalFramesPlayed(); + const f_cnt_t offset = _n->noteOffset(); + + + + int totalFrames = timeShiftedSample.frames(); + int sliceStart = timeShiftedSample.startFrame(); + int sliceEnd = timeShiftedSample.endFrame(); + int sliceFrames = timeShiftedSample.endFrame() - timeShiftedSample.startFrame(); + int noteFramesLeft = sliceFrames - playedFrames; + + int fadeOutFrames = (float)sliceFrames/4; + + // init NotePlayHandle data + if( !_n->m_pluginData ) + { + _n->m_pluginData = new SampleBuffer::handleState( false, SRC_LINEAR ); + + float speedRatio = (float)originalBPM.value() / Engine::getSong()->getTempo() ; + int noteIndex = _n->key() - 69; + int sliceStart, sliceEnd; + + // 0th element is no sound, so play full sample + if (noteIndex > slicePoints.size()-2 || noteIndex < 0) { + sliceStart = 0; + sliceEnd = timeShiftedSample.frames(); + } else { + sliceStart = slicePoints[noteIndex] * speedRatio; + sliceEnd = slicePoints[noteIndex+1] * speedRatio; + } + + + timeShiftedSample.setAllPointFrames( sliceStart, sliceEnd, sliceStart, sliceEnd ); + + printf("%i : %i -> %i\n", noteIndex, sliceStart, sliceEnd); + printf("%i\n", _n->oldKey()); + + + } + + + // if play returns true (success I guess) + if( ! _n->isFinished() ) { + if( timeShiftedSample.play( _working_buffer + offset, + (SampleBuffer::handleState *)_n->m_pluginData, + frames, 440, + static_cast( 0 ) ) ) + { + + applyRelease( _working_buffer, _n ); + instrumentTrack()->processAudioBuffer( _working_buffer, + frames + offset, _n ); + + // exponential fade out + if (noteFramesLeft < fadeOutFrames) { + // printf("fade out started. frames: %i, framesLeft: %i\n", frames, noteFramesLeft); + for (int i = 0;i %f : %f\n", i, currentAvg, lastAvg); if (abs(currentPeak / lastPeak) > 1+noteThreshold.value() && minWindowsPassed <= 0) { c++; - slicePoints.push_back(peakIndex); + slicePoints.push_back(std::max(0, peakIndex-window/2)); // slight offset minWindowsPassed = 2; // wait at least one window for a new note } lastPeak = currentPeak; @@ -135,11 +220,7 @@ void SlicerT::findSlices() { } slicePoints.push_back(originalSample.frames()); - // for (int i : slicePoints) { - // printf("%i\n", i); - // } - - // printf("Found %i notes\n", c); + emit dataChanged(); } @@ -147,100 +228,100 @@ void SlicerT::findSlices() { // } -void SlicerT::playNote( NotePlayHandle * _n, sampleFrame * _working_buffer ) { - - const fpp_t frames = _n->framesLeftForCurrentPeriod(); - const f_cnt_t offset = _n->noteOffset(); +// find the bpm of the sample by assuming its in 4/4 time signature , +// and lies in the 100 - 200 bpm range +void SlicerT::findBPM() { + int bpmSnap = 1; - // 0 at 440hz, or where the square thing is on the keyboard - - - // init NotePlayHandle data - if( !_n->m_pluginData ) - { - _n->m_pluginData = new SampleBuffer::handleState( false, SRC_LINEAR ); - - // 0th element is no sound, so play no sound when out of bounds - // int noteIndex = _n->key() - 68; - // if (noteIndex > slicePoints.size()-2 || noteIndex < 0) { - // noteIndex = 0; - // } - - // int sliceStart = slicePoints[noteIndex]; - // int sliceEnd = slicePoints[noteIndex+1]; - - // m_sampleBuffer.setAllPointFrames( sliceStart, sliceEnd, sliceStart, sliceEnd ); + float sampleRate = originalSample.sampleRate(); + float totalFrames = originalSample.frames(); + float sampleLength = totalFrames / sampleRate; - // printf("%i : %i -> %i\n", noteIndex, sliceStart, sliceEnd); - // printf("%i\n", _n->oldKey()); + // this assumes the sample has a time signature of x/4 + float bpmEstimate = 240.0f / sampleLength; - + // deal with samlpes that are not 1 bar long + while (bpmEstimate < 100) { + bpmEstimate *= 2; } - // if play returns true (success I guess) - if( timeShiftedSample.play( _working_buffer + offset, - (SampleBuffer::handleState *)_n->m_pluginData, - frames, 440, - static_cast( 0 ) ) ) - { - - applyRelease( _working_buffer, _n ); - instrumentTrack()->processAudioBuffer( _working_buffer, - frames + offset, _n ); - + while (bpmEstimate > 200) { + bpmEstimate /= 2; } + // snap bpm + int bpm = bpmEstimate; + bpm += (float)bpmSnap / 2; + bpm -= bpm % bpmSnap; -} + originalBPM.setValue(bpm); + } // create thimeshifted samplebuffer and timeshifted slicePoints -// http://blogs.zynaptiq.com/bernsee/pitch-shifting-using-the-ft/ void SlicerT::timeShiftSample() { - - printf("starting sample timeshifting\n"); - using std::vector; + printf("starting sample timeshifting\n"); + // original sample data + float sampleRate = originalSample.sampleRate(); + int originalFrames = originalSample.frames(); + // target data TODO: fix this mess + bpm_t targetBPM = Engine::getSong()->getTempo(); + float speedRatio = (float)originalBPM.value() / targetBPM ; + int samplesPerBeat = 60.0f / targetBPM * sampleRate; + int outFrames = speedRatio * originalFrames; + // snap to a beat, this should be in the UI + outFrames += (float)samplesPerBeat; + outFrames -= outFrames%samplesPerBeat; - bpm_t targetBPM = Engine::getSong()->getTempo();; // nothing to do here - if (targetBPM == originalBPM) { + if (targetBPM == originalBPM.value()) { timeShiftedSample = SampleBuffer(originalSample.data(), originalSample.frames()); printf("BPM match for sample, finished timeshift\n"); return; } - - - float sampleRate = originalSample.sampleRate(); - float speedRatio = (double)targetBPM / originalBPM; - int originalFrames = originalSample.frames(); - + // buffers vector rawData(originalFrames, 0); - vector outData(originalFrames*2, 0); - vector bufferData(originalFrames*2, sampleFrame()); + vector outData(outFrames, 0); + vector bufferData(outFrames, sampleFrame()); + + // copy channels for processing for (int i = 0;i &dataIn, std::vector &dataOut, float sampleRate, float pitchScale) { @@ -249,15 +330,17 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO // lower windows size seems to work better for time scaling, // this is because the step site is scaled, but not the window size // this causes slight timing differences between windows + // sadly, lower windowsize also reduces audio quality in general + // TODO: find solution // oversampling is better if higher always (probably) const int windowSize = 512; - const int overSampling = 64; + const int overSampling = 32; // audio data int inFrames = dataIn.size(); int outFrames = dataOut.size(); - float lengthRatio = noteThreshold.value() + 0.001f;// inFrames / outFrames; + float lengthRatio = (float)outFrames / inFrames; // values used const int stepSize = windowSize / overSampling; @@ -270,11 +353,8 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO const float expectedPhaseOut = 2.*M_PI*(float)outStepSize/(float)windowSize; - printf("frames: %i , out frames: %i , ratio: %f\n", inFrames, outFrames, (float)outFrames / inFrames); - printf("frames: %i , maxFrames: %i \n", inFrames, (numWindows-overSampling) * stepSize + windowSize); printf("will drop %i\n", inFrames%(inFrames/numWindows)); - printf("pitch shift: %f\n", noteThreshold.value()); // initialize buffers fftwf_complex FFTSpectrum[windowSize]; @@ -299,14 +379,12 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO fftwf_plan ifftPlan; ifftPlan = fftwf_plan_dft_c2r_1d(windowSize, FFTSpectrum, IFFTReconstruction.data(), FFTW_MEASURE); - // remove oversampling, because the actual window is overSampling* bigger than stepsize for (int i = 0;i < numWindows-overSampling;i++) { windowIndex = i * stepSize; // FFT memcpy(FFTInput.data(), dataIn.data() + windowIndex, windowSize*sizeof(float)); - fftwf_execute(fftPlan); // analysis step @@ -315,8 +393,6 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO real = FFTSpectrum[j][0]; imag = FFTSpectrum[j][1]; - // printf("freq: %3d %+9.5f %+9.5f I original: %+9.5f \n", j, real, imag, dataIn.at(i+j)); - magnitude = 2.*sqrt(real*real + imag*imag); phase = atan2(imag,real); @@ -340,8 +416,7 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO } - - // pitch shifting + // TODO: pitch shifting // takes all the values that are below the nyquist frequency (representable with our samplerate) // nyquist frequency = samplerate / 2 // and moves them to a different bin @@ -356,8 +431,6 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO // } // } - - // synthesis, all the operations are the reverse of the analysis for (int j = 0; j < windowSize; j++) { magnitude = allMagnitudes[j]; @@ -385,11 +458,15 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO // this is very bad, audible click at the beginning if we take the average, // but else there is a windowSized delay... // solution would be to take the average but blend it together better - // pls improve + // TODO: make better for (int j = 0; j < windowSize; j++) { - + float outIndex = i * outStepSize + j; - if (outIndex < 0) { + if (outIndex > outFrames) { + printf("too long window size, breaking\n"); + break; + } + if (outIndex < windowLatency) { // calculate amount of windows overlapping float windowsOverlapping = outIndex / windowSize * overSampling + 1; @@ -430,46 +507,24 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO } -void SlicerT::normalizeSample(sampleFrame * data) { - float max = -1; - for (int i = 0;isize();i++) { - max = std::max(max, abs(data[i][0])); - max = std::max(max, abs(data[i][1])); - } - - printf("max: %f\n", max); - - for (int i = 0;isize();i++) { - data[i][0] = 0; - data[i][1] = 0; - } - -} - -void SlicerT::warmupFFT() { - const int dataPoints = 1024; - - std::vector warmupData(dataPoints, sqrt(2)); - fftwf_complex fftOut[dataPoints]; - fftwf_plan p = fftwf_plan_dft_r2c_1d(dataPoints, warmupData.data(), fftOut, FFTW_MEASURE); - fftwf_execute(p); - fftwf_plan d = fftwf_plan_dft_c2r_1d(dataPoints, fftOut, warmupData.data(), FFTW_MEASURE); - fftwf_execute(d); - - fftwf_destroy_plan(p); - fftwf_destroy_plan(d); -} void SlicerT::updateFile(QString file) { printf("updated audio file\n"); originalSample.setAudioFile(file); findSlices(); - // updateBPM() - timeShiftSample(); + findBPM(); // this also updates timeshift because bpm change } +void SlicerT::updateSlices() { + findSlices(); +} + +void SlicerT::updateTimeShift() { + timeShiftSample(); +} + void SlicerT::saveSettings(QDomDocument & _doc, QDomElement & _parent) {} void SlicerT::loadSettings( const QDomElement & _this ) {} @@ -502,80 +557,3 @@ PLUGIN_EXPORT Plugin * lmms_plugin_main( Model *m, void * ) } // namespace lmms - - -/* Implementation of Robust peak detection algorithm -Doesnt work that great, it is too sensitive -https://stackoverflow.com/questions/22583391/peak-signal-detection-in-realtime-timeseries-data/46998001#46998001 -void SlicerT::findSlices() { - int c = 0; - const int lag = 64; - const float influence = 1; - - float peak = 0; - float lastMean = 0; - float lastStd = 0; - int noteDuration = 0; - // init vector with the start of the sample - std::vector lastValues = {}; - - - slicePoints = {}; - for (int i = 0; i noteThreshold.value()) { - if (noteDuration <= 0) { - printf("%f : %f : %f\n", peak, lastMean, lastStd); - // lastValues.push_back(influence*peak + (1-influence)* lastValues.back()); - lastValues.push_back(peak); - lastValues.erase(lastValues.begin()); - c++; - slicePoints.push_back(i); - noteDuration = 20; - } - - } else { - - lastValues.push_back(peak); - lastValues.erase(lastValues.begin()); - - } - noteDuration--; - - lastMean = 0; - for (float v : lastValues) { - lastMean += v; - } - lastMean /= lag; - - lastStd = 0; - for (float v : lastValues) { - lastStd += pow(v - lastMean, 2); - } - - lastStd = sqrt(lastStd / lag); - peak = 0; - } - - - - - } - // for (int i : slicePoints) { - // printf("%i\n", i); - // } - - printf("Found %i notes\n", c); - emit dataChanged(); -} -*/ \ No newline at end of file diff --git a/plugins/Slicer/SlicerT.h b/plugins/Slicer/SlicerT.h index 1301f7b0258..549464e21ab 100644 --- a/plugins/Slicer/SlicerT.h +++ b/plugins/Slicer/SlicerT.h @@ -24,8 +24,8 @@ */ -#ifndef TEST_H -#define TEST_H +#ifndef SLICERT_H +#define SLICERT_H #include "AutomatableModel.h" #include "Instrument.h" @@ -60,27 +60,30 @@ class SlicerT : public Instrument{ public slots: void updateFile(QString file); - void updateParams(); + void updateTimeShift(); + void updateSlices(); + + signals: + void isPlaying( float current, float start, float end ); private: FloatModel noteThreshold; + IntModel originalBPM = 140; SampleBuffer originalSample; SampleBuffer timeShiftedSample; std::vector slicePoints; - std::vector timeShiftedSlices; - int originalBPM = 140; - + void findSlices(); + void findBPM(); void timeShiftSample(); void phaseVocoder(std::vector &in, std::vector &out, float sampleRate, float pitchScale); - void normalizeSample(sampleFrame * data); - void warmupFFT(); // runs one fft cycle to generate wisdom friend class gui::SlicerTUI; + friend class gui::WaveForm; }; diff --git a/plugins/Slicer/SlicerTUI.cpp b/plugins/Slicer/SlicerTUI.cpp index 7137b52754c..7afd12dcd0c 100644 --- a/plugins/Slicer/SlicerTUI.cpp +++ b/plugins/Slicer/SlicerTUI.cpp @@ -23,6 +23,7 @@ // #include #include #include +#include "embed.h" namespace lmms { @@ -35,19 +36,34 @@ SlicerTUI::SlicerTUI( SlicerT * _instrument, InstrumentViewFixedSize( _instrument, _parent ), noteThresholdKnob(KnobType::Bright26, this), slicerTParent(_instrument), - wf(200, 100, (_instrument->slicePoints), _parent) + wf(245, 125, _instrument, this), + bpmBox(3, "21pink", this), + resetButton(embed::getIconPixmap("reload"), QString(), this) { setAcceptDrops( true ); - wf.move(30, 30); + + wf.move(2, 5); + + resetButton.move(100, 150); + connect(&resetButton, SIGNAL( clicked() ), slicerTParent, SLOT( updateSlices() )); + connect(&resetButton, SIGNAL( clicked() ), &wf, SLOT( updateUI() )); + + bpmBox.move(30, 150); + bpmBox.setToolTip(tr("original sample BPM")); + bpmBox.setLabel(tr("BPM")); + bpmBox.setModel(&slicerTParent->originalBPM); + noteThresholdKnob.move(30, 200); noteThresholdKnob.setModel(&slicerTParent->noteThreshold); } void SlicerTUI::mousePressEvent( QMouseEvent * _me ) { - slicerTParent->findSlices(); - slicerTParent->timeShiftSample(); + printf("clicked on TUI\n"); + // slicerTParent->findSlices(); + // slicerTParent->findBPM(); + // slicerTParent->timeShiftSample(); update(); } diff --git a/plugins/Slicer/SlicerTUI.h b/plugins/Slicer/SlicerTUI.h index 7965dc32f94..0f28c05a2e9 100644 --- a/plugins/Slicer/SlicerTUI.h +++ b/plugins/Slicer/SlicerTUI.h @@ -4,13 +4,15 @@ #include "Instrument.h" #include "InstrumentView.h" #include "Knob.h" +#include "LcdSpinBox.h" +#include // #include "Graph.h" // #include "MemoryManager.h" -#ifndef SLICER_UI_H -#define SLICER_UI_H +#ifndef SLICERT_UI_H +#define SLICERT_UI_H namespace lmms { @@ -47,6 +49,9 @@ class SlicerTUI : public InstrumentViewFixedSize SlicerT * slicerTParent; Knob noteThresholdKnob; + LcdSpinBox bpmBox; + + QPushButton resetButton; WaveForm wf; diff --git a/plugins/Slicer/WaveForm.cpp b/plugins/Slicer/WaveForm.cpp index a0bc53ce621..876b8713cd2 100644 --- a/plugins/Slicer/WaveForm.cpp +++ b/plugins/Slicer/WaveForm.cpp @@ -1,6 +1,8 @@ #include #include "WaveForm.h" +#include "SlicerT.h" + namespace lmms { @@ -8,50 +10,202 @@ namespace lmms namespace gui { - WaveForm::WaveForm(int _w, int _h, std::vector & _slicePoints, QWidget * _parent) : + WaveForm::WaveForm(int _w, int _h, SlicerT * _instrument, QWidget * _parent) : QWidget(_parent), - m_graph( QPixmap(_w, _h)), + seeker(QPixmap(_w, _h*seekerRatio)), + sliceEditor(QPixmap(_w, _h*(1 - seekerRatio) - margin)), currentSample(), - slicePoints(_slicePoints) + slicePoints(_instrument->slicePoints) { width = _w; height = _h; - setFixedSize( width, height ); + slicerTParent = _instrument; + setFixedSize(width, height); + setMouseTracking( true ); + setAcceptDrops( true ); + + sliceEditor.fill(waveformBgColor); + seeker.fill(waveformBgColor); - m_graph.fill(QColor(11, 11, 11)); + connect(slicerTParent, + SIGNAL(isPlaying(float, float, float)), + this, + SLOT(isPlaying(float, float, float))); } - void WaveForm::drawWaveForm() { - m_graph.fill(QColor(11, 11, 11)); - QPainter brush(&m_graph); - brush.setPen(QColor(255, 0, 0)); + void WaveForm::drawEditor() { + sliceEditor.fill(waveformBgColor); + QPainter brush(&sliceEditor); + brush.setPen(waveformColor); + + float startFrame = seekerStart * currentSample.frames(); + float endFrame = seekerEnd * currentSample.frames(); + currentSample.visualize( brush, - QRect( 0, 0, m_graph.width(), m_graph.height() ), + QRect( 0, 0, sliceEditor.width(), sliceEditor.height() ), + startFrame, endFrame); + + + for (int i = 0;i= startFrame && sliceIndex <= endFrame) { + float xPos = (float)(sliceIndex - startFrame) / (float)(endFrame - startFrame) * (float)width; + if (i == sliceSelected) { + brush.setPen(QPen(selectedSliceColor, 2)); + } + + brush.drawLine(xPos, 0, xPos, height); + } + + } + + } + + void WaveForm::drawSeeker() { + seeker.fill(waveformBgColor); + QPainter brush(&seeker); + brush.setPen(waveformColor); + + currentSample.visualize( + brush, + QRect( 0, 0, seeker.width(), seeker.height() ), 0, currentSample.frames()); - brush.setPen(QColor(0, 255, 0)); + // draw slice points + brush.setPen(sliceColor); for (int i = 0;ix() / width; + + if (_me->y() < height*seekerRatio) { + if (abs(normalizedClick - seekerStart) < 0.03) { + currentlyDragging = draggingTypes::seekerStart; + } else if (abs(normalizedClick - seekerEnd) < 0.03) { + currentlyDragging = draggingTypes::seekerEnd; + } else if (normalizedClick > seekerStart && normalizedClick < seekerEnd) { + currentlyDragging = draggingTypes::seekerMiddle; + seekerMiddle = normalizedClick; + } + + } else { + float startFrame = seekerStart * currentSample.frames(); + float endFrame = seekerEnd * currentSample.frames(); + for (int i = 0;i= startFrame && sliceIndex <= endFrame) { + float xPos = (float)(sliceIndex - startFrame) / (float)(endFrame - startFrame); + if (abs(xPos - normalizedClick) < 0.03) { + currentlyDragging = draggingTypes::slicePoint; + sliceSelected = i; + // } + } + } + } + + } + + void WaveForm::enterEvent( QEvent * _e ) {} + void WaveForm::leaveEvent( QEvent * _e ) {} + void WaveForm::mouseReleaseEvent( QMouseEvent * _me ) { + isDragging = false; + currentlyDragging = draggingTypes::nothing; + sliceSelected = 0; + updateUI(); } + void WaveForm::mouseMoveEvent( QMouseEvent * _me ) { + float normalizedClick = (float)_me->x() / width; + + // handle dragging events + if (isDragging) { + // printf("drag type:%i , seekerStart: %f , seekerEnd: %f \n", currentlyDragging, seekerStart, seekerEnd); + if (currentlyDragging == draggingTypes::seekerStart) { + seekerStart = std::clamp(normalizedClick, 0.0f, seekerEnd - 0.13f); + + } else if (currentlyDragging == draggingTypes::seekerEnd) { + seekerEnd = std::clamp(normalizedClick, seekerStart + 0.13f, 1.0f);; + + } else if (currentlyDragging == draggingTypes::seekerMiddle) { + float distStart = seekerStart - seekerMiddle; + float distEnd = seekerEnd - seekerMiddle; + + seekerMiddle = normalizedClick; + + if (seekerMiddle + distStart > 0 && seekerMiddle + distEnd < 1) { + seekerStart = seekerMiddle + distStart; + seekerEnd = seekerMiddle + distEnd; + } + + } else if (currentlyDragging == draggingTypes::slicePoint) { + float startFrame = seekerStart * currentSample.frames(); + float endFrame = seekerEnd * currentSample.frames(); + + slicePoints[sliceSelected] = startFrame + normalizedClick * (endFrame - startFrame); + + slicePoints[sliceSelected] = std::clamp(slicePoints[sliceSelected], 0, currentSample.frames()); + + std::sort(slicePoints.begin(), slicePoints.end()); + + } + updateUI(); + } else { + + } + + + + } + void WaveForm::wheelEvent( QWheelEvent * _we ) {} + + + void WaveForm::paintEvent( QPaintEvent * _pe) { + QPainter p( this ); + p.drawPixmap(0, height*0.3f + margin, sliceEditor); + p.drawPixmap(0, 0, seeker); + } + + } } \ No newline at end of file diff --git a/plugins/Slicer/WaveForm.h b/plugins/Slicer/WaveForm.h index 52b54a311df..be66b455d26 100644 --- a/plugins/Slicer/WaveForm.h +++ b/plugins/Slicer/WaveForm.h @@ -11,6 +11,7 @@ namespace lmms { +class SlicerT; namespace gui { @@ -19,30 +20,67 @@ namespace gui class WaveForm : public QWidget { Q_OBJECT protected: - // virtual void enterEvent( QEvent * _e ); - // virtual void leaveEvent( QEvent * _e ); - // virtual void mousePressEvent( QMouseEvent * _me ); - // virtual void mouseReleaseEvent( QMouseEvent * _me ); - // virtual void mouseMoveEvent( QMouseEvent * _me ); - // virtual void wheelEvent( QWheelEvent * _we ); + virtual void enterEvent( QEvent * _e ); + virtual void leaveEvent( QEvent * _e ); + virtual void mousePressEvent( QMouseEvent * _me ); + virtual void mouseReleaseEvent( QMouseEvent * _me ); + virtual void mouseMoveEvent( QMouseEvent * _me ); + virtual void wheelEvent( QWheelEvent * _we ); virtual void paintEvent( QPaintEvent * _pe ); private: - - QPixmap m_graph; - - SampleBuffer currentSample; int width; int height; + float seekerRatio = 0.3f; + int margin = 5; + QColor waveformBgColor = QColor(11, 11, 11); + QColor waveformColor = QColor(124, 49, 214); + QColor playColor = QColor(255, 255, 255, 200); + QColor playHighlighColor = QColor(255, 255, 255, 70); + QColor sliceColor = QColor(49, 214, 124); + QColor selectedSliceColor = QColor(172, 236, 190); + QColor seekerColor = QColor(214, 124, 49); + QColor seekerShadowColor = QColor(0, 0, 0, 175); + + enum class draggingTypes { + nothing, + seekerStart, + seekerEnd, + seekerMiddle, + slicePoint, + }; + draggingTypes currentlyDragging; + bool isDragging = false; + + float seekerStart = 0; + float seekerEnd = 1; + float seekerMiddle = 0.5f; + int sliceSelected = 0; + + float noteCurrent; + float noteStart; + float noteEnd; + + QPixmap sliceEditor; + QPixmap seeker; + + SampleBuffer currentSample; + + void drawEditor(); + void drawSeeker(); + - void drawWaveForm(); + public slots: + void updateUI(); + void isPlaying(float current, float start, float end); public: - WaveForm(int _w, int _h, std::vector & _slicePoints, QWidget * _parent); + WaveForm(int _w, int _h, SlicerT * _instrument, QWidget * _parent); void updateFile(QString file); private: + SlicerT * slicerTParent; std::vector & slicePoints; }; From 330cc1564a7e6f37c3867ca8dedd1a8e28d20e36 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Fri, 8 Sep 2023 05:36:37 +0200 Subject: [PATCH 09/99] slice editor Ui working --- plugins/Slicer/SlicerT.cpp | 12 +++---- plugins/Slicer/SlicerTUI.cpp | 2 +- plugins/Slicer/WaveForm.cpp | 69 ++++++++++++++++++++++++++++-------- plugins/Slicer/WaveForm.h | 8 ++--- 4 files changed, 65 insertions(+), 26 deletions(-) diff --git a/plugins/Slicer/SlicerT.cpp b/plugins/Slicer/SlicerT.cpp index f3756621896..8e8f0fb41e3 100644 --- a/plugins/Slicer/SlicerT.cpp +++ b/plugins/Slicer/SlicerT.cpp @@ -38,7 +38,7 @@ the matchingSpeed buffer will always get initialzed freshly using a buffer, this give it the raw data */ -// TODO: add, remove slices; add gui values (fade out, window sizes?); fix timeshift lag (bpm change) +// TODO: add gui values (fade out, window sizes?); fix timeshift lag (bpm change) // TODO: midi dragging, open file button // TODO: fix empty sample, small sample edge cases, general stability stuff @@ -137,8 +137,8 @@ void SlicerT::playNote( NotePlayHandle * _n, sampleFrame * _working_buffer ) { timeShiftedSample.setAllPointFrames( sliceStart, sliceEnd, sliceStart, sliceEnd ); - printf("%i : %i -> %i\n", noteIndex, sliceStart, sliceEnd); - printf("%i\n", _n->oldKey()); + // printf("%i : %i -> %i\n", noteIndex, sliceStart, sliceEnd); + // printf("%i\n", _n->oldKey()); } @@ -173,13 +173,13 @@ void SlicerT::playNote( NotePlayHandle * _n, sampleFrame * _working_buffer ) { float absoluteCurrentNote = (float)(sliceStart + playedFrames) / totalFrames; float absoluteStartNote = (float)sliceStart / totalFrames; float abslouteEndNote = (float)sliceEnd / totalFrames; - emit isPlaying(absoluteCurrentNote, absoluteStartNote, abslouteEndNote); + // emit isPlaying(absoluteCurrentNote, absoluteStartNote, abslouteEndNote); } else { - emit isPlaying(0, 0, 0); + // emit isPlaying(0, 0, 0); } } else { - emit isPlaying(0, 0, 0); + // emit isPlaying(0, 0, 0); } diff --git a/plugins/Slicer/SlicerTUI.cpp b/plugins/Slicer/SlicerTUI.cpp index 7afd12dcd0c..40f123bc959 100644 --- a/plugins/Slicer/SlicerTUI.cpp +++ b/plugins/Slicer/SlicerTUI.cpp @@ -42,6 +42,7 @@ SlicerTUI::SlicerTUI( SlicerT * _instrument, { setAcceptDrops( true ); + setFocusProxy(&wf); wf.move(2, 5); @@ -64,7 +65,6 @@ void SlicerTUI::mousePressEvent( QMouseEvent * _me ) { // slicerTParent->findSlices(); // slicerTParent->findBPM(); // slicerTParent->timeShiftSample(); - update(); } diff --git a/plugins/Slicer/WaveForm.cpp b/plugins/Slicer/WaveForm.cpp index 876b8713cd2..f2e33c0a5d3 100644 --- a/plugins/Slicer/WaveForm.cpp +++ b/plugins/Slicer/WaveForm.cpp @@ -23,6 +23,7 @@ namespace gui setFixedSize(width, height); setMouseTracking( true ); setAcceptDrops( true ); + setFocusPolicy(Qt::StrongFocus); sliceEditor.fill(waveformBgColor); seeker.fill(waveformBgColor); @@ -59,9 +60,7 @@ namespace gui brush.drawLine(xPos, 0, xPos, height); } - } - } void WaveForm::drawSeeker() { @@ -116,44 +115,55 @@ namespace gui } void WaveForm::mousePressEvent( QMouseEvent * _me ) { - isDragging = true; float normalizedClick = (float)_me->x() / width; - + if (_me->y() < height*seekerRatio) { if (abs(normalizedClick - seekerStart) < 0.03) { currentlyDragging = draggingTypes::seekerStart; + } else if (abs(normalizedClick - seekerEnd) < 0.03) { currentlyDragging = draggingTypes::seekerEnd; + } else if (normalizedClick > seekerStart && normalizedClick < seekerEnd) { currentlyDragging = draggingTypes::seekerMiddle; seekerMiddle = normalizedClick; } } else { + sliceSelected = -1; float startFrame = seekerStart * currentSample.frames(); float endFrame = seekerEnd * currentSample.frames(); + for (int i = 0;i= startFrame && sliceIndex <= endFrame) { - float xPos = (float)(sliceIndex - startFrame) / (float)(endFrame - startFrame); - if (abs(xPos - normalizedClick) < 0.03) { - currentlyDragging = draggingTypes::slicePoint; - sliceSelected = i; - // } + float xPos = (float)(sliceIndex - startFrame) / (float)(endFrame - startFrame); + + if (abs(xPos - normalizedClick) < 0.03) { + currentlyDragging = draggingTypes::slicePoint; + sliceSelected = i; + } } + } + + if (_me->button() == Qt::MouseButton::LeftButton) { + isDragging = true; + + } else if (_me->button() == Qt::MouseButton::RightButton) { + if (sliceSelected != -1 && slicePoints.size() > 2) { + slicePoints.erase(slicePoints.begin() + sliceSelected); + sliceSelected = -1; + } } } - void WaveForm::enterEvent( QEvent * _e ) {} - void WaveForm::leaveEvent( QEvent * _e ) {} void WaveForm::mouseReleaseEvent( QMouseEvent * _me ) { isDragging = false; currentlyDragging = draggingTypes::nothing; - sliceSelected = 0; updateUI(); } + void WaveForm::mouseMoveEvent( QMouseEvent * _me ) { float normalizedClick = (float)_me->x() / width; @@ -190,14 +200,43 @@ namespace gui } updateUI(); } else { - + } } - void WaveForm::wheelEvent( QWheelEvent * _we ) {} + void WaveForm::mouseDoubleClickEvent(QMouseEvent * _me) { + float normalizedClick = (float)_me->x() / width; + float startFrame = seekerStart * currentSample.frames(); + float endFrame = seekerEnd * currentSample.frames(); + + float slicePosition = startFrame + normalizedClick * (endFrame - startFrame); + + for (int i = 0;ikey(); + printf("key: %i\n", ke->key()); + if ((key == 16777219 || key == 16777223) && // delete and backspace + (sliceSelected != -1 && slicePoints.size() > 2)) { + + slicePoints.erase(slicePoints.begin() + sliceSelected); + } + updateUI(); + + } void WaveForm::paintEvent( QPaintEvent * _pe) { QPainter p( this ); diff --git a/plugins/Slicer/WaveForm.h b/plugins/Slicer/WaveForm.h index be66b455d26..844d6c84223 100644 --- a/plugins/Slicer/WaveForm.h +++ b/plugins/Slicer/WaveForm.h @@ -20,14 +20,14 @@ namespace gui class WaveForm : public QWidget { Q_OBJECT protected: - virtual void enterEvent( QEvent * _e ); - virtual void leaveEvent( QEvent * _e ); virtual void mousePressEvent( QMouseEvent * _me ); virtual void mouseReleaseEvent( QMouseEvent * _me ); virtual void mouseMoveEvent( QMouseEvent * _me ); - virtual void wheelEvent( QWheelEvent * _we ); - virtual void paintEvent( QPaintEvent * _pe ); + virtual void mouseDoubleClickEvent(QMouseEvent * _me); + virtual void keyPressEvent(QKeyEvent * ke); + + virtual void paintEvent( QPaintEvent * _pe); private: int width; From 9a2a83dcbfda4884e1bbeefd3436dd0976af4f8c Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Fri, 8 Sep 2023 17:51:45 +0200 Subject: [PATCH 10/99] fundamental functionality done --- plugins/Slicer/SlicerT.cpp | 148 +++++++++++++++++++++-------------- plugins/Slicer/SlicerT.h | 16 +++- plugins/Slicer/SlicerTUI.cpp | 92 +++++++++++++++------- plugins/Slicer/SlicerTUI.h | 16 ++-- plugins/Slicer/WaveForm.cpp | 12 ++- plugins/Slicer/WaveForm.h | 1 + 6 files changed, 182 insertions(+), 103 deletions(-) diff --git a/plugins/Slicer/SlicerT.cpp b/plugins/Slicer/SlicerT.cpp index 8e8f0fb41e3..05c9b478a3a 100644 --- a/plugins/Slicer/SlicerT.cpp +++ b/plugins/Slicer/SlicerT.cpp @@ -22,39 +22,18 @@ * */ -/* -!! always work on the original sample for slicing and whatever, -only use the sped up sample and slices in playNote(). This will keep things organized - - - -load file / slcies get updated -find slices -find bpm / if bpm gets updated -update matchingSpeed sampleBuffer -update matchingSlices vector - -the matchingSpeed buffer will always get initialzed freshly using a buffer, this seems to be the only way -give it the raw data -*/ -// TODO: add gui values (fade out, window sizes?); fix timeshift lag (bpm change) -// TODO: midi dragging, open file button -// TODO: fix empty sample, small sample edge cases, general stability stuff +// TODO: fadeIn +// TODO: saving, loading +// TODO: timeshift samples on notePlay and cache them -// #include #include "SlicerT.h" #include +#include -#include "fft_helpers.h" -#include - -// #include "AudioEngine.h" -// #include "base64.h" #include "Engine.h" -#include "InstrumentTrack.h" #include "Song.h" -#include +#include "InstrumentTrack.h" #include "embed.h" #include "plugin_export.h" @@ -89,33 +68,38 @@ Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = SlicerT::SlicerT(InstrumentTrack * _instrument_track) : Instrument( _instrument_track, &slicert_plugin_descriptor ), + noteThreshold(0.6f, 0.0f, 2.0f, 0.01f, this, tr( "Note threshold" ) ), + fadeOutFrames(2048.0f, 0.0f, 8192.0f, 4.0f, this, tr("FadeOut")), + originalBPM(1, 1, 999, this, tr("Original bpm")), + originalSample(), - timeShiftedSample(), - noteThreshold( 0.6f, 0.0f, 2.0f, 0.01f, this, tr( "Note Threshold" ) ), - originalBPM(1, 1, 999, this, tr("original sample bpm")) + timeShiftedSample() + + + { // connect( ¬eThreshold, SIGNAL( dataChanged() ), this, SLOT( updateSlices() ) ); // TODO: either button to timeshift, threading or generating samples on the fly - connect( &originalBPM, SIGNAL( dataChanged() ), this, SLOT( updateTimeShift() ) ); + // connect( &originalBPM, SIGNAL( dataChanged() ), this, SLOT( updateTimeShift() ) ); printf("Correctly loaded SlicerT!\n"); } void SlicerT::playNote( NotePlayHandle * _n, sampleFrame * _working_buffer ) { + if (originalSample.frames() < 2048) { + return; + } + const fpp_t frames = _n->framesLeftForCurrentPeriod(); const int playedFrames = _n->totalFramesPlayed(); const f_cnt_t offset = _n->noteOffset(); - - int totalFrames = timeShiftedSample.frames(); int sliceStart = timeShiftedSample.startFrame(); int sliceEnd = timeShiftedSample.endFrame(); int sliceFrames = timeShiftedSample.endFrame() - timeShiftedSample.startFrame(); int noteFramesLeft = sliceFrames - playedFrames; - int fadeOutFrames = (float)sliceFrames/4; - // init NotePlayHandle data if( !_n->m_pluginData ) { @@ -137,8 +121,6 @@ void SlicerT::playNote( NotePlayHandle * _n, sampleFrame * _working_buffer ) { timeShiftedSample.setAllPointFrames( sliceStart, sliceEnd, sliceStart, sliceEnd ); - // printf("%i : %i -> %i\n", noteIndex, sliceStart, sliceEnd); - // printf("%i\n", _n->oldKey()); } @@ -157,10 +139,10 @@ void SlicerT::playNote( NotePlayHandle * _n, sampleFrame * _working_buffer ) { frames + offset, _n ); // exponential fade out - if (noteFramesLeft < fadeOutFrames) { + if (noteFramesLeft < fadeOutFrames.value()) { // printf("fade out started. frames: %i, framesLeft: %i\n", frames, noteFramesLeft); for (int i = 0;igetTempo(); float speedRatio = (float)originalBPM.value() / targetBPM ; - int samplesPerBeat = 60.0f / targetBPM * sampleRate; int outFrames = speedRatio * originalFrames; // snap to a beat, this should be in the UI - outFrames += (float)samplesPerBeat; - outFrames -= outFrames%samplesPerBeat; + // outFrames += (float)samplesPerBeat; + // outFrames -= outFrames%samplesPerBeat; // nothing to do here if (targetBPM == originalBPM.value()) { timeShiftedSample = SampleBuffer(originalSample.data(), originalSample.frames()); - printf("BPM match for sample, finished timeshift\n"); + printf("BPM match for sample, finished timeshift. frames: %i\n", timeShiftedSample.frames()); return; } @@ -343,10 +333,10 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO float lengthRatio = (float)outFrames / inFrames; // values used - const int stepSize = windowSize / overSampling; - const int outStepSize = stepSize * lengthRatio; - const int numWindows = inFrames / stepSize; + const int stepSize = (float)windowSize / overSampling; + const int numWindows = (float)inFrames / stepSize; const int windowLatency = (overSampling-1)*stepSize; + const float outStepSize = lengthRatio * (float)stepSize; // float, else inaccurate const float freqPerBin = sampleRate/windowSize; // very important const float expectedPhaseIn = 2.*M_PI*(float)stepSize/(float)windowSize; @@ -354,6 +344,7 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO printf("frames: %i , out frames: %i , ratio: %f\n", inFrames, outFrames, (float)outFrames / inFrames); + printf("stepSize: %i, outStepSize:%i, numWindows: %i", stepSize, outStepSize, numWindows); printf("will drop %i\n", inFrames%(inFrames/numWindows)); // initialize buffers @@ -466,21 +457,31 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO printf("too long window size, breaking\n"); break; } - if (outIndex < windowLatency) { - // calculate amount of windows overlapping - float windowsOverlapping = outIndex / windowSize * overSampling + 1; - // since not all windows overlap, just take the average of the ones that do overlap - outBuffer[outIndex] += overSampling/windowsOverlapping*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); + // calculate windows overlapping at index + float startWindowOverlap = ceil(outIndex / outStepSize + 0.00000001); + float endWindowOverlap = ceil((float)(-outIndex + outFrames) / outStepSize + 0.00000001); + float totalWindowOverlap = std::min( + std::min(startWindowOverlap, endWindowOverlap), + (float)overSampling); - // no averaging, probably worse - // dataOut[outIndex] = overSampling*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); - } else { - // this computes the weight of the window on the final output - float window = -0.5f*cos(2.*M_PI*(float)j/(float)windowSize)+0.5f; - outBuffer[outIndex] += 2.0f*window*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); - } + // completly unsmooth, but ensures same magnitude across + outBuffer[outIndex] += (float)overSampling/totalWindowOverlap*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); + // printf("%f ", outBuffer[outIndex]); + // if (outIndex < (float)outStepSize*overSampling) { + // printf("start: %f, end: %f, total:%f\n", startWindowOverlap, endWindowOverlap, totalWindowOverlap); + // // since not all windows overlap, just take the average of the ones that do overlap + // outBuffer[outIndex] += (float)overSampling/startWindowOverlap*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); + + // // no averaging, probably worse + // // outBuffer[outIndex] = overSampling*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); + // } else { + // // this computes the weight of the window on the final output + // float window = -0.5f*cos(2.*M_PI*(float)j/(float)windowSize)+0.5f; + + // outBuffer[outIndex] += 2.0f*window*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); + // } } @@ -496,6 +497,8 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO max = std::max(max, abs(outBuffer[i])); } + printf("max: %f\n", max); + for (int i = 0;i &dataIn, std::vector &dataO } +void SlicerT::writeToMidi(std::vector * outClip) { + int ticksPerBar = DefaultTicksPerBar; + float sampleRate = timeShiftedSample.sampleRate(); + + float bpm = Engine::getSong()->getTempo(); + float samplesPerBeat = 60.0f / bpm * sampleRate; + float beats = (float)timeShiftedSample.frames() / samplesPerBeat; + + float barsInSample = beats / Engine::getSong()->getTimeSigModel().getDenominator(); + float tickMult = ticksPerBar * barsInSample; + + printf("beats: %f, bars: %f, tickMult: %f\n", beats, barsInSample, tickMult); + + for (int i = 0;ipush_back(sliceNote); + } +} void SlicerT::updateFile(QString file) { printf("updated audio file\n"); originalSample.setAudioFile(file); + if (originalSample.frames() < 2048) { + return; + } findSlices(); - findBPM(); // this also updates timeshift because bpm change + findBPM(); + timeShiftSample(); } diff --git a/plugins/Slicer/SlicerT.h b/plugins/Slicer/SlicerT.h index 549464e21ab..004e4756f1e 100644 --- a/plugins/Slicer/SlicerT.h +++ b/plugins/Slicer/SlicerT.h @@ -27,12 +27,17 @@ #ifndef SLICERT_H #define SLICERT_H -#include "AutomatableModel.h" +#include "SlicerTUI.h" + +#include + +#include "Note.h" #include "Instrument.h" #include "InstrumentView.h" +#include "AutomatableModel.h" #include "SampleBuffer.h" -#include "SlicerTUI.h" -#include + + // #include "Graph.h" // #include "MemoryManager.h" @@ -58,6 +63,8 @@ class SlicerT : public Instrument{ gui::PluginView * instantiateView( QWidget * _parent ) override; + void writeToMidi(std::vector * outClip); + public slots: void updateFile(QString file); void updateTimeShift(); @@ -70,7 +77,8 @@ class SlicerT : public Instrument{ FloatModel noteThreshold; - IntModel originalBPM = 140; + FloatModel fadeOutFrames; + IntModel originalBPM; SampleBuffer originalSample; SampleBuffer timeShiftedSample; diff --git a/plugins/Slicer/SlicerTUI.cpp b/plugins/Slicer/SlicerTUI.cpp index 40f123bc959..d143123b10b 100644 --- a/plugins/Slicer/SlicerTUI.cpp +++ b/plugins/Slicer/SlicerTUI.cpp @@ -1,28 +1,19 @@ #include "SlicerTUI.h" #include "SlicerT.h" -// #include -#include +#include +#include +#include #include "StringPairDrag.h" #include "Clipboard.h" #include "Track.h" #include "DataFile.h" -// #include "AudioEngine.h" -// #include "base64.h" -// #include "Engine.h" -// #include "Graph.h" -// #include "InstrumentTrack.h" -// #include "Knob.h" -// #include "LedCheckBox.h" -// #include "NotePlayHandle.h" -// #include "PixmapButton.h" -// #include "Song.h" -// #include "interpolation.h" - -// #include -#include -#include + +#include "Engine.h" +#include "Song.h" +#include "InstrumentTrack.h" + #include "embed.h" namespace lmms @@ -34,30 +25,73 @@ namespace gui SlicerTUI::SlicerTUI( SlicerT * _instrument, QWidget * _parent ) : InstrumentViewFixedSize( _instrument, _parent ), - noteThresholdKnob(KnobType::Bright26, this), slicerTParent(_instrument), - wf(245, 125, _instrument, this), + noteThresholdKnob(KnobType::Dark28, this), + fadeOutKnob(KnobType::Dark28, this), bpmBox(3, "21pink", this), - resetButton(embed::getIconPixmap("reload"), QString(), this) - + resetButton(embed::getIconPixmap("reload"), QString(), this), + timeShiftButton(embed::getIconPixmap("max_length"), QString(), this), + midiExportButton(embed::getIconPixmap("midi_tab"), QString(), this), + wf(245, 125, _instrument, this) { setAcceptDrops( true ); - setFocusProxy(&wf); wf.move(2, 5); - resetButton.move(100, 150); - connect(&resetButton, SIGNAL( clicked() ), slicerTParent, SLOT( updateSlices() )); - connect(&resetButton, SIGNAL( clicked() ), &wf, SLOT( updateUI() )); - - bpmBox.move(30, 150); - bpmBox.setToolTip(tr("original sample BPM")); + bpmBox.move(2, 150); + bpmBox.setToolTip(tr("Original sample BPM")); bpmBox.setLabel(tr("BPM")); bpmBox.setModel(&slicerTParent->originalBPM); - noteThresholdKnob.move(30, 200); + timeShiftButton.move(70, 150); + timeShiftButton.setToolTip(tr("Timeshift sample")); + connect(&timeShiftButton, SIGNAL( clicked() ), slicerTParent, SLOT( updateTimeShift() )); + + fadeOutKnob.move(200, 150); + fadeOutKnob.setToolTip(tr("FadeOut for notes")); + fadeOutKnob.setLabel(tr("FadeOut")); + fadeOutKnob.setModel(&slicerTParent->fadeOutFrames); + + midiExportButton.move(150, 150); + midiExportButton.setToolTip(tr("Copy midi pattern to clipboard")); + connect(&midiExportButton, SIGNAL( clicked() ), this, SLOT( exportMidi() )); + + noteThresholdKnob.move(7, 200); + noteThresholdKnob.setToolTip(tr("Threshold used for slicing")); + noteThresholdKnob.setLabel(tr("Threshold")); noteThresholdKnob.setModel(&slicerTParent->noteThreshold); + resetButton.move(70, 200); + resetButton.setToolTip(tr("Reset Slices")); + connect(&resetButton, SIGNAL( clicked() ), slicerTParent, SLOT( updateSlices() )); + connect(&resetButton, SIGNAL( clicked() ), &wf, SLOT( updateUI() )); + + + +} + +// copied from piano roll +void SlicerTUI::exportMidi() { + using namespace Clipboard; + + DataFile dataFile( DataFile::Type::ClipboardData ); + QDomElement note_list = dataFile.createElement( "note-list" ); + dataFile.content().appendChild( note_list ); + + std::vector notes; + slicerTParent->writeToMidi(¬es); + + + TimePos start_pos( notes.front().pos().getBar(), 0 ); + for( Note note : notes ) + { + Note clip_note( note ); + clip_note.setPos( clip_note.pos( start_pos ) ); + clip_note.saveState( dataFile, note_list ); + } + + copyString( dataFile.toString(), MimeType::Default ); + } void SlicerTUI::mousePressEvent( QMouseEvent * _me ) { diff --git a/plugins/Slicer/SlicerTUI.h b/plugins/Slicer/SlicerTUI.h index 0f28c05a2e9..b123223fb4f 100644 --- a/plugins/Slicer/SlicerTUI.h +++ b/plugins/Slicer/SlicerTUI.h @@ -1,14 +1,12 @@ -#include #include "WaveForm.h" -// #include "AutomatableModel.h" + +#include +#include + #include "Instrument.h" #include "InstrumentView.h" #include "Knob.h" #include "LcdSpinBox.h" -#include -// #include "Graph.h" -// #include "MemoryManager.h" - #ifndef SLICERT_UI_H @@ -36,7 +34,8 @@ class SlicerTUI : public InstrumentViewFixedSize QWidget * _parent ); ~SlicerTUI() override = default; -// protected slots: +protected slots: + void exportMidi(); //void sampleSizeChanged( float _new_sample_length ); protected: @@ -49,9 +48,12 @@ class SlicerTUI : public InstrumentViewFixedSize SlicerT * slicerTParent; Knob noteThresholdKnob; + Knob fadeOutKnob; LcdSpinBox bpmBox; QPushButton resetButton; + QPushButton timeShiftButton; + QPushButton midiExportButton; WaveForm wf; diff --git a/plugins/Slicer/WaveForm.cpp b/plugins/Slicer/WaveForm.cpp index f2e33c0a5d3..9625172ab9a 100644 --- a/plugins/Slicer/WaveForm.cpp +++ b/plugins/Slicer/WaveForm.cpp @@ -1,8 +1,7 @@ -#include - #include "WaveForm.h" #include "SlicerT.h" +#include namespace lmms { @@ -23,7 +22,6 @@ namespace gui setFixedSize(width, height); setMouseTracking( true ); setAcceptDrops( true ); - setFocusPolicy(Qt::StrongFocus); sliceEditor.fill(waveformBgColor); seeker.fill(waveformBgColor); @@ -117,6 +115,12 @@ namespace gui void WaveForm::mousePressEvent( QMouseEvent * _me ) { float normalizedClick = (float)_me->x() / width; + if (_me->button() == Qt::MouseButton::MiddleButton) { + seekerStart = 0; + seekerEnd = 1; + return; + } + if (_me->y() < height*seekerRatio) { if (abs(normalizedClick - seekerStart) < 0.03) { currentlyDragging = draggingTypes::seekerStart; @@ -154,7 +158,7 @@ namespace gui slicePoints.erase(slicePoints.begin() + sliceSelected); sliceSelected = -1; } - } + } } diff --git a/plugins/Slicer/WaveForm.h b/plugins/Slicer/WaveForm.h index 844d6c84223..3806d6b2b33 100644 --- a/plugins/Slicer/WaveForm.h +++ b/plugins/Slicer/WaveForm.h @@ -3,6 +3,7 @@ #include #include #include + #include "SampleBuffer.h" #ifndef WAVEFORM_H From 76782c1bf3c8d32af515437ecdf7a95d851f10ee Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Fri, 8 Sep 2023 21:19:02 +0200 Subject: [PATCH 11/99] Everything basic works fully --- plugins/Slicer/SlicerT.cpp | 89 ++++++++++++++++++++++++++++-------- plugins/Slicer/SlicerTUI.cpp | 6 +-- plugins/Slicer/WaveForm.cpp | 20 +++++--- plugins/Slicer/WaveForm.h | 5 +- 4 files changed, 88 insertions(+), 32 deletions(-) diff --git a/plugins/Slicer/SlicerT.cpp b/plugins/Slicer/SlicerT.cpp index 05c9b478a3a..6b74511e031 100644 --- a/plugins/Slicer/SlicerT.cpp +++ b/plugins/Slicer/SlicerT.cpp @@ -24,17 +24,19 @@ // TODO: fadeIn -// TODO: saving, loading +// TODO: fix lag multiple notes // TODO: timeshift samples on notePlay and cache them #include "SlicerT.h" #include #include +#include #include "Engine.h" #include "Song.h" #include "InstrumentTrack.h" +#include "PathUtil.h" #include "embed.h" #include "plugin_export.h" @@ -74,14 +76,7 @@ SlicerT::SlicerT(InstrumentTrack * _instrument_track) : originalSample(), timeShiftedSample() - - - { - // connect( ¬eThreshold, SIGNAL( dataChanged() ), this, SLOT( updateSlices() ) ); - // TODO: either button to timeshift, threading or generating samples on the fly - // connect( &originalBPM, SIGNAL( dataChanged() ), this, SLOT( updateTimeShift() ) ); - printf("Correctly loaded SlicerT!\n"); } @@ -133,12 +128,7 @@ void SlicerT::playNote( NotePlayHandle * _n, sampleFrame * _working_buffer ) { frames, 440, static_cast( 0 ) ) ) { - - applyRelease( _working_buffer, _n ); - instrumentTrack()->processAudioBuffer( _working_buffer, - frames + offset, _n ); - - // exponential fade out + // exponential fade out, applyRelease kinda sucks if (noteFramesLeft < fadeOutFrames.value()) { // printf("fade out started. frames: %i, framesLeft: %i\n", frames, noteFramesLeft); for (int i = 0;iprocessAudioBuffer( _working_buffer, + frames + offset, _n ); + + float absoluteCurrentNote = (float)(sliceStart + playedFrames) / totalFrames; float absoluteStartNote = (float)sliceStart / totalFrames; float abslouteEndNote = (float)sliceEnd / totalFrames; - // emit isPlaying(absoluteCurrentNote, absoluteStartNote, abslouteEndNote); + emit isPlaying(absoluteCurrentNote, absoluteStartNote, abslouteEndNote); } else { - // emit isPlaying(0, 0, 0); + emit isPlaying(0, 0, 0); } } else { - // emit isPlaying(0, 0, 0); + emit isPlaying(0, 0, 0); } @@ -243,6 +237,7 @@ void SlicerT::findBPM() { bpm -= bpm % bpmSnap; originalBPM.setValue(bpm); + originalBPM.setInitValue(bpm); } // create thimeshifted samplebuffer and timeshifted slicePoints @@ -511,6 +506,9 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO } void SlicerT::writeToMidi(std::vector * outClip) { + if (originalSample.frames() < 2048) { + return; + } int ticksPerBar = DefaultTicksPerBar; float sampleRate = timeShiftedSample.sampleRate(); @@ -545,6 +543,7 @@ void SlicerT::updateFile(QString file) { findBPM(); timeShiftSample(); + emit dataChanged(); } void SlicerT::updateSlices() { @@ -556,8 +555,60 @@ void SlicerT::updateTimeShift() { } -void SlicerT::saveSettings(QDomDocument & _doc, QDomElement & _parent) {} -void SlicerT::loadSettings( const QDomElement & _this ) {} +void SlicerT::saveSettings(QDomDocument & doc, QDomElement & elem) { + elem.setAttribute("src", originalSample.audioFile()); + if (originalSample.audioFile().isEmpty()) + { + QString s; + elem.setAttribute("sampledata", originalSample.toBase64(s)); + } + + elem.setAttribute("totalSlices", (int)slicePoints.size()); + + for (int i = 0;icollectError(message); + } + } + else if (!elem.attribute("sampledata").isEmpty()) + { + originalSample.loadFromBase64(elem.attribute("srcdata")); + } + + if (!elem.attribute("totalSlices").isEmpty()) { + int totalSlices = elem.attribute("totalSlices").toInt(); + slicePoints = {}; + for (int i = 0;i notes; slicerTParent->writeToMidi(¬es); - + if (notes.size() == 0) { + return; + } TimePos start_pos( notes.front().pos().getBar(), 0 ); for( Note note : notes ) @@ -140,7 +141,6 @@ void SlicerTUI::dropEvent( QDropEvent * _de ) { { printf("type: samplefile\n"); slicerTParent->updateFile( value ); - wf.updateFile( value ); // castModel()->setAudioFile( value ); // _de->accept(); // set wf wave file diff --git a/plugins/Slicer/WaveForm.cpp b/plugins/Slicer/WaveForm.cpp index 9625172ab9a..84a3d249f3f 100644 --- a/plugins/Slicer/WaveForm.cpp +++ b/plugins/Slicer/WaveForm.cpp @@ -13,9 +13,11 @@ namespace gui QWidget(_parent), seeker(QPixmap(_w, _h*seekerRatio)), sliceEditor(QPixmap(_w, _h*(1 - seekerRatio) - margin)), - currentSample(), + currentSample(_instrument->originalSample.data(), _instrument->originalSample.frames()), slicePoints(_instrument->slicePoints) { + + width = _w; height = _h; slicerTParent = _instrument; @@ -30,6 +32,10 @@ namespace gui SIGNAL(isPlaying(float, float, float)), this, SLOT(isPlaying(float, float, float))); + + connect(slicerTParent, SIGNAL(dataChanged()), this, SLOT(updateData())); + + updateUI(); } void WaveForm::drawEditor() { @@ -100,16 +106,19 @@ namespace gui update(); } - void WaveForm::updateFile(QString file) { - currentSample.setAudioFile(file); + void WaveForm::updateData() { + printf("main data changed, updating sample and UI\n"); + currentSample = SampleBuffer(slicerTParent->originalSample.data(), slicerTParent->originalSample.frames()); updateUI(); } + void WaveForm::isPlaying(float current, float start, float end) { noteCurrent = current; noteStart = start; noteEnd = end; - updateUI(); + drawSeeker(); + update(); } void WaveForm::mousePressEvent( QMouseEvent * _me ) { @@ -247,8 +256,5 @@ namespace gui p.drawPixmap(0, height*0.3f + margin, sliceEditor); p.drawPixmap(0, 0, seeker); } - - - } } \ No newline at end of file diff --git a/plugins/Slicer/WaveForm.h b/plugins/Slicer/WaveForm.h index 3806d6b2b33..a63110c5e8a 100644 --- a/plugins/Slicer/WaveForm.h +++ b/plugins/Slicer/WaveForm.h @@ -70,15 +70,14 @@ class WaveForm : public QWidget { void drawEditor(); void drawSeeker(); - + void updateUI(); public slots: - void updateUI(); + void updateData(); void isPlaying(float current, float start, float end); public: WaveForm(int _w, int _h, SlicerT * _instrument, QWidget * _parent); - void updateFile(QString file); private: SlicerT * slicerTParent; From 126672b7837c662a18770d3e3f9f68d666bb33eb Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Fri, 8 Sep 2023 22:57:24 +0200 Subject: [PATCH 12/99] cleanup and code guidelines --- plugins/CMakeLists.txt | 2 +- plugins/SlicerT/CMakeLists.txt | 5 + plugins/SlicerT/SlicerT.cpp | 555 +++++++++++++++++++++++++++++++++ plugins/SlicerT/SlicerT.h | 85 +++++ plugins/SlicerT/SlicerTUI.cpp | 154 +++++++++ plugins/SlicerT/SlicerTUI.h | 61 ++++ plugins/SlicerT/WaveForm.cpp | 247 +++++++++++++++ plugins/SlicerT/WaveForm.h | 88 ++++++ plugins/SlicerT/logo.png | Bin 0 -> 39244 bytes 9 files changed, 1196 insertions(+), 1 deletion(-) create mode 100644 plugins/SlicerT/CMakeLists.txt create mode 100644 plugins/SlicerT/SlicerT.cpp create mode 100644 plugins/SlicerT/SlicerT.h create mode 100644 plugins/SlicerT/SlicerTUI.cpp create mode 100644 plugins/SlicerT/SlicerTUI.h create mode 100644 plugins/SlicerT/WaveForm.cpp create mode 100644 plugins/SlicerT/WaveForm.h create mode 100644 plugins/SlicerT/logo.png diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 9d083dfe1ab..aaa92089a70 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -18,4 +18,4 @@ INCLUDE_DIRECTORIES( FOREACH(PLUGIN ${PLUGIN_LIST}) ADD_SUBDIRECTORY(${PLUGIN}) ENDFOREACH() -ADD_SUBDIRECTORY("Slicer") +ADD_SUBDIRECTORY("SlicerT") diff --git a/plugins/SlicerT/CMakeLists.txt b/plugins/SlicerT/CMakeLists.txt new file mode 100644 index 00000000000..b93021e98f9 --- /dev/null +++ b/plugins/SlicerT/CMakeLists.txt @@ -0,0 +1,5 @@ +INCLUDE(BuildPlugin) + +BUILD_PLUGIN(slicert SlicerT.cpp SlicerT.h SlicerTUI.cpp SlicerTUI.h WaveForm.cpp WaveForm.h MOCFILES SlicerT.h SlicerTUI.h WaveForm.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png") + + diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp new file mode 100644 index 00000000000..1a510bc034c --- /dev/null +++ b/plugins/SlicerT/SlicerT.cpp @@ -0,0 +1,555 @@ +/* + * SlicerT.cpp - simple slicer plugin + * + * Copyright (c) 2006-2008 Andreas Brandmaier + * + * 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 "SlicerT.h" +#include +#include +#include + +#include "Engine.h" +#include "Song.h" +#include "InstrumentTrack.h" + +#include "PathUtil.h" +#include "embed.h" +#include "plugin_export.h" + + +namespace lmms +{ + +extern "C" +{ +Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = +{ + LMMS_STRINGIFY( PLUGINhandleAME ), + "SlicerT", + QT_TRANSLATE_NOOP( "PluginBrowser", + "Basic Slicer" ), + "Daniel Kauss Serna", + 0x0100, + Plugin::Type::Instrument, + new PluginPixmapLoader( "logo" ), + nullptr, + nullptr, +} ; +} // end extern + +SlicerT::SlicerT(InstrumentTrack * instrumentTrack) : + Instrument( instrumentTrack, &slicert_plugin_descriptor ), + m_noteThreshold(0.6f, 0.0f, 2.0f, 0.01f, this, tr( "Note threshold" ) ), + m_fadeOutFrames(0.0f, 0.0f, 8192.0f, 4.0f, this, tr("FadeOut")), + m_originalBPM(1, 1, 999, this, tr("Original bpm")), + + m_originalSample(), + m_timeShiftedSample() +{} + + +void SlicerT::playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) { + if (m_originalSample.frames() < 2048) { + return; + } + + const fpp_t frames = handle->framesLeftForCurrentPeriod(); + const int playedFrames = handle->totalFramesPlayed(); + const f_cnt_t offset = handle->noteOffset(); + + int totalFrames = m_timeShiftedSample.frames(); + int sliceStart = m_timeShiftedSample.startFrame(); + int sliceEnd = m_timeShiftedSample.endFrame(); + int sliceFrames = m_timeShiftedSample.endFrame() - m_timeShiftedSample.startFrame(); + int noteFramesLeft = sliceFrames - playedFrames; + + // init NotePlayHandle data + if( !handle->m_pluginData ) + { + handle->m_pluginData = new SampleBuffer::handleState( false, SRC_LINEAR ); + + float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo() ; + int noteIndex = handle->key() - 69; + int sliceStart, sliceEnd; + + // 0th element is no sound, so play full sample + if (noteIndex > m_slicePoints.size()-2 || noteIndex < 0) { + sliceStart = 0; + sliceEnd = m_timeShiftedSample.frames(); + } else { + sliceStart = m_slicePoints[noteIndex] * speedRatio; + sliceEnd = m_slicePoints[noteIndex+1] * speedRatio; + } + + m_timeShiftedSample.setAllPointFrames( sliceStart, sliceEnd, sliceStart, sliceEnd ); + } + + if( ! handle->isFinished() ) { + if( m_timeShiftedSample.play( workingBuffer + offset, + (SampleBuffer::handleState *)handle->m_pluginData, + frames, 440, + static_cast( 0 ) ) ) + { + // exponential fade out, applyRelease kinda sucks + if (noteFramesLeft < m_fadeOutFrames.value()) { + for (int i = 0;iprocessAudioBuffer( workingBuffer, + frames + offset, handle ); + + + float absoluteCurrentNote = (float)(sliceStart + playedFrames) / totalFrames; + float absoluteStartNote = (float)sliceStart / totalFrames; + float abslouteEndNote = (float)sliceEnd / totalFrames; + emit isPlaying(absoluteCurrentNote, absoluteStartNote, abslouteEndNote); + + } else { + emit isPlaying(0, 0, 0); + } + } else { + emit isPlaying(0, 0, 0); + } +} + + +void SlicerT::findSlices() { + if (m_originalSample.frames() < 2048) { + return; + } + m_slicePoints = {}; + + const int window = 1024; + int minWindowsPassed = 0; + int peakIndex = 0; + + float lastPeak = 0; + float currentPeak = 0; + + for (int i = 0; i currentPeak) { + currentPeak = sampleValue; + peakIndex = i; + } + + if (i%window==0) { + if (abs(currentPeak / lastPeak) > 1+m_noteThreshold.value() && minWindowsPassed <= 0) { + m_slicePoints.push_back(std::max(0, peakIndex-window/2)); // slight offset + minWindowsPassed = 2; // wait at least one window for a new note + } + lastPeak = currentPeak; + currentPeak = 0; + minWindowsPassed--; + } + } + m_slicePoints.push_back(m_originalSample.frames()); + + emit dataChanged(); +} + +// find the bpm of the sample by assuming its in 4/4 time signature , +// and lies in the 100 - 200 bpm range +void SlicerT::findBPM() { + if (m_originalSample.frames() < 2048) { + return; + } + int bpmSnap = 1; + + float sampleRate = m_originalSample.sampleRate(); + float totalFrames = m_originalSample.frames(); + float sampleLength = totalFrames / sampleRate; + + // this assumes the sample has a time signature of x/4 + float bpmEstimate = 240.0f / sampleLength; + + // deal with samlpes that are not 1 bar long + while (bpmEstimate < 100) { + bpmEstimate *= 2; + } + + while (bpmEstimate > 200) { + bpmEstimate /= 2; + } + + // snap bpm + int bpm = bpmEstimate; + bpm += (float)bpmSnap / 2; + bpm -= bpm % bpmSnap; + + m_originalBPM.setValue(bpm); + m_originalBPM.setInitValue(bpm); +} + +// create timeshifted samplebuffer and timeshifted m_slicePoints +void SlicerT::timeShiftSample() { + if (m_originalSample.frames() < 2048) { + return; + } + using std::vector; + + // original sample data + float sampleRate = m_originalSample.sampleRate(); + int originalFrames = m_originalSample.frames(); + + // target data TODO: fix this mess + bpm_t targetBPM = Engine::getSong()->getTempo(); + float speedRatio = (float)m_originalBPM.value() / targetBPM ; + int outFrames = speedRatio * originalFrames; + + // nothing to do here + if (targetBPM == m_originalBPM.value()) { + m_timeShiftedSample = SampleBuffer(m_originalSample.data(), m_originalSample.frames()); + return; + } + + // buffers + vector rawDataL(originalFrames, 0); + vector rawDataR(originalFrames, 0); + vector outDataL(outFrames, 0); + vector outDataR(outFrames, 0); + + vector bufferData(outFrames, sampleFrame()); + + // copy channels for processing + for (int i = 0;i &dataIn, std::vector &dataOut, float sampleRate, float pitchScale) { + using std::vector; + // processing parameters, lower is faster + // lower windows size seems to work better for time scaling, + // this is because the step site is scaled, but not the window size + // this causes slight timing differences between windows + // sadly, lower windowsize also reduces audio quality in general + // TODO: find solution + // oversampling is better if higher always (probably) + const int windowSize = 512; + const int overSampling = 32; + + // audio data + int inFrames = dataIn.size(); + int outFrames = dataOut.size(); + + float lengthRatio = (float)outFrames / inFrames; + + // values used + const int stepSize = (float)windowSize / overSampling; + const int numWindows = (float)inFrames / stepSize; + const float outStepSize = lengthRatio * (float)stepSize; // float, else inaccurate + const float freqPerBin = sampleRate/windowSize; + // very important + const float expectedPhaseIn = 2.*M_PI*(float)stepSize/(float)windowSize; + const float expectedPhaseOut = 2.*M_PI*(float)outStepSize/(float)windowSize; + + // initialize buffers + fftwf_complex FFTSpectrum[windowSize]; + vector FFTInput(windowSize, 0); + vector IFFTReconstruction(windowSize, 0); + vector allMagnitudes(windowSize, 0); + vector allFrequencies(windowSize, 0); + vector processedFreq(windowSize, 0); + vector processedMagn(windowSize, 0); + vector lastPhase(windowSize, 0); + vector sumPhase(windowSize, 0); + + vector outBuffer(outFrames, 0); + + // declare vars + float real, imag, phase, magnitude, freq, deltaPhase = 0; + int windowIndex = 0; + + // fft plans + fftwf_plan fftPlan; + fftPlan = fftwf_plan_dft_r2c_1d(windowSize, FFTInput.data(), FFTSpectrum, FFTW_MEASURE); + fftwf_plan ifftPlan; + ifftPlan = fftwf_plan_dft_c2r_1d(windowSize, FFTSpectrum, IFFTReconstruction.data(), FFTW_MEASURE); + + // remove oversampling, because the actual window is overSampling* bigger than stepsize + for (int i = 0;i < numWindows-overSampling;i++) { + windowIndex = i * stepSize; + + // FFT + memcpy(FFTInput.data(), dataIn.data() + windowIndex, windowSize*sizeof(float)); + fftwf_execute(fftPlan); + + // analysis step + for (int j = 0; j < windowSize; j++) { + + real = FFTSpectrum[j][0]; + imag = FFTSpectrum[j][1]; + + magnitude = 2.*sqrt(real*real + imag*imag); + phase = atan2(imag,real); + + freq = phase - lastPhase[j]; // subtract prev pahse to get phase diference + lastPhase[j] = phase; + + freq -= (float)j*expectedPhaseIn; // subtract expected phase + + // some black magic to get into +/- PI interval, revise later pls + long qpd = freq/M_PI; + if (qpd >= 0) qpd += qpd&1; + else qpd -= qpd&1; + freq -= M_PI*(float)qpd; + + freq = (float)overSampling*freq/(2.*M_PI); // idk + + freq = (float)j*freqPerBin + freq*freqPerBin; // "compute the k-th partials' true frequency" ok i guess + + allMagnitudes[j] = magnitude; + allFrequencies[j] = freq; + + } + + // pitch shifting + // takes all the values that are below the nyquist frequency (representable with our samplerate) + // nyquist frequency = samplerate / 2 + // and moves them to a different bin + // improve for larger pitch shift + // memset(processedFreq.data(), 0, processedFreq.size()*sizeof(float)); + // memset(processedMagn.data(), 0, processedFreq.size()*sizeof(float)); + // for (int j = 0; j < windowSize/2; j++) { + // int index = (float)j;// * m_noteThreshold.value(); + // if (index <= windowSize/2) { + // processedMagn[index] += allMagnitudes[j]; + // processedFreq[index] = allFrequencies[j];// * m_noteThreshold.value(); + // } + // } + + // synthesis, all the operations are the reverse of the analysis + for (int j = 0; j < windowSize; j++) { + magnitude = allMagnitudes[j]; + freq = allFrequencies[j]; + + deltaPhase = freq - (float)j*freqPerBin; + + deltaPhase /= freqPerBin; + + deltaPhase = 2.*M_PI*deltaPhase/overSampling;; + + deltaPhase += (float)j*expectedPhaseOut; + + sumPhase[j] += deltaPhase; + deltaPhase = sumPhase[j]; // this is the bin phase + + FFTSpectrum[j][0] = magnitude*cos(deltaPhase); + FFTSpectrum[j][1] = magnitude*sin(deltaPhase); + } + + // inverse fft + fftwf_execute(ifftPlan); + + // windowing + for (int j = 0; j < windowSize; j++) { + + float outIndex = i * outStepSize + j; + if (outIndex > outFrames) { + break; + } + + // calculate windows overlapping at index + float startWindowOverlap = ceil(outIndex / outStepSize + 0.00000001f); + float endWindowOverlap = ceil((float)(-outIndex + outFrames) / outStepSize + 0.00000001f); + float totalWindowOverlap = std::min( + std::min(startWindowOverlap, endWindowOverlap), + (float)overSampling); + + // discrete windowing + outBuffer[outIndex] += (float)overSampling/totalWindowOverlap*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); + + // continuos windowing + // float window = -0.5f*cos(2.*M_PI*(float)j/(float)windowSize)+0.5f; + // outBuffer[outIndex] += 2.0f*window*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); + } + } + + fftwf_destroy_plan(fftPlan); + fftwf_destroy_plan(ifftPlan); + + // normalize + float max = -1; + for (int i = 0;i * outClip) { + if (m_originalSample.frames() < 2048) { + return; + } + int ticksPerBar = DefaultTicksPerBar; + float sampleRate = m_timeShiftedSample.sampleRate(); + + float bpm = Engine::getSong()->getTempo(); + float samplesPerBeat = 60.0f / bpm * sampleRate; + float beats = (float)m_timeShiftedSample.frames() / samplesPerBeat; + + float barsInSample = beats / Engine::getSong()->getTimeSigModel().getDenominator(); + float totalTicks = ticksPerBar * barsInSample; + + for (int i = 0;ipush_back(sliceNote); + } +} + +void SlicerT::updateFile(QString file) { + m_originalSample.setAudioFile(file); + if (m_originalSample.frames() < 2048) { + return; + } + findSlices(); + findBPM(); + timeShiftSample(); + + emit dataChanged(); +} + +void SlicerT::updateSlices() { + findSlices(); +} + +void SlicerT::updateTimeShift() { + timeShiftSample(); +} + + +void SlicerT::saveSettings(QDomDocument & document, QDomElement & element) { + element.setAttribute("src", m_originalSample.audioFile()); + if (m_originalSample.audioFile().isEmpty()) + { + QString s; + element.setAttribute("sampledata", m_originalSample.toBase64(s)); + } + + element.setAttribute("totalSlices", (int)m_slicePoints.size()); + + for (int i = 0;icollectError(message); + } + } + else if (!element.attribute("sampledata").isEmpty()) + { + m_originalSample.loadFromBase64(element.attribute("srcdata")); + } + + if (!element.attribute("totalSlices").isEmpty()) { + int totalSlices = element.attribute("totalSlices").toInt(); + m_slicePoints = {}; + for (int i = 0;i( m ) ) ); +} +} // extern +} // namespace lmms + diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h new file mode 100644 index 00000000000..745a1e44843 --- /dev/null +++ b/plugins/SlicerT/SlicerT.h @@ -0,0 +1,85 @@ +/* + * SlicerT.h - declaration of class SlicerT + * + * Copyright (c) 2006-2008 Andreas Brandmaier + * + * 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 SLICERT_H +#define SLICERT_H + +#include "SlicerTUI.h" + +#include + +#include "Note.h" +#include "Instrument.h" +#include "InstrumentView.h" +#include "AutomatableModel.h" +#include "SampleBuffer.h" + + +namespace lmms +{ + +class SlicerT : public Instrument{ + Q_OBJECT + public: + SlicerT(InstrumentTrack * instrumentTrack); + ~SlicerT() override = default; + + void playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) override; + + void saveSettings( QDomDocument & document, QDomElement & element ) override; + void loadSettings( const QDomElement & element ) override; + + QString nodeName() const override; + gui::PluginView * instantiateView( QWidget * parent ) override; + + void writeToMidi(std::vector * outClip); + + public slots: + void updateFile(QString file); + void updateTimeShift(); + void updateSlices(); + + signals: + void isPlaying(float current, float start, float end); + + private: + FloatModel m_noteThreshold; + FloatModel m_fadeOutFrames; + IntModel m_originalBPM; + + SampleBuffer m_originalSample; + SampleBuffer m_timeShiftedSample; + std::vector m_slicePoints; + + void findSlices(); + void findBPM(); + void timeShiftSample(); + void phaseVocoder(std::vector &in, std::vector &out, float sampleRate, float pitchScale); + + friend class gui::SlicerTUI; + friend class gui::WaveForm; +}; +} + +#endif diff --git a/plugins/SlicerT/SlicerTUI.cpp b/plugins/SlicerT/SlicerTUI.cpp new file mode 100644 index 00000000000..8ee2e79b28d --- /dev/null +++ b/plugins/SlicerT/SlicerTUI.cpp @@ -0,0 +1,154 @@ +#include "SlicerTUI.h" +#include "SlicerT.h" + +#include +#include +#include + +#include "StringPairDrag.h" +#include "Clipboard.h" +#include "Track.h" +#include "DataFile.h" + +#include "Engine.h" +#include "Song.h" +#include "InstrumentTrack.h" + +#include "embed.h" + +namespace lmms +{ + + +namespace gui +{ + +SlicerTUI::SlicerTUI( SlicerT * instrument, + QWidget * parent ) : + InstrumentViewFixedSize( instrument, parent ), + m_slicerTParent(instrument), + m_noteThresholdKnob(KnobType::Dark28, this), + m_fadeOutKnob(KnobType::Dark28, this), + m_bpmBox(3, "21pink", this), + m_resetButton(embed::getIconPixmap("reload"), QString(), this), + m_timeShiftButton(embed::getIconPixmap("max_length"), QString(), this), + m_midiExportButton(embed::getIconPixmap("midi_tab"), QString(), this), + m_wf(245, 125, instrument, this) +{ + setAcceptDrops( true ); + + m_wf.move(2, 5); + + m_bpmBox.move(2, 150); + m_bpmBox.setToolTip(tr("Original sample BPM")); + m_bpmBox.setLabel(tr("BPM")); + m_bpmBox.setModel(&m_slicerTParent->m_originalBPM); + + m_timeShiftButton.move(70, 150); + m_timeShiftButton.setToolTip(tr("Timeshift sample")); + connect(&m_timeShiftButton, SIGNAL( clicked() ), m_slicerTParent, SLOT( updateTimeShift() )); + + m_fadeOutKnob.move(200, 150); + m_fadeOutKnob.setToolTip(tr("FadeOut for notes")); + m_fadeOutKnob.setLabel(tr("FadeOut")); + m_fadeOutKnob.setModel(&m_slicerTParent->m_fadeOutFrames); + + m_midiExportButton.move(150, 150); + m_midiExportButton.setToolTip(tr("Copy midi pattern to clipboard")); + connect(&m_midiExportButton, SIGNAL( clicked() ), this, SLOT( exportMidi() )); + + m_noteThresholdKnob.move(7, 200); + m_noteThresholdKnob.setToolTip(tr("Threshold used for slicing")); + m_noteThresholdKnob.setLabel(tr("Threshold")); + m_noteThresholdKnob.setModel(&m_slicerTParent->m_noteThreshold); + + m_resetButton.move(70, 200); + m_resetButton.setToolTip(tr("Reset Slices")); + connect(&m_resetButton, SIGNAL( clicked() ), m_slicerTParent, SLOT( updateSlices() )); + + + +} + +// copied from piano roll +void SlicerTUI::exportMidi() { + using namespace Clipboard; + + DataFile dataFile( DataFile::Type::ClipboardData ); + QDomElement note_list = dataFile.createElement( "note-list" ); + dataFile.content().appendChild( note_list ); + + std::vector notes; + m_slicerTParent->writeToMidi(¬es); + if (notes.size() == 0) { + return; + } + + TimePos start_pos( notes.front().pos().getBar(), 0 ); + for( Note note : notes ) + { + Note clip_note( note ); + clip_note.setPos( clip_note.pos( start_pos ) ); + clip_note.saveState( dataFile, note_list ); + } + + copyString( dataFile.toString(), MimeType::Default ); + +} + +// all the drag stuff is copied from AudioFileProcessor +void SlicerTUI::dragEnterEvent( QDragEnterEvent * dee ) +{ + // For mimeType() and MimeType enum class + using namespace Clipboard; + + if( dee->mimeData()->hasFormat( mimeType( MimeType::StringPair ) ) ) + { + QString txt = dee->mimeData()->data( + mimeType( MimeType::StringPair ) ); + if( txt.section( ':', 0, 0 ) == QString( "clip_%1" ).arg( + static_cast(Track::Type::Sample) ) ) + { + dee->acceptProposedAction(); + } + else if( txt.section( ':', 0, 0 ) == "samplefile" ) + { + dee->acceptProposedAction(); + } + else + { + dee->ignore(); + } + } + else + { + dee->ignore(); + } + +} + +void SlicerTUI::dropEvent( QDropEvent * de ) { + QString type = StringPairDrag::decodeKey( de ); + QString value = StringPairDrag::decodeValue( de ); + if( type == "samplefile" ) + { + m_slicerTParent->updateFile( value ); + // castModel()->setAudioFile( value ); + // de->accept(); + // set m_wf wave file + return; + } + else if( type == QString( "clip_%1" ).arg( static_cast(Track::Type::Sample) ) ) + { + DataFile dataFile( value.toUtf8() ); + m_slicerTParent->updateFile( dataFile.content().firstChild().toElement().attribute( "src" ) ); + de->accept(); + return; + } + + de->ignore(); +} +} +} + + diff --git a/plugins/SlicerT/SlicerTUI.h b/plugins/SlicerT/SlicerTUI.h new file mode 100644 index 00000000000..4bd77ba5267 --- /dev/null +++ b/plugins/SlicerT/SlicerTUI.h @@ -0,0 +1,61 @@ +#include "WaveForm.h" + +#include +#include + +#include "Instrument.h" +#include "InstrumentView.h" +#include "Knob.h" +#include "LcdSpinBox.h" + + +#ifndef SLICERT_UI_H +#define SLICERT_UI_H + +namespace lmms +{ + +class SlicerT; + +namespace gui +{ + + +class SlicerTUI : public InstrumentViewFixedSize +{ + Q_OBJECT +public: + SlicerTUI( SlicerT * instrument, + QWidget * parent ); + ~SlicerTUI() override = default; + +protected slots: + void exportMidi(); + //void sampleSizeChanged( float _new_sample_length ); + +protected: + virtual void dragEnterEvent( QDragEnterEvent * _dee ); + virtual void dropEvent( QDropEvent * _de ); + +private: + SlicerT * m_slicerTParent; + + Knob m_noteThresholdKnob; + Knob m_fadeOutKnob; + LcdSpinBox m_bpmBox; + + QPushButton m_resetButton; + QPushButton m_timeShiftButton; + QPushButton m_midiExportButton; + + WaveForm m_wf; + + +} ; + + +} // namespace gui + +} // namespace lmms + +#endif \ No newline at end of file diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/WaveForm.cpp new file mode 100644 index 00000000000..9257cd9bccf --- /dev/null +++ b/plugins/SlicerT/WaveForm.cpp @@ -0,0 +1,247 @@ +#include "WaveForm.h" +#include "SlicerT.h" + +#include + +namespace lmms +{ + + +namespace gui +{ + WaveForm::WaveForm(int w, int h, SlicerT * instrument, QWidget * parent) : + QWidget(parent), + m_sliceEditor(QPixmap(w, h*(1 - m_m_seekerRatio) - m_margin)), + m_seeker(QPixmap(w, h*m_m_seekerRatio)), + m_currentSample(instrument->m_originalSample.data(), instrument->m_originalSample.frames()), + m_slicePoints(instrument->m_slicePoints) + { + + + m_width = w; + m_height = h; + m_slicerTParent = instrument; + setFixedSize(m_width, m_height); + setMouseTracking( true ); + setAcceptDrops( true ); + + m_sliceEditor.fill(m_waveformBgColor); + m_seeker.fill(m_waveformBgColor); + + connect(m_slicerTParent, + SIGNAL(isPlaying(float, float, float)), + this, + SLOT(isPlaying(float, float, float))); + + connect(m_slicerTParent, SIGNAL(dataChanged()), this, SLOT(updateData())); + + updateUI(); + } + + void WaveForm::drawEditor() { + m_sliceEditor.fill(m_waveformBgColor); + QPainter brush(&m_sliceEditor); + brush.setPen(m_waveformColor); + + float startFrame = m_seekerStart * m_currentSample.frames(); + float endFrame = m_seekerEnd * m_currentSample.frames(); + + m_currentSample.visualize( + brush, + QRect( 0, 0, m_sliceEditor.width(), m_sliceEditor.height() ), + startFrame, endFrame); + + + for (int i = 0;i= startFrame && sliceIndex <= endFrame) { + float xPos = (float)(sliceIndex - startFrame) / (float)(endFrame - startFrame) * (float)m_width; + if (i == m_sliceSelected) { + brush.setPen(QPen(m_selectedSliceColor, 2)); + } + + brush.drawLine(xPos, 0, xPos, m_height); + } + } + } + + void WaveForm::drawSeeker() { + m_seeker.fill(m_waveformBgColor); + QPainter brush(&m_seeker); + brush.setPen(m_waveformColor); + + m_currentSample.visualize( + brush, + QRect( 0, 0, m_seeker.width(), m_seeker.height() ), + 0, m_currentSample.frames()); + + // draw slice points + brush.setPen(m_sliceColor); + for (int i = 0;im_originalSample.data(), m_slicerTParent->m_originalSample.frames()); + updateUI(); + } + + + void WaveForm::isPlaying(float current, float start, float end) { + m_noteCurrent = current; + m_noteStart = start; + m_noteEnd = end; + drawSeeker(); + update(); + } + + void WaveForm::mousePressEvent( QMouseEvent * me ) { + float normalizedClick = (float)me->x() / m_width; + + if (me->button() == Qt::MouseButton::MiddleButton) { + m_seekerStart = 0; + m_seekerEnd = 1; + return; + } + + if (me->y() < m_height*m_m_seekerRatio) { + if (abs(normalizedClick - m_seekerStart) < 0.03) { + m_currentlyDragging = m_draggingTypes::m_seekerStart; + + } else if (abs(normalizedClick - m_seekerEnd) < 0.03) { + m_currentlyDragging = m_draggingTypes::m_seekerEnd; + + } else if (normalizedClick > m_seekerStart && normalizedClick < m_seekerEnd) { + m_currentlyDragging = m_draggingTypes::m_seekerMiddle; + m_seekerMiddle = normalizedClick; + } + + } else { + m_sliceSelected = -1; + float startFrame = m_seekerStart * m_currentSample.frames(); + float endFrame = m_seekerEnd * m_currentSample.frames(); + + for (int i = 0;ibutton() == Qt::MouseButton::LeftButton) { + m_isDragging = true; + + } else if (me->button() == Qt::MouseButton::RightButton) { + if (m_sliceSelected != -1 && m_slicePoints.size() > 2) { + m_slicePoints.erase(m_slicePoints.begin() + m_sliceSelected); + m_sliceSelected = -1; + } + } + + } + + void WaveForm::mouseReleaseEvent( QMouseEvent * me ) { + m_isDragging = false; + m_currentlyDragging = m_draggingTypes::nothing; + updateUI(); + } + + void WaveForm::mouseMoveEvent( QMouseEvent * me ) { + float normalizedClick = (float)me->x() / m_width; + + // handle dragging events + if (m_isDragging) { + // printf("drag type:%i , m_seekerStart: %f , m_seekerEnd: %f \n", m_currentlyDragging, m_seekerStart, m_seekerEnd); + if (m_currentlyDragging == m_draggingTypes::m_seekerStart) { + m_seekerStart = std::clamp(normalizedClick, 0.0f, m_seekerEnd - 0.13f); + + } else if (m_currentlyDragging == m_draggingTypes::m_seekerEnd) { + m_seekerEnd = std::clamp(normalizedClick, m_seekerStart + 0.13f, 1.0f);; + + } else if (m_currentlyDragging == m_draggingTypes::m_seekerMiddle) { + float distStart = m_seekerStart - m_seekerMiddle; + float distEnd = m_seekerEnd - m_seekerMiddle; + + m_seekerMiddle = normalizedClick; + + if (m_seekerMiddle + distStart > 0 && m_seekerMiddle + distEnd < 1) { + m_seekerStart = m_seekerMiddle + distStart; + m_seekerEnd = m_seekerMiddle + distEnd; + } + + } else if (m_currentlyDragging == m_draggingTypes::slicePoint) { + float startFrame = m_seekerStart * m_currentSample.frames(); + float endFrame = m_seekerEnd * m_currentSample.frames(); + + m_slicePoints[m_sliceSelected] = startFrame + normalizedClick * (endFrame - startFrame); + + m_slicePoints[m_sliceSelected] = std::clamp(m_slicePoints[m_sliceSelected], 0, m_currentSample.frames()); + + std::sort(m_slicePoints.begin(), m_slicePoints.end()); + + } + updateUI(); + } else { + + } + + + + } + + void WaveForm::mouseDoubleClickEvent(QMouseEvent * me) { + float normalizedClick = (float)me->x() / m_width; + float startFrame = m_seekerStart * m_currentSample.frames(); + float endFrame = m_seekerEnd * m_currentSample.frames(); + + float slicePosition = startFrame + normalizedClick * (endFrame - startFrame); + + for (int i = 0;i +#include +#include +#include +#include + +#include "SampleBuffer.h" + +#ifndef WAVEFORM_H +#define WAVEFORM_H + +namespace lmms +{ + +class SlicerT; + +namespace gui +{ + + +class WaveForm : public QWidget { + Q_OBJECT + protected: + virtual void mousePressEvent(QMouseEvent * me); + virtual void mouseReleaseEvent(QMouseEvent * me); + virtual void mouseMoveEvent(QMouseEvent * me); + virtual void mouseDoubleClickEvent(QMouseEvent * me); + + virtual void paintEvent(QPaintEvent * pe); + + private: + int m_width; + int m_height; + float m_m_seekerRatio = 0.3f; + int m_margin = 5; + QColor m_waveformBgColor = QColor(11, 11, 11); + QColor m_waveformColor = QColor(124, 49, 214); + QColor m_playColor = QColor(255, 255, 255, 200); + QColor m_playHighlighColor = QColor(255, 255, 255, 70); + QColor m_sliceColor = QColor(49, 214, 124); + QColor m_selectedSliceColor = QColor(172, 236, 190); + QColor m_seekerColor = QColor(214, 124, 49); + QColor m_seekerShadowColor = QColor(0, 0, 0, 175); + + enum class m_draggingTypes { + nothing, + m_seekerStart, + m_seekerEnd, + m_seekerMiddle, + slicePoint, + }; + m_draggingTypes m_currentlyDragging; + bool m_isDragging = false; + + float m_seekerStart = 0; + float m_seekerEnd = 1; + float m_seekerMiddle = 0.5f; + int m_sliceSelected = 0; + + float m_noteCurrent; + float m_noteStart; + float m_noteEnd; + + QPixmap m_sliceEditor; + QPixmap m_seeker; + + SampleBuffer m_currentSample; + + void drawEditor(); + void drawSeeker(); + void updateUI(); + + public slots: + void updateData(); + void isPlaying(float current, float start, float end); + + public: + WaveForm(int w, int h, SlicerT * instrument, QWidget * parent); + + private: + SlicerT * m_slicerTParent; + std::vector & m_slicePoints; + +}; +}} + + +#endif \ No newline at end of file diff --git a/plugins/SlicerT/logo.png b/plugins/SlicerT/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..aead03f79a8a4d5b6be99c7b70d15a1f96022482 GIT binary patch literal 39244 zcmeFYbx@p5^Dc}8x8Ux<-QC>@PHsXFzm zI_JN4sbTt_>A9x+nx5X<_N%J03<@FvA_N2kikz&Z8n}M^Q{Z92|07O534yCAfR3Dn zoE$v_Joo^t3Hc5J@{bOnKy1qlrS4Q~4aE*N0>kItV$015L~9SxQV|1KwiW!k^% zK!Ig8NGJ#taGeD%>|hxSTvvdLb@<=r>ITbyKJb72$%3Un$p5HiFiMEEgoLV`ganzBi=&0L9RLEt zE6YDYNVZRusNYDvpB_8hZW*L96#&mo?X}1i!@4rRMFiL;P zcNr>&`ypVbee05N|Jx|kWOT;Tn4oi#4x@YBOn;S~&9B*CY?`xlMnm3JF6&!Wn#WcC zDz87C#Ef+J_{TpMeUG4$eV?Bfi&*g;y;PH?U;N8$qVOyF0!xAdxi(BR5mK30$jRf6 zw++u0EoYk<1w@+9>$rmtc+ig?pSAj&yW@yU-J#y)erD*}uI2i?q%uQD%;fP7*&Z5q zfmmq?n%OgGlSYCYpp7_#6)Cmx65UOQCh73`GgocPt*|m_5Tko?HzX?R=SvJg?Ng_~ z+}cWy2GZ$+oH)}@+vOkz6rjG7zLKGC>ezJeqXYZt>^iOyP*T1s>=7Yt{ z#EFHCnU%%fp5@<1xVlMsfIgeub29WXqIJi;%JH($2^iPC8 zE?HXJJN@Mpn7sd(%-rmsdQR>xc7N%Zo3Q}w0QO)|SFn9-|Do?@ZSgO%{=>FEJ%73L z?}mV<`zQW?X#d0azm&mRN=p2aj%MzECY6&EqWEJkzqzBCwK@M^q6x1Fhbg<61rsZ$ zi8&J|D=&b_l$V=>iIa!hgw>pnot=Y~?cbo}99-Q@9LxZJpuphF)?ge~J^&jp4;v2? zhlK?Xcpw;ukIjsYiOtN!f{oMMgom4(>)#+$T&%%SX=3;9R{eo82Sb^$bFs2=uyZj1 z0ANEnIRU0jyxg2-U{Fq0Rx@4;GhTL{zo7oC1HYK6oDc;&GwZ)vRP9XMEF4|zg(wuQ z9o)VC)uCZ+4^VeA`9m5THy@j9(gz zXaWu#YkLz*0E?4@o?63io*D{y%J^#w&n`_~H6{iiLSR)9at115}#m5+&)_y0>6%ikSl`I9sL;jtjg z|Bn;FzZCv0$$-cGtqq*Kz`2m+pULp=oPq8AfBE_QTKvD9f{g6Hll+hP{V!errR#sh z!2d}3ztQzyy8cHD{Ew9X8(sg`=tBJ06eQK2naF=IY}`MujP|2 z+o|SbT^tXRg{$flP7{GJ#2FOTrsdmk*4aaq5Ly7{jM#g?DyybmBK%Jc@^`Gk@)!|h zkEQCR{PtlQAF8*@f)MiNNMCFR9|sQ=n-Ys6(;~l_`%k)G54ze>vI_{;N{0XAH?=#Vs65N;gh8( zBFr|;!Q12Q{!!0O$I|5DdA$hf_T(w{T0jl%Iagp$lq^Fx(i-sLJb4Y8ZasW>fAwlc z`&U zR-<(6k#0<1LNl|D;fr$5aN?q;mZdeKj;JXBM`450@;=Dx8wLlxBTSg?!~ zF7DC_?yQFaJ?!!o&_Av-*`IZ9Nt|LM&-iumI2E=-K1|Z!(e{Nz0Yl*4ysi$91g?M5 z)jLsgPWb0Ua=#L$y3%|y88Cow~v z`EtDy{k%b2xWl#P!DVH6AC?e%V1V}mYPksSnCz$r2Cm{|{bK&W204nW)^8P9_WhY^ zaDINk0G~=iIG*$;y{XoD=IVAbdOuLcnsw=BL=G1HT-~py!ImlR@KiXY7<1@+_xY~0 zzsX!zF79r!U;gQEEek0Wxc!JlykF@U^dWYfBNp4eTB2CwYL+|$Z8PF18rA((D2c-o zGlBXzg@rIv1h`rr)|gC9=G{A0&V|;~*t3g=55K7WMU0Od#3(k47Cu5LLwQSw8$s=X%hyhA-NzS(O(Nb$;i=3U22wotSO}=L{3jPZA;T(JMo? zjuoSm7D(Ml@M^Q>e(_e{7e%NfPxtw$Fh6gW@i)ijk@BXFrC+GjU$LCa)9y#i_C+-|k)o6>%H- z=I4HN;lM?~!46{%ra@}I^lnVreA4UU82;$wqUr5DXW$>;h@TfzNv~Z2OzpdidIKf~ zT7JWSP@6E)MG`9=fv~oDqs&T@!y;`G=s?^?w9LFiMoj}1=eI#*ybZQXOLw^AuFeoj zo{Cw#kRciOirIJ8ME~w}#(?IDtziT0b@R{V*W=j)7%;}KS1CKvL^P!-7Am`cl}xD)>ny%#Ov0G&v<98* z;n9g(zIXR(`LkFKb`GwC0ae*^){($BSs$s{vQGiI2k#|rfIFJIu&G`SZ5%hvF}lRk zXz%l(oT#@J!+N{fcx(G`OZdAjU5z7PfmT61X*2uA>K?Sd-@fs9O1q`KWV5mqY-wu9Tj^4aB*VFIY8l=8pj#{vLqyd#82X;ze%}xCwYh6ZkeGLfn6=$F)aJ$% z(&g9LzS4=PLs>@!w0X&a#tUD?Z+v2GvBHY4&Xj?6)er}GGrARp!9^xo^R5&LR?b3V zZM#Ki%rHJVkhZi1Hvu~uhxut^QZm~$1ukys{M4Iy^D>_Hyprqa@tb+`}}8;A=hEUlPWC_{9{g$H%VKE&nL&fe^JSsGess$(V{8r1+ReY&!w+q}7#rJWO%2>z%#R46!UTx|cdWY9po& z3>4oPTz@Lg@~e=OM2H{mlw#!lapQc!9B29CtH5vUkIrN@;Z`hY#L&cMN~rH$O1f-b zPC+jF5uM`N-A<4xOP{0Y*ogyvVC+W}tJ!6e+dKc9YaiE7eAPq1$3UC*vd?^(IsFv+4ny}L7Z0b2An~Wuji||+x zj^F3QM%SCeaVTNaE9UOIg-pV>xPOkq5pw%GD zsctjb;A#g^v3K3_9H2g4?!qZ8KnX{&Z8Lf!o;@IUY%Zy{YUrnYDVR^)_dCs-`&QtW zapTR>fsPGH*Q|Qy+g#*lI-Fi>1wS#A01h=YBK=g{`%yp2vWKfh#rvH>W8KVjn zF>0?4rq6F8oV7mx zG#79Q$u`-RACe*g(y^7XW#osg-9gViRTvRzg59mrkSCbm1 znoWmL8hU(5E^ty-i{|l3%It@3`WRFtD2PKHe9@JIdfl`l?u5s)v5&DQ@g-FEIc#~t zB(fVwxK?~azmS)vL3HNt>bDMT$-+_C7cH*ZTrqJyNlT!GnMx(U7J@``hS>U%#NCR^ zPQ-%~njs6Aru^8(Jked~sbODo<|;}h*OSyPRhU-OvWh*v5{W9S=DCvn>vP?$|Mcc$ z>^q!_96KTWfr3<`VVZ#W0g4^jl2eFwyRyNMmXh?ntXru~2qzBP zrMS*dD(q1im#`AqLz2N8M|Ne)aO5h}t|M7I#LUHtgJ^@0JDfA!`)xOjR4Yd&JQrWu z(Y7QdNX-%mxhay_5iG<#4GwzH->18Xez7^M!Q@_hS51EZnRFLCUKIe{a zaoaPNJ{pAc*i<|$--ot}@Tpj+%aMBN=G2=Hc^GVCCxr9nq>PBQdmDt``0YrQ^?+M7 zg!n6HiX4zRMXR#W9mIkft#hThP+m^c3$f3;{)>CPjcbHW{#wkMBsUxpeH_i>17eAD zu#qOM^;StrkMSJs01jN6Lo%JA_x?l$=+ohGRucTt5+BS}nCKUxYuGu$U|9R4S^W}E zv(3AF$_B_ouC8C6Ds*XGCYnSF8?s{XR}%I**)8HZ1?=0XND-7GotZe)b6~h=sknA6 zPurvb8ATyzh;Sm#DZKZX@7~Qmx+ZA`Xo>KA?L%5ZnIy3g#uy2G({BM7f3a7jKw{qU z@rPx9tMxrp3YGsJe?{y5SsxD1L(>#SpRu~TzIui0M%T0)p-ee4FYcRdJ-K7T8+&)Z zl7G?piM^uOc;H|KPEIT|&#>f?;q*F7=AK2JuK3H@SCtN#5Y5}{7Abe>DV2if=>sRJ znZs|1g>J6Ndp=UQ(q(LAR5M%foKe`NiEVrduuKmFv=UPIOKT``*5A^0W7V3@N0;FP z@;=ioJVs26Kp_-s*i;IBP}uYCZVK5Rg!RN%X1wiOG(w zTXaZ1L&xe+_@!MrI()43n9ij!sJ^CtZpO4I)y!E0z+GYxF;WqIU4?SvV#T% zc?=I#=YY^}2=q4N7N5PbUsBTkA_`GlEMmK%UoNBjR-$Jdwf8Mba+I0?CJ>XKRHKGn zk6|yOiNcxjqt2*DS5+=9;9yPO(eW*w!WuC8CibPo&T{0)jCS3Xd-vou|p_`Z~NpX_`OB7xc9IiZp$5H+EuljcHh% zNI#^d{oL6}lUuKryCm&S$*xz~CgsZM#-td3xum(V`WQ^x(T1hrMnJR_~pmEgpFPY@)j1 z@zEAQ9FC_xMzB^5QjqyQ9Nd6^%7|?SU)~@2X124U@sOUY8XHF3X4fap0$$Veb9wMc1|G-55=6JOV@J6o|HqR7xmbL6)(@&0 zdv8WR8yi{^R0!fHRZk&|*Y{w_zPJ>OuYj-X+d^y$To;)df$ zVka_Ngv+KJ{mkQiEn?>hGxx13=*P#TM$D8*b46gjs?Ka> zK4Ot5dIaWz>72dJ)v(|aHN~WxuqUb#Mc)jH zGjpaoggys@cWc42TS`O!e7?;A!lZMEM#J+_z0_BGi8?C6DGj00FX|=H=qb=Y*{xdh zH`ib@8kb*IGa_}B(57bEL9~0EpOkhmAl4EJ+kkdk+6E6_d(zykk;{r>mbSa^KjyCN zGa1wi1;2J$V9d19BBCwNrc_C{v(XzTD_w^+M3Nw_*Z+u~&3=SuG24xwEy+OI`a#*} zaAg{(6q`zQ--sl&RRvnLk)#$qj7XkD5f!pWHfI-w^K@`?sfFl~Iyek~BMs*;#dcIreq;37QL|afo`b&dIZnUovaZMw6m$?HF z_`Od+!&;!O7K)Mz%Cz0v)709rC$kASVBKDaH`{`Xd-Z#WhCSZ;beHGSt;0OJ&-eagt9LM%sj!)h8 zVVz=Q9NY(dxd20Pthk+A-hD+LKhxq;#$bC52Uz-fjnGq5&poae>&4z>j5^qzg7WN> zEuJn@yl(Puv^YfS#mdK#Esf~?M~U`dR$nEm2WutSTRV^z@HOGyFBwCA?;62XAn#&A zHWg{Is4!fXiinCY%nc*1yYZblav7(P;$3rGP?jGpVV`9589c~I^adG<_bsW8d4ayj zn&FpRd_>@+U{@*95rF2SxY$?RGYcg|=uMcnq?i;5Ocr?p26S}Zy4NPF?6}9lUu_@j z`aVz@4${WHjlW&8yhS{(0v|*W*^zg2`1RqksN0mgfAS*>XhcVGg@B9rV~>vJYzGH*D#>ulxg3BE&jT`wmr9r6A$@bV#USmed>t;286GGK4JEx@nK zsy5gAO=UE-=aq1Geo_dX4o7WGKAKUI>@f2nf(4E^53wm6KSXI$yx}XJgBfy`AB1_W zPebg19QE<&ha$7Q11AD2_bVVU!uA{LS_A6E<<0%$FV~>Ep7YXO+ny7&A={jeA{0&Z zgtvQF5$*0*5nqXPn&;!VSHg$0yH_gj8uzwR1yr8EaPu7*jZ%6{Y@RjNlf$1kDDWMJ z`shIF(=;AL&e?%r!=1&sGg>5f)=N3HTRaB0dTD7=qqth!!J)uA?Kg5)2Y-{hD3w~! zU^fVDlrZ2bHTC?4CEyZwXC>gICvkrnvu>2nAGK3ji^(B?UMBzx-jI_&ex63ggwR1D7``PKUso;wDJ6stt4Ct{XI;BkXa|VHB z_gcMbi>L(C1<{yHWtpi8o%SWwk1VTEkn>$Kz~pL7Mzx*Td~F6BT}uXo!dMl3lAZ?_A9A)QBUG{qyV5D%oUl_Hdm&jq#5?}QJn9J)-?*=bqW zW@#j2K0RET^vGH7U1Vs_O43KJcb296dgzd{N7}}1b41wMKEAxU#CteTSrEEI zzzs8rC)Z{vz_D((cW;+*0FE(!ZV*f0%fje!j;}Jsu6Id8(%UQAYr0DBP#h<=V1vK% z9`Wg?HLMMvO1(xR2JAh~=lS~BhwNNQIx0C>n$0P3x+Gm6XK0rcbme=`KkNzB>LH!J zfCgB=i#wu_So&gyariqv?z-}MF*ER9N^!w`w{pqFAa<}}n;J;z{ssQ+2LRs85Q^f& z28De;7;OksWXMqlZcBQw{UO<2rTi00=eD#kIDGC19-ek8yH7@UyuF1%^>fFAKv0g# z)KEiE(zMyBVriCASjBSIu!f$pzvl0vrhC@e+ZA(?LGD}{@8}!$2#p(`COa{iYeUJ0 zXkop}s6Rn}weSSI6ZYQL_6rkyd%g-6xm$1Plv%$hz;7E@72r6prz=CLO|p|!kg@y_ zCCW{BqfZ)u|Exi*X8VrT^y%PIJV|o;agp)ys0~!PD3+`P%oC+O*M{}>6Y7=>cziie zt-ZJJQL0%uHjs|Rt>#=U_uN;1;8A(2?aO_gpL`iGA;krxapq+h@U1141e>(&lj8_f zs7S9$uO<(OHP<3xaCLm#3Z&rr-1~}5_bo1OWZ0r*aA8{k+VMP<#rNe{CFXe$_)PL} z#PV2yyEFth_gjRz=lVLglWdUeYjYMBgVe?eUH@f%)tWv@G6jZ1VZXqS^#<-Wlttp& zy5Zlb`yysEAqCQRg>@p`XKWzg>*GRhQGGmV;8olRVb6V~;Wyh>koUvTO)lc@;b1a? zT~vKymMVfk_LNOCg@(dG-{dgeTAM{bY)8ca6GG2=#7xm|om!p}^GH_4hA&`Cj{_(j ztw0Ow&y?VR216bGFdQ8-~d`NTKL%vGz#4Au&kj zifUgrvPj*|e>wU2e9a$>xA7IlxS$;yNkju z)7nM4=d2$N@48gFw-kD=SBJonm`ZE#CTZluSrt`8Cz2>1umZit>C5-Z<>Tz+^VPlt z-rynTtjNDGLF+K0!+5V;a0Yvjw#8VaJ7AaK;Up0_EAG3E>pwqwo#_VH@o_k%pOKe3FT#}^mhN4e3ud366d!=St|Xy z6>nlVvcNSQnz^%W{_!xMkMr2+K(Bv+?)CQ9z|Pl8d&r2grbAEd-S*Ru#wr-U4VrCA zg{aTsYV~rT6}oq7++He;niE#{X_St#LsA51SA$xcg;`W|5j3p0%B5RCX&;3;Bbw(i zh)4j~CF?CwhqrSpW=F49JD$*vlc5Q|kSRn9AU}*E7!DMD!`(h#LeOd_r4MO@;YhKzkIv<@#?ABsxK&V*AeP2jg-CPk z3ol>;(W59Vq>1k`WkLfNjx0wnNQ~m7g;gdMR%imtDSVpKxs;n9Kq>T?iw>W^V#?NA zlGzN{E7p?UM^Mg0WA*I6C4FwvE2WF8HKMEaecH*qyk&$5v9VN5^{H+tgIG#Bl_U;l zgj+xEr3USKB=$#SP2bf^wRLk1)T`w)z{OXUd;7hNc#R7`q^@Zu_Uz*;EL5hFZpT6< zk^ell%k;!x8eOuW+-XwhZlV`x&^bItd z>wm7de)SEdtaIn9fWN`|w;(x7^7S-#wYY+(GZD|XsF~iLg>>mSFJj+N(vli{_f}aW|Fmrm-TD1eF`E`~z zI(GP-I`%{}0wNUFn^sR*y#xs!YC{*^9?;%4*Ze`a+MHX7I1AYbJ|+68ikvQ)9vtJ2 zxy0LK^+;qfyKx$Zr3zo-XUn?T@2QIt0`>X*YcA7zMRpe8L@pp7emlPI3D|(ZA*0LV zQqo+k9cHOO*mW~?YSkn-pSUcxe2Mr$B>8u-nti{)%6x~UTY|0Q(>l&WYsz7MxA4Vh zv?0>x0mr1fw>gz$kt@5~;kfF9)@`O5WtqoBTvr#=2wJ1xQwgd~eCwO)4kUHU9spXoC)mnM!v09^Cj zHrMuc-aM%NFo#@dyw|Gh=e>5JBo1PywgZD)=TG-OZKcP78+R|l4+0_&iQw$d8sn#j zH--mXD1O~{Tzb@|L_PlkKCgh{d}oWusBdn`i7X7Lk-10kze?W(aowO#Rxw| z=>hQqQG31dR@lL(w+in3$F<@l^!zAcHVCz;@2kr_hds1Cb8kJCAiQU|p0kBvk$ai9 z1Hg94>!rx+Ac(MM11UgP__%9Q7U|UJd5dMpvh#4Ea|8G^Kk1*p*mYol-$7GlFeZ2B z``ewpXZArK<5O-i&FUc~lwZo4yEEd#UD|s5xy|qX72^L|?nP^Y@XXUVz;fnBl z&aklv&)Q9DT*4E3%X((FeGLIgVLjFQu2xb$Y4J|tk3vk)bHQox54x$-b+VTYA;}U7 zt;}jJoo^6QMG3HgP_43by@PwJ6SNx`Yf$@WY5&-`yGN}bJ!$vr^XSkJL&wO>mEm&- z@V7kN3^8Hb$%RsC#lAja(Q&+xL7Z(ea<)d2KToNE46^_X9g3+YOZm}Ln+*6h1D~-~ zQl}4&Ai7kQ)F}Nd4$=vKqG-MLR~HT!zONhKf?SqyPA4Tt_-8%E9S)XS^||~9B)*H% z95BIPe3)$Z$~{MTYGq}CO_Ky)hAZQJ8gnc@ zGnPPa4561fNg>mSMK*fSq+HzlP{$DAV*fDf^zkP2)~p=;kv$n5IOTU(n47ozL z79YPos+nA=uw>LO{?Jq>|EahkA=u!F?A%EebiX&`}<>khSo@2yFheG%e~e2V z@BG0$C}k0wmt#-3w8~1PFCi%ulg$9=n=` zF$;DQK^}RJ$axVZw+((HE8M7zoUpLTMh=-@Lq6n8nL?vXA$IhYqYO;Zn_x$txv-U- zxp~qlcsJQ;gfT|y?sc=2td-T`B`u^A6v~+a_iC|O@C+-$#m_bu))horKb&gSB8c=`s}9#%3! zpLxw^9KEv&Tkl1718nACi?WGWMqT(S9^f;)vB}h zbu*o$`PdH)gsteqc3}b}|F9qB%=Gh)SO6WkZdysTmpj@9tX4qW6BAwoY+9C+*Qv2tl&Y~QY~TA7@ahO6rL2PZMIh&bvpZmKNJFyOhb&$2kYTshb{VJ{2oT!kV>@q7qlkh_4lHmb1V3cXqh{( zUSd+NpXUo#fNdTm8_t+2x$qNT71nyz{iC!X1n=Qf*MLbEAYT>TNHXr0Q8l(ne18-@ z;c0PLEsVWD-IT{1MyI7Y%HW!wWeT6X-Fux@HNzfhXxal;(!&PrILLCEE2q4?gYK!b zzTdtEmldWFY0aCUqAnSo24+2P+Chzz>+Lt`v66Mj&g`V;9H=vZnFnpJ8 z7KQJ@XR$o5IY7$5JMb;Qi~a5!g&sNKbM*jwPJxrsa|JU4n6()@1(Ic02LW1DV)CSU zpNT}B{TS33qw|=c1jjnHq5ca{>#R$2LDo&jd$I?$UxlYK-C4gMs^>FpEjFay=s0Qw zR>LnpxZl|tmmne&OUM}YidGIvbd_mj@plj~@r2M)9442vhTfeqm5=_S5>##aEo`1~ zg29no!wSngDeBrj3%EgL4QNxnIoGWrqX01(Jt~2OdbT@uXAQ5)twqBlL-Uo2audBR z?6oENWqVzFd_Kdvc~TQSVVHgv9#AYqNiDEF-3O#vFi<84XZhlO935kHI^bHZ0I_A_ z71s|kC;v<|^TwO_4E#N{MHYp15pZ3o$SRD)+j`GOJsp~wSsslEJYXQ^ZREEu7R_%N z>TItpPqYogUCCs+hdJt%Gh8b`V(-mu7E$JNy55f;r0E};Oko(4eWmHy6WE=v^;1HJ zyZ-Y1(4mbK|CbZBM@F^s1p7d%c3zE(TWOfLlTdFi{G>*aQb@A!&yoPn#PtCF4&VZJOy&8?G2|CWmJucOU;{T2a4qD>J-b`!cm;!erR>z^zU;cP43tZeku zK)>2?74U73&ivq&Ao0GS!-X}89pylAnYdg>)cM~Z5wy?9H{=;I10M8ioGf;Lpkm+$ z$G430B%_-VBX<`Kf0Cv5VPj$dwi#hw+k=ZW%*=)`B}6l%HM~ z$a}yJTST|$Ouko0Y@s?8W=^V57}v;0>slKLX0vc?lVjhHQB>mQM{+3KXDSW0d2zd!ZZL4zH*Y;8k>D&rH1;9WcR%@DS`ZVg_cNjHlyw}iKA?!6kUjz!5yL-1JNomJ=LFOPU;Y8>#t*j?>c*YUU zmj{p)Rg=DAT#6Ns&xdn|KUmD+OWr+;N5Vt2b9EPk9U`yo&w4mUHwQXi{&BZOD2XgFuv>9C*huI#>YG)@g3o8FcTx}hnTIp9oQv63*9K=Gi+Zc-uN=;` z_~F&hqF0&KvS>~+->9Vzf7W|y09}-rP7%1u<`oR|d$^$88cphFZkuzkUhf3kD>(Q= zhQ_R>ly1XsmRO16l6ur-gD(@`bp&2Olg0z@P=I8C=azxDMbv=}e!zvm+Rh^wytXz# z25;gR)?%@^Yf64OPL(g(_Ch{AFVB2GS|V>)@q3w-W)G^CmO~1c&9q)I=ivH` z-wVe>HS`Lbnnm0GfP^?x?)Rf*&e$ZgH^;4T(V4w#%`1=qW6@%`lrb!rm69 zWuuH*6%s%!EGm0{I*hiO(3)b~^3(mC7G6{G`=HhcYg{K;QEyzm zc50S9K7hHN?Q)G;5nC@d?ROP11DJa=WbBxdTKj#kKZ-;!Yy~C19-#y zaE3|wR@igH1iJNo+&Q~B^qN=u4r3;o{r12Dw$-dcFe@9|02lavPZ$F*qQ42FT{{Cg z#Ot+AjudJkscQ~|@GJUJkC1T8OOVaJ{8=d-PYa)xTtqaTa!~*3;Q4m$-rg0rqF~|V zSDwj!4^IDEuNRS-SGz#fgXt;SSh1yKim5NI% znmMbTa_hzo$&=8;^1j5?dK?umYqk6R+(5&Ci>Dr=?&E4lq31S7pO?%X-=5o37TRx7 z5rW~=xYnO|KKKzM?)I_A=xbch#RmR%UbwtxmbfBCg$Uam_(8suP#m;uVGOAm{=yUp z#O~Q*|0WwW^Lvq9F>{`ob(XCeJ7HP9k>O_s)=fLiK4ax%*%T3^xKrXLyrvg5BVdK0 z!F?wA$qF$dWTqmA&bNUCcU(%sJT2p%O%1K<$K@sN41cotBF4I))~KCLhW+C z>6TzNk_hr^-1%ax?Uf&eg!(JxYI2bC!Co-4nA(;!LuV|<)IKbZO7!Q&a0%dg9${$w z#F&nxL{T3k!5m;P=Gpzq+jiU9yUAY&jx`NvN{Zg_+y_VHC7*4toND70)Nhg!MlsJ(9XnAqUm3mwe#;n= zefvgsSTk-0dt(rnLQfeKaba_(X?3>UZoryTQ~3I2Na z9{ei{w`q6rlAcU#^_-A?m3D|u`l|8OLH(MF3}J)>10xNgt+Os{C&2gp?}bmgUJdF8 zN_()ZLsDLe?ZIgY^Hr}U8p?U}Yi=bAR4lb|8e6~I=EL)k@jRJ?yYXFz3GIB27-NYc z3abaoTc|DS@sP%ea+f;_`uSC1x(x3>71?nbWpiUV2{O*&c2z@D#QzQq^@*$6Jq)1w z_T+U7BV2>CjMjOT4a%lDeC)ZR%cTiCI)2@WA{)xXkhlgmr=lo;(jsc+!wRsfzrRI3NH~ zkfk%}WSl^&dwlJ=eXLpDj~&8|>fmzNb>w4F-Rf>Hu0~IMpziZ~Jr+DK2KyD_#K2*rZ*jy9T<+>d z@}|h23?t>SVYOthTahdHWf&dU#PQp3Ul)I5bjOkv&E5_=>JCicqsuZi+gpvs1jKKC zEKs{ju)>ZJP05g%+_WW`J3~f5jTw8_xr3Jc8V0pn@_h^EB3!?>^=er9}5ubtoY;Ek8bAI&tJ8Bx$_c=q+XbQTDOY zh?Sa2rlqVJPod4Y5x#C%n06xDU5-`l+hEzwM_3&MRZeYDMrKh=xdiv5FK2;2&s-HP zoFM0+`zzWcqYaN_L&)u;i=hym(Guk`61+BkSa>0l#2C>a{PRLiy11RGv#C@{}!dQ$W!68rw4^;({@J#cQ)hjrBUCwE_1% zl-`Q=i;S}Rwge+G&}U~pd2{^IZN^yI;L0Jzti7ydDQV5lO90p6a$zMhWm)WaDjC5I z+ZXr~QWc43gwc@xBk(P{opxfA%@jJs$TS!i=JWnqI`k6&cT53CRK6Sg}^&b zP5tUT(*uV#k}n)Cojn@vW(i4R;q8RjA7pfwZbU0`^SkE;{G*`ohlG#}K{iRdLrb!c zJDSQ)pYwb)@SIu|e$TjAJ!PcL(WUmmloWBoPWm5>(e$b47mgV;b7L?u+d(+o54e7G z1@^^9b$J-!-8Z3+FD$MrDi;V9st$(QZo69w(Iy@cQ=CD z87loc-4UZdHlP>jn8}w7(PIr$T-FLDonES$pNS7CJi~mwn3p+;Lo>j6xMVP{Go`t; z_Tz^jR#-*2PP@5=TSd^=jW;rtc;feOw_|-|ZS&aoP3r1ZKA))zZ74t*mlNeS%_I&^ zwYJQA)=JG-sR-zqZV>M=--mM+r1CFwcpD0a!l2zLtO0gJp6v${o<$y80uwv$;$9~~ zFu6AmEOHFz-fi8il%97c9*kBFi(`vP34KV}y|qAA8@?i=8ag? zK-TRRbI=#2PjorY(wabdK584OQNG=G^}RDQ*?mL)Ufeq1RE>VWE>n?J zyVKjBjaKKoXk5DORE1~I- zRhqumfWYF-V=1c&rq0LxUjR=)u)pm1txKt_KWmB-i`1LNgc+RxH&vXqF zlC-*l!jD>OPBbOtsd1a{VD&kj^b*p~%3?UZYL`41GE0{xL(5%(Fi@+P&0rvWeyEiU z64j6sl+yIjReESh5nii%vXQzt@xc+9^nJ<6mJbnP|~K$Zc#={cLl;KkIf*bk>QGBBd~A4wQ#A`McloPWcOOd;2;# zSu*!|ulrp2+3)(PXT->1ja_3GP^2o{$SYt}d!->XGbh7B?0T4r5}S;h+^9Pn*YESp zDj`!NSTodGp@=h@iHXA*hfHZ%>J%}Ux;Xhsm`vGCv7k(S<=|MtL2!^*^Tp$x^$lv_q|LZdn$@P{cMPAa;onF`xV@5nHjUYvn z{5Uj?HzT8Y17c1}8Wm^iPPld2SqD7J9DLGUrR|v%bj6CZAh_?1wf#mHi3uuF5Ww)o zliklAYYc+};FemT?WtluSTeRe477++Q#(iAJ+83du&Ze#aF8G1)HU^WD&Wv=p^1*z z?+&Qjm(j?7=-a;AV7?eN+F1T1zxK0#gS_`!{_4vccq=n^wfw?}hFr(FvVdq4>)7F- zs90{gKACB&(YEq*Kw6u0`%b|DRUKmhaZ`P!N~vw-o%gPeG$1O_N7DB0JQosBsb$q& za1bYQe0U=y2Z^=prcPE(e@Gn zm7Ak{K&mD}4)QX?42x9k)+c|r{Lr`kiDdxuzU{cu4n>e9k7bY7`?`+}@iCxxv zWKryIRb7N8xqcO-?xkltS{Nv%Vf)jRc_yTderf|0&sa4CG?#oj0CN3*B=fO!lCdGt zB0iSF*>*_5dhRV>l&b1Ci}HwMsv)^>5!ODqjlPSOqJq{abFhGxp9)*LB$bQLebFsN z{m8%m*YaoI^)C70?|8d=7P?l0eEO$;j{Mkv^VgoebUk!Vtjem@WZx1>8u+B&h!Kvv zt7oZhzQT%vd%)&pZgF84;_Cd^zNRsUCc&C=goBfqDlA4^D+rsNbiP^|h#A^#c570z z-Xp6KEVnqj!8y;?d6t2wf=w_ss#4wiNs?(IvPE?L_DSepaGrTa`mEfrkR0s?D?b6# zHq+-~Q?IpC!|L^#ny<*l0Eba3kMQCD=l_)-e(U!fcx=k2y!PLczy9ri{2U&_J>mDa#Ym-^F>C7eRiWuOI1J@aSkUSe^(iPB#A1iidS#X)! zM>pqr@0!HjVuG93C)M2OoZo^^mnza>g_u;Sn(I0)bjeK=El-!8fu%o9F zp@MR}A2`)rYhrY$HKUSQ?9?bzTc4nxN>egdbLBG)>g^-*JzI}58_OIQ4(oq$1)jDd zw|lLA(|CbJZ&Nbvv6f2eV@q-hjDoJi1!Q~GnJHeX;Imbr5AKCF)b zCTWRklj&Y1<)dts+e&IL*@=Kyu)N9zBZb-0l!(dZ@nwyYXn1kQj?z%kHa@G=2J<774 zO)QSk_X(~&F|d&__W_BGEC(*zIVU|M8G|8N z37fi|Zso)qT*6M%1H-0oQw}I+WgX+saiaCH=ew|l7CGv(o54eER9)ghMn^6vfP1hF z5$Flx1?ILR%)M`~t~AQlvRt)=0Zg19Ls?qRBymitbK zkM%|~7Dv`=Z^L+BFJ$g%VmrDwUFs)`A$45J8IT4aLe{I&HrPdyov9wFkCi@5$D$n> zvYe@Fik4UOQB$+T73W=Ub8Ir#Af+g@zqoAQwPG=34^yr0i3zi7T~TbOaWVB;DjkzY zl6@LMH@#I_C`iV!1|HDW@9lxv z$==w{-cRcFv|kYHuy0$cNio(4aqwXXkV6{tc{K&`imljt zs3!&1WOI_Jhv*@4EkdDFx72Cr?Hl8fPV4}%l4F#l6se4z{#@EVZZbN}vr{ph&Vvi= znp|)h&!Y?FG8kuczFM_geVTk&;y6YekMSlf*=B@gi5JDWXu57Dl6GOP!qwS=&swJMS`JI`M^P+{CF;)}>8=J>H4F z(@)G+9NbD^y_hrWS2|W5vOcGMnsdgikD9qN5>Weih}|qB-Yb^S?Zs zBH7WH!uGUNI?AjEa)>@v{tZWgNSpY~nwV1qPe%S(je{azIi68tJi-;YCZ`ni$QMAee8m%Pn{Daz zgAeE-658OzNwYXPcO++67$yPF1Jqw!d z&kB3k8-=FCw;9eNorII4RR^dqn+IuyA+rp;o68!PRl;mQG!)o-B%fpIknh9vbqSnL zyKZ%v0dNRz#DqS*X~TLxE2`5HQw)R31DD!D0>PY4D8qYQI7x792AA&jVy|}I>`Jek zqK5xLi9HCj4@k2ps^3EI4^-IMMUz@b%{vUP^)OPp!3(M$JgVV}{d3fG-6Y-AqpU=f zLFPj-^pMCl!>KX_6-D&o101d;(hd|>l%xRSxJX1CS?h_io;1E7 z=X=oUbdRzz@uT)OBo4sRWJPWirX}@x1zxq1;+NIpK6|1zmcc61BKbN?_4_TF)bnws zePTo@khfGSjuLHwy>%tFj@foF&DBW$z$vEROhG}jL!eG;A9y$2jEqvKJ0wI1790P$ zqhZBjT~{z7rS7Y_^-HZ_d@4n*7|0T>F5-^ha1$U};si++42h>6g;Apf;`|18DJRm? zwaIn=_Lg2{DZDt*W(iSprS(eP11eq8{ct1*WzQb6Z>eRIZPiXBBrp$}orc3M8$>P< z?VsQx_gwN${hQnEcFSr&NA-XeC^fA&RfawoYiHX*1x@_2tPDrRVgBE0paaznj^U}h zaA)L6W~G)0_d~K>??-logZewG*L(Rr<2bm4>>{DSW-vbtjt69I01M9{!Aaq2y%?8sx56@EsvDETNUR1f4DI<79Z)CEZQDm#({e ziXaUYCyR1_s2AN%vIgm@AH*ul67uwPoCA(@zoKA65)UnXp9uSIO;M^6E2+hi^e97Z z?rtQxL0*~sj3$1${Sqo&Brc8EZxRcV$ci*z9@EI<@j|(%P{VKsBpe{l1+;AOTrHHv&FPZXcUVkqs{=3 zB;7kE61u77_oiwsmuyu>q-RGp!d3v{8N&{ynJt5cR)K)cgKas1M&yW2DoMjF01Za6 z*N&Lp@UyL=-N&!AFyZTG&RxC3q)w|gAf}xIpS!3O410EdEz*O!Z@8=)KinPKZ$Tg| z&o!8G(PMu&sr%kRhlA`2A_gd{0jtzgBz3!8p94BmSAo0JmlO^qq9>?iYfO#~BHYAy zKCn{FqBE)3k^J02>aPFaE?y+Fw4{YLoAq{L8d)}VzN|X9j~m?tGs9YGd(*%h9IT+k z*5P)sr;4LRiy~L-*w8z5sfVcMA#zoH{dOX~=P{5WoH--PTtfIDUoeVWR5DF^%-4)5 zpEwPZDSD$O!-z9wa5i8y6Z!;*xzBBg8W8 zy25&eGEm7vqx3&48w5&BKz6>Gc_9J{XJfoCK#YPCi8A({XQ-V$WNw30i4V0d8cA^r(m^qhyE(Bs!(T9E58oC`N=GMICQHBXUCmX!d6rTw)EpG^HKa90;VDQt>kw z03{F5X$tKF5uWhCE@LfQE&nh92w8hVoB}69JrBiO7jn~6)og`+^3!4a-7Anv{({MA z(dWLOwACJD6Ia&+?;MR0VOHJ&gH!q#gdgYW-)XAd@*1)W_yw^w+`?Wau&_TTPT;G2Y12 z6@97&yo{cCB`|hwmnu0v?_9d~DRtr`@wuvnBa&u8`#2{>?&}T{1YCrFI8$s$qBYG3M&vEJr3{Cen)jSavm3F*6mE&}x#aUURoR9<0S0qV zBi-ju1L>%0_5brrU8I^~qsxZ99ApnxY|;02%*c({9)Pm9=RKQ!jLnhs=-fe9+c|DO z*+xGT5V=xSylaJ%6XAzjb`Ofx{n1fD+^jlswAPd&n#R-CAOv*XkMY2Y==SkS*&d0I)j3 zR4PK*8Z;VEn`st02XDFpvGamEc^sbg!=i_QVBB$vO<$s}@iz`|tA*O$Gd8TJjgM-h zCDmt@qv)k9sZ)puyxz!Awrp%QX1{F-E6QH^t2Tpbt$8!X>Wp1YwlO@Khs1$(s5<>V z5nn8|=n=!Pz>&PJU&jb(W3|`fOmLTpLTyxP-yJUNGb6#Ox!_i-X34S z$zimDn?PTq>wfx0G6ryd!SMc9kx%GSj!ZRULZ15_`dVYG{oL(6^p!ymga_sccT^A1 zfopNS5IdsQC9{W&dl$4RCv<0FwtP(I-b~_&q>J1hQj2Z2(I6+WF`MaC29Kn6M{bS_ z?ci>acC~Z^dXx4tri-fA<3~O*p@s9R2_?H^x}R$$uZse0T+p@ znvN4PTRGI4S#c`-I`q13(@YS`m6S$?ThLW3-NYX7L*uJZ0K2dJshtP_& zbE@rPfHu-+PAK$n9!ia#EgEzZ#~yS=A&Q^MW$%;Moi;^xZ^<c{+Dbnww6u$=>;=W?iID(Na1x4w0C*PqLBdEoB*5HmKtjoQ_v|SQNo` z_)lC(juhGkSAM5;UEjqLUKy$^EiGlJkA73?91k4#y^p`{l55~x(pHLl5Lcs_Ln6Zh2OcUL4UdDlIxy-L;|Og$DgV8lBT=4ElaN)6 z>Di`QzNex|w#R&5&g`l0omkURH;psGrqF~Um%!^~YiA;nGU?lI>>(uznqp$Pe^wQC z$`Q@Yn;T47sDozJX>o+P(+v|9ZkFT!5Nh-^Sm^SrnGQwV%cK`BAUNhJ0z%lD-cF0l z5`65~*#x1_P&N%a9v%UdcP$B@U)U`UFN7!?^&v0^=C@P6>8;NJWw?GDPOz|rfd9c0Q!z)?Zt~2Kd{#Gl=wczcMOT5u|Il1n8 z4;fPNx_f|?I<=3hX>_`?rF=;rd8ve%MS&|W$s6=V5y;g@H`%^==$gT|-z2u^=-1?+ zY$nF`8Wx&s-W7H+q)G8-MnWCw+kTcr`$nn~YMn6DzzU*>>8&o^b$JhueP&D) zn2o4i>Y*n()|_7Wh_aQ4VBoFDma`6po%PUZdzU!)70%`;W18*OX8t2npGjnMa~L2s z2M^0Fe+!q6VYlC?5Nw^2^;tu~f^fHV1*UEb*QHbTipYQjODM9I;JFttcTtbZlY~{| z=0@%}232Jd&uXTQqK$cw<%Qecb|lB1`hslb7pmt=QZ6bA_VrkA_y`v*dhQ3z!jptp zeii^xIE&gW^X+VYB{JPu_)(!V|EA?~@l_(wk_V&*1S*j``$d>uG$H~p4mqf?#wHzG zqSx5N9E2I4(gXIN5HJ=4$_Z5flI%1t4r|p6(e=C6^4>`5-YPT@-#t62R{A`oXzmWZ zaqvvGxy+SVQv9A;$e|To2j7)r6Og*C3KLRnyF`#Af6BN+5CkXXsW<10Yv;2bTPWb3 zPaEH{!L`D8I0Ja^ajU;148zFJ@3!P7LV;X2f83-k;Rnj@v1_DZ;o+0Y3U^I#Q)hyD zBNg0MPIi%!)m1l-(hW9+e1RVldRVU`aMR0xO{0#B8fG0%+Voh9k@_)*+xWv!0HP`i zp$hoq#NVg%gCsDXIfJ+nDSQ{BHV?P5>fn4Rah9eJo^HdR8*e`Vrzo{<^-R=zlCsz> zl$%A?Os|S_%@Mh#QRUW@Tc3R$!hby=m`cjYZY9`qoZH2IISr0O7fAYnQKP7i zpuS-9Lk4xiSdFr0J@;e5JA|ei%$8^Ru9j$BwmukRJ8i#;A7#~9!TyY{2sV3&-RWl| zwr#AJaGP2~Nl>ZJibyiwo~hJ@B8l>F5_lx3IZ*IuygnS&RH2Q!bFVqDcc(2g!9Eks zG-5`ESS%D{0n6E{+SA1EDRndQex-Y0~MtD4lgRyKZVBW^`O@h-F8C<%SMX0c+VSh}oi2d4-FiO?>~G{o1ZnRhx$Wxa^KRiRncit+xk zR^3iCR!_Cv*4#QBR_xlZsT`uhW-!L_H3272?2SYWHI}Qf9FsmPTp~HA_hz&{Gmc;!jqz*l3W_d3^>HwuCngx@qw0AS=}C*=Xv2hGzk|dd!FAN_ zze_Tewke6vNt**8En9L(k=}!NfCn6zjY0bUx>OmL=ink)*_=%&ofSL~c}PvRz*}N2 zC~*j7!3-hl^1qAqxn0p?f$SoX6st^{UH7Esm8x$hH69NULRxMECQf=;EBVx8wQB9b z!g`NB*d`Or8GAcvwdJc~vvGTxe_%*|)Pn*F80&H!q7GvD?NA98kotAIoY~dyWWtz( z*^nCUxMR6(-oAHyIB<*z(c{6?=mCd9mTT_?f+Jhgbk7;JSCpp`AX(aaats#%ket%D!(QXIP&)og!O2BbYNURJuT_3|VbY%+`dGe(* zbM{f(J^&=`3Iy9zR9Q~#HA&JO4AfmrPrGM}@q9Z+ylA7YCo5--gX%9+;a1qdktpJW zdifD$JD0HfKTtB^fLV-`)~QNGHz4cS+z-oC^d}mJ@)(kWUre%IE#cf#T#F7ogIF?F z52Im{)bMINS1H`;v)8Oa&^0FDyje9K$mGA%9mZ1i)~%oO&5eB>2=qm+10ifTU3AqX zrMks-?;_T?s9o~cv)`U(EwUNmZ+r?ag;!R$Gt7_s zg;z&nj*-HA{PF}dDT17x(PUs#aK8iUv>o(#$F|)Dlg#*AJJtcah#w93$;nNq@WK|F zP5G`d=7t>}lOrz>(y10E7+GH}AfRhA#6*m=TOy2~Iam{wF<&&dexZgu0B;_cjl+FM zSO?z)YB`6^3gNT{b--?%>$lsgOK;R-u}d^O-ebB|%&&qm_y5B5@7yLj&Q|XfR+98LAG+6d1uEHfcC(MRKDIrg`vv0B|r% zAgU!{BK4T@ar>-G&62D3n`>th%ZMDny(3E+{E! z*q?up7Qfq|s}5CKft=H0XgifA7kV_gWnEEZNxdg;D#+6Dmyisp?6a=-x$qzo*XzWy zEyS%bGm9}ql3E>Ft9+V%54@9uDeHT<5qFq&)X^BLZXdNfTND|oM%UuAoakX7geB#4 zln>}@ts7ZYrzGv?#|}MI+iUZNuVQy%1Gm3t+Bi#o z95ibyq`jsC&nYP%2qiw?9uyqk$&8?Cl^TvibL+huywnil^t9VF&}Ou~$)14n(=jx2 zm@O*b)Mgfsvdc*{d4|nh-Q-vMndE47>+ar*n=( z14cA+`oxai{xtj3d28D?T*l@eeI687qAU+}N$P6&w3$`!GKl})ia*P_Bzs-9tf)>$ z(K!m{E0dhM_2izZ$99r{K`vta)yktZwX5HE-UEV}*D$k&ioE$;7u^pUR1b0fOb>O5 zc0^cceC9}omOb~P2rct|%~}~$FriXG;cN>*;SFeYT`oEW@Th!yklDK>R1coRJgn-L zY}081-sHo`zG$sXHniyX%}FnmddXuuh~h_Q{1|X%V9*=WOGMO;^It8z5i|~E3bN=H z;b4c}gZD6?rMkVqWL)0&8nd3GB=tKFt{^U@wajJf;So^#iO5DK)I@jZjw)RmKuZcM z4|!F>lXJcEGI+6t|3c`UCRx;{gI!D@FEDF-23}pD)+BKE!`J;BlHF^g&EQYW9x>Hs zA&NN#kx*@WN49D1kyy&C$! zz-g)ssx8yDEpQ}CkCPjA55yeF{#~-2<#c$0ORM3@JZ+0rLdJ$x4FbZ-e#6enQ$(&x z+G@v+T^>xpt}A(AVsq+ae@)E#=rE=5Oh~H8|vWcWv}{PG?9^h zLhu*Jr@nCO5>o>^BtwZRe8o@Jz%+LhVOT{Pa>6X0YLK}`QwbLH!) zr)M{Hw-CjRe$3U~n5;IEip##sKOi6DtPGOTy*PwjQtXK3pixzq$4mjrLa1AcVkhiy z5FSmWcN>L8y_4b0Gv0?Ma6P#yncfsoE3jAVz-2S&M-o&#Z~%D&kS^6?(rBn8FT%+A z9B~~{6}Y8H1nmY;ZoN9ta}TV@em5IdJ(C^_fHK!bE>KVgG3QpR_6L)?uX{DV8zzXP zS7f2dBD`V%xoD-+pDM2?;;7wT-~}4xpwmy zV;VWu5(zDs56u1omePlwSLyC9byH7zm)94H3bcm=z1#8+DtMSP_@qSBGlg6qhjF?= z{EVB7B3*Auz>?sAY^DpFv@8yYA8$AXg%YtG<4VAV>a{EwPVfC1RzU4|4=} zqYCB`B3(vOZ*6N~1U4N)>(FK(a2ebL%w#d)xL8bW5o}hW!JWDo_PQ&glD?MJj1PA9q z0dEMMs{(*CgI8^5dumFLu2qRJWw}XW%_P2*QntvaL=Y(kN-ToR)Y$c84-=zzDLLut zd?`|X2@yR@#Pn1gofh~)%TvSwf-&|gvo(a-f47H`E*qIs`hEa+tEtemFQGuCF7?*0 zXAX>GY;Vaz);R#TmO65ZUz4Pe`w+uv48HF8@C0<5Pvdfu9f)h=+4&75kTh^0;=S}t z5CGW?@LC!Ub=v4^(>6Di&`I>1)mbq`jyb^IrGXbvm-q0XfmT%8$9z5%7=nZ;ALkVp zBC3>JsoEBUvO2fOV5DO!%EN+B^Zl=)lx}oSoqFlLEq)%NL?nsOm5?2CB-`p^N0ucO zM$y`GQK{%?FZ&UaaFIBv)(*;0m?x*ZQr^0v(@VM&Zg$@@N#Rof^Yi=f+UgBMGj8r( zlFT$+5i<~jLV_woGAnSyrQdTZt@P41mCyd3Mf%UmN3R<_uk#{(^vN}*3o5<&_4jj- z#-`<(*}7zuREf}~Vg)|Ssr!iFT#|(b7qL+~J6jR+$@PjdBd?T>j#Q4ji^ci*v>J9k zH`5Z3xT`Xs(~*Rh50`ze(saMhUYbd5xF@V|Qm^}Tc~6YDAoK*Y_dN4}O>$kW{3;NJ zydX#WrFX*I+!98JT`j32f*Ne};*4qw1_vEe4;nPy9>(>SmrDajG#52OIn#DKMAMBb z2vXnuU<}^iGfkn&ek#4%6HiwSSJBxwKIH{O^fOMon~F3kb+u9VICL&$BdGleuwZg3 z0296vwMwqKSz1B!pnf=>(6xSxuAOjP5a0l$s~%3KC~MQ7Y`ovMTe8`?HM$G97NdjN z&B5JZ$%H^Uw-U-_0&)&GrJ5O@N~Tv&jeih%ySmeBgg3`dMCtvsQ7I(F{an$qDq{($1 zl&5TCn(eYd7FZTG69|J3#nNFBV!vcVlKt^w>fM%9vX#bLOYwSpcAC4{-k`4%nVd`} z7PA1OuKr}vF1Sm%TL#&UPP@k}_T{*>}k)a>{@TKVXi{JjKFQHll~lC-e8mWz5= zH?R&%uo7zyeeMpsO>Q-{>Q~Ir%rl+2I}rvLz?9*ch`~)La-iILf300b_@nuAUw1bb z9NJ=TgiksvznGMoD~0D?&XATU$=a|)fwt`f$OOpRQa`*iqI41Cb#0vt@tw^G(6u5m zHkU4=sPAiaS{@t(26b8MvU-Zp+iTN0Xu>wyNLoGS;!?kfu8ycNkmwtjcWdJ<@a_Rc zbiLxz?g(pX{mes^9hksl--jpNA|9aaM|lW>wwtB2y0gL)d1WtlD0?bQ?}2o-RY@qkKC8FIsN_TEhMq?*pf{c%z0dGwX z*_b8HgHb*lCCqjX;~s=vXz>c&Rw6mrlR*_OUcW&m=Va!H=w{+ge~9e)Z9?K)_ko|( zRE10<+o#1WBYXD1dg4cuD4=>)K*oM*p{$4tahiykjLUdexrCI~iC-G|fXxEvshDk# zW#Te)P7xJn9!QO@(P^LgF+FejE>R;Rno2uE&J~ICnRPk_8Xfk}WOWpa9((vy_*R@- z8%+or)`p=qTD{nVs(?q*{vAJyTb2lmvZp;l@$0M0N9Uuq?zG#K_e_Yw8~D-ogP4K= za!`{YAg4JpVC8uCaLgX5%=O}+e$S<4`c6UJRH{XebRgpcnOv(gFG!_lz~Y2eX92F? zY)ieP9p;thy9Zfm4O2XW5v5#32xc6F7-Mn{lujACTI38xwXuayIP;KjQSjTBH7h44 zFYo~Oj5z8&Ou5R9w4|PDQm=>d=~$J{)&E{Ej5vbon3OUtU)KvKAt#;)@#F!TIKtUD z-WT9IF(~Tsqcz=r(Lz5AHa0!)w)+y$b*9?K)hWCA_PTExdhmoT=JkDGu^(Fe5B}wCuq;4Km z5!=o>Ob@dmnS9008B)J7*tQH@gKe>fXA<*${b!kB03OVkDid6!234yRy%i-K%3?Bi z1g#7zb|Nlv?|c-0Va3XxEio->v;o3$b=Y{s1O+47j%GO3khZtq_@Rs7rRcmLH;z;t zK;PJ6>D6~M@2L<~g_`o<6R$;G=HI_bz0TP$(Y4!FZTyKHdN{qh_LiP zXw)-3>bdIb>Qm=rX86P0-0aS10p)XMH?~X+R-ze?v4T=t>{X)v^b3?lQbjnxF&&tE zMo0vT;I?OHL!jl(gq}3WaG2@?J&GHrYlWjI z`$lu%qgp4*veJ^sK3Z{$nI5=YDpo9M5A87d!T)WP?sfyLb{#DW5Ai!ZCYJ6W>m|n{ z2IF>&vb+H_i=HTVH+I0k z))8u;pxP*8t48yHC^%R3+h+HUF&#X_=5@|PQ^k8UfMQ&3Kw@}hwL9f}vn7)JE%fP? zZC8~PhSh?hot9hICHL3y)L^r%ORLTva;SM1*qnC11QLF+AhV3 z&IYp|`yYVeavv#VWzsuS5uQ)_^cc=2<>yCSH`@Kz^MtZOQ}4{_y9dl}O8kn09|;xJ z5C}F~;%X!^+Oz)jy> z+eU<~F=l^v;A8wWaj;-2tUJ#7C=NKAF^gI49+Mv?K9+#E0R=4wPP5V%EK4O-AkyDv zr+)8}Vpj9e`U|N6LOnVr132LC#~Pz2p=1P+I}s&?B5|k z$gdynl}eJ3P&~w(-m`SD$@q9tXwv#=W!7q+K^EeR{ms@2Dynx{lc`5Dc9{+4zqGYu zG0hhTzMs{{u*$;xYW=)_{?~u8fBWOV|BgfT=MNG9>_7hdKmTOxUH7YjJJQp-HzNe< z^|hZ3hEt>Qu*z~c?wrS&c|T`~w|8k5Y@H2+@IjeoxlvkU9HI7H{L;9mO)>oxfl_!{ zJIxHg{o$XOOWz>maP3Z4myeu6JCr>mzUyL8Kq7R9p2YxU6a-d2)C}6YPxI8spT)bH z-^fape)t_OP){K$CSk_yqmFFE<$Y)QKR^G={&)WIm&xUS|KlI+gWt?<_Rik`r!^Rd z+oa-AkjP7?MdstpruS_TcNCf`A?oI-1%d$Svw<=&F5`<(0W7pztffcE5@UEkmlrtm zV~f~ZalKFao%UCUCR;n9k&;3HRCvpYpTz;JME{d3(}B0a11-wShxf!^@t~s{cP+J;|omDd_=2ki*=2tV{<_Y4dA z*`7W)wry}<@J47Q@2+3|*n1%a4nMz)i_BAfx`Uu(oA~i*dDH35dZRUnZ?zfKZ(Y?R z!6)Q7gQorX=~o`H>3jZ!_x@?_;e4!G=s5Ir&kXmPieEFbSI}ym)g#B;RpmvaM!$q^ zt)XUeU7A-G4+^8Gmbkux&<`E_`{-?&w}kbN1*;%E%S6f0yB(2Q$&(qlYee(>6RI+`q!Xesevlg8B2R!qT>n zDfDXrkEB8H?JK5&p{r%(S(p7f2XJ*L~+jHhx6aF z(gw@**yA|u`z{YjS-mbG4?(rL6gS$YWm%z7^x)R?k@tP~eUW%a+pV_n;@^#L9+JQG z;f~kpb2Czd^_ylfKzP43SX5YFiU25#zV92J)PzB|J43?|SmIcE?G=XuMJS=FyvS!- z8u+;OIhxG5bR*_g=UXV2I;=xDu0XSn6Sd1le#!09Q>@25^PdmkFs;_hW>K0cXf?~- zb|zE4{*Zm-;p;9^ld=}TytfLqA*)_TZjSI&? zn%*eDJJUi@Oji8}&CUEXZhn%OXxM4^IcFteMQ^01H3$er7QtyLn4&HA{@6%eG`r)l z{?og8LeA_svc@OZZhOc**f(NFI({P;c-0oVUfx^d!0mu#sOHW(_bwAp%iBbEr6zu7 zhV6pIn;`J39`s>;#EEe5pNr{!ZF4&2B(B3>Z#FzHWI4@6H7|r#urc@uy^Fpv+d`}R zt5eYO94J(74EpyR&K7J7jmkM@Kp=h?Xy7o~ MVTO5`1y)&O`aU8xI*N^-e>=If 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 v;vj@{lp#sf(_HZ?f?HnvWVT-5_4)oE6D Date: Fri, 8 Sep 2023 23:08:40 +0200 Subject: [PATCH 13/99] more file cleanup --- plugins/Slicer/CMakeLists.txt | 5 - plugins/Slicer/SlicerT.cpp | 640 ---------------------------------- plugins/Slicer/SlicerT.h | 101 ------ plugins/Slicer/SlicerTUI.cpp | 165 --------- plugins/Slicer/SlicerTUI.h | 68 ---- plugins/Slicer/WaveForm.cpp | 260 -------------- plugins/Slicer/WaveForm.h | 90 ----- plugins/Slicer/logo.png | Bin 1109 -> 0 bytes 8 files changed, 1329 deletions(-) delete mode 100644 plugins/Slicer/CMakeLists.txt delete mode 100644 plugins/Slicer/SlicerT.cpp delete mode 100644 plugins/Slicer/SlicerT.h delete mode 100644 plugins/Slicer/SlicerTUI.cpp delete mode 100644 plugins/Slicer/SlicerTUI.h delete mode 100644 plugins/Slicer/WaveForm.cpp delete mode 100644 plugins/Slicer/WaveForm.h delete mode 100644 plugins/Slicer/logo.png diff --git a/plugins/Slicer/CMakeLists.txt b/plugins/Slicer/CMakeLists.txt deleted file mode 100644 index b93021e98f9..00000000000 --- a/plugins/Slicer/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -INCLUDE(BuildPlugin) - -BUILD_PLUGIN(slicert SlicerT.cpp SlicerT.h SlicerTUI.cpp SlicerTUI.h WaveForm.cpp WaveForm.h MOCFILES SlicerT.h SlicerTUI.h WaveForm.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png") - - diff --git a/plugins/Slicer/SlicerT.cpp b/plugins/Slicer/SlicerT.cpp deleted file mode 100644 index 6b74511e031..00000000000 --- a/plugins/Slicer/SlicerT.cpp +++ /dev/null @@ -1,640 +0,0 @@ -/* - * Slicer.cpp - instrument which uses a usereditable wavetable - * - * Copyright (c) 2006-2008 Andreas Brandmaier - * - * 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. - * - */ - - -// TODO: fadeIn -// TODO: fix lag multiple notes -// TODO: timeshift samples on notePlay and cache them - -#include "SlicerT.h" -#include -#include -#include - -#include "Engine.h" -#include "Song.h" -#include "InstrumentTrack.h" - -#include "PathUtil.h" -#include "embed.h" -#include "plugin_export.h" - - - - -namespace lmms -{ - - -extern "C" -{ - -Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = -{ - LMMS_STRINGIFY( PLUGIN_NAME ), - "SlicerT", - QT_TRANSLATE_NOOP( "PluginBrowser", - "A cool testPlugin" ), - "Daniel Kauss Serna>", - 0x0100, - Plugin::Type::Instrument, - new PluginPixmapLoader( "logo" ), - nullptr, - nullptr, -} ; - -} - - - -SlicerT::SlicerT(InstrumentTrack * _instrument_track) : - Instrument( _instrument_track, &slicert_plugin_descriptor ), - noteThreshold(0.6f, 0.0f, 2.0f, 0.01f, this, tr( "Note threshold" ) ), - fadeOutFrames(2048.0f, 0.0f, 8192.0f, 4.0f, this, tr("FadeOut")), - originalBPM(1, 1, 999, this, tr("Original bpm")), - - originalSample(), - timeShiftedSample() -{ - printf("Correctly loaded SlicerT!\n"); -} - -void SlicerT::playNote( NotePlayHandle * _n, sampleFrame * _working_buffer ) { - if (originalSample.frames() < 2048) { - return; - } - - const fpp_t frames = _n->framesLeftForCurrentPeriod(); - const int playedFrames = _n->totalFramesPlayed(); - const f_cnt_t offset = _n->noteOffset(); - - int totalFrames = timeShiftedSample.frames(); - int sliceStart = timeShiftedSample.startFrame(); - int sliceEnd = timeShiftedSample.endFrame(); - int sliceFrames = timeShiftedSample.endFrame() - timeShiftedSample.startFrame(); - int noteFramesLeft = sliceFrames - playedFrames; - - // init NotePlayHandle data - if( !_n->m_pluginData ) - { - _n->m_pluginData = new SampleBuffer::handleState( false, SRC_LINEAR ); - - float speedRatio = (float)originalBPM.value() / Engine::getSong()->getTempo() ; - int noteIndex = _n->key() - 69; - int sliceStart, sliceEnd; - - // 0th element is no sound, so play full sample - if (noteIndex > slicePoints.size()-2 || noteIndex < 0) { - sliceStart = 0; - sliceEnd = timeShiftedSample.frames(); - } else { - sliceStart = slicePoints[noteIndex] * speedRatio; - sliceEnd = slicePoints[noteIndex+1] * speedRatio; - } - - - timeShiftedSample.setAllPointFrames( sliceStart, sliceEnd, sliceStart, sliceEnd ); - - - - } - - - // if play returns true (success I guess) - if( ! _n->isFinished() ) { - if( timeShiftedSample.play( _working_buffer + offset, - (SampleBuffer::handleState *)_n->m_pluginData, - frames, 440, - static_cast( 0 ) ) ) - { - // exponential fade out, applyRelease kinda sucks - if (noteFramesLeft < fadeOutFrames.value()) { - // printf("fade out started. frames: %i, framesLeft: %i\n", frames, noteFramesLeft); - for (int i = 0;iprocessAudioBuffer( _working_buffer, - frames + offset, _n ); - - - float absoluteCurrentNote = (float)(sliceStart + playedFrames) / totalFrames; - float absoluteStartNote = (float)sliceStart / totalFrames; - float abslouteEndNote = (float)sliceEnd / totalFrames; - emit isPlaying(absoluteCurrentNote, absoluteStartNote, abslouteEndNote); - - } else { - emit isPlaying(0, 0, 0); - } - } else { - emit isPlaying(0, 0, 0); - } - - - -} - - -void SlicerT::findSlices() { - if (originalSample.frames() < 2048) { - return; - } - int c = 0; - const int window = 1024; - int peakIndex = 0; - float lastPeak = 0; - float currentPeak = 0; - int minWindowsPassed = 0; - slicePoints = {}; - for (int i = 0; i currentPeak) { - currentPeak = sampleValue; - peakIndex = i; - } - - if (i%window==0) { - //printf("%i -> %f : %f\n", i, currentAvg, lastAvg); - if (abs(currentPeak / lastPeak) > 1+noteThreshold.value() && minWindowsPassed <= 0) { - c++; - slicePoints.push_back(std::max(0, peakIndex-window/2)); // slight offset - minWindowsPassed = 2; // wait at least one window for a new note - } - lastPeak = currentPeak; - currentPeak = 0; - - - minWindowsPassed--; - - } - - } - slicePoints.push_back(originalSample.frames()); - - emit dataChanged(); -} - -// void SlicerT::updateParams() { - -// } - -// find the bpm of the sample by assuming its in 4/4 time signature , -// and lies in the 100 - 200 bpm range -void SlicerT::findBPM() { - if (originalSample.frames() < 2048) { - return; - } - int bpmSnap = 1; - - float sampleRate = originalSample.sampleRate(); - float totalFrames = originalSample.frames(); - float sampleLength = totalFrames / sampleRate; - - // this assumes the sample has a time signature of x/4 - float bpmEstimate = 240.0f / sampleLength; - - // deal with samlpes that are not 1 bar long - while (bpmEstimate < 100) { - bpmEstimate *= 2; - } - - while (bpmEstimate > 200) { - bpmEstimate /= 2; - } - - // snap bpm - int bpm = bpmEstimate; - bpm += (float)bpmSnap / 2; - bpm -= bpm % bpmSnap; - - originalBPM.setValue(bpm); - originalBPM.setInitValue(bpm); -} - -// create thimeshifted samplebuffer and timeshifted slicePoints -void SlicerT::timeShiftSample() { - if (originalSample.frames() < 2048) { - return; - } - using std::vector; - printf("starting sample timeshifting\n"); - - // original sample data - float sampleRate = originalSample.sampleRate(); - int originalFrames = originalSample.frames(); - - // target data TODO: fix this mess - bpm_t targetBPM = Engine::getSong()->getTempo(); - float speedRatio = (float)originalBPM.value() / targetBPM ; - int outFrames = speedRatio * originalFrames; - - // snap to a beat, this should be in the UI - // outFrames += (float)samplesPerBeat; - // outFrames -= outFrames%samplesPerBeat; - - // nothing to do here - if (targetBPM == originalBPM.value()) { - timeShiftedSample = SampleBuffer(originalSample.data(), originalSample.frames()); - printf("BPM match for sample, finished timeshift. frames: %i\n", timeShiftedSample.frames()); - return; - } - - // buffers - vector rawData(originalFrames, 0); - vector outData(outFrames, 0); - - vector bufferData(outFrames, sampleFrame()); - - // copy channels for processing - for (int i = 0;i &dataIn, std::vector &dataOut, float sampleRate, float pitchScale) { - using std::vector; - // processing parameters, lower is faster - // lower windows size seems to work better for time scaling, - // this is because the step site is scaled, but not the window size - // this causes slight timing differences between windows - // sadly, lower windowsize also reduces audio quality in general - // TODO: find solution - // oversampling is better if higher always (probably) - const int windowSize = 512; - const int overSampling = 32; - - // audio data - int inFrames = dataIn.size(); - int outFrames = dataOut.size(); - - float lengthRatio = (float)outFrames / inFrames; - - // values used - const int stepSize = (float)windowSize / overSampling; - const int numWindows = (float)inFrames / stepSize; - const int windowLatency = (overSampling-1)*stepSize; - const float outStepSize = lengthRatio * (float)stepSize; // float, else inaccurate - const float freqPerBin = sampleRate/windowSize; - // very important - const float expectedPhaseIn = 2.*M_PI*(float)stepSize/(float)windowSize; - const float expectedPhaseOut = 2.*M_PI*(float)outStepSize/(float)windowSize; - - - printf("frames: %i , out frames: %i , ratio: %f\n", inFrames, outFrames, (float)outFrames / inFrames); - printf("stepSize: %i, outStepSize:%i, numWindows: %i", stepSize, outStepSize, numWindows); - printf("will drop %i\n", inFrames%(inFrames/numWindows)); - - // initialize buffers - fftwf_complex FFTSpectrum[windowSize]; - vector FFTInput(windowSize, 0); - vector IFFTReconstruction(windowSize, 0); - vector allMagnitudes(windowSize, 0); - vector allFrequencies(windowSize, 0); - vector processedFreq(windowSize, 0); - vector processedMagn(windowSize, 0); - vector lastPhase(windowSize, 0); - vector sumPhase(windowSize, 0); - - vector outBuffer(outFrames, 0); - - // declare vars - float real, imag, phase, magnitude, freq, deltaPhase = 0; - int windowIndex = 0; - - // fft plans - fftwf_plan fftPlan; - fftPlan = fftwf_plan_dft_r2c_1d(windowSize, FFTInput.data(), FFTSpectrum, FFTW_MEASURE); - fftwf_plan ifftPlan; - ifftPlan = fftwf_plan_dft_c2r_1d(windowSize, FFTSpectrum, IFFTReconstruction.data(), FFTW_MEASURE); - - // remove oversampling, because the actual window is overSampling* bigger than stepsize - for (int i = 0;i < numWindows-overSampling;i++) { - windowIndex = i * stepSize; - - // FFT - memcpy(FFTInput.data(), dataIn.data() + windowIndex, windowSize*sizeof(float)); - fftwf_execute(fftPlan); - - // analysis step - for (int j = 0; j < windowSize; j++) { - - real = FFTSpectrum[j][0]; - imag = FFTSpectrum[j][1]; - - magnitude = 2.*sqrt(real*real + imag*imag); - phase = atan2(imag,real); - - freq = phase - lastPhase[j]; // subtract prev pahse to get phase diference - lastPhase[j] = phase; - - freq -= (float)j*expectedPhaseIn; // subtract expected phase - - // some black magic to get into +/- PI interval, revise later pls - long qpd = freq/M_PI; - if (qpd >= 0) qpd += qpd&1; - else qpd -= qpd&1; - freq -= M_PI*(float)qpd; - - freq = (float)overSampling*freq/(2.*M_PI); // idk - - freq = (float)j*freqPerBin + freq*freqPerBin; // "compute the k-th partials' true frequency" ok i guess - - allMagnitudes[j] = magnitude; - allFrequencies[j] = freq; - - } - - // TODO: pitch shifting - // takes all the values that are below the nyquist frequency (representable with our samplerate) - // nyquist frequency = samplerate / 2 - // and moves them to a different bin - // improve for larger pitch shift - // memset(processedFreq.data(), 0, processedFreq.size()*sizeof(float)); - // memset(processedMagn.data(), 0, processedFreq.size()*sizeof(float)); - // for (int j = 0; j < windowSize/2; j++) { - // int index = (float)j;// * noteThreshold.value(); - // if (index <= windowSize/2) { - // processedMagn[index] += allMagnitudes[j]; - // processedFreq[index] = allFrequencies[j];// * noteThreshold.value(); - // } - // } - - // synthesis, all the operations are the reverse of the analysis - for (int j = 0; j < windowSize; j++) { - magnitude = allMagnitudes[j]; - freq = allFrequencies[j]; - - deltaPhase = freq - (float)j*freqPerBin; - - deltaPhase /= freqPerBin; - - deltaPhase = 2.*M_PI*deltaPhase/overSampling;; - - deltaPhase += (float)j*expectedPhaseOut; - - sumPhase[j] += deltaPhase; - deltaPhase = sumPhase[j]; // this is the bin phase - - FFTSpectrum[j][0] = magnitude*cos(deltaPhase); - FFTSpectrum[j][1] = magnitude*sin(deltaPhase); - } - - // inverse fft - fftwf_execute(ifftPlan); - - // windowing - // this is very bad, audible click at the beginning if we take the average, - // but else there is a windowSized delay... - // solution would be to take the average but blend it together better - // TODO: make better - for (int j = 0; j < windowSize; j++) { - - float outIndex = i * outStepSize + j; - if (outIndex > outFrames) { - printf("too long window size, breaking\n"); - break; - } - - // calculate windows overlapping at index - float startWindowOverlap = ceil(outIndex / outStepSize + 0.00000001); - float endWindowOverlap = ceil((float)(-outIndex + outFrames) / outStepSize + 0.00000001); - float totalWindowOverlap = std::min( - std::min(startWindowOverlap, endWindowOverlap), - (float)overSampling); - - - // completly unsmooth, but ensures same magnitude across - outBuffer[outIndex] += (float)overSampling/totalWindowOverlap*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); - // printf("%f ", outBuffer[outIndex]); - // if (outIndex < (float)outStepSize*overSampling) { - // printf("start: %f, end: %f, total:%f\n", startWindowOverlap, endWindowOverlap, totalWindowOverlap); - // // since not all windows overlap, just take the average of the ones that do overlap - // outBuffer[outIndex] += (float)overSampling/startWindowOverlap*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); - - // // no averaging, probably worse - // // outBuffer[outIndex] = overSampling*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); - // } else { - // // this computes the weight of the window on the final output - // float window = -0.5f*cos(2.*M_PI*(float)j/(float)windowSize)+0.5f; - - // outBuffer[outIndex] += 2.0f*window*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); - // } - - } - - - } - - fftwf_destroy_plan(fftPlan); - fftwf_destroy_plan(ifftPlan); - - // normalize - float max = -1; - for (int i = 0;i * outClip) { - if (originalSample.frames() < 2048) { - return; - } - int ticksPerBar = DefaultTicksPerBar; - float sampleRate = timeShiftedSample.sampleRate(); - - float bpm = Engine::getSong()->getTempo(); - float samplesPerBeat = 60.0f / bpm * sampleRate; - float beats = (float)timeShiftedSample.frames() / samplesPerBeat; - - float barsInSample = beats / Engine::getSong()->getTimeSigModel().getDenominator(); - float tickMult = ticksPerBar * barsInSample; - - printf("beats: %f, bars: %f, tickMult: %f\n", beats, barsInSample, tickMult); - - for (int i = 0;ipush_back(sliceNote); - } -} - -void SlicerT::updateFile(QString file) { - printf("updated audio file\n"); - originalSample.setAudioFile(file); - if (originalSample.frames() < 2048) { - return; - } - findSlices(); - findBPM(); - timeShiftSample(); - - emit dataChanged(); -} - -void SlicerT::updateSlices() { - findSlices(); -} - -void SlicerT::updateTimeShift() { - timeShiftSample(); -} - - -void SlicerT::saveSettings(QDomDocument & doc, QDomElement & elem) { - elem.setAttribute("src", originalSample.audioFile()); - if (originalSample.audioFile().isEmpty()) - { - QString s; - elem.setAttribute("sampledata", originalSample.toBase64(s)); - } - - elem.setAttribute("totalSlices", (int)slicePoints.size()); - - for (int i = 0;icollectError(message); - } - } - else if (!elem.attribute("sampledata").isEmpty()) - { - originalSample.loadFromBase64(elem.attribute("srcdata")); - } - - if (!elem.attribute("totalSlices").isEmpty()) { - int totalSlices = elem.attribute("totalSlices").toInt(); - slicePoints = {}; - for (int i = 0;i( m ) ) ); -} - - -} - - -} // namespace lmms - diff --git a/plugins/Slicer/SlicerT.h b/plugins/Slicer/SlicerT.h deleted file mode 100644 index 004e4756f1e..00000000000 --- a/plugins/Slicer/SlicerT.h +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Slicer.h - declaration of class Slicer and BSynth which - * are a wavetable synthesizer - * - * Copyright (c) 2006-2008 Andreas Brandmaier - * - * 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 SLICERT_H -#define SLICERT_H - -#include "SlicerTUI.h" - -#include - -#include "Note.h" -#include "Instrument.h" -#include "InstrumentView.h" -#include "AutomatableModel.h" -#include "SampleBuffer.h" - - -// #include "Graph.h" -// #include "MemoryManager.h" - - -namespace lmms -{ - -class SlicerT : public Instrument{ - Q_OBJECT - public: - SlicerT(InstrumentTrack * _instrument_track); - ~SlicerT() override = default; - - void playNote( NotePlayHandle * _n, - sampleFrame * _working_buffer ) override; - - - void saveSettings( QDomDocument & _doc, - QDomElement & _parent ) override; - void loadSettings( const QDomElement & _this ) override; - - QString nodeName() const override; - - gui::PluginView * instantiateView( QWidget * _parent ) override; - - void writeToMidi(std::vector * outClip); - - public slots: - void updateFile(QString file); - void updateTimeShift(); - void updateSlices(); - - signals: - void isPlaying( float current, float start, float end ); - - private: - - - FloatModel noteThreshold; - FloatModel fadeOutFrames; - IntModel originalBPM; - - SampleBuffer originalSample; - SampleBuffer timeShiftedSample; - std::vector slicePoints; - - - void findSlices(); - void findBPM(); - void timeShiftSample(); - void phaseVocoder(std::vector &in, std::vector &out, float sampleRate, float pitchScale); - - friend class gui::SlicerTUI; - friend class gui::WaveForm; - -}; - - -} - -#endif diff --git a/plugins/Slicer/SlicerTUI.cpp b/plugins/Slicer/SlicerTUI.cpp deleted file mode 100644 index 823cb162028..00000000000 --- a/plugins/Slicer/SlicerTUI.cpp +++ /dev/null @@ -1,165 +0,0 @@ -#include "SlicerTUI.h" -#include "SlicerT.h" - -#include -#include -#include - -#include "StringPairDrag.h" -#include "Clipboard.h" -#include "Track.h" -#include "DataFile.h" - -#include "Engine.h" -#include "Song.h" -#include "InstrumentTrack.h" - -#include "embed.h" - -namespace lmms -{ - - -namespace gui -{ -SlicerTUI::SlicerTUI( SlicerT * _instrument, - QWidget * _parent ) : - InstrumentViewFixedSize( _instrument, _parent ), - slicerTParent(_instrument), - noteThresholdKnob(KnobType::Dark28, this), - fadeOutKnob(KnobType::Dark28, this), - bpmBox(3, "21pink", this), - resetButton(embed::getIconPixmap("reload"), QString(), this), - timeShiftButton(embed::getIconPixmap("max_length"), QString(), this), - midiExportButton(embed::getIconPixmap("midi_tab"), QString(), this), - wf(245, 125, _instrument, this) -{ - setAcceptDrops( true ); - - wf.move(2, 5); - - bpmBox.move(2, 150); - bpmBox.setToolTip(tr("Original sample BPM")); - bpmBox.setLabel(tr("BPM")); - bpmBox.setModel(&slicerTParent->originalBPM); - - timeShiftButton.move(70, 150); - timeShiftButton.setToolTip(tr("Timeshift sample")); - connect(&timeShiftButton, SIGNAL( clicked() ), slicerTParent, SLOT( updateTimeShift() )); - - fadeOutKnob.move(200, 150); - fadeOutKnob.setToolTip(tr("FadeOut for notes")); - fadeOutKnob.setLabel(tr("FadeOut")); - fadeOutKnob.setModel(&slicerTParent->fadeOutFrames); - - midiExportButton.move(150, 150); - midiExportButton.setToolTip(tr("Copy midi pattern to clipboard")); - connect(&midiExportButton, SIGNAL( clicked() ), this, SLOT( exportMidi() )); - - noteThresholdKnob.move(7, 200); - noteThresholdKnob.setToolTip(tr("Threshold used for slicing")); - noteThresholdKnob.setLabel(tr("Threshold")); - noteThresholdKnob.setModel(&slicerTParent->noteThreshold); - - resetButton.move(70, 200); - resetButton.setToolTip(tr("Reset Slices")); - connect(&resetButton, SIGNAL( clicked() ), slicerTParent, SLOT( updateSlices() )); - - - -} - -// copied from piano roll -void SlicerTUI::exportMidi() { - using namespace Clipboard; - - DataFile dataFile( DataFile::Type::ClipboardData ); - QDomElement note_list = dataFile.createElement( "note-list" ); - dataFile.content().appendChild( note_list ); - - std::vector notes; - slicerTParent->writeToMidi(¬es); - if (notes.size() == 0) { - return; - } - - TimePos start_pos( notes.front().pos().getBar(), 0 ); - for( Note note : notes ) - { - Note clip_note( note ); - clip_note.setPos( clip_note.pos( start_pos ) ); - clip_note.saveState( dataFile, note_list ); - } - - copyString( dataFile.toString(), MimeType::Default ); - -} - -void SlicerTUI::mousePressEvent( QMouseEvent * _me ) { - printf("clicked on TUI\n"); - // slicerTParent->findSlices(); - // slicerTParent->findBPM(); - // slicerTParent->timeShiftSample(); - update(); -} - -// all the drag stuff is copied from AudioFileProcessor -void SlicerTUI::dragEnterEvent( QDragEnterEvent * _dee ) -{ - // For mimeType() and MimeType enum class - using namespace Clipboard; - - if( _dee->mimeData()->hasFormat( mimeType( MimeType::StringPair ) ) ) - { - QString txt = _dee->mimeData()->data( - mimeType( MimeType::StringPair ) ); - if( txt.section( ':', 0, 0 ) == QString( "clip_%1" ).arg( - static_cast(Track::Type::Sample) ) ) - { - _dee->acceptProposedAction(); - } - else if( txt.section( ':', 0, 0 ) == "samplefile" ) - { - _dee->acceptProposedAction(); - } - else - { - _dee->ignore(); - } - } - else - { - _dee->ignore(); - } - -} - -void SlicerTUI::dropEvent( QDropEvent * _de ) { - QString type = StringPairDrag::decodeKey( _de ); - QString value = StringPairDrag::decodeValue( _de ); - if( type == "samplefile" ) - { - printf("type: samplefile\n"); - slicerTParent->updateFile( value ); - // castModel()->setAudioFile( value ); - // _de->accept(); - // set wf wave file - return; - } - else if( type == QString( "clip_%1" ).arg( static_cast(Track::Type::Sample) ) ) - { - printf("type: clip file\n"); - DataFile dataFile( value.toUtf8() ); - slicerTParent->updateFile( dataFile.content().firstChild().toElement().attribute( "src" ) ); - _de->accept(); - return; - } - - _de->ignore(); -} - - -} -} - - diff --git a/plugins/Slicer/SlicerTUI.h b/plugins/Slicer/SlicerTUI.h deleted file mode 100644 index b123223fb4f..00000000000 --- a/plugins/Slicer/SlicerTUI.h +++ /dev/null @@ -1,68 +0,0 @@ -#include "WaveForm.h" - -#include -#include - -#include "Instrument.h" -#include "InstrumentView.h" -#include "Knob.h" -#include "LcdSpinBox.h" - - -#ifndef SLICERT_UI_H -#define SLICERT_UI_H - -namespace lmms -{ - -// forward declaration, to be able to use SlicerT as a parameter -class SlicerT; - -namespace gui -{ - -// class Knob; -// class LedCheckBox; -// class PixmapButton; - - -class SlicerTUI : public InstrumentViewFixedSize -{ - Q_OBJECT -public: - SlicerTUI( SlicerT * _instrument, - QWidget * _parent ); - ~SlicerTUI() override = default; - -protected slots: - void exportMidi(); - //void sampleSizeChanged( float _new_sample_length ); - -protected: - virtual void dragEnterEvent( QDragEnterEvent * _dee ); - virtual void dropEvent( QDropEvent * _de ); - virtual void mousePressEvent( QMouseEvent * _me ); - - -private: - SlicerT * slicerTParent; - - Knob noteThresholdKnob; - Knob fadeOutKnob; - LcdSpinBox bpmBox; - - QPushButton resetButton; - QPushButton timeShiftButton; - QPushButton midiExportButton; - - WaveForm wf; - - -} ; - - -} // namespace gui - -} // namespace lmms - -#endif \ No newline at end of file diff --git a/plugins/Slicer/WaveForm.cpp b/plugins/Slicer/WaveForm.cpp deleted file mode 100644 index 84a3d249f3f..00000000000 --- a/plugins/Slicer/WaveForm.cpp +++ /dev/null @@ -1,260 +0,0 @@ -#include "WaveForm.h" -#include "SlicerT.h" - -#include - -namespace lmms -{ - - -namespace gui -{ - WaveForm::WaveForm(int _w, int _h, SlicerT * _instrument, QWidget * _parent) : - QWidget(_parent), - seeker(QPixmap(_w, _h*seekerRatio)), - sliceEditor(QPixmap(_w, _h*(1 - seekerRatio) - margin)), - currentSample(_instrument->originalSample.data(), _instrument->originalSample.frames()), - slicePoints(_instrument->slicePoints) - { - - - width = _w; - height = _h; - slicerTParent = _instrument; - setFixedSize(width, height); - setMouseTracking( true ); - setAcceptDrops( true ); - - sliceEditor.fill(waveformBgColor); - seeker.fill(waveformBgColor); - - connect(slicerTParent, - SIGNAL(isPlaying(float, float, float)), - this, - SLOT(isPlaying(float, float, float))); - - connect(slicerTParent, SIGNAL(dataChanged()), this, SLOT(updateData())); - - updateUI(); - } - - void WaveForm::drawEditor() { - sliceEditor.fill(waveformBgColor); - QPainter brush(&sliceEditor); - brush.setPen(waveformColor); - - float startFrame = seekerStart * currentSample.frames(); - float endFrame = seekerEnd * currentSample.frames(); - - currentSample.visualize( - brush, - QRect( 0, 0, sliceEditor.width(), sliceEditor.height() ), - startFrame, endFrame); - - - for (int i = 0;i= startFrame && sliceIndex <= endFrame) { - float xPos = (float)(sliceIndex - startFrame) / (float)(endFrame - startFrame) * (float)width; - if (i == sliceSelected) { - brush.setPen(QPen(selectedSliceColor, 2)); - } - - brush.drawLine(xPos, 0, xPos, height); - } - } - } - - void WaveForm::drawSeeker() { - seeker.fill(waveformBgColor); - QPainter brush(&seeker); - brush.setPen(waveformColor); - - currentSample.visualize( - brush, - QRect( 0, 0, seeker.width(), seeker.height() ), - 0, currentSample.frames()); - - // draw slice points - brush.setPen(sliceColor); - for (int i = 0;ioriginalSample.data(), slicerTParent->originalSample.frames()); - updateUI(); - } - - - void WaveForm::isPlaying(float current, float start, float end) { - noteCurrent = current; - noteStart = start; - noteEnd = end; - drawSeeker(); - update(); - } - - void WaveForm::mousePressEvent( QMouseEvent * _me ) { - float normalizedClick = (float)_me->x() / width; - - if (_me->button() == Qt::MouseButton::MiddleButton) { - seekerStart = 0; - seekerEnd = 1; - return; - } - - if (_me->y() < height*seekerRatio) { - if (abs(normalizedClick - seekerStart) < 0.03) { - currentlyDragging = draggingTypes::seekerStart; - - } else if (abs(normalizedClick - seekerEnd) < 0.03) { - currentlyDragging = draggingTypes::seekerEnd; - - } else if (normalizedClick > seekerStart && normalizedClick < seekerEnd) { - currentlyDragging = draggingTypes::seekerMiddle; - seekerMiddle = normalizedClick; - } - - } else { - sliceSelected = -1; - float startFrame = seekerStart * currentSample.frames(); - float endFrame = seekerEnd * currentSample.frames(); - - for (int i = 0;ibutton() == Qt::MouseButton::LeftButton) { - isDragging = true; - - } else if (_me->button() == Qt::MouseButton::RightButton) { - if (sliceSelected != -1 && slicePoints.size() > 2) { - slicePoints.erase(slicePoints.begin() + sliceSelected); - sliceSelected = -1; - } - } - - } - - void WaveForm::mouseReleaseEvent( QMouseEvent * _me ) { - isDragging = false; - currentlyDragging = draggingTypes::nothing; - updateUI(); - } - - void WaveForm::mouseMoveEvent( QMouseEvent * _me ) { - float normalizedClick = (float)_me->x() / width; - - // handle dragging events - if (isDragging) { - // printf("drag type:%i , seekerStart: %f , seekerEnd: %f \n", currentlyDragging, seekerStart, seekerEnd); - if (currentlyDragging == draggingTypes::seekerStart) { - seekerStart = std::clamp(normalizedClick, 0.0f, seekerEnd - 0.13f); - - } else if (currentlyDragging == draggingTypes::seekerEnd) { - seekerEnd = std::clamp(normalizedClick, seekerStart + 0.13f, 1.0f);; - - } else if (currentlyDragging == draggingTypes::seekerMiddle) { - float distStart = seekerStart - seekerMiddle; - float distEnd = seekerEnd - seekerMiddle; - - seekerMiddle = normalizedClick; - - if (seekerMiddle + distStart > 0 && seekerMiddle + distEnd < 1) { - seekerStart = seekerMiddle + distStart; - seekerEnd = seekerMiddle + distEnd; - } - - } else if (currentlyDragging == draggingTypes::slicePoint) { - float startFrame = seekerStart * currentSample.frames(); - float endFrame = seekerEnd * currentSample.frames(); - - slicePoints[sliceSelected] = startFrame + normalizedClick * (endFrame - startFrame); - - slicePoints[sliceSelected] = std::clamp(slicePoints[sliceSelected], 0, currentSample.frames()); - - std::sort(slicePoints.begin(), slicePoints.end()); - - } - updateUI(); - } else { - - } - - - - } - - void WaveForm::mouseDoubleClickEvent(QMouseEvent * _me) { - float normalizedClick = (float)_me->x() / width; - float startFrame = seekerStart * currentSample.frames(); - float endFrame = seekerEnd * currentSample.frames(); - - float slicePosition = startFrame + normalizedClick * (endFrame - startFrame); - - for (int i = 0;ikey(); - printf("key: %i\n", ke->key()); - if ((key == 16777219 || key == 16777223) && // delete and backspace - (sliceSelected != -1 && slicePoints.size() > 2)) { - - slicePoints.erase(slicePoints.begin() + sliceSelected); - } - updateUI(); - - } - - void WaveForm::paintEvent( QPaintEvent * _pe) { - QPainter p( this ); - p.drawPixmap(0, height*0.3f + margin, sliceEditor); - p.drawPixmap(0, 0, seeker); - } -} -} \ No newline at end of file diff --git a/plugins/Slicer/WaveForm.h b/plugins/Slicer/WaveForm.h deleted file mode 100644 index a63110c5e8a..00000000000 --- a/plugins/Slicer/WaveForm.h +++ /dev/null @@ -1,90 +0,0 @@ -#include -#include -#include -#include -#include - -#include "SampleBuffer.h" - -#ifndef WAVEFORM_H -#define WAVEFORM_H - -namespace lmms -{ - -class SlicerT; - -namespace gui -{ - - -class WaveForm : public QWidget { - Q_OBJECT - protected: - virtual void mousePressEvent( QMouseEvent * _me ); - virtual void mouseReleaseEvent( QMouseEvent * _me ); - virtual void mouseMoveEvent( QMouseEvent * _me ); - virtual void mouseDoubleClickEvent(QMouseEvent * _me); - - virtual void keyPressEvent(QKeyEvent * ke); - - virtual void paintEvent( QPaintEvent * _pe); - - private: - int width; - int height; - float seekerRatio = 0.3f; - int margin = 5; - QColor waveformBgColor = QColor(11, 11, 11); - QColor waveformColor = QColor(124, 49, 214); - QColor playColor = QColor(255, 255, 255, 200); - QColor playHighlighColor = QColor(255, 255, 255, 70); - QColor sliceColor = QColor(49, 214, 124); - QColor selectedSliceColor = QColor(172, 236, 190); - QColor seekerColor = QColor(214, 124, 49); - QColor seekerShadowColor = QColor(0, 0, 0, 175); - - enum class draggingTypes { - nothing, - seekerStart, - seekerEnd, - seekerMiddle, - slicePoint, - }; - draggingTypes currentlyDragging; - bool isDragging = false; - - float seekerStart = 0; - float seekerEnd = 1; - float seekerMiddle = 0.5f; - int sliceSelected = 0; - - float noteCurrent; - float noteStart; - float noteEnd; - - QPixmap sliceEditor; - QPixmap seeker; - - SampleBuffer currentSample; - - void drawEditor(); - void drawSeeker(); - void updateUI(); - - public slots: - void updateData(); - void isPlaying(float current, float start, float end); - - public: - WaveForm(int _w, int _h, SlicerT * _instrument, QWidget * _parent); - - private: - SlicerT * slicerTParent; - std::vector & slicePoints; - -}; -}} - - -#endif \ No newline at end of file diff --git a/plugins/Slicer/logo.png b/plugins/Slicer/logo.png deleted file mode 100644 index 2e84cba5a1377f9a65f4b1db243846c05b713dab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1109 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sjKx9jP7LeL$-D$|I14-?iy0WW zg+Z8+Vb&Z8pdfpRr>`sf4NgXJQJvX>d|!Zim;!u4T!B&}8T|kM|KEoPYk)xzQWE4B z3=9-z7FITP4o)6kK7IiqVKH$DDQOv5Ie7&|WffI5bxkcDJ$(ZsV>5FbTRUercTX>G zAOC>Bppe+Ogv8|3w9MSR{F2i0iptu$=C+QmNmCXqHI%PT1Dem6$-r|AKVTYt_};w_U!NXB7K(*S6We|JSFoOiXsz8FdcnrTOt2&-)|dIM-^*%*&JceQt?Pec4@e;H|XO%RORm zJGWT29Px>-t+FOzYLvO;^eNcK04<`;f1yG=XO=pJT7uNUR(9=-MyZ|evNJ5+=Dl~Z4JU&4-aUTUP}up-@@>18x9vZm z`?`a3wX0b1{r8`b`8V%pdUGe>(P~!Sf0F`#aQn*cpRcO&&I6RsJzf1=);T3K0RVE& BY2^R_ From efe1a7ec5ca39cbd67480ab5b5f2315624972539 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sat, 9 Sep 2023 08:33:39 +0200 Subject: [PATCH 14/99] Tried fixing multi slice playback (still broken) --- plugins/SlicerT/SlicerT.cpp | 41 ++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 1a510bc034c..1da5d4a1cc5 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -47,7 +47,7 @@ Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = "SlicerT", QT_TRANSLATE_NOOP( "PluginBrowser", "Basic Slicer" ), - "Daniel Kauss Serna", + "Daniel Kauss Serna ", 0x0100, Plugin::Type::Instrument, new PluginPixmapLoader( "logo" ), @@ -71,36 +71,35 @@ void SlicerT::playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) { if (m_originalSample.frames() < 2048) { return; } - const fpp_t frames = handle->framesLeftForCurrentPeriod(); const int playedFrames = handle->totalFramesPlayed(); const f_cnt_t offset = handle->noteOffset(); + const int totalFrames = m_timeShiftedSample.frames(); + + int sliceStart, sliceEnd; + float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo() ; + int noteIndex = handle->key() - 69; + + // 0th element is no sound, so play full sample + if (noteIndex > m_slicePoints.size()-2 || noteIndex < 0) { + sliceStart = 0; + sliceEnd = totalFrames; + } else { + sliceStart = m_slicePoints[noteIndex] * speedRatio; + sliceEnd = m_slicePoints[noteIndex+1] * speedRatio; + } - int totalFrames = m_timeShiftedSample.frames(); - int sliceStart = m_timeShiftedSample.startFrame(); - int sliceEnd = m_timeShiftedSample.endFrame(); - int sliceFrames = m_timeShiftedSample.endFrame() - m_timeShiftedSample.startFrame(); + int sliceFrames = sliceEnd - sliceStart; int noteFramesLeft = sliceFrames - playedFrames; + // pretty sure this causes issues when playing multipple slices at once + m_timeShiftedSample.setAllPointFrames( sliceStart, sliceEnd, sliceStart, sliceEnd ); + // init NotePlayHandle data if( !handle->m_pluginData ) { handle->m_pluginData = new SampleBuffer::handleState( false, SRC_LINEAR ); - - float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo() ; - int noteIndex = handle->key() - 69; - int sliceStart, sliceEnd; - - // 0th element is no sound, so play full sample - if (noteIndex > m_slicePoints.size()-2 || noteIndex < 0) { - sliceStart = 0; - sliceEnd = m_timeShiftedSample.frames(); - } else { - sliceStart = m_slicePoints[noteIndex] * speedRatio; - sliceEnd = m_slicePoints[noteIndex+1] * speedRatio; - } - - m_timeShiftedSample.setAllPointFrames( sliceStart, sliceEnd, sliceStart, sliceEnd ); + ((SampleBuffer::handleState *)handle->m_pluginData)->setFrameIndex( sliceStart ); } if( ! handle->isFinished() ) { From 6bda6dfcf228ac7d629287ab03f012b3d0f78815 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sat, 9 Sep 2023 08:39:47 +0200 Subject: [PATCH 15/99] remove includes, add license --- plugins/SlicerT/SlicerT.cpp | 2 +- plugins/SlicerT/SlicerTUI.cpp | 25 ++++++++++++++++++++++++- plugins/SlicerT/SlicerTUI.h | 25 ++++++++++++++++++++++++- plugins/SlicerT/WaveForm.cpp | 28 ++++++++++++++++++++++++---- plugins/SlicerT/WaveForm.h | 24 ++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 7 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 1da5d4a1cc5..f7d33eb7ccf 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -23,7 +23,7 @@ */ #include "SlicerT.h" -#include + #include #include diff --git a/plugins/SlicerT/SlicerTUI.cpp b/plugins/SlicerT/SlicerTUI.cpp index 8ee2e79b28d..ba3e1c59df9 100644 --- a/plugins/SlicerT/SlicerTUI.cpp +++ b/plugins/SlicerT/SlicerTUI.cpp @@ -1,7 +1,30 @@ +/* + * SlicerTUI.cpp - controls the UI for slicerT + * + * Copyright (c) 2006-2008 Andreas Brandmaier + * + * 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 "SlicerTUI.h" #include "SlicerT.h" -#include #include #include diff --git a/plugins/SlicerT/SlicerTUI.h b/plugins/SlicerT/SlicerTUI.h index 4bd77ba5267..a0234cf096a 100644 --- a/plugins/SlicerT/SlicerTUI.h +++ b/plugins/SlicerT/SlicerTUI.h @@ -1,6 +1,29 @@ +/* + * SlicerTUI.h - declaration of class SlicerTUI + * + * Copyright (c) 2006-2008 Andreas Brandmaier + * + * 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 "WaveForm.h" -#include #include #include "Instrument.h" diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/WaveForm.cpp index 9257cd9bccf..e72b737a6da 100644 --- a/plugins/SlicerT/WaveForm.cpp +++ b/plugins/SlicerT/WaveForm.cpp @@ -1,7 +1,30 @@ +/* + * WaveForm.cpp - slice editor for SlicerT + * + * Copyright (c) 2006-2008 Andreas Brandmaier + * + * 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 "WaveForm.h" #include "SlicerT.h" -#include namespace lmms { @@ -86,7 +109,6 @@ namespace gui // draw current playBack brush.setPen(m_playColor); - // printf("noteplay index: %i\n", m_noteCurrent); brush.drawLine(m_noteCurrent*m_width, 0, m_noteCurrent*m_width, m_height); brush.fillRect(m_noteStart*m_width, 0, (m_noteEnd-m_noteStart)*m_width, m_height, m_playHighlighColor); @@ -107,7 +129,6 @@ namespace gui } void WaveForm::updateData() { - printf("main data changed, updating sample and UI\n"); m_currentSample = SampleBuffer(m_slicerTParent->m_originalSample.data(), m_slicerTParent->m_originalSample.frames()); updateUI(); } @@ -182,7 +203,6 @@ namespace gui // handle dragging events if (m_isDragging) { - // printf("drag type:%i , m_seekerStart: %f , m_seekerEnd: %f \n", m_currentlyDragging, m_seekerStart, m_seekerEnd); if (m_currentlyDragging == m_draggingTypes::m_seekerStart) { m_seekerStart = std::clamp(normalizedClick, 0.0f, m_seekerEnd - 0.13f); diff --git a/plugins/SlicerT/WaveForm.h b/plugins/SlicerT/WaveForm.h index dd448025a8a..34e0120a945 100644 --- a/plugins/SlicerT/WaveForm.h +++ b/plugins/SlicerT/WaveForm.h @@ -1,3 +1,27 @@ +/* + * WaveForm.h - declaration of class WaveForm + * + * Copyright (c) 2006-2008 Andreas Brandmaier + * + * 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 #include #include From d13ca1a0b70e608798c28d0b22f2d456ee61ba43 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sat, 9 Sep 2023 09:17:47 +0200 Subject: [PATCH 16/99] code factoring issues --- plugins/SlicerT/SlicerT.cpp | 8 +------- plugins/SlicerT/SlicerTUI.cpp | 2 -- plugins/SlicerT/SlicerTUI.h | 5 ++--- plugins/SlicerT/WaveForm.cpp | 8 -------- plugins/SlicerT/WaveForm.h | 2 +- 5 files changed, 4 insertions(+), 21 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index f7d33eb7ccf..7cd28ad961e 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -323,7 +323,6 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO // analysis step for (int j = 0; j < windowSize; j++) { - real = FFTSpectrum[j][0]; imag = FFTSpectrum[j][1]; @@ -347,9 +346,7 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO allMagnitudes[j] = magnitude; allFrequencies[j] = freq; - } - // pitch shifting // takes all the values that are below the nyquist frequency (representable with our samplerate) // nyquist frequency = samplerate / 2 @@ -390,7 +387,6 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO // windowing for (int j = 0; j < windowSize; j++) { - float outIndex = i * outStepSize + j; if (outIndex > outFrames) { break; @@ -492,8 +488,8 @@ void SlicerT::saveSettings(QDomDocument & document, QDomElement & element) { m_fadeOutFrames.saveSettings(document, element, "fadeOut"); m_noteThreshold.saveSettings(document, element, "threshold"); m_originalBPM.saveSettings(document, element, "origBPM"); - } + void SlicerT::loadSettings( const QDomElement & element ) { if (!element.attribute("src").isEmpty()) { @@ -515,7 +511,6 @@ void SlicerT::loadSettings( const QDomElement & element ) { int totalSlices = element.attribute("totalSlices").toInt(); m_slicePoints = {}; for (int i = 0;iignore(); } - } void SlicerTUI::dropEvent( QDropEvent * de ) { diff --git a/plugins/SlicerT/SlicerTUI.h b/plugins/SlicerT/SlicerTUI.h index a0234cf096a..ed4689325dc 100644 --- a/plugins/SlicerT/SlicerTUI.h +++ b/plugins/SlicerT/SlicerTUI.h @@ -47,6 +47,7 @@ namespace gui class SlicerTUI : public InstrumentViewFixedSize { Q_OBJECT + public: SlicerTUI( SlicerT * instrument, QWidget * parent ); @@ -72,9 +73,7 @@ protected slots: QPushButton m_midiExportButton; WaveForm m_wf; - - -} ; +}; } // namespace gui diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/WaveForm.cpp index e72b737a6da..6a6fcde6c24 100644 --- a/plugins/SlicerT/WaveForm.cpp +++ b/plugins/SlicerT/WaveForm.cpp @@ -175,7 +175,6 @@ namespace gui if (abs(xPos - normalizedClick) < 0.03) { m_currentlyDragging = m_draggingTypes::slicePoint; m_sliceSelected = i; - } } } @@ -229,15 +228,9 @@ namespace gui m_slicePoints[m_sliceSelected] = std::clamp(m_slicePoints[m_sliceSelected], 0, m_currentSample.frames()); std::sort(m_slicePoints.begin(), m_slicePoints.end()); - } updateUI(); - } else { - } - - - } void WaveForm::mouseDoubleClickEvent(QMouseEvent * me) { @@ -255,7 +248,6 @@ namespace gui } std::sort(m_slicePoints.begin(), m_slicePoints.end()); - } void WaveForm::paintEvent( QPaintEvent * pe) { diff --git a/plugins/SlicerT/WaveForm.h b/plugins/SlicerT/WaveForm.h index 34e0120a945..611e7b463e4 100644 --- a/plugins/SlicerT/WaveForm.h +++ b/plugins/SlicerT/WaveForm.h @@ -44,6 +44,7 @@ namespace gui class WaveForm : public QWidget { Q_OBJECT + protected: virtual void mousePressEvent(QMouseEvent * me); virtual void mouseReleaseEvent(QMouseEvent * me); @@ -104,7 +105,6 @@ class WaveForm : public QWidget { private: SlicerT * m_slicerTParent; std::vector & m_slicePoints; - }; }} From 6482c16636091b42e3105581a8df27fbf0d02106 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sat, 9 Sep 2023 09:21:13 +0200 Subject: [PATCH 17/99] more code factoring --- plugins/SlicerT/SlicerT.h | 1 + plugins/SlicerT/SlicerTUI.cpp | 3 --- plugins/SlicerT/WaveForm.cpp | 3 --- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 745a1e44843..41fe486eaf8 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -41,6 +41,7 @@ namespace lmms class SlicerT : public Instrument{ Q_OBJECT + public: SlicerT(InstrumentTrack * instrumentTrack); ~SlicerT() override = default; diff --git a/plugins/SlicerT/SlicerTUI.cpp b/plugins/SlicerT/SlicerTUI.cpp index c83d7cfa262..88d44fa81eb 100644 --- a/plugins/SlicerT/SlicerTUI.cpp +++ b/plugins/SlicerT/SlicerTUI.cpp @@ -88,9 +88,6 @@ SlicerTUI::SlicerTUI( SlicerT * instrument, m_resetButton.move(70, 200); m_resetButton.setToolTip(tr("Reset Slices")); connect(&m_resetButton, SIGNAL( clicked() ), m_slicerTParent, SLOT( updateSlices() )); - - - } // copied from piano roll diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/WaveForm.cpp index 6a6fcde6c24..dfed512ae9f 100644 --- a/plugins/SlicerT/WaveForm.cpp +++ b/plugins/SlicerT/WaveForm.cpp @@ -39,8 +39,6 @@ namespace gui m_currentSample(instrument->m_originalSample.data(), instrument->m_originalSample.frames()), m_slicePoints(instrument->m_slicePoints) { - - m_width = w; m_height = h; m_slicerTParent = instrument; @@ -178,7 +176,6 @@ namespace gui } } } - if (me->button() == Qt::MouseButton::LeftButton) { m_isDragging = true; From 5df2b894cde36cf4da3b5bee25e89512f4ed5171 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 10 Sep 2023 13:46:32 +0200 Subject: [PATCH 18/99] fixed multinote playback and bpm check --- plugins/SlicerT/SlicerT.cpp | 132 +++++++++++++++++----------------- plugins/SlicerT/SlicerT.h | 38 ++++++++-- plugins/SlicerT/SlicerTUI.cpp | 9 ++- plugins/SlicerT/SlicerTUI.h | 12 ++-- plugins/SlicerT/WaveForm.cpp | 4 +- plugins/SlicerT/WaveForm.h | 12 ++-- 6 files changed, 116 insertions(+), 91 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 7cd28ad961e..e0217ce3dff 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -22,6 +22,9 @@ * */ +// TODO: add fft cache, maybe this is overkill but whatever +// TODO: optimize waveform a LOT, mostly during playback + #include "SlicerT.h" #include @@ -56,14 +59,13 @@ Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = } ; } // end extern + SlicerT::SlicerT(InstrumentTrack * instrumentTrack) : Instrument( instrumentTrack, &slicert_plugin_descriptor ), m_noteThreshold(0.6f, 0.0f, 2.0f, 0.01f, this, tr( "Note threshold" ) ), m_fadeOutFrames(0.0f, 0.0f, 8192.0f, 4.0f, this, tr("FadeOut")), m_originalBPM(1, 1, 999, this, tr("Original bpm")), - - m_originalSample(), - m_timeShiftedSample() + m_originalSample() {} @@ -71,16 +73,18 @@ void SlicerT::playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) { if (m_originalSample.frames() < 2048) { return; } + const float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo() ; + const int noteIndex = handle->key() - 69; const fpp_t frames = handle->framesLeftForCurrentPeriod(); - const int playedFrames = handle->totalFramesPlayed(); const f_cnt_t offset = handle->noteOffset(); + const int playedFrames = handle->totalFramesPlayed(); + + if (m_currentSpeedRatio != speedRatio) { + timeShiftSample(); + } const int totalFrames = m_timeShiftedSample.frames(); - + int sliceStart, sliceEnd; - float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo() ; - int noteIndex = handle->key() - 69; - - // 0th element is no sound, so play full sample if (noteIndex > m_slicePoints.size()-2 || noteIndex < 0) { sliceStart = 0; sliceEnd = totalFrames; @@ -90,51 +94,37 @@ void SlicerT::playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) { } int sliceFrames = sliceEnd - sliceStart; + int currentNoteFrame = sliceStart + playedFrames; int noteFramesLeft = sliceFrames - playedFrames; - // pretty sure this causes issues when playing multipple slices at once - m_timeShiftedSample.setAllPointFrames( sliceStart, sliceEnd, sliceStart, sliceEnd ); - // init NotePlayHandle data - if( !handle->m_pluginData ) - { - handle->m_pluginData = new SampleBuffer::handleState( false, SRC_LINEAR ); - ((SampleBuffer::handleState *)handle->m_pluginData)->setFrameIndex( sliceStart ); - } - - if( ! handle->isFinished() ) { - if( m_timeShiftedSample.play( workingBuffer + offset, - (SampleBuffer::handleState *)handle->m_pluginData, - frames, 440, - static_cast( 0 ) ) ) - { - // exponential fade out, applyRelease kinda sucks - if (noteFramesLeft < m_fadeOutFrames.value()) { - for (int i = 0;i 0) { + int bufferSize = frames * BYTES_PER_FRAME; + memcpy(workingBuffer + offset, m_timeShiftedSample.data() + currentNoteFrame, bufferSize); - instrumentTrack()->processAudioBuffer( workingBuffer, - frames + offset, handle ); + // exponential fade out, applyRelease kinda sucks + if (noteFramesLeft < m_fadeOutFrames.value()) { + for (int i = 0;iprocessAudioBuffer( workingBuffer, frames + offset, handle ); - } else { - emit isPlaying(0, 0, 0); - } + // !! disabled until it is optimized, because it lags the whole ui + // calculate absolute for the waveform + // float absoluteCurrentNote = (float)currentNoteFrame / totalFrames; + // float absoluteStartNote = (float)sliceStart / totalFrames; + // float abslouteEndNote = (float)sliceEnd / totalFrames; + // emit isPlaying(absoluteCurrentNote, absoluteStartNote, abslouteEndNote); } else { - emit isPlaying(0, 0, 0); + // emit isPlaying(0, 0, 0); } } @@ -146,7 +136,7 @@ void SlicerT::findSlices() { m_slicePoints = {}; const int window = 1024; - int minWindowsPassed = 0; + int minWindowsPassed = 1; int peakIndex = 0; float lastPeak = 0; @@ -181,7 +171,7 @@ void SlicerT::findBPM() { if (m_originalSample.frames() < 2048) { return; } - int bpmSnap = 1; + int bpmSnap = 1; // 1 = disabled float sampleRate = m_originalSample.sampleRate(); float totalFrames = m_originalSample.frames(); @@ -210,23 +200,29 @@ void SlicerT::findBPM() { // create timeshifted samplebuffer and timeshifted m_slicePoints void SlicerT::timeShiftSample() { + using std::vector; + // initial checks if (m_originalSample.frames() < 2048) { return; } - using std::vector; + if (m_timeshiftLock) { // needed + return; + } + m_timeshiftLock = true; + // original sample data float sampleRate = m_originalSample.sampleRate(); int originalFrames = m_originalSample.frames(); - // target data TODO: fix this mess bpm_t targetBPM = Engine::getSong()->getTempo(); - float speedRatio = (float)m_originalBPM.value() / targetBPM ; - int outFrames = speedRatio * originalFrames; + m_currentSpeedRatio = (float)m_originalBPM.value() / targetBPM ; + int outFrames = m_currentSpeedRatio * originalFrames; // nothing to do here if (targetBPM == m_originalBPM.value()) { - m_timeShiftedSample = SampleBuffer(m_originalSample.data(), m_originalSample.frames()); + m_timeShiftedSample.setData(m_originalSample.data(), m_originalSample.frames()); + m_timeshiftLock = false; return; } @@ -243,18 +239,15 @@ void SlicerT::timeShiftSample() { rawDataL[i] = (float)m_originalSample.data()[i][0]; rawDataR[i] = (float)m_originalSample.data()[i][1]; } + // process channels phaseVocoder(rawDataL, outDataL, sampleRate, 1); phaseVocoder(rawDataR, outDataR, sampleRate, 1); // write processed channels - for (int i = 0;i &dataIn, std::vector &dataO for (int i = 0;i < numWindows-overSampling;i++) { windowIndex = i * stepSize; - // FFT memcpy(FFTInput.data(), dataIn.data() + windowIndex, windowSize*sizeof(float)); + + // int hash = hashFttWindow(FFTInput); + // printf("%i\n", hash); + + // FFT fftwf_execute(fftPlan); // analysis step @@ -424,12 +421,20 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO memcpy(dataOut.data(), outBuffer.data(), outFrames*sizeof(float)); } +int SlicerT::hashFttWindow(std::vector & in) { + int hash = 0; + for (float value : in) { + hash += (324723947 + (int)(value * 10689354)) ^ 93485734985;; + } + return hash; +} + void SlicerT::writeToMidi(std::vector * outClip) { if (m_originalSample.frames() < 2048) { return; } int ticksPerBar = DefaultTicksPerBar; - float sampleRate = m_timeShiftedSample.sampleRate(); + float sampleRate = m_originalSample.sampleRate(); float bpm = Engine::getSong()->getTempo(); float samplesPerBeat = 60.0f / bpm * sampleRate; @@ -466,11 +471,6 @@ void SlicerT::updateSlices() { findSlices(); } -void SlicerT::updateTimeShift() { - timeShiftSample(); -} - - void SlicerT::saveSettings(QDomDocument & document, QDomElement & element) { element.setAttribute("src", m_originalSample.audioFile()); if (m_originalSample.audioFile().isEmpty()) diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 41fe486eaf8..a6e6f36bbfa 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -39,6 +39,32 @@ namespace lmms { +// small helper class, since SampleBuffer is inadequate (not thread safe, no dinamic startpoint) +class PlaybackBuffer { + public: + std::vector mainBuffer; + + int frames() { return mainBuffer.size(); }; + sampleFrame * data() { return mainBuffer.data(); }; + + void setData(const sampleFrame * data, int newFrames) + { + mainBuffer = {}; + mainBuffer.resize(newFrames); + memcpy(mainBuffer.data(), data, newFrames * sizeof(sampleFrame)); + }; + void setData(std::vector & leftData, std::vector & rightData) { + int newFrames = std::min(leftData.size(), rightData.size()); + mainBuffer = {}; + mainBuffer.resize(newFrames); + + for (int i = 0;i < newFrames;i++) { + mainBuffer[i][0] = leftData[i]; + mainBuffer[i][1] = rightData[i]; + } + } +}; + class SlicerT : public Instrument{ Q_OBJECT @@ -70,17 +96,21 @@ class SlicerT : public Instrument{ IntModel m_originalBPM; SampleBuffer m_originalSample; - SampleBuffer m_timeShiftedSample; + PlaybackBuffer m_timeShiftedSample; std::vector m_slicePoints; + float m_currentSpeedRatio = 0; + bool m_timeshiftLock; // dont run timeshifting at the same time, instant crash + // std::unordered_map > m_fftWindowCache; + void findSlices(); void findBPM(); void timeShiftSample(); void phaseVocoder(std::vector &in, std::vector &out, float sampleRate, float pitchScale); + int hashFttWindow(std::vector & in); friend class gui::SlicerTUI; friend class gui::WaveForm; }; -} - -#endif +} // namespace lmms +#endif // SLICERT_H diff --git a/plugins/SlicerT/SlicerTUI.cpp b/plugins/SlicerT/SlicerTUI.cpp index 88d44fa81eb..1602e847f86 100644 --- a/plugins/SlicerT/SlicerTUI.cpp +++ b/plugins/SlicerT/SlicerTUI.cpp @@ -66,10 +66,11 @@ SlicerTUI::SlicerTUI( SlicerT * instrument, m_bpmBox.setToolTip(tr("Original sample BPM")); m_bpmBox.setLabel(tr("BPM")); m_bpmBox.setModel(&m_slicerTParent->m_originalBPM); + // connect(&m_bpmBox, SIGNAL( manualChange() ), m_slicerTParent, SLOT( updateTimeShift() ), Qt::QueuedConnection); m_timeShiftButton.move(70, 150); m_timeShiftButton.setToolTip(tr("Timeshift sample")); - connect(&m_timeShiftButton, SIGNAL( clicked() ), m_slicerTParent, SLOT( updateTimeShift() )); + // connect(&m_timeShiftButton, SIGNAL( clicked() ), m_slicerTParent, SLOT( updateTimeShift() ), Qt::QueuedConnection); m_fadeOutKnob.move(200, 150); m_fadeOutKnob.setToolTip(tr("FadeOut for notes")); @@ -166,7 +167,5 @@ void SlicerTUI::dropEvent( QDropEvent * de ) { de->ignore(); } -} -} - - +} // namespace gui +} // namespace lmms \ No newline at end of file diff --git a/plugins/SlicerT/SlicerTUI.h b/plugins/SlicerT/SlicerTUI.h index ed4689325dc..11a238ac01f 100644 --- a/plugins/SlicerT/SlicerTUI.h +++ b/plugins/SlicerT/SlicerTUI.h @@ -22,6 +22,9 @@ * */ +#ifndef SLICERT_UI_H +#define SLICERT_UI_H + #include "WaveForm.h" #include @@ -32,9 +35,6 @@ #include "LcdSpinBox.h" -#ifndef SLICERT_UI_H -#define SLICERT_UI_H - namespace lmms { @@ -74,10 +74,6 @@ protected slots: WaveForm m_wf; }; - - } // namespace gui - } // namespace lmms - -#endif \ No newline at end of file +#endif // SLICERT_UI_H \ No newline at end of file diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/WaveForm.cpp index dfed512ae9f..d5bdc261ac1 100644 --- a/plugins/SlicerT/WaveForm.cpp +++ b/plugins/SlicerT/WaveForm.cpp @@ -252,5 +252,5 @@ namespace gui p.drawPixmap(0, m_height*0.3f + m_margin, m_sliceEditor); p.drawPixmap(0, 0, m_seeker); } -} -} \ No newline at end of file +} // namespace gui +} // namespace lmms \ No newline at end of file diff --git a/plugins/SlicerT/WaveForm.h b/plugins/SlicerT/WaveForm.h index 611e7b463e4..46bba6e6b2a 100644 --- a/plugins/SlicerT/WaveForm.h +++ b/plugins/SlicerT/WaveForm.h @@ -22,6 +22,9 @@ * */ +#ifndef WAVEFORM_H +#define WAVEFORM_H + #include #include #include @@ -30,8 +33,6 @@ #include "SampleBuffer.h" -#ifndef WAVEFORM_H -#define WAVEFORM_H namespace lmms { @@ -106,7 +107,6 @@ class WaveForm : public QWidget { SlicerT * m_slicerTParent; std::vector & m_slicePoints; }; -}} - - -#endif \ No newline at end of file +} // namespace gui +} // namespace lmms +#endif // WAVEFORM_H \ No newline at end of file From 6b7e442039772cb0aebe7601cf913ef72128f698 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 10 Sep 2023 18:06:22 +0200 Subject: [PATCH 19/99] UI performance improvments + code style --- plugins/SlicerT/SlicerT.cpp | 126 ++++++++++++++++++---------------- plugins/SlicerT/SlicerT.h | 18 ++--- plugins/SlicerT/SlicerTUI.cpp | 15 ++-- plugins/SlicerT/SlicerTUI.h | 5 +- plugins/SlicerT/WaveForm.cpp | 111 +++++++++++++++++++----------- plugins/SlicerT/WaveForm.h | 8 ++- 6 files changed, 159 insertions(+), 124 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index e0217ce3dff..d93be93da3d 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -2,7 +2,7 @@ * SlicerT.cpp - simple slicer plugin * * Copyright (c) 2006-2008 Andreas Brandmaier - * + * * This file is part of LMMS - https://lmms.io * * This program is free software; you can redistribute it and/or @@ -22,8 +22,9 @@ * */ +// TODO: fade in mode +// TODO: general UI improvements, maybe open folders button // TODO: add fft cache, maybe this is overkill but whatever -// TODO: optimize waveform a LOT, mostly during playback #include "SlicerT.h" @@ -60,7 +61,7 @@ Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = } // end extern -SlicerT::SlicerT(InstrumentTrack * instrumentTrack) : +SlicerT::SlicerT(InstrumentTrack * instrumentTrack) : Instrument( instrumentTrack, &slicert_plugin_descriptor ), m_noteThreshold(0.6f, 0.0f, 2.0f, 0.01f, this, tr( "Note threshold" ) ), m_fadeOutFrames(0.0f, 0.0f, 8192.0f, 4.0f, this, tr("FadeOut")), @@ -70,35 +71,37 @@ SlicerT::SlicerT(InstrumentTrack * instrumentTrack) : void SlicerT::playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) { - if (m_originalSample.frames() < 2048) { - return; - } + if (m_originalSample.frames() < 2048) { return; } + const float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo() ; const int noteIndex = handle->key() - 69; const fpp_t frames = handle->framesLeftForCurrentPeriod(); const f_cnt_t offset = handle->noteOffset(); const int playedFrames = handle->totalFramesPlayed(); - if (m_currentSpeedRatio != speedRatio) { + if (m_currentSpeedRatio != speedRatio) + { timeShiftSample(); } const int totalFrames = m_timeShiftedSample.frames(); int sliceStart, sliceEnd; - if (noteIndex > m_slicePoints.size()-2 || noteIndex < 0) { + if (noteIndex > m_slicePoints.size()-2 || noteIndex < 0) + { sliceStart = 0; sliceEnd = totalFrames; } else { sliceStart = m_slicePoints[noteIndex] * speedRatio; sliceEnd = m_slicePoints[noteIndex+1] * speedRatio; } - + int sliceFrames = sliceEnd - sliceStart; int currentNoteFrame = sliceStart + playedFrames; int noteFramesLeft = sliceFrames - playedFrames; - if( noteFramesLeft > 0) { + if( noteFramesLeft > 0) + { int bufferSize = frames * BYTES_PER_FRAME; memcpy(workingBuffer + offset, m_timeShiftedSample.data() + currentNoteFrame, bufferSize); @@ -119,20 +122,18 @@ void SlicerT::playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) { // !! disabled until it is optimized, because it lags the whole ui // calculate absolute for the waveform - // float absoluteCurrentNote = (float)currentNoteFrame / totalFrames; - // float absoluteStartNote = (float)sliceStart / totalFrames; - // float abslouteEndNote = (float)sliceEnd / totalFrames; - // emit isPlaying(absoluteCurrentNote, absoluteStartNote, abslouteEndNote); + float absoluteCurrentNote = (float)currentNoteFrame / totalFrames; + float absoluteStartNote = (float)sliceStart / totalFrames; + float abslouteEndNote = (float)sliceEnd / totalFrames; + emit isPlaying(absoluteCurrentNote, absoluteStartNote, abslouteEndNote); } else { - // emit isPlaying(0, 0, 0); + emit isPlaying(0, 0, 0); } } void SlicerT::findSlices() { - if (m_originalSample.frames() < 2048) { - return; - } + if (m_originalSample.frames() < 2048) { return; } m_slicePoints = {}; const int window = 1024; @@ -141,17 +142,20 @@ void SlicerT::findSlices() { float lastPeak = 0; float currentPeak = 0; - + for (int i = 0; i currentPeak) { + if (sampleValue > currentPeak) + { currentPeak = sampleValue; peakIndex = i; } - - if (i%window==0) { - if (abs(currentPeak / lastPeak) > 1+m_noteThreshold.value() && minWindowsPassed <= 0) { + + if (i%window==0) + { + if (abs(currentPeak / lastPeak) > 1+m_noteThreshold.value() && minWindowsPassed <= 0) + { m_slicePoints.push_back(std::max(0, peakIndex-window/2)); // slight offset minWindowsPassed = 2; // wait at least one window for a new note } @@ -168,9 +172,7 @@ void SlicerT::findSlices() { // find the bpm of the sample by assuming its in 4/4 time signature , // and lies in the 100 - 200 bpm range void SlicerT::findBPM() { - if (m_originalSample.frames() < 2048) { - return; - } + if (m_originalSample.frames() < 2048) { return; } int bpmSnap = 1; // 1 = disabled float sampleRate = m_originalSample.sampleRate(); @@ -181,11 +183,13 @@ void SlicerT::findBPM() { float bpmEstimate = 240.0f / sampleLength; // deal with samlpes that are not 1 bar long - while (bpmEstimate < 100) { + while (bpmEstimate < 100) + { bpmEstimate *= 2; } - while (bpmEstimate > 200) { + while (bpmEstimate > 200) + { bpmEstimate /= 2; } @@ -202,14 +206,11 @@ void SlicerT::findBPM() { void SlicerT::timeShiftSample() { using std::vector; // initial checks - if (m_originalSample.frames() < 2048) { - return; - } - if (m_timeshiftLock) { // needed - return; - } + if (m_originalSample.frames() < 2048) { return; } + + if (m_timeshiftLock) { return; } m_timeshiftLock = true; - + // original sample data float sampleRate = m_originalSample.sampleRate(); @@ -220,7 +221,8 @@ void SlicerT::timeShiftSample() { int outFrames = m_currentSpeedRatio * originalFrames; // nothing to do here - if (targetBPM == m_originalBPM.value()) { + if (targetBPM == m_originalBPM.value()) + { m_timeShiftedSample.setData(m_originalSample.data(), m_originalSample.frames()); m_timeshiftLock = false; return; @@ -235,7 +237,8 @@ void SlicerT::timeShiftSample() { vector bufferData(outFrames, sampleFrame()); // copy channels for processing - for (int i = 0;i &dataIn, std::vector &dataO // oversampling is better if higher always (probably) const int windowSize = 512; const int overSampling = 32; - + // audio data int inFrames = dataIn.size(); int outFrames = dataOut.size(); @@ -275,10 +278,10 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO float lengthRatio = (float)outFrames / inFrames; // values used - const int stepSize = (float)windowSize / overSampling; + const int stepSize = (float)windowSize / overSampling; const int numWindows = (float)inFrames / stepSize; const float outStepSize = lengthRatio * (float)stepSize; // float, else inaccurate - const float freqPerBin = sampleRate/windowSize; + const float freqPerBin = sampleRate/windowSize; // very important const float expectedPhaseIn = 2.*M_PI*(float)stepSize/(float)windowSize; const float expectedPhaseOut = 2.*M_PI*(float)outStepSize/(float)windowSize; @@ -307,7 +310,8 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO ifftPlan = fftwf_plan_dft_c2r_1d(windowSize, FFTSpectrum, IFFTReconstruction.data(), FFTW_MEASURE); // remove oversampling, because the actual window is overSampling* bigger than stepsize - for (int i = 0;i < numWindows-overSampling;i++) { + for (int i = 0;i < numWindows-overSampling;i++) + { windowIndex = i * stepSize; memcpy(FFTInput.data(), dataIn.data() + windowIndex, windowSize*sizeof(float)); @@ -324,7 +328,7 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO imag = FFTSpectrum[j][1]; magnitude = 2.*sqrt(real*real + imag*imag); - phase = atan2(imag,real); + phase = atan2(imag,real); freq = phase - lastPhase[j]; // subtract prev pahse to get phase diference lastPhase[j] = phase; @@ -336,7 +340,7 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO if (qpd >= 0) qpd += qpd&1; else qpd -= qpd&1; freq -= M_PI*(float)qpd; - + freq = (float)overSampling*freq/(2.*M_PI); // idk freq = (float)j*freqPerBin + freq*freqPerBin; // "compute the k-th partials' true frequency" ok i guess @@ -385,15 +389,13 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO // windowing for (int j = 0; j < windowSize; j++) { float outIndex = i * outStepSize + j; - if (outIndex > outFrames) { - break; - } + if (outIndex > outFrames) { break; } // calculate windows overlapping at index float startWindowOverlap = ceil(outIndex / outStepSize + 0.00000001f); float endWindowOverlap = ceil((float)(-outIndex + outFrames) / outStepSize + 0.00000001f); float totalWindowOverlap = std::min( - std::min(startWindowOverlap, endWindowOverlap), + std::min(startWindowOverlap, endWindowOverlap), (float)overSampling); // discrete windowing @@ -408,13 +410,15 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO fftwf_destroy_plan(fftPlan); fftwf_destroy_plan(ifftPlan); - // normalize + // normalize float max = -1; - for (int i = 0;i &dataIn, std::vector &dataO int SlicerT::hashFttWindow(std::vector & in) { int hash = 0; - for (float value : in) { + for (float value : in) + { hash += (324723947 + (int)(value * 10689354)) ^ 93485734985;; } return hash; } void SlicerT::writeToMidi(std::vector * outClip) { - if (m_originalSample.frames() < 2048) { - return; - } + if (m_originalSample.frames() < 2048) { return; } + int ticksPerBar = DefaultTicksPerBar; float sampleRate = m_originalSample.sampleRate(); @@ -457,13 +461,12 @@ void SlicerT::writeToMidi(std::vector * outClip) { void SlicerT::updateFile(QString file) { m_originalSample.setAudioFile(file); - if (m_originalSample.frames() < 2048) { - return; - } + if (m_originalSample.frames() < 2048) { return; } + findSlices(); - findBPM(); + findBPM(); timeShiftSample(); - + emit dataChanged(); } @@ -507,7 +510,8 @@ void SlicerT::loadSettings( const QDomElement & element ) { m_originalSample.loadFromBase64(element.attribute("srcdata")); } - if (!element.attribute("totalSlices").isEmpty()) { + if (!element.attribute("totalSlices").isEmpty()) + { int totalSlices = element.attribute("totalSlices").toInt(); m_slicePoints = {}; for (int i = 0;i - * + * * This file is part of LMMS - https://lmms.io * * This program is free software; you can redistribute it and/or @@ -47,18 +47,20 @@ class PlaybackBuffer { int frames() { return mainBuffer.size(); }; sampleFrame * data() { return mainBuffer.data(); }; - void setData(const sampleFrame * data, int newFrames) + void setData(const sampleFrame * data, int newFrames) { mainBuffer = {}; - mainBuffer.resize(newFrames); + mainBuffer.resize(newFrames); memcpy(mainBuffer.data(), data, newFrames * sizeof(sampleFrame)); }; - void setData(std::vector & leftData, std::vector & rightData) { + void setData(std::vector & leftData, std::vector & rightData) + { int newFrames = std::min(leftData.size(), rightData.size()); mainBuffer = {}; mainBuffer.resize(newFrames); - for (int i = 0;i < newFrames;i++) { + for (int i = 0;i < newFrames;i++) + { mainBuffer[i][0] = leftData[i]; mainBuffer[i][1] = rightData[i]; } @@ -67,7 +69,7 @@ class PlaybackBuffer { class SlicerT : public Instrument{ Q_OBJECT - + public: SlicerT(InstrumentTrack * instrumentTrack); ~SlicerT() override = default; @@ -98,7 +100,7 @@ class SlicerT : public Instrument{ SampleBuffer m_originalSample; PlaybackBuffer m_timeShiftedSample; std::vector m_slicePoints; - + float m_currentSpeedRatio = 0; bool m_timeshiftLock; // dont run timeshifting at the same time, instant crash // std::unordered_map > m_fftWindowCache; diff --git a/plugins/SlicerT/SlicerTUI.cpp b/plugins/SlicerT/SlicerTUI.cpp index 1602e847f86..ccbdea7c674 100644 --- a/plugins/SlicerT/SlicerTUI.cpp +++ b/plugins/SlicerT/SlicerTUI.cpp @@ -2,7 +2,7 @@ * SlicerTUI.cpp - controls the UI for slicerT * * Copyright (c) 2006-2008 Andreas Brandmaier - * + * * This file is part of LMMS - https://lmms.io * * This program is free software; you can redistribute it and/or @@ -54,30 +54,24 @@ SlicerTUI::SlicerTUI( SlicerT * instrument, m_fadeOutKnob(KnobType::Dark28, this), m_bpmBox(3, "21pink", this), m_resetButton(embed::getIconPixmap("reload"), QString(), this), - m_timeShiftButton(embed::getIconPixmap("max_length"), QString(), this), m_midiExportButton(embed::getIconPixmap("midi_tab"), QString(), this), m_wf(245, 125, instrument, this) { setAcceptDrops( true ); - m_wf.move(2, 5); + m_wf.move(2, 5); m_bpmBox.move(2, 150); m_bpmBox.setToolTip(tr("Original sample BPM")); m_bpmBox.setLabel(tr("BPM")); m_bpmBox.setModel(&m_slicerTParent->m_originalBPM); - // connect(&m_bpmBox, SIGNAL( manualChange() ), m_slicerTParent, SLOT( updateTimeShift() ), Qt::QueuedConnection); - - m_timeShiftButton.move(70, 150); - m_timeShiftButton.setToolTip(tr("Timeshift sample")); - // connect(&m_timeShiftButton, SIGNAL( clicked() ), m_slicerTParent, SLOT( updateTimeShift() ), Qt::QueuedConnection); m_fadeOutKnob.move(200, 150); m_fadeOutKnob.setToolTip(tr("FadeOut for notes")); m_fadeOutKnob.setLabel(tr("FadeOut")); m_fadeOutKnob.setModel(&m_slicerTParent->m_fadeOutFrames); - m_midiExportButton.move(150, 150); + m_midiExportButton.move(150, 200); m_midiExportButton.setToolTip(tr("Copy midi pattern to clipboard")); connect(&m_midiExportButton, SIGNAL( clicked() ), this, SLOT( exportMidi() )); @@ -101,7 +95,8 @@ void SlicerTUI::exportMidi() { std::vector notes; m_slicerTParent->writeToMidi(¬es); - if (notes.size() == 0) { + if (notes.size() == 0) + { return; } diff --git a/plugins/SlicerT/SlicerTUI.h b/plugins/SlicerT/SlicerTUI.h index 11a238ac01f..1a0837bffe7 100644 --- a/plugins/SlicerT/SlicerTUI.h +++ b/plugins/SlicerT/SlicerTUI.h @@ -2,7 +2,7 @@ * SlicerTUI.h - declaration of class SlicerTUI * * Copyright (c) 2006-2008 Andreas Brandmaier - * + * * This file is part of LMMS - https://lmms.io * * This program is free software; you can redistribute it and/or @@ -47,7 +47,7 @@ namespace gui class SlicerTUI : public InstrumentViewFixedSize { Q_OBJECT - + public: SlicerTUI( SlicerT * instrument, QWidget * parent ); @@ -69,7 +69,6 @@ protected slots: LcdSpinBox m_bpmBox; QPushButton m_resetButton; - QPushButton m_timeShiftButton; QPushButton m_midiExportButton; WaveForm m_wf; diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/WaveForm.cpp index d5bdc261ac1..eae9a5e9e80 100644 --- a/plugins/SlicerT/WaveForm.cpp +++ b/plugins/SlicerT/WaveForm.cpp @@ -2,7 +2,7 @@ * WaveForm.cpp - slice editor for SlicerT * * Copyright (c) 2006-2008 Andreas Brandmaier - * + * * This file is part of LMMS - https://lmms.io * * This program is free software; you can redistribute it and/or @@ -36,6 +36,7 @@ namespace gui QWidget(parent), m_sliceEditor(QPixmap(w, h*(1 - m_m_seekerRatio) - m_margin)), m_seeker(QPixmap(w, h*m_m_seekerRatio)), + m_seekerWaveform(QPixmap(w, h*m_m_seekerRatio)), m_currentSample(instrument->m_originalSample.data(), instrument->m_originalSample.frames()), m_slicePoints(instrument->m_slicePoints) { @@ -44,14 +45,13 @@ namespace gui m_slicerTParent = instrument; setFixedSize(m_width, m_height); setMouseTracking( true ); - setAcceptDrops( true ); m_sliceEditor.fill(m_waveformBgColor); - m_seeker.fill(m_waveformBgColor); + m_seekerWaveform.fill(m_waveformBgColor); - connect(m_slicerTParent, - SIGNAL(isPlaying(float, float, float)), - this, + connect(m_slicerTParent, + SIGNAL(isPlaying(float, float, float)), + this, SLOT(isPlaying(float, float, float))); connect(m_slicerTParent, SIGNAL(dataChanged()), this, SLOT(updateData())); @@ -62,11 +62,14 @@ namespace gui void WaveForm::drawEditor() { m_sliceEditor.fill(m_waveformBgColor); QPainter brush(&m_sliceEditor); - brush.setPen(m_waveformColor); float startFrame = m_seekerStart * m_currentSample.frames(); float endFrame = m_seekerEnd * m_currentSample.frames(); + brush.setPen(m_playHighlighColor); + brush.drawLine(0, m_sliceEditor.height()/2, m_sliceEditor.width(), m_sliceEditor.height()/2); + + brush.setPen(m_waveformColor); m_currentSample.visualize( brush, QRect( 0, 0, m_sliceEditor.width(), m_sliceEditor.height() ), @@ -77,9 +80,11 @@ namespace gui int sliceIndex = m_slicePoints[i]; brush.setPen(QPen(m_sliceColor, 2)); - if (sliceIndex >= startFrame && sliceIndex <= endFrame) { + if (sliceIndex >= startFrame && sliceIndex <= endFrame) + { float xPos = (float)(sliceIndex - startFrame) / (float)(endFrame - startFrame) * (float)m_width; - if (i == m_sliceSelected) { + if (i == m_sliceSelected) + { brush.setPen(QPen(m_selectedSliceColor, 2)); } @@ -88,19 +93,26 @@ namespace gui } } - void WaveForm::drawSeeker() { - m_seeker.fill(m_waveformBgColor); - QPainter brush(&m_seeker); + void WaveForm::drawSeekerWaveform() { + QPainter brush(&m_seekerWaveform); brush.setPen(m_waveformColor); + m_seekerWaveform.fill(m_waveformBgColor); m_currentSample.visualize( brush, - QRect( 0, 0, m_seeker.width(), m_seeker.height() ), + QRect( 0, 0, m_seekerWaveform.width(), m_seekerWaveform.height() ), 0, m_currentSample.frames()); + } + + void WaveForm::drawSeeker() { + m_seeker.fill(QColor(0, 0, 0, 0)); + QPainter brush(&m_seeker); + brush.setPen(m_waveformColor); // draw slice points brush.setPen(m_sliceColor); - for (int i = 0;ix() / m_width; - - if (me->button() == Qt::MouseButton::MiddleButton) { + + if (me->button() == Qt::MouseButton::MiddleButton) + { m_seekerStart = 0; m_seekerEnd = 1; return; } - if (me->y() < m_height*m_m_seekerRatio) { - if (abs(normalizedClick - m_seekerStart) < 0.03) { + if (me->y() < m_height*m_m_seekerRatio) + { + if (abs(normalizedClick - m_seekerStart) < 0.03) + { m_currentlyDragging = m_draggingTypes::m_seekerStart; - } else if (abs(normalizedClick - m_seekerEnd) < 0.03) { + } else if (abs(normalizedClick - m_seekerEnd) < 0.03) + { m_currentlyDragging = m_draggingTypes::m_seekerEnd; - } else if (normalizedClick > m_seekerStart && normalizedClick < m_seekerEnd) { + } else if (normalizedClick > m_seekerStart && normalizedClick < m_seekerEnd) + { m_currentlyDragging = m_draggingTypes::m_seekerMiddle; m_seekerMiddle = normalizedClick; } @@ -165,26 +182,31 @@ namespace gui m_sliceSelected = -1; float startFrame = m_seekerStart * m_currentSample.frames(); float endFrame = m_seekerEnd * m_currentSample.frames(); - - for (int i = 0;ibutton() == Qt::MouseButton::LeftButton) { + } + if (me->button() == Qt::MouseButton::LeftButton) + { m_isDragging = true; - } else if (me->button() == Qt::MouseButton::RightButton) { - if (m_sliceSelected != -1 && m_slicePoints.size() > 2) { + } else if (me->button() == Qt::MouseButton::RightButton) + { + if (m_sliceSelected != -1 && m_slicePoints.size() > 2) + { m_slicePoints.erase(m_slicePoints.begin() + m_sliceSelected); m_sliceSelected = -1; } - } + } } @@ -196,27 +218,32 @@ namespace gui void WaveForm::mouseMoveEvent( QMouseEvent * me ) { float normalizedClick = (float)me->x() / m_width; - + // handle dragging events - if (m_isDragging) { - if (m_currentlyDragging == m_draggingTypes::m_seekerStart) { + if (m_isDragging) { + if (m_currentlyDragging == m_draggingTypes::m_seekerStart) + { m_seekerStart = std::clamp(normalizedClick, 0.0f, m_seekerEnd - 0.13f); - } else if (m_currentlyDragging == m_draggingTypes::m_seekerEnd) { + } else if (m_currentlyDragging == m_draggingTypes::m_seekerEnd) + { m_seekerEnd = std::clamp(normalizedClick, m_seekerStart + 0.13f, 1.0f);; - } else if (m_currentlyDragging == m_draggingTypes::m_seekerMiddle) { + } else if (m_currentlyDragging == m_draggingTypes::m_seekerMiddle) + { float distStart = m_seekerStart - m_seekerMiddle; float distEnd = m_seekerEnd - m_seekerMiddle; m_seekerMiddle = normalizedClick; - if (m_seekerMiddle + distStart > 0 && m_seekerMiddle + distEnd < 1) { + if (m_seekerMiddle + distStart > 0 && m_seekerMiddle + distEnd < 1) + { m_seekerStart = m_seekerMiddle + distStart; m_seekerEnd = m_seekerMiddle + distEnd; } - } else if (m_currentlyDragging == m_draggingTypes::slicePoint) { + } else if (m_currentlyDragging == m_draggingTypes::slicePoint) + { float startFrame = m_seekerStart * m_currentSample.frames(); float endFrame = m_seekerEnd * m_currentSample.frames(); @@ -237,8 +264,10 @@ namespace gui float slicePosition = startFrame + normalizedClick * (endFrame - startFrame); - for (int i = 0;i - * + * * This file is part of LMMS - https://lmms.io * * This program is free software; you can redistribute it and/or @@ -68,13 +68,15 @@ class WaveForm : public QWidget { QColor m_seekerColor = QColor(214, 124, 49); QColor m_seekerShadowColor = QColor(0, 0, 0, 175); - enum class m_draggingTypes { + enum class m_draggingTypes + { nothing, m_seekerStart, m_seekerEnd, m_seekerMiddle, slicePoint, }; + m_draggingTypes m_currentlyDragging; bool m_isDragging = false; @@ -89,10 +91,12 @@ class WaveForm : public QWidget { QPixmap m_sliceEditor; QPixmap m_seeker; + QPixmap m_seekerWaveform; SampleBuffer m_currentSample; void drawEditor(); + void drawSeekerWaveform(); void drawSeeker(); void updateUI(); From d94046c0b68cdf77c86ead730f40ece82b6b5a53 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Mon, 11 Sep 2023 00:08:08 +0200 Subject: [PATCH 20/99] initial UI changes + more code style --- plugins/SlicerT/SlicerT.cpp | 63 ++++-- plugins/SlicerT/SlicerTUI.cpp | 21 +- plugins/SlicerT/SlicerTUI.h | 3 + plugins/SlicerT/WaveForm.cpp | 389 ++++++++++++++++++---------------- plugins/SlicerT/WaveForm.h | 3 +- plugins/SlicerT/artwork.png | Bin 0 -> 10943 bytes 6 files changed, 263 insertions(+), 216 deletions(-) create mode 100644 plugins/SlicerT/artwork.png diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index d93be93da3d..0b7cc6ea7e3 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -47,7 +47,7 @@ extern "C" { Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = { - LMMS_STRINGIFY( PLUGINhandleAME ), + LMMS_STRINGIFY( PLUGIN_NAME ), "SlicerT", QT_TRANSLATE_NOOP( "PluginBrowser", "Basic Slicer" ), @@ -70,7 +70,8 @@ SlicerT::SlicerT(InstrumentTrack * instrumentTrack) : {} -void SlicerT::playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) { +void SlicerT::playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) +{ if (m_originalSample.frames() < 2048) { return; } const float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo() ; @@ -106,8 +107,10 @@ void SlicerT::playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) { memcpy(workingBuffer + offset, m_timeShiftedSample.data() + currentNoteFrame, bufferSize); // exponential fade out, applyRelease kinda sucks - if (noteFramesLeft < m_fadeOutFrames.value()) { - for (int i = 0;iprocessAudioBuffer( workingBuffer, frames + offset, handle ); - // !! disabled until it is optimized, because it lags the whole ui // calculate absolute for the waveform float absoluteCurrentNote = (float)currentNoteFrame / totalFrames; float absoluteStartNote = (float)sliceStart / totalFrames; @@ -132,7 +134,8 @@ void SlicerT::playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) { } -void SlicerT::findSlices() { +void SlicerT::findSlices() +{ if (m_originalSample.frames() < 2048) { return; } m_slicePoints = {}; @@ -143,7 +146,8 @@ void SlicerT::findSlices() { float lastPeak = 0; float currentPeak = 0; - for (int i = 0; i currentPeak) @@ -171,7 +175,8 @@ void SlicerT::findSlices() { // find the bpm of the sample by assuming its in 4/4 time signature , // and lies in the 100 - 200 bpm range -void SlicerT::findBPM() { +void SlicerT::findBPM() +{ if (m_originalSample.frames() < 2048) { return; } int bpmSnap = 1; // 1 = disabled @@ -203,7 +208,8 @@ void SlicerT::findBPM() { } // create timeshifted samplebuffer and timeshifted m_slicePoints -void SlicerT::timeShiftSample() { +void SlicerT::timeShiftSample() +{ using std::vector; // initial checks if (m_originalSample.frames() < 2048) { return; } @@ -259,7 +265,8 @@ void SlicerT::timeShiftSample() { // https://sethares.engr.wisc.edu/vocoders/phasevocoder.html // https://dsp.stackexchange.com/questions/40101/audio-time-stretching-without-pitch-shifting/40367#40367 // https://www.guitarpitchshifter.com/ -void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataOut, float sampleRate, float pitchScale) { +void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataOut, float sampleRate, float pitchScale) +{ using std::vector; // processing parameters, lower is faster // lower windows size seems to work better for time scaling, @@ -323,7 +330,8 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO fftwf_execute(fftPlan); // analysis step - for (int j = 0; j < windowSize; j++) { + for (int j = 0; j < windowSize; j++) + { real = FFTSpectrum[j][0]; imag = FFTSpectrum[j][1]; @@ -364,7 +372,8 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO // } // synthesis, all the operations are the reverse of the analysis - for (int j = 0; j < windowSize; j++) { + for (int j = 0; j < windowSize; j++) + { magnitude = allMagnitudes[j]; freq = allFrequencies[j]; @@ -387,7 +396,8 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO fftwf_execute(ifftPlan); // windowing - for (int j = 0; j < windowSize; j++) { + for (int j = 0; j < windowSize; j++) + { float outIndex = i * outStepSize + j; if (outIndex > outFrames) { break; } @@ -425,7 +435,8 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO memcpy(dataOut.data(), outBuffer.data(), outFrames*sizeof(float)); } -int SlicerT::hashFttWindow(std::vector & in) { +int SlicerT::hashFttWindow(std::vector & in) +{ int hash = 0; for (float value : in) { @@ -434,7 +445,8 @@ int SlicerT::hashFttWindow(std::vector & in) { return hash; } -void SlicerT::writeToMidi(std::vector * outClip) { +void SlicerT::writeToMidi(std::vector * outClip) +{ if (m_originalSample.frames() < 2048) { return; } int ticksPerBar = DefaultTicksPerBar; @@ -447,7 +459,8 @@ void SlicerT::writeToMidi(std::vector * outClip) { float barsInSample = beats / Engine::getSong()->getTimeSigModel().getDenominator(); float totalTicks = ticksPerBar * barsInSample; - for (int i = 0;i * outClip) { } } -void SlicerT::updateFile(QString file) { +void SlicerT::updateFile(QString file) +{ m_originalSample.setAudioFile(file); if (m_originalSample.frames() < 2048) { return; } @@ -470,11 +484,13 @@ void SlicerT::updateFile(QString file) { emit dataChanged(); } -void SlicerT::updateSlices() { +void SlicerT::updateSlices() +{ findSlices(); } -void SlicerT::saveSettings(QDomDocument & document, QDomElement & element) { +void SlicerT::saveSettings(QDomDocument & document, QDomElement & element) +{ element.setAttribute("src", m_originalSample.audioFile()); if (m_originalSample.audioFile().isEmpty()) { @@ -484,7 +500,8 @@ void SlicerT::saveSettings(QDomDocument & document, QDomElement & element) { element.setAttribute("totalSlices", (int)m_slicePoints.size()); - for (int i = 0;iignore(); } + +void SlicerTUI::paintEvent(QPaintEvent * pe) +{ + QPainter p( this ); + p.drawPixmap(0, 0, m_backgroundImage); +} + } // namespace gui } // namespace lmms \ No newline at end of file diff --git a/plugins/SlicerT/SlicerTUI.h b/plugins/SlicerT/SlicerTUI.h index 1a0837bffe7..13cf132fd57 100644 --- a/plugins/SlicerT/SlicerTUI.h +++ b/plugins/SlicerT/SlicerTUI.h @@ -61,8 +61,11 @@ protected slots: virtual void dragEnterEvent( QDragEnterEvent * _dee ); virtual void dropEvent( QDropEvent * _de ); + virtual void paintEvent(QPaintEvent * pe); + private: SlicerT * m_slicerTParent; + QPixmap m_backgroundImage; Knob m_noteThresholdKnob; Knob m_fadeOutKnob; diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/WaveForm.cpp index eae9a5e9e80..66c168d947e 100644 --- a/plugins/SlicerT/WaveForm.cpp +++ b/plugins/SlicerT/WaveForm.cpp @@ -32,256 +32,269 @@ namespace lmms namespace gui { - WaveForm::WaveForm(int w, int h, SlicerT * instrument, QWidget * parent) : - QWidget(parent), - m_sliceEditor(QPixmap(w, h*(1 - m_m_seekerRatio) - m_margin)), - m_seeker(QPixmap(w, h*m_m_seekerRatio)), - m_seekerWaveform(QPixmap(w, h*m_m_seekerRatio)), - m_currentSample(instrument->m_originalSample.data(), instrument->m_originalSample.frames()), - m_slicePoints(instrument->m_slicePoints) - { - m_width = w; - m_height = h; - m_slicerTParent = instrument; - setFixedSize(m_width, m_height); - setMouseTracking( true ); +WaveForm::WaveForm(int w, int h, SlicerT * instrument, QWidget * parent) : + QWidget(parent), + m_sliceEditor(QPixmap(w, h*(1 - m_m_seekerRatio) - m_margin)), + m_seeker(QPixmap(w, h*m_m_seekerRatio)), + m_seekerWaveform(QPixmap(w, h*m_m_seekerRatio)), + m_currentSample(instrument->m_originalSample.data(), instrument->m_originalSample.frames()), + m_slicePoints(instrument->m_slicePoints) + { + m_width = w; + m_height = h; + m_slicerTParent = instrument; + setFixedSize(m_width, m_height); + setMouseTracking( true ); - m_sliceEditor.fill(m_waveformBgColor); - m_seekerWaveform.fill(m_waveformBgColor); + m_sliceEditor.fill(m_waveformBgColor); + m_seekerWaveform.fill(m_waveformBgColor); - connect(m_slicerTParent, - SIGNAL(isPlaying(float, float, float)), - this, - SLOT(isPlaying(float, float, float))); + connect(m_slicerTParent, + SIGNAL(isPlaying(float, float, float)), + this, + SLOT(isPlaying(float, float, float))); - connect(m_slicerTParent, SIGNAL(dataChanged()), this, SLOT(updateData())); + connect(m_slicerTParent, SIGNAL(dataChanged()), this, SLOT(updateData())); - updateUI(); - } + updateUI(); + } - void WaveForm::drawEditor() { - m_sliceEditor.fill(m_waveformBgColor); - QPainter brush(&m_sliceEditor); +void WaveForm::drawEditor() +{ + m_sliceEditor.fill(m_waveformBgColor); + QPainter brush(&m_sliceEditor); - float startFrame = m_seekerStart * m_currentSample.frames(); - float endFrame = m_seekerEnd * m_currentSample.frames(); + float startFrame = m_seekerStart * m_currentSample.frames(); + float endFrame = m_seekerEnd * m_currentSample.frames(); - brush.setPen(m_playHighlighColor); - brush.drawLine(0, m_sliceEditor.height()/2, m_sliceEditor.width(), m_sliceEditor.height()/2); + brush.setPen(m_playHighlighColor); + brush.drawLine(0, m_sliceEditor.height()/2, m_sliceEditor.width(), m_sliceEditor.height()/2); - brush.setPen(m_waveformColor); - m_currentSample.visualize( - brush, - QRect( 0, 0, m_sliceEditor.width(), m_sliceEditor.height() ), - startFrame, endFrame); + brush.setPen(m_waveformColor); + m_currentSample.visualize( + brush, + QRect( 0, 0, m_sliceEditor.width(), m_sliceEditor.height() ), + startFrame, endFrame); - for (int i = 0;i= startFrame && sliceIndex <= endFrame) + if (sliceIndex >= startFrame && sliceIndex <= endFrame) + { + float xPos = (float)(sliceIndex - startFrame) / (float)(endFrame - startFrame) * (float)m_width; + if (i == m_sliceSelected) { - float xPos = (float)(sliceIndex - startFrame) / (float)(endFrame - startFrame) * (float)m_width; - if (i == m_sliceSelected) - { - brush.setPen(QPen(m_selectedSliceColor, 2)); - } - - brush.drawLine(xPos, 0, xPos, m_height); + brush.setPen(QPen(m_selectedSliceColor, 2)); } + + brush.drawLine(xPos, 0, xPos, m_height); } } +} + +void WaveForm::drawSeekerWaveform() +{ + QPainter brush(&m_seekerWaveform); + brush.setPen(m_waveformColor); - void WaveForm::drawSeekerWaveform() { - QPainter brush(&m_seekerWaveform); - brush.setPen(m_waveformColor); + m_seekerWaveform.fill(m_waveformBgColor); + m_currentSample.visualize( + brush, + QRect( 0, 0, m_seekerWaveform.width(), m_seekerWaveform.height() ), + 0, m_currentSample.frames()); +} - m_seekerWaveform.fill(m_waveformBgColor); - m_currentSample.visualize( - brush, - QRect( 0, 0, m_seekerWaveform.width(), m_seekerWaveform.height() ), - 0, m_currentSample.frames()); +void WaveForm::drawSeeker() +{ + m_seeker.fill(QColor(0, 0, 0, 0)); + QPainter brush(&m_seeker); + brush.setPen(m_waveformColor); + + // draw slice points + brush.setPen(m_sliceColor); + for (int i = 0;im_originalSample.data(), m_slicerTParent->m_originalSample.frames()); + updateUI(); +} - void WaveForm::updateData() { - m_currentSample = SampleBuffer(m_slicerTParent->m_originalSample.data(), m_slicerTParent->m_originalSample.frames()); - updateUI(); - } +void WaveForm::isPlaying(float current, float start, float end) +{ + m_noteCurrent = current; + m_noteStart = start; + m_noteEnd = end; + drawSeeker(); // only update seeker, else horrible performance + update(); +} + +void WaveForm::mousePressEvent( QMouseEvent * me ) +{ + float normalizedClick = (float)me->x() / m_width; - void WaveForm::isPlaying(float current, float start, float end) { - m_noteCurrent = current; - m_noteStart = start; - m_noteEnd = end; - drawSeeker(); // only update seeker, else horrible performance - update(); + if (me->button() == Qt::MouseButton::MiddleButton) + { + m_seekerStart = 0; + m_seekerEnd = 1; + return; } - void WaveForm::mousePressEvent( QMouseEvent * me ) { - float normalizedClick = (float)me->x() / m_width; - - if (me->button() == Qt::MouseButton::MiddleButton) + if (me->y() < m_height*m_m_seekerRatio) + { + if (abs(normalizedClick - m_seekerStart) < 0.03) { - m_seekerStart = 0; - m_seekerEnd = 1; - return; - } + m_currentlyDragging = m_draggingTypes::m_seekerStart; - if (me->y() < m_height*m_m_seekerRatio) + } else if (abs(normalizedClick - m_seekerEnd) < 0.03) { - if (abs(normalizedClick - m_seekerStart) < 0.03) - { - m_currentlyDragging = m_draggingTypes::m_seekerStart; + m_currentlyDragging = m_draggingTypes::m_seekerEnd; - } else if (abs(normalizedClick - m_seekerEnd) < 0.03) - { - m_currentlyDragging = m_draggingTypes::m_seekerEnd; + } else if (normalizedClick > m_seekerStart && normalizedClick < m_seekerEnd) + { + m_currentlyDragging = m_draggingTypes::m_seekerMiddle; + m_seekerMiddle = normalizedClick; + } - } else if (normalizedClick > m_seekerStart && normalizedClick < m_seekerEnd) - { - m_currentlyDragging = m_draggingTypes::m_seekerMiddle; - m_seekerMiddle = normalizedClick; - } + } else { + m_sliceSelected = -1; + float startFrame = m_seekerStart * m_currentSample.frames(); + float endFrame = m_seekerEnd * m_currentSample.frames(); - } else { - m_sliceSelected = -1; - float startFrame = m_seekerStart * m_currentSample.frames(); - float endFrame = m_seekerEnd * m_currentSample.frames(); + for (int i = 0;ibutton() == Qt::MouseButton::LeftButton) - { - m_isDragging = true; + } + if (me->button() == Qt::MouseButton::LeftButton) + { + m_isDragging = true; - } else if (me->button() == Qt::MouseButton::RightButton) + } else if (me->button() == Qt::MouseButton::RightButton) + { + if (m_sliceSelected != -1 && m_slicePoints.size() > 2) { - if (m_sliceSelected != -1 && m_slicePoints.size() > 2) - { - m_slicePoints.erase(m_slicePoints.begin() + m_sliceSelected); - m_sliceSelected = -1; - } + m_slicePoints.erase(m_slicePoints.begin() + m_sliceSelected); + m_sliceSelected = -1; } - } - void WaveForm::mouseReleaseEvent( QMouseEvent * me ) { - m_isDragging = false; - m_currentlyDragging = m_draggingTypes::nothing; - updateUI(); - } +} - void WaveForm::mouseMoveEvent( QMouseEvent * me ) { - float normalizedClick = (float)me->x() / m_width; +void WaveForm::mouseReleaseEvent( QMouseEvent * me ) +{ + m_isDragging = false; + m_currentlyDragging = m_draggingTypes::nothing; + updateUI(); +} - // handle dragging events - if (m_isDragging) { - if (m_currentlyDragging == m_draggingTypes::m_seekerStart) - { - m_seekerStart = std::clamp(normalizedClick, 0.0f, m_seekerEnd - 0.13f); +void WaveForm::mouseMoveEvent( QMouseEvent * me ) +{ + float normalizedClick = (float)me->x() / m_width; - } else if (m_currentlyDragging == m_draggingTypes::m_seekerEnd) - { - m_seekerEnd = std::clamp(normalizedClick, m_seekerStart + 0.13f, 1.0f);; + // handle dragging events + if (m_isDragging) + { + if (m_currentlyDragging == m_draggingTypes::m_seekerStart) + { + m_seekerStart = std::clamp(normalizedClick, 0.0f, m_seekerEnd - 0.13f); - } else if (m_currentlyDragging == m_draggingTypes::m_seekerMiddle) - { - float distStart = m_seekerStart - m_seekerMiddle; - float distEnd = m_seekerEnd - m_seekerMiddle; + } else if (m_currentlyDragging == m_draggingTypes::m_seekerEnd) + { + m_seekerEnd = std::clamp(normalizedClick, m_seekerStart + 0.13f, 1.0f);; - m_seekerMiddle = normalizedClick; + } else if (m_currentlyDragging == m_draggingTypes::m_seekerMiddle) + { + float distStart = m_seekerStart - m_seekerMiddle; + float distEnd = m_seekerEnd - m_seekerMiddle; - if (m_seekerMiddle + distStart > 0 && m_seekerMiddle + distEnd < 1) - { - m_seekerStart = m_seekerMiddle + distStart; - m_seekerEnd = m_seekerMiddle + distEnd; - } + m_seekerMiddle = normalizedClick; - } else if (m_currentlyDragging == m_draggingTypes::slicePoint) + if (m_seekerMiddle + distStart > 0 && m_seekerMiddle + distEnd < 1) { - float startFrame = m_seekerStart * m_currentSample.frames(); - float endFrame = m_seekerEnd * m_currentSample.frames(); + m_seekerStart = m_seekerMiddle + distStart; + m_seekerEnd = m_seekerMiddle + distEnd; + } + + } else if (m_currentlyDragging == m_draggingTypes::slicePoint) + { + float startFrame = m_seekerStart * m_currentSample.frames(); + float endFrame = m_seekerEnd * m_currentSample.frames(); - m_slicePoints[m_sliceSelected] = startFrame + normalizedClick * (endFrame - startFrame); + m_slicePoints[m_sliceSelected] = startFrame + normalizedClick * (endFrame - startFrame); - m_slicePoints[m_sliceSelected] = std::clamp(m_slicePoints[m_sliceSelected], 0, m_currentSample.frames()); + m_slicePoints[m_sliceSelected] = std::clamp(m_slicePoints[m_sliceSelected], 0, m_currentSample.frames()); - std::sort(m_slicePoints.begin(), m_slicePoints.end()); - } - updateUI(); + std::sort(m_slicePoints.begin(), m_slicePoints.end()); } + updateUI(); } +} - void WaveForm::mouseDoubleClickEvent(QMouseEvent * me) { - float normalizedClick = (float)me->x() / m_width; - float startFrame = m_seekerStart * m_currentSample.frames(); - float endFrame = m_seekerEnd * m_currentSample.frames(); +void WaveForm::mouseDoubleClickEvent(QMouseEvent * me) +{ + float normalizedClick = (float)me->x() / m_width; + float startFrame = m_seekerStart * m_currentSample.frames(); + float endFrame = m_seekerEnd * m_currentSample.frames(); - float slicePosition = startFrame + normalizedClick * (endFrame - startFrame); + float slicePosition = startFrame + normalizedClick * (endFrame - startFrame); - for (int i = 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 literal 0 HcmV?d00001 From 7391d6ddfe3bce3636d55aa405773dd90708f3d8 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Fri, 15 Sep 2023 17:27:51 +0200 Subject: [PATCH 21/99] threadsafe(maybe) + UI finished --- plugins/SlicerT/CopyMidiBtn.png | Bin 0 -> 749 bytes plugins/SlicerT/ResetBtn.png | Bin 0 -> 14573 bytes plugins/SlicerT/SlicerT.cpp | 18 +++++------ plugins/SlicerT/SlicerT.h | 18 +++++++---- plugins/SlicerT/SlicerTUI.cpp | 51 +++++++++++++++++++++----------- plugins/SlicerT/SlicerTUI.h | 25 ++++++++++++---- plugins/SlicerT/WaveForm.cpp | 1 - plugins/SlicerT/WaveForm.h | 2 +- plugins/SlicerT/artwork.png | Bin 10943 -> 11467 bytes 9 files changed, 75 insertions(+), 40 deletions(-) create mode 100644 plugins/SlicerT/CopyMidiBtn.png create mode 100644 plugins/SlicerT/ResetBtn.png diff --git a/plugins/SlicerT/CopyMidiBtn.png b/plugins/SlicerT/CopyMidiBtn.png new file mode 100644 index 0000000000000000000000000000000000000000..8687fb29b44a5a3c213d0eeee6c75d8c8ad3cc92 GIT binary patch literal 749 zcmVEX>4Tx04R}tkv&MmKpe$iQ>7}^4t5Z62w0u$q9Ts93Pq?8YK2xEOfLO`CJjl8 zi=*ILaPVWX>fqw6tAnc`2!4RLx;QDiNQwVT3N2zhIPS;0dyl(!fY2y2&FYE)nqDC`-Nm{=@yu+qV-XllgM#1U1~DPPFA zta9Gstd(o5bx;1nU`}6I<~q$0B(R7jND!f*h7!uCB1)@HiiH&I$36VRj$a~|Laq`R zITlcX2HEk0|H1EWt^Cxan-q)#-7mKNF$M&7fo9#dzmILZc>?&Kfh)c3uQY(!Ptxmc zEph~ewtmpj1FlOdb3Bl&3x`8@D`M&FbL25*7BHMh6cK29HiGeSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{006~FL_t(o!|j(r4ul{KMX7Nyr}S#sH;S&gMCrLi;@J-feW9S3ZbF<@bX8c7#Q?VHQ( zeRVlA0BB{we}>0O)i_!c0Ge5qem>Uv^7qJhkh5YSqFlb*zll$JLA&-@uNt}EY`^v@ f`Tqq{3M1PJL7j|s^ABO~00000NkvXXu0mjf@~%XQ literal 0 HcmV?d00001 diff --git a/plugins/SlicerT/ResetBtn.png b/plugins/SlicerT/ResetBtn.png new file mode 100644 index 0000000000000000000000000000000000000000..0d9713132ae8e4d87411f355efa0c83058eafdb5 GIT binary patch literal 14573 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 literal 0 HcmV?d00001 diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 0b7cc6ea7e3..b88e77a57eb 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -103,8 +103,9 @@ void SlicerT::playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) if( noteFramesLeft > 0) { - int bufferSize = frames * BYTES_PER_FRAME; - memcpy(workingBuffer + offset, m_timeShiftedSample.data() + currentNoteFrame, bufferSize); + // int bufferSize = frames * BYTES_PER_FRAME; + int framesToCopy = std::min((int)frames, noteFramesLeft); + m_timeShiftedSample.copyFrames(workingBuffer + offset, currentNoteFrame, framesToCopy); // exponential fade out, applyRelease kinda sucks if (noteFramesLeft < m_fadeOutFrames.value()) @@ -129,7 +130,7 @@ void SlicerT::playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) float abslouteEndNote = (float)sliceEnd / totalFrames; emit isPlaying(absoluteCurrentNote, absoluteStartNote, abslouteEndNote); } else { - emit isPlaying(0, 0, 0); + emit isPlaying(-1, 0, 0); } } @@ -213,10 +214,7 @@ void SlicerT::timeShiftSample() using std::vector; // initial checks if (m_originalSample.frames() < 2048) { return; } - - if (m_timeshiftLock) { return; } - m_timeshiftLock = true; - + m_timeshiftLock.lock(); // original sample data float sampleRate = m_originalSample.sampleRate(); @@ -230,7 +228,7 @@ void SlicerT::timeShiftSample() if (targetBPM == m_originalBPM.value()) { m_timeShiftedSample.setData(m_originalSample.data(), m_originalSample.frames()); - m_timeshiftLock = false; + m_timeshiftLock.unlock(); return; } @@ -256,7 +254,7 @@ void SlicerT::timeShiftSample() // write processed channels m_timeShiftedSample.setData(outDataL, outDataR); - m_timeshiftLock = false; + m_timeshiftLock.unlock(); } // basic phase vocoder implementation that time shifts without pitch change @@ -399,7 +397,7 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO for (int j = 0; j < windowSize; j++) { float outIndex = i * outStepSize + j; - if (outIndex > outFrames) { break; } + if (outIndex >= outFrames) { break; } // calculate windows overlapping at index float startWindowOverlap = ceil(outIndex / outStepSize + 0.00000001f); diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 9f80575091c..34fb6bd4b46 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -42,19 +42,27 @@ namespace lmms // small helper class, since SampleBuffer is inadequate (not thread safe, no dinamic startpoint) class PlaybackBuffer { public: + QMutex dataLock; std::vector mainBuffer; - int frames() { return mainBuffer.size(); }; - sampleFrame * data() { return mainBuffer.data(); }; - + int frames() { return mainBuffer.size(); }; // not thread safe yet, but shouldnt be a big issue + sampleFrame * data() { return mainBuffer.data(); }; + void copyFrames(sampleFrame * outData, int start, int framesToCopy) { + dataLock.lock(); + memcpy(outData, mainBuffer.data() + start, framesToCopy * sizeof(sampleFrame)); + dataLock.unlock(); + } void setData(const sampleFrame * data, int newFrames) { + dataLock.lock(); mainBuffer = {}; mainBuffer.resize(newFrames); memcpy(mainBuffer.data(), data, newFrames * sizeof(sampleFrame)); + dataLock.unlock(); }; void setData(std::vector & leftData, std::vector & rightData) { + dataLock.lock(); int newFrames = std::min(leftData.size(), rightData.size()); mainBuffer = {}; mainBuffer.resize(newFrames); @@ -64,6 +72,7 @@ class PlaybackBuffer { mainBuffer[i][0] = leftData[i]; mainBuffer[i][1] = rightData[i]; } + dataLock.unlock(); } }; @@ -86,7 +95,6 @@ class SlicerT : public Instrument{ public slots: void updateFile(QString file); - void updateTimeShift(); void updateSlices(); signals: @@ -102,7 +110,7 @@ class SlicerT : public Instrument{ std::vector m_slicePoints; float m_currentSpeedRatio = 0; - bool m_timeshiftLock; // dont run timeshifting at the same time, instant crash + QMutex m_timeshiftLock; // should be unecesaty since playbackBuffer is safe // std::unordered_map > m_fftWindowCache; void findSlices(); diff --git a/plugins/SlicerT/SlicerTUI.cpp b/plugins/SlicerT/SlicerTUI.cpp index dfeb154f4d6..137b764a7aa 100644 --- a/plugins/SlicerT/SlicerTUI.cpp +++ b/plugins/SlicerT/SlicerTUI.cpp @@ -46,43 +46,59 @@ namespace lmms namespace gui { + SlicerTUI::SlicerTUI( SlicerT * instrument, QWidget * parent ) : InstrumentViewFixedSize( instrument, parent ), m_slicerTParent(instrument), - m_backgroundImage(PLUGIN_NAME::getIconPixmap( - "artwork" )), - m_noteThresholdKnob(KnobType::Dark28, this), - m_fadeOutKnob(KnobType::Dark28, this), + m_noteThresholdKnob(this), + m_fadeOutKnob(this), m_bpmBox(3, "19green", this), - m_resetButton(embed::getIconPixmap("reload"), QString(), this), - m_midiExportButton(embed::getIconPixmap("midi_tab"), QString(), this), + m_resetButton(this, nullptr), + m_midiExportButton(this, nullptr), m_wf(244, 125, instrument, this) { setAcceptDrops( true ); + setAutoFillBackground( true ); + + QPalette pal; + pal.setBrush( backgroundRole(), PLUGIN_NAME::getIconPixmap( "artwork" ) ); + pal.setColor(QPalette::All, QPalette::ColorRole::Foreground, Qt::red); + pal.setColor(QPalette::All, QPalette::ColorRole::Button, Qt::red); + pal.setColor(QPalette::All, QPalette::ColorRole::Base, Qt::red); + pal.setColor(QPalette::All, QPalette::ColorRole::Highlight, Qt::red); + setPalette( pal ); m_wf.move(3, 5); - m_bpmBox.move(2, 150); + m_bpmBox.move(7, 153); m_bpmBox.setToolTip(tr("Original sample BPM")); m_bpmBox.setLabel(tr("BPM")); m_bpmBox.setModel(&m_slicerTParent->m_originalBPM); - m_fadeOutKnob.move(200, 150); + m_noteThresholdKnob.move(7, 200); + m_noteThresholdKnob.setToolTip(tr("Threshold used for slicing")); + // m_noteThresholdKnob.setLabel(tr("Threshold")); + m_noteThresholdKnob.setModel(&m_slicerTParent->m_noteThreshold); + + m_fadeOutKnob.move(200, 200); m_fadeOutKnob.setToolTip(tr("FadeOut for notes")); - m_fadeOutKnob.setLabel(tr("FadeOut")); + // m_fadeOutKnob.setLabel(tr("FadeOut")); m_fadeOutKnob.setModel(&m_slicerTParent->m_fadeOutFrames); - m_midiExportButton.move(150, 200); + m_midiExportButton.move(145, 198); + m_midiExportButton.setActiveGraphic( + PLUGIN_NAME::getIconPixmap( "CopyMidiBtn" ) ); + m_midiExportButton.setInactiveGraphic( + PLUGIN_NAME::getIconPixmap( "CopyMidiBtn" ) ); m_midiExportButton.setToolTip(tr("Copy midi pattern to clipboard")); connect(&m_midiExportButton, SIGNAL( clicked() ), this, SLOT( exportMidi() )); - m_noteThresholdKnob.move(7, 200); - m_noteThresholdKnob.setToolTip(tr("Threshold used for slicing")); - m_noteThresholdKnob.setLabel(tr("Threshold")); - m_noteThresholdKnob.setModel(&m_slicerTParent->m_noteThreshold); - - m_resetButton.move(70, 200); + m_resetButton.move(80, 198); + m_resetButton.setActiveGraphic( + PLUGIN_NAME::getIconPixmap( "ResetBtn" ) ); + m_resetButton.setInactiveGraphic( + PLUGIN_NAME::getIconPixmap( "ResetBtn" ) ); m_resetButton.setToolTip(tr("Reset Slices")); connect(&m_resetButton, SIGNAL( clicked() ), m_slicerTParent, SLOT( updateSlices() )); } @@ -97,6 +113,7 @@ void SlicerTUI::exportMidi() dataFile.content().appendChild( note_list ); std::vector notes; + m_slicerTParent->timeShiftSample(); m_slicerTParent->writeToMidi(¬es); if (notes.size() == 0) { @@ -169,8 +186,6 @@ void SlicerTUI::dropEvent( QDropEvent * de ) void SlicerTUI::paintEvent(QPaintEvent * pe) { - QPainter p( this ); - p.drawPixmap(0, 0, m_backgroundImage); } } // namespace gui diff --git a/plugins/SlicerT/SlicerTUI.h b/plugins/SlicerT/SlicerTUI.h index 13cf132fd57..c9be4e2aeeb 100644 --- a/plugins/SlicerT/SlicerTUI.h +++ b/plugins/SlicerT/SlicerTUI.h @@ -32,6 +32,7 @@ #include "Instrument.h" #include "InstrumentView.h" #include "Knob.h" +#include "PixmapButton.h" #include "LcdSpinBox.h" @@ -43,6 +44,21 @@ class SlicerT; namespace gui { +class SlicerTKnob : public Knob { + public: + SlicerTKnob( QWidget * _parent ) : + Knob( KnobType::Styled, _parent ) + { + setFixedSize( 46, 40 ); + setCenterPointX( 23.0 ); + setCenterPointY( 15.0 ); + setInnerRadius( 8 ); + setOuterRadius( 11 ); + // setTotalAngle( 270.0 ); + setLineWidth( 3 ); + setOuterColor( QColor(255, 161, 247) ); + } +}; class SlicerTUI : public InstrumentViewFixedSize { @@ -65,14 +81,13 @@ protected slots: private: SlicerT * m_slicerTParent; - QPixmap m_backgroundImage; - Knob m_noteThresholdKnob; - Knob m_fadeOutKnob; + SlicerTKnob m_noteThresholdKnob; + SlicerTKnob m_fadeOutKnob; LcdSpinBox m_bpmBox; - QPushButton m_resetButton; - QPushButton m_midiExportButton; + PixmapButton m_resetButton; + PixmapButton m_midiExportButton; WaveForm m_wf; }; diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/WaveForm.cpp index 66c168d947e..b6749776d00 100644 --- a/plugins/SlicerT/WaveForm.cpp +++ b/plugins/SlicerT/WaveForm.cpp @@ -294,7 +294,6 @@ void WaveForm::paintEvent( QPaintEvent * pe) p.drawPixmap(0, 0 ,m_seekerWaveform); p.drawPixmap(0, 0, m_seeker); p.drawPixmap(0, m_height*0.3f + m_margin, m_sliceEditor); - } } // namespace gui } // namespace lmms \ No newline at end of file diff --git a/plugins/SlicerT/WaveForm.h b/plugins/SlicerT/WaveForm.h index 114655f1a67..6b78989b049 100644 --- a/plugins/SlicerT/WaveForm.h +++ b/plugins/SlicerT/WaveForm.h @@ -59,7 +59,7 @@ class WaveForm : public QWidget { int m_height; float m_m_seekerRatio = 0.3f; int m_margin = 5; - QColor m_waveformBgColor = QColor(11, 11, 11); + QColor m_waveformBgColor = QColor(11, 11, 11, 200); QColor m_waveformColor = QColor(113, 0, 177); // QColor m_waveformColor = QColor(255, 161, 247); // logo color QColor m_playColor = QColor(255, 255, 255, 200); diff --git a/plugins/SlicerT/artwork.png b/plugins/SlicerT/artwork.png index a617f3c6ab3dd7a00de261fce9438cb4fd91130d..d3a44cc9e5ae9720cdb65a2168a485b82e4d667b 100644 GIT binary patch delta 11425 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 O002n`MNUMnLSTYnPGSrI From c9ac279ab33607fd1e84979b78a41b3a37d61947 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sat, 16 Sep 2023 20:52:38 +0200 Subject: [PATCH 22/99] preparing for dinamic timeshifting --- plugins/SlicerT/SlicerT.cpp | 97 ++++++++++++++++++------------------- plugins/SlicerT/SlicerT.h | 32 +++++++++++- 2 files changed, 77 insertions(+), 52 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index b88e77a57eb..333025dfac1 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -22,9 +22,8 @@ * */ -// TODO: fade in mode -// TODO: general UI improvements, maybe open folders button -// TODO: add fft cache, maybe this is overkill but whatever +// write in frames to outputBuffer +// store the stuff that has to be reused in a fullsized buffer #include "SlicerT.h" @@ -66,15 +65,39 @@ SlicerT::SlicerT(InstrumentTrack * instrumentTrack) : m_noteThreshold(0.6f, 0.0f, 2.0f, 0.01f, this, tr( "Note threshold" ) ), m_fadeOutFrames(0.0f, 0.0f, 8192.0f, 4.0f, this, tr("FadeOut")), m_originalBPM(1, 1, 999, this, tr("Original bpm")), - m_originalSample() + m_originalSample(), + + FFTInput(windowSize, 0), + IFFTReconstruction(windowSize, 0), + allMagnitudes(windowSize, 0), + allFrequencies(windowSize, 0), + processedFreq(windowSize, 0), + processedMagn(windowSize, 0) {} +void SlicerT::updateParams(float newRatio) { + stepSize = (float)windowSize / overSampling; + numWindows = (float)m_originalSample.frames() / stepSize; + outStepSize = newRatio * (float)stepSize; // float, else inaccurate + freqPerBin = m_originalSample.sampleRate()/windowSize; + expectedPhaseIn = 2.*M_PI*(float)stepSize/(float)windowSize; + expectedPhaseOut = 2.*M_PI*(float)outStepSize/(float)windowSize; + + // resize all the buffers that rely on the final size + m_timeshiftedBufferL = {}; + m_timeshiftedBufferL.resize(newRatio*m_originalSample.frames()); + lastPhase = {}; + lastPhase.resize(numWindows*windowSize); + sumPhase = {}; + sumPhase.resize(numWindows*windowSize); +} void SlicerT::playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) { if (m_originalSample.frames() < 2048) { return; } const float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo() ; + // const float lengthRatio = 1.0f / speedRatio; // inverse, because longer is slower const int noteIndex = handle->key() - 69; const fpp_t frames = handle->framesLeftForCurrentPeriod(); const f_cnt_t offset = handle->noteOffset(); @@ -82,6 +105,7 @@ void SlicerT::playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) if (m_currentSpeedRatio != speedRatio) { + updateParams(speedRatio); timeShiftSample(); } const int totalFrames = m_timeShiftedSample.frames(); @@ -248,8 +272,8 @@ void SlicerT::timeShiftSample() } // process channels - phaseVocoder(rawDataL, outDataL, sampleRate, 1); - phaseVocoder(rawDataR, outDataR, sampleRate, 1); + phaseVocoder(rawDataL, outDataL); + phaseVocoder(rawDataR, outDataR); // write processed channels m_timeShiftedSample.setData(outDataL, outDataR); @@ -263,50 +287,18 @@ void SlicerT::timeShiftSample() // https://sethares.engr.wisc.edu/vocoders/phasevocoder.html // https://dsp.stackexchange.com/questions/40101/audio-time-stretching-without-pitch-shifting/40367#40367 // https://www.guitarpitchshifter.com/ -void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataOut, float sampleRate, float pitchScale) +void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataOut) { - using std::vector; - // processing parameters, lower is faster - // lower windows size seems to work better for time scaling, - // this is because the step site is scaled, but not the window size - // this causes slight timing differences between windows - // sadly, lower windowsize also reduces audio quality in general - // TODO: find solution - // oversampling is better if higher always (probably) - const int windowSize = 512; - const int overSampling = 32; - - // audio data - int inFrames = dataIn.size(); + memset(lastPhase.data(), 0, numWindows*windowSize*sizeof(float)); + memset(sumPhase.data(), 0, numWindows*windowSize*sizeof(float)); + int outFrames = dataOut.size(); - float lengthRatio = (float)outFrames / inFrames; - - // values used - const int stepSize = (float)windowSize / overSampling; - const int numWindows = (float)inFrames / stepSize; - const float outStepSize = lengthRatio * (float)stepSize; // float, else inaccurate - const float freqPerBin = sampleRate/windowSize; - // very important - const float expectedPhaseIn = 2.*M_PI*(float)stepSize/(float)windowSize; - const float expectedPhaseOut = 2.*M_PI*(float)outStepSize/(float)windowSize; - - // initialize buffers - fftwf_complex FFTSpectrum[windowSize]; - vector FFTInput(windowSize, 0); - vector IFFTReconstruction(windowSize, 0); - vector allMagnitudes(windowSize, 0); - vector allFrequencies(windowSize, 0); - vector processedFreq(windowSize, 0); - vector processedMagn(windowSize, 0); - vector lastPhase(windowSize, 0); - vector sumPhase(windowSize, 0); - - vector outBuffer(outFrames, 0); + std::vector outBuffer(outFrames, 0); // declare vars float real, imag, phase, magnitude, freq, deltaPhase = 0; - int windowIndex = 0; + int windowIndex = 0; // fft plans fftwf_plan fftPlan; @@ -322,7 +314,7 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO memcpy(FFTInput.data(), dataIn.data() + windowIndex, windowSize*sizeof(float)); // int hash = hashFttWindow(FFTInput); - // printf("%i\n", hash); + // printf("%i\n", hash); // FFT fftwf_execute(fftPlan); @@ -330,14 +322,16 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO // analysis step for (int j = 0; j < windowSize; j++) { + int windowIndex = (float)i*windowSize; + real = FFTSpectrum[j][0]; imag = FFTSpectrum[j][1]; magnitude = 2.*sqrt(real*real + imag*imag); phase = atan2(imag,real); - freq = phase - lastPhase[j]; // subtract prev pahse to get phase diference - lastPhase[j] = phase; + freq = phase - lastPhase[std::max(0, windowIndex + j - windowSize)]; // subtract prev pahse to get phase diference + lastPhase[windowIndex + j] = phase; freq -= (float)j*expectedPhaseIn; // subtract expected phase @@ -372,6 +366,8 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO // synthesis, all the operations are the reverse of the analysis for (int j = 0; j < windowSize; j++) { + int windowIndex = (float)i*windowSize; + magnitude = allMagnitudes[j]; freq = allFrequencies[j]; @@ -383,8 +379,9 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO deltaPhase += (float)j*expectedPhaseOut; - sumPhase[j] += deltaPhase; - deltaPhase = sumPhase[j]; // this is the bin phase + sumPhase[windowIndex + j] += deltaPhase; + deltaPhase = sumPhase[windowIndex + j]; // this is the bin phase + sumPhase[windowIndex + j + windowSize] = deltaPhase; // copy into the next window for accurate sum FFTSpectrum[j][0] = magnitude*cos(deltaPhase); FFTSpectrum[j][1] = magnitude*sin(deltaPhase); @@ -430,7 +427,7 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO outBuffer[i] = outBuffer[i] / max; } - memcpy(dataOut.data(), outBuffer.data(), outFrames*sizeof(float)); +memcpy(dataOut.data(), outBuffer.data(), outFrames*sizeof(float)); } int SlicerT::hashFttWindow(std::vector & in) diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 34fb6bd4b46..e064bc5dde4 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -106,19 +106,47 @@ class SlicerT : public Instrument{ IntModel m_originalBPM; SampleBuffer m_originalSample; + PlaybackBuffer m_timeShiftedSample; + std::vector m_timeshiftedBufferL; + std::vector m_timeshiftedBufferR; + + std::vector m_slicePoints; - float m_currentSpeedRatio = 0; + float m_currentSpeedRatio = -1; QMutex m_timeshiftLock; // should be unecesaty since playbackBuffer is safe // std::unordered_map > m_fftWindowCache; + void updateParams(float newRatio); void findSlices(); void findBPM(); void timeShiftSample(); - void phaseVocoder(std::vector &in, std::vector &out, float sampleRate, float pitchScale); + void phaseVocoder(std::vector &in, std::vector &out); int hashFttWindow(std::vector & in); + // timeshift stuff + static const int windowSize = 512; + static const int overSampling = 32; + + int stepSize = 0; + int numWindows = 0; + float outStepSize = 0; + float freqPerBin = 0; + // very important + float expectedPhaseIn = 0; + float expectedPhaseOut = 0; + + fftwf_complex FFTSpectrum[windowSize]; + std::vector FFTInput; + std::vector IFFTReconstruction; + std::vector allMagnitudes; + std::vector allFrequencies; + std::vector processedFreq; + std::vector processedMagn; + std::vector lastPhase; + std::vector sumPhase; + friend class gui::SlicerTUI; friend class gui::WaveForm; }; From 1a09f5f1dff040931c553801b5d0f9008d9c07bb Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 17 Sep 2023 13:14:23 +0200 Subject: [PATCH 23/99] dynamic timeshifting start --- plugins/SlicerT/SlicerT.cpp | 158 ++++++++++++++++++---------------- plugins/SlicerT/SlicerT.h | 38 ++++++-- plugins/SlicerT/SlicerTUI.cpp | 2 +- 3 files changed, 118 insertions(+), 80 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 333025dfac1..6aa7b36e595 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -22,8 +22,9 @@ * */ -// write in frames to outputBuffer -// store the stuff that has to be reused in a fullsized buffer +// TODO: deinterlace only once, always work with seperated buffers +// TODO: start working from the start of the note +// TODO: create a seperate class for phaseVocoder, that handles all the sampleData #include "SlicerT.h" @@ -73,19 +74,31 @@ SlicerT::SlicerT(InstrumentTrack * instrumentTrack) : allFrequencies(windowSize, 0), processedFreq(windowSize, 0), processedMagn(windowSize, 0) -{} +{ + fftPlan = fftwf_plan_dft_r2c_1d(windowSize, FFTInput.data(), FFTSpectrum, FFTW_MEASURE); + ifftPlan = fftwf_plan_dft_c2r_1d(windowSize, FFTSpectrum, IFFTReconstruction.data(), FFTW_MEASURE); + +} + +void SlicerT::updateParams() { + float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo() ; -void SlicerT::updateParams(float newRatio) { stepSize = (float)windowSize / overSampling; numWindows = (float)m_originalSample.frames() / stepSize; - outStepSize = newRatio * (float)stepSize; // float, else inaccurate + outStepSize = speedRatio * (float)stepSize; // float, else inaccurate freqPerBin = m_originalSample.sampleRate()/windowSize; expectedPhaseIn = 2.*M_PI*(float)stepSize/(float)windowSize; expectedPhaseOut = 2.*M_PI*(float)outStepSize/(float)windowSize; // resize all the buffers that rely on the final size + // is there a better way for this? probably m_timeshiftedBufferL = {}; - m_timeshiftedBufferL.resize(newRatio*m_originalSample.frames()); + m_timeshiftedBufferL.resize(speedRatio*m_originalSample.frames()); + m_processedFrames = {}; + m_processedFrames.resize(numWindows); + for (bool b : m_processedFrames) { + b = false; + } lastPhase = {}; lastPhase.resize(numWindows*windowSize); sumPhase = {}; @@ -105,10 +118,11 @@ void SlicerT::playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) if (m_currentSpeedRatio != speedRatio) { - updateParams(speedRatio); - timeShiftSample(); + updateParams(); + // timeShiftSample(16); + m_currentSpeedRatio = speedRatio; } - const int totalFrames = m_timeShiftedSample.frames(); + const int totalFrames = m_timeshiftedBufferL.size(); int sliceStart, sliceEnd; if (noteIndex > m_slicePoints.size()-2 || noteIndex < 0) @@ -127,9 +141,23 @@ void SlicerT::playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) if( noteFramesLeft > 0) { + std::vector originalBuffer(m_originalSample.frames()); + for (int i = 0;igetTempo(); - m_currentSpeedRatio = (float)m_originalBPM.value() / targetBPM ; - int outFrames = m_currentSpeedRatio * originalFrames; - - // nothing to do here - if (targetBPM == m_originalBPM.value()) - { - m_timeShiftedSample.setData(m_originalSample.data(), m_originalSample.frames()); - m_timeshiftLock.unlock(); - return; - } - // buffers - vector rawDataL(originalFrames, 0); - vector rawDataR(originalFrames, 0); - vector outDataL(outFrames, 0); - vector outDataR(outFrames, 0); - - vector bufferData(outFrames, sampleFrame()); + std::vector rawDataL(originalFrames, 0); + std::vector rawDataR(originalFrames, 0); // copy channels for processing for (int i = 0;i= windowsToProcess) { break; } + } m_timeshiftLock.unlock(); } @@ -287,31 +303,28 @@ void SlicerT::timeShiftSample() // https://sethares.engr.wisc.edu/vocoders/phasevocoder.html // https://dsp.stackexchange.com/questions/40101/audio-time-stretching-without-pitch-shifting/40367#40367 // https://www.guitarpitchshifter.com/ -void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataOut) +void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataOut, int start, int steps) { - memset(lastPhase.data(), 0, numWindows*windowSize*sizeof(float)); - memset(sumPhase.data(), 0, numWindows*windowSize*sizeof(float)); - - int outFrames = dataOut.size(); + // memset(lastPhase.data(), 0, numWindows*windowSize*sizeof(float)); + // memset(sumPhase.data(), 0, numWindows*windowSize*sizeof(float)); - std::vector outBuffer(outFrames, 0); // declare vars float real, imag, phase, magnitude, freq, deltaPhase = 0; - int windowIndex = 0; + int windowStart = 0; // fft plans - fftwf_plan fftPlan; - fftPlan = fftwf_plan_dft_r2c_1d(windowSize, FFTInput.data(), FFTSpectrum, FFTW_MEASURE); - fftwf_plan ifftPlan; - ifftPlan = fftwf_plan_dft_c2r_1d(windowSize, FFTSpectrum, IFFTReconstruction.data(), FFTW_MEASURE); + // fftwf_plan fftPlan; + // fftPlan = fftwf_plan_dft_r2c_1d(windowSize, FFTInput.data(), FFTSpectrum, FFTW_MEASURE); + // fftwf_plan ifftPlan; + // ifftPlan = fftwf_plan_dft_c2r_1d(windowSize, FFTSpectrum, IFFTReconstruction.data(), FFTW_MEASURE); // remove oversampling, because the actual window is overSampling* bigger than stepsize - for (int i = 0;i < numWindows-overSampling;i++) + for (int i = start;i < start + steps;i++) { - windowIndex = i * stepSize; - - memcpy(FFTInput.data(), dataIn.data() + windowIndex, windowSize*sizeof(float)); + printf("%i\n", i); + windowStart = i * stepSize; + memcpy(FFTInput.data(), dataIn.data() + windowStart, windowSize*sizeof(float)); // int hash = hashFttWindow(FFTInput); // printf("%i\n", hash); @@ -394,40 +407,37 @@ void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataO for (int j = 0; j < windowSize; j++) { float outIndex = i * outStepSize + j; - if (outIndex >= outFrames) { break; } + if (outIndex >= dataOut.size()) { break; } // calculate windows overlapping at index float startWindowOverlap = ceil(outIndex / outStepSize + 0.00000001f); - float endWindowOverlap = ceil((float)(-outIndex + outFrames) / outStepSize + 0.00000001f); + float endWindowOverlap = ceil((float)(-outIndex + dataOut.size()) / outStepSize + 0.00000001f); float totalWindowOverlap = std::min( std::min(startWindowOverlap, endWindowOverlap), (float)overSampling); // discrete windowing - outBuffer[outIndex] += (float)overSampling/totalWindowOverlap*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); - + dataOut[outIndex] += (float)overSampling/totalWindowOverlap*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); + // printf("timeshifted in phase: %f\n", m_timeshiftedBufferL[outIndex]); // continuos windowing // float window = -0.5f*cos(2.*M_PI*(float)j/(float)windowSize)+0.5f; // outBuffer[outIndex] += 2.0f*window*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); } } - fftwf_destroy_plan(fftPlan); - fftwf_destroy_plan(ifftPlan); - // normalize - float max = -1; - for (int i = 0;i & in) @@ -467,6 +477,10 @@ void SlicerT::writeToMidi(std::vector * outClip) } } +void SlicerT::extractOriginalData() { + +} + void SlicerT::updateFile(QString file) { m_originalSample.setAudioFile(file); @@ -474,7 +488,7 @@ void SlicerT::updateFile(QString file) findSlices(); findBPM(); - timeShiftSample(); + updateParams(); emit dataChanged(); } @@ -537,7 +551,7 @@ void SlicerT::loadSettings( const QDomElement & element ) m_noteThreshold.loadSettings(element, "threshold"); m_originalBPM.loadSettings(element, "origBPM"); - timeShiftSample(); + updateParams(); emit dataChanged(); diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index e064bc5dde4..6838c4eeeba 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -44,6 +44,9 @@ class PlaybackBuffer { public: QMutex dataLock; std::vector mainBuffer; + std::vector leftBuffer; + std::vector rightBuffer; + float sampleMax = -1; int frames() { return mainBuffer.size(); }; // not thread safe yet, but shouldnt be a big issue sampleFrame * data() { return mainBuffer.data(); }; @@ -52,15 +55,28 @@ class PlaybackBuffer { memcpy(outData, mainBuffer.data() + start, framesToCopy * sizeof(sampleFrame)); dataLock.unlock(); } - void setData(const sampleFrame * data, int newFrames) - { - dataLock.lock(); + void resetAndResize(int newFrames) { mainBuffer = {}; mainBuffer.resize(newFrames); + leftBuffer = {}; + leftBuffer.resize(newFrames); + rightBuffer = {}; + rightBuffer.resize(newFrames); + } + void loadSample(const sampleFrame * data, int newFrames) + { + dataLock.lock(); + resetAndResize(newFrames); memcpy(mainBuffer.data(), data, newFrames * sizeof(sampleFrame)); + for (int i = 0;i & leftData, std::vector & rightData) + void setFrames(std::vector & leftData, std::vector & rightData) { dataLock.lock(); int newFrames = std::min(leftData.size(), rightData.size()); @@ -106,10 +122,14 @@ class SlicerT : public Instrument{ IntModel m_originalBPM; SampleBuffer m_originalSample; + std::vector m_originalBufferL; + std::vector m_originalBufferR; + int originalMax; // for later rescaling PlaybackBuffer m_timeShiftedSample; std::vector m_timeshiftedBufferL; std::vector m_timeshiftedBufferR; + std::vector m_processedFrames; // check if a frame is processed std::vector m_slicePoints; @@ -118,11 +138,12 @@ class SlicerT : public Instrument{ QMutex m_timeshiftLock; // should be unecesaty since playbackBuffer is safe // std::unordered_map > m_fftWindowCache; - void updateParams(float newRatio); + void updateParams(); + void extractOriginalData(); void findSlices(); void findBPM(); - void timeShiftSample(); - void phaseVocoder(std::vector &in, std::vector &out); + void timeShiftSample(int windowsToProcess); + void phaseVocoder(std::vector &in, std::vector &out, int start, int steps); int hashFttWindow(std::vector & in); // timeshift stuff @@ -147,6 +168,9 @@ class SlicerT : public Instrument{ std::vector lastPhase; std::vector sumPhase; + fftwf_plan fftPlan; + fftwf_plan ifftPlan; + friend class gui::SlicerTUI; friend class gui::WaveForm; }; diff --git a/plugins/SlicerT/SlicerTUI.cpp b/plugins/SlicerT/SlicerTUI.cpp index 137b764a7aa..fe3d0da0f8d 100644 --- a/plugins/SlicerT/SlicerTUI.cpp +++ b/plugins/SlicerT/SlicerTUI.cpp @@ -113,7 +113,7 @@ void SlicerTUI::exportMidi() dataFile.content().appendChild( note_list ); std::vector notes; - m_slicerTParent->timeShiftSample(); + m_slicerTParent->timeShiftSample(-1); m_slicerTParent->writeToMidi(¬es); if (notes.size() == 0) { From 382f73b388950a7f6b12d2e25240004185657852 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 17 Sep 2023 21:03:27 +0200 Subject: [PATCH 24/99] realtime time scaling (no stereo) --- plugins/SlicerT/SlicerT.cpp | 475 ++++++++++++++++------------------ plugins/SlicerT/SlicerT.h | 140 ++++------ plugins/SlicerT/SlicerTUI.cpp | 1 - plugins/SlicerT/WaveForm.cpp | 5 +- plugins/SlicerT/WaveForm.h | 2 +- 5 files changed, 282 insertions(+), 341 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 6aa7b36e595..3a237c2c3a1 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -22,9 +22,8 @@ * */ -// TODO: deinterlace only once, always work with seperated buffers -// TODO: start working from the start of the note -// TODO: create a seperate class for phaseVocoder, that handles all the sampleData +// TODO: rewrite PhaseVocoder for only one channel, interlace in playNote +// maybe cleaner, maybe not #include "SlicerT.h" @@ -61,13 +60,7 @@ Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = } // end extern -SlicerT::SlicerT(InstrumentTrack * instrumentTrack) : - Instrument( instrumentTrack, &slicert_plugin_descriptor ), - m_noteThreshold(0.6f, 0.0f, 2.0f, 0.01f, this, tr( "Note threshold" ) ), - m_fadeOutFrames(0.0f, 0.0f, 8192.0f, 4.0f, this, tr("FadeOut")), - m_originalBPM(1, 1, 999, this, tr("Original bpm")), - m_originalSample(), - +PhaseVocoder::PhaseVocoder() : FFTInput(windowSize, 0), IFFTReconstruction(windowSize, 0), allMagnitudes(windowSize, 0), @@ -77,52 +70,240 @@ SlicerT::SlicerT(InstrumentTrack * instrumentTrack) : { fftPlan = fftwf_plan_dft_r2c_1d(windowSize, FFTInput.data(), FFTSpectrum, FFTW_MEASURE); ifftPlan = fftwf_plan_dft_c2r_1d(windowSize, FFTSpectrum, IFFTReconstruction.data(), FFTW_MEASURE); + // loadSample(originalData, frames, sampleRate); +} +PhaseVocoder::~PhaseVocoder() { + fftwf_destroy_plan(fftPlan); + fftwf_destroy_plan(ifftPlan); } -void SlicerT::updateParams() { - float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo() ; +void PhaseVocoder::loadSample(const sampleFrame * originalData, int frames, int sampleRate, float newRatio) { + originalBufferL.resize(frames); + originalBufferR.resize(frames); + + for (int i = 0;i &dataIn, std::vector &dataOut, int start) +{ + // declare vars + float real, imag, phase, magnitude, freq, deltaPhase = 0; + int windowStart = (float)start*stepSize; + int windowIndex = (float)start*windowSize; + + memcpy(FFTInput.data(), dataIn.data() + windowStart, windowSize*sizeof(float)); + + // FFT + fftwf_execute(fftPlan); + + // analysis step + for (int j = 0; j < windowSize; j++) + { + real = FFTSpectrum[j][0]; + imag = FFTSpectrum[j][1]; + + magnitude = 2.*sqrt(real*real + imag*imag); + phase = atan2(imag,real); + + freq = phase; + freq = phase - lastPhase[std::max(0, windowIndex + j - windowSize)]; // subtract prev pahse to get phase diference + lastPhase[windowIndex + j] = phase; + + freq -= (float)j*expectedPhaseIn; // subtract expected phase + + // some black magic to get into +/- PI interval, revise later pls + long qpd = freq/M_PI; + if (qpd >= 0) qpd += qpd&1; + else qpd -= qpd&1; + freq -= M_PI*(float)qpd; + + freq = (float)overSampling*freq/(2.*M_PI); // idk + + freq = (float)j*freqPerBin + freq*freqPerBin; // "compute the k-th partials' true frequency" ok i guess + + allMagnitudes[j] = magnitude; + allFrequencies[j] = freq; + } + + + // pitch shifting + // takes all the values that are below the nyquist frequency (representable with our samplerate) + // nyquist frequency = samplerate / 2 + // and moves them to a different bin + // improve for larger pitch shift + // memset(processedFreq.data(), 0, processedFreq.size()*sizeof(float)); + // memset(processedMagn.data(), 0, processedFreq.size()*sizeof(float)); + // for (int j = 0; j < windowSize/2; j++) { + // int index = (float)j;// * m_noteThreshold.value(); + // if (index <= windowSize/2) { + // processedMagn[index] += allMagnitudes[j]; + // processedFreq[index] = allFrequencies[j];// * m_noteThreshold.value(); + // } + // } + + // synthesis, all the operations are the reverse of the analysis + for (int j = 0; j < windowSize; j++) + { + magnitude = allMagnitudes[j]; + freq = allFrequencies[j]; + + deltaPhase = freq - (float)j*freqPerBin; + + deltaPhase /= freqPerBin; + + deltaPhase = 2.*M_PI*deltaPhase/overSampling;; + + deltaPhase += (float)j*expectedPhaseOut; + + sumPhase[windowIndex + j] += deltaPhase; + deltaPhase = sumPhase[windowIndex + j]; // this is the bin phase + if (windowIndex + j + windowSize < sumPhase.size()) { // only if not last window + sumPhase[windowIndex + j + windowSize] = deltaPhase; // copy to the next + } + + FFTSpectrum[j][0] = magnitude*cos(deltaPhase); + FFTSpectrum[j][1] = magnitude*sin(deltaPhase); + } + + // inverse fft + fftwf_execute(ifftPlan); + + // windowing + for (int j = 0; j < windowSize; j++) + { + float outIndex = start * outStepSize + j; + if (outIndex >= dataOut.size()) { break; } + + // calculate windows overlapping at index + // float startWindowOverlap = ceil(outIndex / outStepSize + 0.00000001f); + // float endWindowOverlap = ceil((float)(-outIndex + dataOut.size()) / outStepSize + 0.00000001f); + // float totalWindowOverlap = std::min( + // std::min(startWindowOverlap, endWindowOverlap), + // (float)overSampling); + + // discrete windowing + // dataOut[outIndex] += (float)overSampling/totalWindowOverlap*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); + // printf("timeshifted in phase: %f\n", m_timeshiftedBufferL[outIndex]); + // continuos windowing + float window = -0.5f*cos(2.*M_PI*(float)j/(float)windowSize)+0.5f; + dataOut[outIndex] += 2.0f*window*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); + } + +} + +int PhaseVocoder::hashFttWindow(std::vector & in) +{ + int hash = 0; + for (float value : in) + { + hash += (324723947 + (int)(value * 10689354)) ^ 93485734985;; + } + return hash; +} + +// ################################# SlicerT #################################### + +SlicerT::SlicerT(InstrumentTrack * instrumentTrack) : + Instrument( instrumentTrack, &slicert_plugin_descriptor ), + m_noteThreshold(0.6f, 0.0f, 2.0f, 0.01f, this, tr( "Note threshold" ) ), + m_fadeOutFrames(0.0f, 0.0f, 8192.0f, 4.0f, this, tr("FadeOut")), + m_originalBPM(1, 1, 999, this, tr("Original bpm")), + m_originalSample(), + m_phaseVocoder() +{ } + + void SlicerT::playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) { if (m_originalSample.frames() < 2048) { return; } const float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo() ; - // const float lengthRatio = 1.0f / speedRatio; // inverse, because longer is slower + const int noteIndex = handle->key() - 69; const fpp_t frames = handle->framesLeftForCurrentPeriod(); const f_cnt_t offset = handle->noteOffset(); const int playedFrames = handle->totalFramesPlayed(); + m_phaseVocoder.setScaleRatio(speedRatio); - if (m_currentSpeedRatio != speedRatio) - { - updateParams(); - // timeShiftSample(16); - m_currentSpeedRatio = speedRatio; - } - const int totalFrames = m_timeshiftedBufferL.size(); + const int totalFrames = m_phaseVocoder.frames(); int sliceStart, sliceEnd; if (noteIndex > m_slicePoints.size()-2 || noteIndex < 0) @@ -141,23 +322,8 @@ void SlicerT::playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) if( noteFramesLeft > 0) { - std::vector originalBuffer(m_originalSample.frames()); - for (int i = 0;i rawDataL(originalFrames, 0); - std::vector rawDataR(originalFrames, 0); - - // copy channels for processing - for (int i = 0;i= windowsToProcess) { break; } - } - - m_timeshiftLock.unlock(); -} - -// basic phase vocoder implementation that time shifts without pitch change -// resources: -// http://blogs.zynaptiq.com/bernsee/pitch-shifting-using-the-ft/ -// https://sethares.engr.wisc.edu/vocoders/phasevocoder.html -// https://dsp.stackexchange.com/questions/40101/audio-time-stretching-without-pitch-shifting/40367#40367 -// https://www.guitarpitchshifter.com/ -void SlicerT::phaseVocoder(std::vector &dataIn, std::vector &dataOut, int start, int steps) -{ - // memset(lastPhase.data(), 0, numWindows*windowSize*sizeof(float)); - // memset(sumPhase.data(), 0, numWindows*windowSize*sizeof(float)); - - - // declare vars - float real, imag, phase, magnitude, freq, deltaPhase = 0; - int windowStart = 0; - - // fft plans - // fftwf_plan fftPlan; - // fftPlan = fftwf_plan_dft_r2c_1d(windowSize, FFTInput.data(), FFTSpectrum, FFTW_MEASURE); - // fftwf_plan ifftPlan; - // ifftPlan = fftwf_plan_dft_c2r_1d(windowSize, FFTSpectrum, IFFTReconstruction.data(), FFTW_MEASURE); - - // remove oversampling, because the actual window is overSampling* bigger than stepsize - for (int i = start;i < start + steps;i++) - { - printf("%i\n", i); - windowStart = i * stepSize; - memcpy(FFTInput.data(), dataIn.data() + windowStart, windowSize*sizeof(float)); - - // int hash = hashFttWindow(FFTInput); - // printf("%i\n", hash); - - // FFT - fftwf_execute(fftPlan); - - // analysis step - for (int j = 0; j < windowSize; j++) - { - int windowIndex = (float)i*windowSize; - - real = FFTSpectrum[j][0]; - imag = FFTSpectrum[j][1]; - - magnitude = 2.*sqrt(real*real + imag*imag); - phase = atan2(imag,real); - - freq = phase - lastPhase[std::max(0, windowIndex + j - windowSize)]; // subtract prev pahse to get phase diference - lastPhase[windowIndex + j] = phase; - - freq -= (float)j*expectedPhaseIn; // subtract expected phase - - // some black magic to get into +/- PI interval, revise later pls - long qpd = freq/M_PI; - if (qpd >= 0) qpd += qpd&1; - else qpd -= qpd&1; - freq -= M_PI*(float)qpd; - - freq = (float)overSampling*freq/(2.*M_PI); // idk - - freq = (float)j*freqPerBin + freq*freqPerBin; // "compute the k-th partials' true frequency" ok i guess - - allMagnitudes[j] = magnitude; - allFrequencies[j] = freq; - } - // pitch shifting - // takes all the values that are below the nyquist frequency (representable with our samplerate) - // nyquist frequency = samplerate / 2 - // and moves them to a different bin - // improve for larger pitch shift - // memset(processedFreq.data(), 0, processedFreq.size()*sizeof(float)); - // memset(processedMagn.data(), 0, processedFreq.size()*sizeof(float)); - // for (int j = 0; j < windowSize/2; j++) { - // int index = (float)j;// * m_noteThreshold.value(); - // if (index <= windowSize/2) { - // processedMagn[index] += allMagnitudes[j]; - // processedFreq[index] = allFrequencies[j];// * m_noteThreshold.value(); - // } - // } - - // synthesis, all the operations are the reverse of the analysis - for (int j = 0; j < windowSize; j++) - { - int windowIndex = (float)i*windowSize; - - magnitude = allMagnitudes[j]; - freq = allFrequencies[j]; - - deltaPhase = freq - (float)j*freqPerBin; - - deltaPhase /= freqPerBin; - - deltaPhase = 2.*M_PI*deltaPhase/overSampling;; - - deltaPhase += (float)j*expectedPhaseOut; - - sumPhase[windowIndex + j] += deltaPhase; - deltaPhase = sumPhase[windowIndex + j]; // this is the bin phase - sumPhase[windowIndex + j + windowSize] = deltaPhase; // copy into the next window for accurate sum - - FFTSpectrum[j][0] = magnitude*cos(deltaPhase); - FFTSpectrum[j][1] = magnitude*sin(deltaPhase); - } - - // inverse fft - fftwf_execute(ifftPlan); - - // windowing - for (int j = 0; j < windowSize; j++) - { - float outIndex = i * outStepSize + j; - if (outIndex >= dataOut.size()) { break; } - - // calculate windows overlapping at index - float startWindowOverlap = ceil(outIndex / outStepSize + 0.00000001f); - float endWindowOverlap = ceil((float)(-outIndex + dataOut.size()) / outStepSize + 0.00000001f); - float totalWindowOverlap = std::min( - std::min(startWindowOverlap, endWindowOverlap), - (float)overSampling); - - // discrete windowing - dataOut[outIndex] += (float)overSampling/totalWindowOverlap*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); - // printf("timeshifted in phase: %f\n", m_timeshiftedBufferL[outIndex]); - // continuos windowing - // float window = -0.5f*cos(2.*M_PI*(float)j/(float)windowSize)+0.5f; - // outBuffer[outIndex] += 2.0f*window*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); - } - } - - // normalize - // float max = -1; - // for (int i = 0;i & in) -{ - int hash = 0; - for (float value : in) - { - hash += (324723947 + (int)(value * 10689354)) ^ 93485734985;; - } - return hash; -} - void SlicerT::writeToMidi(std::vector * outClip) { if (m_originalSample.frames() < 2048) { return; } @@ -459,7 +435,7 @@ void SlicerT::writeToMidi(std::vector * outClip) float bpm = Engine::getSong()->getTempo(); float samplesPerBeat = 60.0f / bpm * sampleRate; - float beats = (float)m_timeShiftedSample.frames() / samplesPerBeat; + float beats = (float)m_phaseVocoder.frames() / samplesPerBeat; float barsInSample = beats / Engine::getSong()->getTimeSigModel().getDenominator(); float totalTicks = ticksPerBar * barsInSample; @@ -477,10 +453,6 @@ void SlicerT::writeToMidi(std::vector * outClip) } } -void SlicerT::extractOriginalData() { - -} - void SlicerT::updateFile(QString file) { m_originalSample.setAudioFile(file); @@ -488,7 +460,12 @@ void SlicerT::updateFile(QString file) findSlices(); findBPM(); - updateParams(); + + float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo() ; + m_phaseVocoder.loadSample(m_originalSample.data(), + m_originalSample.frames(), + m_originalSample.sampleRate(), + speedRatio); emit dataChanged(); } @@ -551,7 +528,11 @@ void SlicerT::loadSettings( const QDomElement & element ) m_noteThreshold.loadSettings(element, "threshold"); m_originalBPM.loadSettings(element, "origBPM"); - updateParams(); + float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo() ; + m_phaseVocoder.loadSample(m_originalSample.data(), + m_originalSample.frames(), + m_originalSample.sampleRate(), + speedRatio); emit dataChanged(); diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 6838c4eeeba..5c4fdb1c985 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -39,57 +39,59 @@ namespace lmms { -// small helper class, since SampleBuffer is inadequate (not thread safe, no dinamic startpoint) -class PlaybackBuffer { +// main class that handles everything audio related +class PhaseVocoder { public: + PhaseVocoder(); + ~PhaseVocoder(); + void loadSample(const sampleFrame* originalData, int frames, int sampleRate, float newRatio); + void setScaleRatio(float newRatio) { updateParams(newRatio); } + void getFrames(sampleFrame * outData, int start, int frames); + int frames() { return leftBuffer.size(); } + private: QMutex dataLock; - std::vector mainBuffer; + // original data + std::vector originalBufferL; + std::vector originalBufferR; + int originalSampleRate = 0; + + float scaleRatio = -1; // to force on fisrt load + + // output data std::vector leftBuffer; std::vector rightBuffer; - float sampleMax = -1; - - int frames() { return mainBuffer.size(); }; // not thread safe yet, but shouldnt be a big issue - sampleFrame * data() { return mainBuffer.data(); }; - void copyFrames(sampleFrame * outData, int start, int framesToCopy) { - dataLock.lock(); - memcpy(outData, mainBuffer.data() + start, framesToCopy * sizeof(sampleFrame)); - dataLock.unlock(); - } - void resetAndResize(int newFrames) { - mainBuffer = {}; - mainBuffer.resize(newFrames); - leftBuffer = {}; - leftBuffer.resize(newFrames); - rightBuffer = {}; - rightBuffer.resize(newFrames); - } - void loadSample(const sampleFrame * data, int newFrames) - { - dataLock.lock(); - resetAndResize(newFrames); - memcpy(mainBuffer.data(), data, newFrames * sizeof(sampleFrame)); - for (int i = 0;i & leftData, std::vector & rightData) - { - dataLock.lock(); - int newFrames = std::min(leftData.size(), rightData.size()); - mainBuffer = {}; - mainBuffer.resize(newFrames); - - for (int i = 0;i < newFrames;i++) - { - mainBuffer[i][0] = leftData[i]; - mainBuffer[i][1] = rightData[i]; - } - dataLock.unlock(); - } + std::vector m_processedWindows; // marks a window processed + + // timeshift stuff + static const int windowSize = 512; + static const int overSampling = 32; + + // depending on scaleRatio + int stepSize = 0; + int numWindows = 0; + float outStepSize = 0; + float freqPerBin = 0; + float expectedPhaseIn = 0; + float expectedPhaseOut = 0; + + // buffers + fftwf_complex FFTSpectrum[windowSize]; + std::vector FFTInput; + std::vector IFFTReconstruction; + std::vector allMagnitudes; + std::vector allFrequencies; + std::vector processedFreq; + std::vector processedMagn; + std::vector lastPhase; + std::vector sumPhase; + + // fftw plans + fftwf_plan fftPlan; + fftwf_plan ifftPlan; + + void updateParams(float newRatio); + void generateWindow(std::vector &in, std::vector &out, int start); + int hashFttWindow(std::vector & in); }; class SlicerT : public Instrument{ @@ -122,54 +124,12 @@ class SlicerT : public Instrument{ IntModel m_originalBPM; SampleBuffer m_originalSample; - std::vector m_originalBufferL; - std::vector m_originalBufferR; - int originalMax; // for later rescaling - - PlaybackBuffer m_timeShiftedSample; - std::vector m_timeshiftedBufferL; - std::vector m_timeshiftedBufferR; - std::vector m_processedFrames; // check if a frame is processed - + PhaseVocoder m_phaseVocoder; std::vector m_slicePoints; - float m_currentSpeedRatio = -1; - QMutex m_timeshiftLock; // should be unecesaty since playbackBuffer is safe - // std::unordered_map > m_fftWindowCache; - - void updateParams(); - void extractOriginalData(); void findSlices(); void findBPM(); - void timeShiftSample(int windowsToProcess); - void phaseVocoder(std::vector &in, std::vector &out, int start, int steps); - int hashFttWindow(std::vector & in); - - // timeshift stuff - static const int windowSize = 512; - static const int overSampling = 32; - - int stepSize = 0; - int numWindows = 0; - float outStepSize = 0; - float freqPerBin = 0; - // very important - float expectedPhaseIn = 0; - float expectedPhaseOut = 0; - - fftwf_complex FFTSpectrum[windowSize]; - std::vector FFTInput; - std::vector IFFTReconstruction; - std::vector allMagnitudes; - std::vector allFrequencies; - std::vector processedFreq; - std::vector processedMagn; - std::vector lastPhase; - std::vector sumPhase; - - fftwf_plan fftPlan; - fftwf_plan ifftPlan; friend class gui::SlicerTUI; friend class gui::WaveForm; diff --git a/plugins/SlicerT/SlicerTUI.cpp b/plugins/SlicerT/SlicerTUI.cpp index fe3d0da0f8d..d251f30ad91 100644 --- a/plugins/SlicerT/SlicerTUI.cpp +++ b/plugins/SlicerT/SlicerTUI.cpp @@ -113,7 +113,6 @@ void SlicerTUI::exportMidi() dataFile.content().appendChild( note_list ); std::vector notes; - m_slicerTParent->timeShiftSample(-1); m_slicerTParent->writeToMidi(¬es); if (notes.size() == 0) { diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/WaveForm.cpp index b6749776d00..86c67831245 100644 --- a/plugins/SlicerT/WaveForm.cpp +++ b/plugins/SlicerT/WaveForm.cpp @@ -37,7 +37,7 @@ WaveForm::WaveForm(int w, int h, SlicerT * instrument, QWidget * parent) : m_sliceEditor(QPixmap(w, h*(1 - m_m_seekerRatio) - m_margin)), m_seeker(QPixmap(w, h*m_m_seekerRatio)), m_seekerWaveform(QPixmap(w, h*m_m_seekerRatio)), - m_currentSample(instrument->m_originalSample.data(), instrument->m_originalSample.frames()), + m_currentSample(instrument->m_originalSample), m_slicePoints(instrument->m_slicePoints) { m_width = w; @@ -146,7 +146,8 @@ void WaveForm::updateUI() void WaveForm::updateData() { - m_currentSample = SampleBuffer(m_slicerTParent->m_originalSample.data(), m_slicerTParent->m_originalSample.frames()); + // I hate SampleBuffer, fot whatever reason this crashes sometimes + // m_currentSample = SampleBuffer(m_slicerTParent->m_originalSample.data(), m_slicerTParent->m_originalSample.frames()); updateUI(); } diff --git a/plugins/SlicerT/WaveForm.h b/plugins/SlicerT/WaveForm.h index 6b78989b049..d5169e1fab7 100644 --- a/plugins/SlicerT/WaveForm.h +++ b/plugins/SlicerT/WaveForm.h @@ -94,7 +94,7 @@ class WaveForm : public QWidget { QPixmap m_seeker; QPixmap m_seekerWaveform; - SampleBuffer m_currentSample; + SampleBuffer & m_currentSample; void drawEditor(); void drawSeekerWaveform(); From d88b8708ecc4e7a6a28a0fb286fe06a9b10b68d2 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 17 Sep 2023 23:59:35 +0200 Subject: [PATCH 25/99] stereo added, very slow --- plugins/SlicerT/SlicerT.cpp | 46 +++++++++++++++--------------- plugins/SlicerT/SlicerT.h | 56 +++++++++++++++++++++++++++++++------ 2 files changed, 69 insertions(+), 33 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 3a237c2c3a1..da3ce8fa28f 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -22,8 +22,15 @@ * */ -// TODO: rewrite PhaseVocoder for only one channel, interlace in playNote -// maybe cleaner, maybe not +// TODO: fix changing sample while playing broken +// TODO: add cache to PhaseVocoder +// TODO: cleaup loading, saving +// TODO: maybe add grace period while changing velocity, there are artifacts when changing bpm repetedly +// TODO: general performance, set frames from getFrames to fixed, reduce vector creation as much as posible +// TODO: switch to arrayVector +// TODO: volume normalizazion, atm just reduce by x amount +// TODO: cleaunp UI classes +// TODO: implment roxas new UI #include "SlicerT.h" @@ -78,30 +85,22 @@ PhaseVocoder::~PhaseVocoder() { fftwf_destroy_plan(ifftPlan); } -void PhaseVocoder::loadSample(const sampleFrame * originalData, int frames, int sampleRate, float newRatio) { - originalBufferL.resize(frames); - originalBufferR.resize(frames); - - for (int i = 0;i originalData, int sampleRate, float newRatio) { + originalBuffer = originalData; originalSampleRate = sampleRate; scaleRatio = -1; // force update, kinda hacky - updateParams(newRatio); for (int i = 0;i & outData, int start, int frames) { + if (originalBuffer.size() < 2048) { return; } dataLock.lock(); int windowMargin = overSampling / 2; // numbers of windows before full quality @@ -111,42 +110,41 @@ void PhaseVocoder::getFrames(sampleFrame * outData, int start, int frames) { // which must be computed for (int i = startWindow;i originalData, int sampleRate, float newRatio); void setScaleRatio(float newRatio) { updateParams(newRatio); } - void getFrames(sampleFrame * outData, int start, int frames); - int frames() { return leftBuffer.size(); } + void getFrames(std::vector & outData, int start, int frames); + int frames() { return processedBuffer.size(); } private: QMutex dataLock; // original data - std::vector originalBufferL; - std::vector originalBufferR; + std::vector originalBuffer; int originalSampleRate = 0; float scaleRatio = -1; // to force on fisrt load // output data - std::vector leftBuffer; - std::vector rightBuffer; + std::vector processedBuffer; std::vector m_processedWindows; // marks a window processed // timeshift stuff @@ -94,6 +92,46 @@ class PhaseVocoder { int hashFttWindow(std::vector & in); }; +// simple helper class that handles the different audio channels +class dinamicPlaybackBuffer { + public: + dinamicPlaybackBuffer() : + leftChannel(), + rightChannel() + {} + void loadSample(const sampleFrame * outData, int frames, int sampleRate, float newRatio) { + std::vector leftData(frames, 0); + std::vector rightData(frames, 0); + for (int i = 0;i leftOut(frames, 0); + std::vector rightOut(frames, 0); + + leftChannel.getFrames(leftOut, startFrame, frames); + rightChannel.getFrames(rightOut, startFrame, frames); + + for (int i = 0;i m_slicePoints; From 248fe741e8e7d33edeaa16e4b8e80c6ace0adae7 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Mon, 18 Sep 2023 17:22:25 +0200 Subject: [PATCH 26/99] playback performance improvments --- plugins/SlicerT/SlicerT.cpp | 141 +++++++++++++++++------------------- plugins/SlicerT/SlicerT.h | 11 ++- 2 files changed, 72 insertions(+), 80 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index da3ce8fa28f..3dfd3f3cf3b 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -22,15 +22,11 @@ * */ -// TODO: fix changing sample while playing broken -// TODO: add cache to PhaseVocoder -// TODO: cleaup loading, saving -// TODO: maybe add grace period while changing velocity, there are artifacts when changing bpm repetedly -// TODO: general performance, set frames from getFrames to fixed, reduce vector creation as much as posible -// TODO: switch to arrayVector -// TODO: volume normalizazion, atm just reduce by x amount +// better ounset detection + bpm cleanup +// TODO: switch to arrayVector (maybe) // TODO: cleaunp UI classes // TODO: implment roxas new UI +// TODO: code cleaunp, style and test #include "SlicerT.h" @@ -77,7 +73,6 @@ PhaseVocoder::PhaseVocoder() : { fftPlan = fftwf_plan_dft_r2c_1d(windowSize, FFTInput.data(), FFTSpectrum, FFTW_MEASURE); ifftPlan = fftwf_plan_dft_c2r_1d(windowSize, FFTSpectrum, IFFTReconstruction.data(), FFTW_MEASURE); - // loadSample(originalData, frames, sampleRate); } PhaseVocoder::~PhaseVocoder() { @@ -89,14 +84,26 @@ void PhaseVocoder::loadData(std::vector originalData, int sampleRate, flo originalBuffer = originalData; originalSampleRate = sampleRate; scaleRatio = -1; // force update, kinda hacky + updateParams(newRatio); + dataLock.lock(); + + // set buffer size + m_processedWindows.resize(numWindows, false); + lastPhase.resize(numWindows*windowSize, 0); + sumPhase.resize(numWindows*windowSize, 0); + freqCache.resize(numWindows*windowSize, 0); + magCache.resize(numWindows*windowSize, 0); + for (int i = 0;i & outData, int start, int frames) { @@ -110,7 +117,7 @@ void PhaseVocoder::getFrames(std::vector & outData, int start, int frames // which must be computed for (int i = startWindow;i & outData, int start, int frames dataLock.unlock(); } +// adjust pv params and reset buffers void PhaseVocoder::updateParams(float newRatio) { if (originalBuffer.size() < 2048) { return; } if (newRatio == scaleRatio) { return; } dataLock.lock(); - printf("ratio: %f\n", newRatio); - + // TODO: remove static stuff from here, like stepsize scaleRatio = newRatio; stepSize = (float)windowSize / overSampling; numWindows = (float)originalBuffer.size() / stepSize - overSampling - 1; @@ -137,84 +144,75 @@ void PhaseVocoder::updateParams(float newRatio) { expectedPhaseIn = 2.*M_PI*(float)stepSize/(float)windowSize; expectedPhaseOut = 2.*M_PI*(float)outStepSize/(float)windowSize; - // clear all buffers - processedBuffer = {}; - m_processedWindows = {}; - lastPhase = {}; - sumPhase = {}; - - // resize all buffers processedBuffer.resize(scaleRatio*originalBuffer.size(), 0); - lastPhase.resize(numWindows*windowSize, 0); - sumPhase.resize(numWindows*windowSize, 0); - m_processedWindows.resize(numWindows, false); + + // very slow :( + std::fill(m_processedWindows.begin(), m_processedWindows.end(), false); + std::fill(processedBuffer.begin(), processedBuffer.end(), 0); + // somehow this works if this is commented, idk why but its faster + // std::fill(lastPhase.begin(), lastPhase.end(), 0); + // std::fill(sumPhase.begin(), sumPhase.end(), 0); dataLock.unlock(); } -// basic phase vocoder implementation that time shifts without pitch change +// time shifts one window from originalBuffer and writes to processedBuffer // resources: // http://blogs.zynaptiq.com/bernsee/pitch-shifting-using-the-ft/ // https://sethares.engr.wisc.edu/vocoders/phasevocoder.html // https://dsp.stackexchange.com/questions/40101/audio-time-stretching-without-pitch-shifting/40367#40367 // https://www.guitarpitchshifter.com/ -void PhaseVocoder::generateWindow(std::vector &dataIn, std::vector &dataOut, int start) +void PhaseVocoder::generateWindow(int windowNum, bool useCache) { // declare vars float real, imag, phase, magnitude, freq, deltaPhase = 0; - int windowStart = (float)start*stepSize; - int windowIndex = (float)start*windowSize; + int windowStart = (float)windowNum*stepSize; + int windowIndex = (float)windowNum*windowSize; - memcpy(FFTInput.data(), dataIn.data() + windowStart, windowSize*sizeof(float)); + if (!useCache) { // normal stuff + memcpy(FFTInput.data(), originalBuffer.data() + windowStart, windowSize*sizeof(float)); - // FFT - fftwf_execute(fftPlan); + // FFT + fftwf_execute(fftPlan); - // analysis step - for (int j = 0; j < windowSize; j++) - { - real = FFTSpectrum[j][0]; - imag = FFTSpectrum[j][1]; + // analysis step + for (int j = 0; j < windowSize; j++) + { + real = FFTSpectrum[j][0]; + imag = FFTSpectrum[j][1]; - magnitude = 2.*sqrt(real*real + imag*imag); - phase = atan2(imag,real); + magnitude = 2.*sqrt(real*real + imag*imag); + phase = atan2(imag,real); - freq = phase; - freq = phase - lastPhase[std::max(0, windowIndex + j - windowSize)]; // subtract prev pahse to get phase diference - lastPhase[windowIndex + j] = phase; + freq = phase; + freq = phase - lastPhase[std::max(0, windowIndex + j - windowSize)]; // subtract prev pahse to get phase diference + lastPhase[windowIndex + j] = phase; - freq -= (float)j*expectedPhaseIn; // subtract expected phase + freq -= (float)j*expectedPhaseIn; // subtract expected phase - // some black magic to get into +/- PI interval, revise later pls - long qpd = freq/M_PI; - if (qpd >= 0) qpd += qpd&1; - else qpd -= qpd&1; - freq -= M_PI*(float)qpd; + // some black magic to get into +/- PI interval, revise later pls + long qpd = freq/M_PI; + if (qpd >= 0) qpd += qpd&1; + else qpd -= qpd&1; + freq -= M_PI*(float)qpd; - freq = (float)overSampling*freq/(2.*M_PI); // idk + freq = (float)overSampling*freq/(2.*M_PI); // idk - freq = (float)j*freqPerBin + freq*freqPerBin; // "compute the k-th partials' true frequency" ok i guess + freq = (float)j*freqPerBin + freq*freqPerBin; // "compute the k-th partials' true frequency" ok i guess - allMagnitudes[j] = magnitude; - allFrequencies[j] = freq; + allMagnitudes[j] = magnitude; + allFrequencies[j] = freq; + } + // write cache + memcpy(freqCache.data() + windowIndex, allFrequencies.data(), windowSize*sizeof(float)); + memcpy(magCache.data() + windowIndex, allMagnitudes.data(), windowSize*sizeof(float)); + } else { + // read cache + memcpy(allFrequencies.data(), freqCache.data() + windowIndex, windowSize*sizeof(float)); + memcpy(allMagnitudes.data(), magCache.data() + windowIndex, windowSize*sizeof(float)); } - // pitch shifting - // takes all the values that are below the nyquist frequency (representable with our samplerate) - // nyquist frequency = samplerate / 2 - // and moves them to a different bin - // improve for larger pitch shift - // memset(processedFreq.data(), 0, processedFreq.size()*sizeof(float)); - // memset(processedMagn.data(), 0, processedFreq.size()*sizeof(float)); - // for (int j = 0; j < windowSize/2; j++) { - // int index = (float)j;// * m_noteThreshold.value(); - // if (index <= windowSize/2) { - // processedMagn[index] += allMagnitudes[j]; - // processedFreq[index] = allFrequencies[j];// * m_noteThreshold.value(); - // } - // } - // synthesis, all the operations are the reverse of the analysis for (int j = 0; j < windowSize; j++) { @@ -245,8 +243,8 @@ void PhaseVocoder::generateWindow(std::vector &dataIn, std::vector // windowing for (int j = 0; j < windowSize; j++) { - float outIndex = start * outStepSize + j; - if (outIndex >= dataOut.size()) { break; } + float outIndex = windowNum * outStepSize + j; + if (outIndex >= frames()) { break; } // calculate windows overlapping at index // float startWindowOverlap = ceil(outIndex / outStepSize + 0.00000001f); @@ -260,20 +258,11 @@ void PhaseVocoder::generateWindow(std::vector &dataIn, std::vector // printf("timeshifted in phase: %f\n", m_timeshiftedBufferL[outIndex]); // continuos windowing float window = -0.5f*cos(2.*M_PI*(float)j/(float)windowSize)+0.5f; - dataOut[outIndex] += 2.0f*window*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); + processedBuffer[outIndex] += window*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); } } -int PhaseVocoder::hashFttWindow(std::vector & in) -{ - int hash = 0; - for (float value : in) - { - hash += (324723947 + (int)(value * 10689354)) ^ 93485734985;; - } - return hash; -} // ################################# SlicerT #################################### diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index df7f920abb0..3f4dc482cad 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -62,7 +62,7 @@ class PhaseVocoder { // timeshift stuff static const int windowSize = 512; - static const int overSampling = 32; + static const int overSampling = 64; // depending on scaleRatio int stepSize = 0; @@ -83,13 +83,16 @@ class PhaseVocoder { std::vector lastPhase; std::vector sumPhase; + // cache + std::vector freqCache; + std::vector magCache; + // fftw plans fftwf_plan fftPlan; fftwf_plan ifftPlan; void updateParams(float newRatio); - void generateWindow(std::vector &in, std::vector &out, int start); - int hashFttWindow(std::vector & in); + void generateWindow(int windowNum, bool useCache); }; // simple helper class that handles the different audio channels @@ -110,7 +113,7 @@ class dinamicPlaybackBuffer { rightChannel.loadData(rightData, sampleRate, newRatio); } void getFrames(sampleFrame * outData, int startFrame, int frames) { - std::vector leftOut(frames, 0); + std::vector leftOut(frames, 0); // not a huge performance issue std::vector rightOut(frames, 0); leftChannel.getFrames(leftOut, startFrame, frames); From b1cfb3cdb4d6e09a126bda30c11b5b64ae7d8775 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Thu, 21 Sep 2023 17:08:55 +0200 Subject: [PATCH 27/99] Roxas new UI start --- plugins/SlicerT/CopyMidiBtn.png | Bin 749 -> 0 bytes plugins/SlicerT/ResetBtn.png | Bin 14573 -> 0 bytes plugins/SlicerT/SlicerT.cpp | 4 +- plugins/SlicerT/SlicerTUI.cpp | 18 ++++----- plugins/SlicerT/SlicerTUI.h | 4 +- plugins/SlicerT/WaveForm.cpp | 44 +++++++++++++--------- plugins/SlicerT/WaveForm.h | 12 +++--- plugins/SlicerT/artwork.png | Bin 11467 -> 0 bytes plugins/SlicerT/bg.png | Bin 0 -> 19736 bytes plugins/SlicerT/bg_no_logo.png | Bin 0 -> 16125 bytes plugins/SlicerT/icon.png | Bin 0 -> 759 bytes plugins/SlicerT/knob_center.png | Bin 0 -> 6455 bytes plugins/SlicerT/logo.png | Bin 39244 -> 11243 bytes plugins/SlicerT/slide_indicator_arrow.png | Bin 0 -> 234 bytes plugins/SlicerT/slide_indicator_bar.png | Bin 0 -> 197 bytes plugins/SlicerT/slide_indicator_full.png | Bin 0 -> 290 bytes 16 files changed, 45 insertions(+), 37 deletions(-) delete mode 100644 plugins/SlicerT/CopyMidiBtn.png delete mode 100644 plugins/SlicerT/ResetBtn.png delete mode 100644 plugins/SlicerT/artwork.png create mode 100644 plugins/SlicerT/bg.png create mode 100644 plugins/SlicerT/bg_no_logo.png create mode 100644 plugins/SlicerT/icon.png create mode 100644 plugins/SlicerT/knob_center.png create mode 100644 plugins/SlicerT/slide_indicator_arrow.png create mode 100644 plugins/SlicerT/slide_indicator_bar.png create mode 100644 plugins/SlicerT/slide_indicator_full.png diff --git a/plugins/SlicerT/CopyMidiBtn.png b/plugins/SlicerT/CopyMidiBtn.png deleted file mode 100644 index 8687fb29b44a5a3c213d0eeee6c75d8c8ad3cc92..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 749 zcmVEX>4Tx04R}tkv&MmKpe$iQ>7}^4t5Z62w0u$q9Ts93Pq?8YK2xEOfLO`CJjl8 zi=*ILaPVWX>fqw6tAnc`2!4RLx;QDiNQwVT3N2zhIPS;0dyl(!fY2y2&FYE)nqDC`-Nm{=@yu+qV-XllgM#1U1~DPPFA zta9Gstd(o5bx;1nU`}6I<~q$0B(R7jND!f*h7!uCB1)@HiiH&I$36VRj$a~|Laq`R zITlcX2HEk0|H1EWt^Cxan-q)#-7mKNF$M&7fo9#dzmILZc>?&Kfh)c3uQY(!Ptxmc zEph~ewtmpj1FlOdb3Bl&3x`8@D`M&FbL25*7BHMh6cK29HiGeSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{006~FL_t(o!|j(r4ul{KMX7Nyr}S#sH;S&gMCrLi;@J-feW9S3ZbF<@bX8c7#Q?VHQ( zeRVlA0BB{we}>0O)i_!c0Ge5qem>Uv^7qJhkh5YSqFlb*zll$JLA&-@uNt}EY`^v@ f`Tqq{3M1PJL7j|s^ABO~00000NkvXXu0mjf@~%XQ diff --git a/plugins/SlicerT/ResetBtn.png b/plugins/SlicerT/ResetBtn.png deleted file mode 100644 index 0d9713132ae8e4d87411f355efa0c83058eafdb5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14573 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 diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 3dfd3f3cf3b..d60764b6459 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -22,7 +22,7 @@ * */ -// better ounset detection + bpm cleanup +// TODO: better onset detection + bpm cleanup // TODO: switch to arrayVector (maybe) // TODO: cleaunp UI classes // TODO: implment roxas new UI @@ -56,7 +56,7 @@ Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = "Daniel Kauss Serna ", 0x0100, Plugin::Type::Instrument, - new PluginPixmapLoader( "logo" ), + new PluginPixmapLoader( "icon" ), nullptr, nullptr, } ; diff --git a/plugins/SlicerT/SlicerTUI.cpp b/plugins/SlicerT/SlicerTUI.cpp index d251f30ad91..711470bf7c7 100644 --- a/plugins/SlicerT/SlicerTUI.cpp +++ b/plugins/SlicerT/SlicerTUI.cpp @@ -56,20 +56,16 @@ SlicerTUI::SlicerTUI( SlicerT * instrument, m_bpmBox(3, "19green", this), m_resetButton(this, nullptr), m_midiExportButton(this, nullptr), - m_wf(244, 125, instrument, this) + m_wf(248, 132, instrument, this) { setAcceptDrops( true ); setAutoFillBackground( true ); QPalette pal; - pal.setBrush( backgroundRole(), PLUGIN_NAME::getIconPixmap( "artwork" ) ); - pal.setColor(QPalette::All, QPalette::ColorRole::Foreground, Qt::red); - pal.setColor(QPalette::All, QPalette::ColorRole::Button, Qt::red); - pal.setColor(QPalette::All, QPalette::ColorRole::Base, Qt::red); - pal.setColor(QPalette::All, QPalette::ColorRole::Highlight, Qt::red); + pal.setBrush( backgroundRole(), PLUGIN_NAME::getIconPixmap( "bg" ) ); setPalette( pal ); - m_wf.move(3, 5); + m_wf.move(2, 2); m_bpmBox.move(7, 153); m_bpmBox.setToolTip(tr("Original sample BPM")); @@ -88,17 +84,17 @@ SlicerTUI::SlicerTUI( SlicerT * instrument, m_midiExportButton.move(145, 198); m_midiExportButton.setActiveGraphic( - PLUGIN_NAME::getIconPixmap( "CopyMidiBtn" ) ); + embed::getIconPixmap("midi_tab") ); m_midiExportButton.setInactiveGraphic( - PLUGIN_NAME::getIconPixmap( "CopyMidiBtn" ) ); + embed::getIconPixmap("midi_tab")); m_midiExportButton.setToolTip(tr("Copy midi pattern to clipboard")); connect(&m_midiExportButton, SIGNAL( clicked() ), this, SLOT( exportMidi() )); m_resetButton.move(80, 198); m_resetButton.setActiveGraphic( - PLUGIN_NAME::getIconPixmap( "ResetBtn" ) ); + embed::getIconPixmap("reload") ); m_resetButton.setInactiveGraphic( - PLUGIN_NAME::getIconPixmap( "ResetBtn" ) ); + embed::getIconPixmap("reload") ); m_resetButton.setToolTip(tr("Reset Slices")); connect(&m_resetButton, SIGNAL( clicked() ), m_slicerTParent, SLOT( updateSlices() )); } diff --git a/plugins/SlicerT/SlicerTUI.h b/plugins/SlicerT/SlicerTUI.h index c9be4e2aeeb..40ac38cbbe5 100644 --- a/plugins/SlicerT/SlicerTUI.h +++ b/plugins/SlicerT/SlicerTUI.h @@ -52,11 +52,11 @@ class SlicerTKnob : public Knob { setFixedSize( 46, 40 ); setCenterPointX( 23.0 ); setCenterPointY( 15.0 ); - setInnerRadius( 8 ); + setInnerRadius( 3 ); setOuterRadius( 11 ); // setTotalAngle( 270.0 ); setLineWidth( 3 ); - setOuterColor( QColor(255, 161, 247) ); + setOuterColor( QColor(178, 115, 255) ); } }; diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/WaveForm.cpp index 86c67831245..115e24c258e 100644 --- a/plugins/SlicerT/WaveForm.cpp +++ b/plugins/SlicerT/WaveForm.cpp @@ -34,14 +34,16 @@ namespace gui { WaveForm::WaveForm(int w, int h, SlicerT * instrument, QWidget * parent) : QWidget(parent), - m_sliceEditor(QPixmap(w, h*(1 - m_m_seekerRatio) - m_margin)), - m_seeker(QPixmap(w, h*m_m_seekerRatio)), - m_seekerWaveform(QPixmap(w, h*m_m_seekerRatio)), + m_sliceEditor(QPixmap(w, h - m_seekerHeight - m_margin)), + m_seeker(QPixmap(w, m_seekerHeight)), + m_seekerWaveform(QPixmap(w, m_seekerHeight)), m_currentSample(instrument->m_originalSample), m_slicePoints(instrument->m_slicePoints) { m_width = w; m_height = h; + m_editorHeight = h - m_seekerHeight - m_margin; + m_slicerTParent = instrument; setFixedSize(m_width, m_height); setMouseTracking( true ); @@ -68,12 +70,12 @@ void WaveForm::drawEditor() float endFrame = m_seekerEnd * m_currentSample.frames(); brush.setPen(m_playHighlighColor); - brush.drawLine(0, m_sliceEditor.height()/2, m_sliceEditor.width(), m_sliceEditor.height()/2); + brush.drawLine(0, m_editorHeight/2, m_width, m_editorHeight/2); brush.setPen(m_waveformColor); m_currentSample.visualize( brush, - QRect( 0, 0, m_sliceEditor.width(), m_sliceEditor.height() ), + QRect( 0, 0, m_width, m_editorHeight ), startFrame, endFrame); @@ -90,7 +92,7 @@ void WaveForm::drawEditor() brush.setPen(QPen(m_selectedSliceColor, 2)); } - brush.drawLine(xPos, 0, xPos, m_height); + brush.drawLine(xPos, 0, xPos, m_editorHeight); } } } @@ -118,22 +120,30 @@ void WaveForm::drawSeeker() for (int i = 0;iy() < m_height*m_m_seekerRatio) + if (me->y() < m_seekerHeight) { if (abs(normalizedClick - m_seekerStart) < 0.03) { @@ -248,7 +258,7 @@ void WaveForm::mouseMoveEvent( QMouseEvent * me ) m_seekerMiddle = normalizedClick; - if (m_seekerMiddle + distStart > 0 && m_seekerMiddle + distEnd < 1) + if (m_seekerMiddle + distStart >= 0 && m_seekerMiddle + distEnd <= 1) { m_seekerStart = m_seekerMiddle + distStart; m_seekerEnd = m_seekerMiddle + distEnd; @@ -294,7 +304,7 @@ void WaveForm::paintEvent( QPaintEvent * pe) QPainter p( this ); p.drawPixmap(0, 0 ,m_seekerWaveform); p.drawPixmap(0, 0, m_seeker); - p.drawPixmap(0, m_height*0.3f + m_margin, m_sliceEditor); + p.drawPixmap(0, m_seekerHeight + m_margin, m_sliceEditor); } } // namespace gui } // namespace lmms \ No newline at end of file diff --git a/plugins/SlicerT/WaveForm.h b/plugins/SlicerT/WaveForm.h index d5169e1fab7..4c85c19d1b7 100644 --- a/plugins/SlicerT/WaveForm.h +++ b/plugins/SlicerT/WaveForm.h @@ -57,16 +57,18 @@ class WaveForm : public QWidget { private: int m_width; int m_height; - float m_m_seekerRatio = 0.3f; + int m_seekerHeight = 46; + int m_editorHeight; int m_margin = 5; - QColor m_waveformBgColor = QColor(11, 11, 11, 200); - QColor m_waveformColor = QColor(113, 0, 177); + QColor m_waveformBgColor = QColor(255, 255, 255, 0); + QColor m_waveformColor = QColor(123, 49, 212); // QColor m_waveformColor = QColor(255, 161, 247); // logo color QColor m_playColor = QColor(255, 255, 255, 200); QColor m_playHighlighColor = QColor(255, 255, 255, 70); - QColor m_sliceColor = QColor(49, 214, 124); + QColor m_sliceColor = QColor(218, 193, 255); QColor m_selectedSliceColor = QColor(172, 236, 190); - QColor m_seekerColor = QColor(214, 124, 49); + QColor m_seekerColor = QColor(178, 115, 255); + QColor m_seekerHighlightColor = QColor(178, 115, 255, 100); QColor m_seekerShadowColor = QColor(0, 0, 0, 175); enum class m_draggingTypes diff --git a/plugins/SlicerT/artwork.png b/plugins/SlicerT/artwork.png deleted file mode 100644 index d3a44cc9e5ae9720cdb65a2168a485b82e4d667b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11467 zcmV;+EHu-JP)EX>4Tx04R}tkv&MmKpe$iQ>7}^4t5Z62w0u$q9Ts93Pq?8YK2xEOfLO`CJjl8 zi=*ILaPVWX>fqw6tAnc`2!4RLx;QDiNQwVT3N2zhIPS;0dyl(!fY2y2&FYE)nqDC`-Nm{=@yu+qV-XllgM#1U1~DPPFA zta9Gstd(o5bx;1nU`}6I<~q$0B(R7jND!f*h7!uCB1)@HiiH&I$36VRj$a~|Laq`R zITlcX2HEk0|H1EWt^Cxan-q)#-7mKNF$M&7fo9#dzmILZc>?&Kfh)c3uQY(!Ptxmc zEph~ewtmpj1FlOdb3Bl&3x`8@D`M&FbL25*7BHMh6cK29HiGeSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{03ZNKL_t(|+U;GrjwCsfwMewmdP@lm*h0$l38i>r#3rzM zbbo{8C+r85e?x8S3s{MZO{G3zB;d%%INa%Un%T1( z0N`GadhGQ;pa-~5GW<^ozc(IrGu30?1c1RCKO1pGo?|{=3b;0vTF1Jf>T$56fjbIX zfzVCF>LXGPQB%Mib^un|1?G&*#=yti%)hD9s~+_z;46;0Q-#29Sj9>FWzQB?iC9(z+o@yWB%+iQ2Uw#8vuCjUblZ#09(giE%Gwqb13v%j#XX)0zs>&3s0LY;9LLlTLC7L z#f?$NW4qc^eLBXyw!LklUDy&%7Ttu`Aw04k^~m93JEm%Bps(s0sz*Ke`-pZvSF*gW z540ZjUMtzpC2tgJ)Z;pA-hYkJafsofI%q*nHN0F@ z_gXzp5g(DCD6yd(3qSI>*^IC2#;M0aqP?!`mPtldF^a?N>$-s|W2nb@o_DWyEHgvv z1Fgr=2`g3F(z%$pnC@v%L26`N9n~Fn<{<#!Mo1c8??cI>f06x~baSt|VKvOv<8=GuG}&uaAFHDdF0C9Gx>q8vqYjR$feoC&7*{2G z9d-D|35;>BB7TpOI!cM3*RkJcR|(y}ctSqNCYRRk997LJJ5W&4=W9NjbHqU_NeI{s zA%G8J*=?#f0JaV}-GAM=+@W{dyAnJ?P@%J$chbaTLCC(`TJ0Zta;oEmT&V0e3H=wZ!y$%c)u~GWX ziFt4A3f;Fiz|mgR;`*Qt?N(F@0Cq$Vrs^W~cyO!y(>ZmfN3ia8&#WCi9t7~-zkhpF z&%+Vy5ppvXxswK0%3S&KtVIa-+E}kXyLvb!IstwHfS-oK?{&0)$Q3z!sxj;A-iGsz zl?`$X zK~dF^ecm4(a($j2jjJ8TAb6|KW&8V+iSyeJih0U&!*x97kOx0Mt#}UFoS9{TnJDH> z>FTjB2ucTr&4oNdO=`@|TaY(%OQCfdg$-ghyD-U$uEmZ?~vgc*Kufh>q^~h?Z zYw8-D)5v!KiC#-C*HOp5=_OMYSot6iC+>H6-Gc@Bk=FdtjhMlNUN>i+hr{LIb-l+S z`lF6)qmV99_4UVGFIw4CKy|OJr=uU)+WC0;S{;u!p7Ov*Vt$7TZ|oyN-&)ME4wXJO zf$q7W+M4jH!nl2|E!F4uc+kC$I;zUG=JlV-O@b=ydsqT^gdq=g{U`GSqNoQvb@Y@T z*GMJ)bqR=8-hi~Dd#__yL{BT95I2YN`e8bnKC=6b>N*^=lvHZL`5bZY^+DFPsmBV0 zzJ1aLULSo^lQpffZrFOn98X*M^lYPj2Y`<~7&~@oXnCu;Y8`Q-y4QAVc#~0Xlsf7# z?U|>#D0{S|%V;-B^)%EqGVPg4=sV)Nbud+!hi7mU{8@$Wb=2_yBOs}z!l*jxII7z6 z#D`54X4Z|wIVaWiI+jGQW1UU1)drG<&UsRSAWEiIhlHQU?U$;fuR`Q9; z&bLlgq8FFk^1<)_k=J8C$fy!}F*wZ8SLMlwxfa0Ovu)r+$s0B4qwF)FlT@*J$b_c4 zsi9`DtyK1l9DUSih5Mx}NmtvfdnNRG)Z-M4whR-YkpAd(R2?w%oLiL=D}B8(b`>lZ zgV4=)2ky1KxLc~*CqJ>t`W#kWyWJb(suArt@RW9{N7I9-0#tSM%0No49j$adiZoc& zmR8jm_i)6&m4u}LMnUyiZC^_2%;W6(UrrsjUj>7ntOM zWg(AB#AP-!olrfYjQ32Q*R(Rzq<}EqoQ#Mv?klS9wRIO?Eha!LgA^Wy5tNQxBfDg6 z;ErOgh&uAT-lO|>bwZ~K=>yjR#nFhFB8i5(hK9}DJSR4^x_o7!sW}7T|?|Cv5}KL0`?GmW2ypFCHg}g zoona_)mVs0kQ@~t)lL$NB65YTa_)z$Hia4)S0(7q(|9RD+T|c{Vxj~tk>;9<=N|i_ z7bs}@%ZuWx8?zE$OhN|T(dky3Z^G1Eehy*!h?o5NJ3C-rC1vJN|Bl5uxGfH+pL#YIM?zB`2+_02*9-j`a!|TvUM$De8 zUNU?Y7Wy>`TlA*qo|;ha0aQXSoy)wpFkIbl&aJ(kJD?Xlk^=4o{<%& zF+^jMO;CRPk>bYpFUe7mgKO_qSI!m39Rk@)x#nUl=qoVW1sso^d-OA@#F_{CbubjK zp@y9eZ3WF75^@qtLN=SCQ(u=TBRmJ6B6Z`C6wh+cZpqxpnn7oE=ZTO=z}*o#okF6c zm^Sbc&=6*kYFt8}0y{7pI^af@af4w>BqlMl&IBipKQxcuNa(d8U8-J=ga3Fl)hD;U z8e^y;ha1f#%_-wOa#avAFzGdF1^VV560;vX->O%qNJCUC$6XMK$&$PlJj@jWTFz39 zSGXg_Cf0D0QbN>>64hO~$u{Ki-#gxKqUKul?21~}E8~o!C-x>q&kn5`>xcZ<ZlQzCMpO6coaua8W)?s#luOWj;( zhilYwNIb#GKBJ7p&DG6)K|h=fyIuRysp{~%i~%sX!gU8H6O}=)_y9%c)+kj(Ze$W@#TU8J##*uNmZ-gEsulxUwHT8cl_gxa5(7Z8QNvc&tU@Nh&=fnWiXRUizIxpw0-=cahyi+&7Mn@1;X~%JghP5?gZFlxU;9b`tm6 zc~U<4*MBNyXZ<3WN9Up2VT3Y(B^>ADk9p2-Ki4!l#ZDx3|MQXM+?G3rfwY02+Zn( z3d6^u4Ng5ghOwojkuDQf@S3x?+L9TWd$;Eg-PQB>OLc~MxSDaLI%^V-QGmVej;~FV z&#;97J+^T+r|~l}Cn7tsBOCsw1pI_p`%{cyrQ`L<8O&3rMc2a{cPQPV{zT9767dpE#sbCRKM^w=JSQ>yD3dYH`T4 zY$FkJp4f^k0QgSarlN8nVwX6Rt!0zb^#$`yKB0U;E>(9P|J0hK)-0b8G7% z9pBMMYrHoJs4}Bx^?MT9TTigh1n@BtZ#ySw(J?m=0MO7#&}euDtwQk2dyu^K&B&BP ziJW(B{&R^Vc)1WB9g&;Xl#&J81BJcv8}a3XkYt6Xw)t_+g{GKu#Tcg==f*@4^*dUM zs_rL8;V%>AH{fxTab}Q|)yxsty7{`QWlDN-N_esVx=;DJ)tYJmSvW6Qq^V?ZM}qtP z*F@>^l`RzpiHH_SI7zA=+SyRfGXOsKQhWI9S-IwWci_7+ul_v{lZ0-21OPW$%u#*a z8qr0Fgpmp|kfW-E|3Lts=tRVU8Sg3N{M)zGFN0 zd>{i&pw-8&N@|+iW~yafOO>?bpf~1oqnM)xGQSQAs$nFp4ENwkBh}ZNc4{OpX>1Pb zuo`WQ!-fE!d#?P)dYl(Qb~@xuH3r$ZU5F9QZ4g@OfMx@lNmuk4+2)3=;d`%_ubM90 z_PTfrvNIF{0KVu3wDI*j0NnaFoBsb7$1eDY~~%kixCk*Izn=-e9yFuF*ys zhH@CYIP2}dUS8vE9;2jhJ``mXjx)vy0Qf5a`~(0$5ul)=#H~Wxb}(944LoFkKBOFS zWK?)n=Wd5x6BH*4^XN8Qx9Mp1J@OiKmWHkFeAD~7;SPHX`asb5K3UL*_eKMpm@Ml9 z@waaP@Gb#g^k+I}80|>Ra%sR5x(b1oO^QLAnHUDlT*hDmyh5V!)^`mV`FrVDgAnMS zXpBSA7sf|erK#eOw6QSW2vfLfXc0s3!ggSUaB|=)&wxZdWG8EKix#UcCYX)01Qr~i zy`g-R>T}m#tBEkGuf1=jjMgf!`~r_TU~-%Rj#f6j$j*E3Ax*9!% z8Z*<}Wg+gUl=|^;#$XJ|;1gNt>^K=h7~GYTf~B#0c~sHbv~l5!lPdO8UmPZ1zPkq?~jTI1JI zGb0fW^B5&~|De^FY*TqCFHs{crTOTJQ!?4EZh}Ms4r%UQj}YRHA#^=%okM*5vR5P8 z2`ZIEmWr?jq7}YGm#h-~EG2okhB)^E`Zr`;^98rmqJ@8aNWM@DMsSE+^Qq&^zkW49 zb_bDep7&+|xn1gtDZB9_3QIh)AH+xF*vT80wM6mg!G8d`F6Z;RxiiG1t z8JQ9Xq{Hno_j&qVI$h-4T}etY&B-(d#ZbRI>1o&fl52(34{c+-MAGl<>kCyT5OZR7&oHS4N zkt`o|7ke%ZQFFHBESs;M412@p-jmsU{VQj$feiqz3^L>5{d`e^ zO+e76K5WbUF27u+(AS^(bN<;A{GYO4hiOfQ zjr94yezho75n?>G!Q?zHLvOUCud8QvJ_C!pk0xQ}_4Ta-t8}5x1I1b5j_n zFsIDoECP`iK~^U64V-f^!T1@`3D-WN8B^yl1Ojv9rb$3>Q&l&D?uZ2K|4D$i984p< z&_mcYz>5^__oZ$=mn;*E)ZrLA*4=u(Ns4}+o$K{Cg&B?eYp|f+{nQI{O8Qg(6OeY; z&UwxN05>18kW)8r-vHWnAo5aD9xRa9MtHXTh)L46@6rLi&1=!+RFK0MY(vETd~Rt- zIlqoxgve7MehO2NUiE}m#l-%QRKj)XwDW`$>-dl}!UwyK_#C!zUVr)8S>4|N@QoIz zf+wZcHrm>tGl4rHO5ax~l=p7rK%8V;kM`!;2PRJq$76G7W1NKY8B8+Z{BHLd4>E$$ z*k?8fnWed2{J~D(?MUQug6{+I4`rlAJ>Kk2eEsbLufJU2_2=%m{Ct7e%VmVC*fXWe zY7Whv$#|DTD}{MLlUHM za*qCJlI(LPpcjU`u+R1S`;UtMydzetd+k}7=f|wr<6udDCTKqfffG%fU`Mz2{sx_{c z1a6>9oZ1w74l+wttIKwJ4_{ow2M+k(|6a{`7cds?Y(Kh~2@pN;J7IkKTI`6>r9pS4 zm7tza7YbkKsgy8B*XJ1Rz-zGUq;^m6gG7Jt^~?9}^=ObArL$$+l;Rb}?~&q4OJRII zJvpAC`qedC)SF^>dvodUK6^m#?i2ucrD^Ry0PvZh{U)bEAPY2*7s>8SM^`xJ+x$J5 z;oLrbQATYzyFt!X>`_|uZ8A#nmNAdd|8>`OaiFo()Fh>qb3IGI_s+<+IM@9?+6MFr zIMZ6Sv**Gme3$DfXOgC#{UIDG<{Nco&;9_Vx=&wz?M6&BBAqEd?T5h0 zy3m+KnoAb9H`iy6A#xtWX=em)cdt#+Tdh;iRjR=Dm0s}$I%85#^!MMsYk@M8yU4^~ zWiMWX6vu%y41u+><|DKpikae$>J$f?4eWMGuebM#r@wM8Z_95dpPW{9URhJWe~c4Zga7z3;`_!^!m$Xvc3;6{RQCj zZ{Nq`{-4TqKW4wRFqiqp8X^GAYoMzkEepDr@T`zSO^^;^q6N`nLx3+d>C!Jf!K>F1 zCLP`uNoQgzE;TQmA^yOE(Zvm7HZI7?@X2M+St5tI)Qpqf(xZnEYCKF>GlN+py z@h50M^-+ay{W*tB!5-gg#?_TWn74_+ zsLyx{gp?*$ml(l{Xhmox>Z9SCJ{zL%ZK$q;p_uoK+?s(I47N#8da{D@DoT#^2 zc&p)k&DmX1L@jx3>yI|pms5JH8CxhTrLs-E(udN{xTqjQQ#t6M-fJ|?qu6sGdyO|? zs}$qeD?^Lv2HcfskPJQJc2$e<3{4h~b)cgvC^~wuqoatV`W*y?h=o#QP|OH_R+ zLZkySg%G$1&Q2&MB!C*!Rv5y(3rj;@_P3sxo zuwf`RIl*)~ioiMok>r9yQ~*(w+_XG308JZR>484ATOy74=Q_O+83?5UtmV02_VwUa zT%{GBuVHO9`^aGgPt}>%d02V$P_<1l8jN)F_ci02rv)-t`s_LN;VW6b6WZh;uJek! zsRwzOMaUdD*Vy%bJQ^`lL5WL;Vn!|~jY?Y|)DDHnud_yqqotiJ#x2T#mU1;U3^j&P zM)Y<_o6^w;o3dXAvv%1G?E}W?L>dF(RUKQ=$A#V`LN(HvyVG*Jp17M7U{ZG-@HAQ= zPh*7CJwy@Dv|8Oa)Tc|HpCSo_OVH$GP6$iuxoVUHq5I3H_Q;bSu0wnd1Ec`r4_gzy zBqt{0&Ml#-O7wj|_Rt4I8|vr2e&XbGf1$x@k1mV8O5mVMdVA0Zvi-+~>o#Q3seSE| z<_1Pu7#!Vk3Y>b3w^e=`4bCOt?;aSx8@#CndUy`h^=Nn=`-=sfoY&`R!$xCcFL6mSquAiURtJjm0> zB1h!_01B5$L_t)L&lk`F8jRN@p8$uk%_kN4VYN=vP-y>mvu@&^_G391;gli!e{sc;(jHc~UjZMP?&P-uu8Z+RYPfd_ge}N8Ia5 z^pF8!4X&Hp|H;>>!=P(odTnrgdg#1e5YJz@Va=_tODBl%rSgK^MXg6X6zqlggN*K{7Nx+rSH@UuL;Z`o)JMiMSeY_;+($XK#Mh zIH`|O5Iof@5Swt~BYMvHLyU2y<0Q7&mwB$UQKuF|UGF!7_Kje>Ve8xhk?-3&kPzc~ zrUs1ePA=wixaydzokPemU&&ZA0&`aqTSLfMOC#qR&HI`I`1_LC);+GbUf6Z^J67Z} zp}`Sp*TXFN<6IX|PQ<|Xy`wQrGk58xwQCJffIh$7v6eK?biIT?fS?Qe9^TdqOPP#w zd31a|1eiS5=#cpngXzYm_fjIX4lQ&MtFP0e6Z#q#rNC&YEf;r7$w85fA{2daE!vbQ zu_@szZR&{1>8lpn z8p@B28g%3Ho)^?=r-;-d@>}drcZ3VhTjfGdee(47`uh>VeON z=2H#i3>Hg+ia?fUV^U8{b0)`Jr9p)#5hf}jPY7$NA$NbyV(2qX7ka42)O06L!?5zxN zc;)?=a=ikag-!{X@=J+%78*rb*#qIcBXDV1Jkkx}%Qo7eU81KUU~0=)_lVz?3a~$J zlK(mzNiEZq>avST9*NDaE1i6@S0+J+&R$D`?w8+xWLTN=LpJku_coYC90Wic9FRj6 z%Q{(Lv@f~FWQm^z&@|m#-Edv5x(~u(dTauVxj@EN`!MFZ+1zVO-9ZLy*!}53>6_CR zStUms0KO5l{d>sLsZA~|X<2h(Fbn-RQ#RqCw*%li0o+{7#^xHNqAt_??kXAaEX(iD z)bn2k{GMwP`MDgy{mb7DCe1?_iN3K=0Kf}C`y>8A z>&~U@{?B&)Jcg9gRbNV+AG5b+%284&w%J2SrC5GxFBD{`55MQ`qO_UIYrXSRX=B`$ zgps*3Y2Q5rqGJHchrnV&)5UNjp5gam-mQgJT&E+1%90^7bGOHlgGG*BGAc*GNM<~G z6B_TXGC(@+f}KH>(j!x< ztX5QJ+B3XHhcv5SK(jqZUc+xnL&bpa^G^^qi8!vcEurs0%1{9aTcQjTtqhyjVoGG$ zGrlZ7($LM=?pr7fUMzp-r!!iE(8+M~sm%i28_jgx^AP%;jeK+h-8ipAPub7+`+ZSF z6b;pA=8`{TzXl=!GpC*iZaD$d$>*P$CS&4e^Uqw@@V%c44y~t0=Q8$C1hb|4{*77N zooBIsdTG4eQ_D8r!$74bfcE}%N(S;t^q9}HK=(Pp+6dePLDa>uO>lW0n%M}*d&52^ za2n5nNY<5hha+*t`d(G2b&d+rQ@hOiT1D^nHthG-=N#WLlTdvG*UMfzW;DP#42R z=(Zbk5pX#|-!GBlHMDp~N)d}adl*VXcQaG73oe^bo1FBooY?bI%I-tEm_a1VHP>Jq` zhyVWfzgai7x!dlPTdoETW$)cyF4^FN7F!tO=>E@t{?lrlHmj8`Bewmse1xts z^iV?AC5f2`$p~3&?umc>>tFjM^ut98auZwDCg|9{b2{?Cpni4vCHg=9@eiw-IY**) zJ-fHMH*kjrR+H#Igyr(yp=H;Clkj>W=;op1#a_O4sdx8Z#|+uwJ#S9!<)WSMa=9#0 zJlmW9d)tlB)KQsm9bZj4x#4|F&pl81^(d%I+Z~=UkRgSao421f{_>Zz1C zd-|g7`Ni)pT@8ZyUS0my{dq@Ach)}700T*>r)7a#S66aGjh$a7G_&*?#>~TmLIm6q z$xxdvAV^58fl^XScNgE=sZmg5Sws}&F@LyJyMn0ralq=L2v@)C|(EE5}= z35gk`7rwgnRCo=jfUTqmp;P9KHs{Kw2NkjiRg&}5R*z6+)nCX0RU@5nG5rwY&g(uz zXY21&PeS%%$W_i1Q!RM0!RD$Zoa!l+=+B*=OIkKuSK$V>g1bo>fXE7$T|Ik^@;tN# zyn4LfI@~ji<}u=`ABVUt&w0$5H}3~t&HEfepkx^@ju(Ws*3n~w9T)~YM;IsKy6tGs z&gr3(Q6L0|ioGLRZF6B$4((cLN zNoDR$@XixZ?%CB`z|=UPuUjq%+M5Jy`p=#L+HX>+5l)_So?)EnUUIC3&6mEX8t|p= zdAq$(xMGO{&xPVgbudv zt|R@&r~`5zD)iK^#q}5%KgcO0qM%5nT>3sY3_hP=ra8a+rK=*}dxmM1UwSH8KCQ@q zY~0~4B-F4tyivK>NWz;eknwbvJrytyLHO6temsfwf9hasVGns?8j_-CZm!BUR z(o)R#YkGy`?(Srt#P3sb294O`b4YZM{Yoca zU3|`G`lE9tz9V9&Zng2Ps7V{%B=P_wUVH*Lf$ii1&IEO#ZG2DPGpZ5{>_?_!6v)AA zluVS12ztwW+pxAi_jrTs(pt+S43I5cxS{`kJ(=sQdG2ejS-% zrJi}~1F$ava%*Ct*xj>@-5R|Ogr+xs6Ezsr4Z|HA8U4#_TMkD!3hotG^^uo7aivt4 zZG|*$qat1)t)WeqDW9I6lP1UeAoA~uUFKj>c;jL;il-RSwm|I*wqJOv1LeO6bk3wH z8FNyxly$E7HWF2b>Nb*7=$id7Wa;Z-Rb-1wJbckXk>A1n85>Vu49nPHTm!nj=`Cud6sv0u%>t`-Ts1!Z;97%dS#1Pv<^iF zMy&-?woCDYH=ss{U>Fg@L#w0Vr)R3D=N|o*tY2kM3OwMMit(OhpGNl*;#Zd)!9>%a z4D9*_=$kxtmR?kuYIBy~Nad$e`-M0eed@610)%R0nB#CS|Ul`uQ^Q@fEGwe{S39D04I9FFp)LbDBsQo?izwg$bM4b!W5)qCeSB zW;00u(KkfM*=~wTa*VTl*05Q{-H2N_E%R6piZ;{v)9S*9WI2V_(-97s#;HfCQBjo9 z^<57(R2-MEv*;?h9f%Y9bPffL{P4bT61p4#1_#WgQpfOLNuiRRftl#zd<1PcXc%pT zj)45Y8u65rFQYA7iVe^=;(>8I4^EJa;D2M9}Rt%YY{Fj+6I;y;kw+)TR+nf(MH z?(ACsxaGb|h++{Il=RV=dYG-RyI(~0e2Uc&-HM*pg{j=j>xo8^q0x^OR49xf@I}#Q zYwk)mKQY~mOCQZkZ|Lr}D~v$gKi+5m87&UOEwI!S+(P6bxbB(cfvvl45#~4`n*>#HOzPj8|k})%+7|T3PuhpFh_n$MB1b zQI~1uvl88sBOXC3Fu+P0(H&NA255^x(KjMV6Upk609;lA@dsuV0Qc~Ia!Ib}`3El0;e02p}shLtCM-}~wR6bx3 zB|G>$yxCqYL_Mx`FJ7(l+S_GkfO3)LFw)Dccey;}_RXuOlEs0d)0Uqpx3W+0!4|&V zwp&HFLUYJ~*R;-rZX(|#MmM?<`BOm(!vG< zNSio;FhM>Ufvr7L=AirJyrTvdl1P{gxgYPb;H7F8+k2T>bz>usvGkyGcjfK1F{rdQ zxllh?(`bsi&A%f0fw*wqJpdSPdIdqw$)8xfv-Az&-ahuMt^VpOcagtZ1?@RF2gY|G(`)-i* zwlSz9x5j00TqGw;%e08-Sw6`ut1l13X7dV&xl6`Udth33c-B`q$QG*Q z8^QBcZ}uhYBu)lSa-Pb}cih_URYzs!yic_MBsD-G8#V&uJa(uBL`>HlolDV_i}IeK zWmtVd2wap2qMbj4FTwdHW?bpVp)nRy1>>KeKCs^&CLU>=|AwL}sJ|zzXIMot%%d*LlF%Y&p5e~^Wzu!mP_hE<>&`suar(W>KRr-1MH~~ z7Ru&?F=w$zhbC*4NNP*V{K&M^h)?;(X1W#8nP2ivBPTRLp>yVSJ@rSw5SmN}e3=^w z-L7i<5%B>J4^14MR|xZ_ol=_$X-rb1qEaX2TLRT2GliC$#WX+Q6h~D34QZtaYt@xi zqIZeJ=kU@F#*>*;Omia|_)=p>gsM`@I? zcFuK&fwGPG!m1o5?b5cLoi~6l4QznnvxE@tUlHkpqVubZ`C;_)dudo)bL~k~nq|wE2L=_y$c>{7rE+8z z2kU}Suf3>In{S~h0%SUC!?Qmaleh5Fh(?B2W(l5fpj3vSkh3F+dutd;)7out|3779!Ebd6~c_9LnAT%;Hxk6*8S|nPf)-a=MzH{+@Ll@@( zx58PplO373sGwI1PMhgLQBaBEUMN*aKtkGeO=`AG^f%wv4;iQug$j!iN!J%}U6@)O zh54y;jT=6L8?E7|SJdyJYx48?3jwSn6ANsyvN?wtBNmp9)`!YD8antKNK*aMr$+CeyzZRVf;S0)nK=6OVEi+(ByN( zvu0xy5Pp=_isciS#TJHfX8u@UUp)SOg7ImADNZ@-&|`B2AtEO;v!Js!A_8`Sly8R4 zL-q|82DAbC)1KL_hb3TlM(L`^-%GG3Pdc1W}G3ho1z@Eatb1r z2udHrFm+Awr;Y5JG?Aop0PUxGy+2@xC2_RV2LVza$%uDp)lyl^{paWC(ts7lO$D?q z#8o}87bYCw4s~ICKzhl%$j2I3&Cl`gnLnydq##gbUhs;U6KEvIV_|Dr+wc|}*&92E zqb{7p)T&4_wvHRU*Pt9TGe9QfX^Q@|&f7Fn+8qARK4L>T4)A6}~j(g<_E?_8VviZsoD z$aJzJb+ba%D(KZ8Y^<~cz45mCb(87asg*=UYIQDG(YJbP>DuXPF^}T7y;L_GOROo` z&_V(+?*4f1H~=X*-=$6>>KIBZdwhb}se@yY@uHM2wk-2ycf%Ew-z+Hc)?Ah*dasPX z_UiRGf6dp1DJ=6T3E7W~%^F8B8OfRo|1CUY>}qOP8f0$jLV&_B$MUy$tw|16t?Q9c|y6SZVOdAOzbsEHvz;-<_fbpb}3MBZ~L zYcI@YcR7c8IlVIKYwXmS{8gzYvXIb+G0Xxh(9J1o69A5n378m0(0!q;A3c$!8gzRln}##fzo&np z%1!yXlC{2p7EQ&iBW|}gbN3@a9s|t-slMl{gF5TZS8}-maRgbM94UI>BzdnYaC^bJ zP?6qq9O#r44!c-jt%aTyd{_K*J^)&kZVgbu@XPx+LeHujGeHcPykX-G$_{kqfI8r9 zeG>RM^0Dx`6NVe6kCi#{^VoW(%t!TUM7n|C#aB;1KKZSk!lifkz-(&bD`rz4$Yt*X zZAdO35XS2I8L)kO8nI|SxK$KgT1bALt&er=0#mwRm^a2c-?dS}vcR4A^DAli8%wP! z?~C^Ql$6&G4MGN$irhAET6s(>v7mh(q4mgY4o8m5!KFfjbL@~AlLJKM4B(vfCId1= zlRcOd_tXtZ&{q(&y(P*lf{Ze!T$C`ay{(Lc;Pz^{;3kFDWo=7zWVHmav-#CHfC)bk z>#U|UCfO+9!kZN2n#k!gzQpZ*>iSk=vza%Z*`rj}Pxrn$1V&d&(Z;N|B(eS5w0cL*-|ms$*`dCYmtP z6`B3dyUxx(q-)9N)NPI6&){{k4_J1`9+%uE+$hvHpd2KT{}@T37n;}O}bCX*_5}tBg#2rgg zU=A?2ip9nN=e(-fa;*XQm{U)>#aRQid-j^$PG9d3JaYUPF5MQKEc7tgXj+OO$Us1B z@zv`?L{0fk@zyi6jQzp0(;)I*(#RMr!BBzE597xSzv2N04WT(Dy7NqL7$O*;tm>-99qI1=w;WRBs>ET|J@M zhp`T)qoW<5pitE;UrvX#ksn6l7s3<>3fY^-xeV@vt`GGo=s`L=r$sWMM;_B+k?8?Dx=V;Et#>dCU!phFV&d&5A!31%)buw~evV~CohWHah z3+7m%$J<=>6kgPkFNx9JQ4{T}+mZ?LHu%ZpQgSpK_?q_n)!KXiW6 zXl`j^|A)qJ>Ay>wn*2k~-q{iShm5HS3kVFdd2s~tg3R_`AZHRV;Fp%Q6Q)It2|40Thfxp5WAZKS{^gA8?5C)n4&Xm7$Ma0OQ<&Ula%fAx- zUyxKS>|AaC&v5=z^dBg~j!v$2j@F8fipEwT6Q_TT^Us9;fu!F=w+Q>a_EnBCZqi;%;^5BH?9_--!a6>&cVdW$Hc~?#?B7ltWCAR|E8~C6R^WH-ke3JioezQEwFMykJK$8E?d`#qAjkj8(|^Rx|3>a_`hUgf zf2aO;u|Ks%?d;uOlGVaV(be`}n*S$)eG~fr@IMm%cXa)muKy7O|0Cgl zN7w%~x)A^KQ5j_Wa^L6r@*sTUTH5~d5dGR%R#FTK_4oJ6i3`lj2!g%T2M81tGv)6O zv|Eml%gf*!Cuw={H`@s4SZrj!1fIKJhVY%lwVXulY<@p1LH%{NPUdRqWI^`()?V`y z3N92B8I-h`u$tT4p|@u&NZoz=c>dbSSyNMJ_YJbemz`g`u$^Qf%95|!-cRrfzE5r2 zq$S&-8jz{jLZ8O#j;m!arR^m!Bl@sPZj3epD?G~hxnB$y7A69koC1YRqz0PcK=TDm zbNO+QbbA4EC1{?py|}%gI=QTvQrN8LOyYUG{FHuwzZ1scn?{bt2*V0_aK3nB$ogsk zaF^_b^BC^{2ZR1TJ5%i}koKV_CkzV8=j72>>vNgJ)&)RFQIQ$@LFei63E64rDn4c5IVaE74}#DuJ((CTJna9z;kN_x>3tA08vMU#jDq0*m6d-u@V~e64+s8NEAIlK zpc35?nhE%uG2tV`z;A;Ed3b0{$kBrK{_-mGnftEH=HW+}-}@h?r+A-NBlQE`pe~TT z$S^9WK4c+@Ipf0rlK7X?{KJ9&W}3f!L(?Ne`%UhD*Z9}G|G~(==KcTNp@9E(ng6ev z``i2(VPIviCworuYn*IK?T-cQJC=2dwf2ttO^S!F(*3zfyov0xmSH_9KNOGzF3TXM_70BG;mTIChQW6hX7AS-?bGeX=+Vqqlw!| z^-(llYDC3?A|xEe&UT8Px24lRv3}y}1qb{2k?OuXN4jX_wj{9$fu+r1B_iocUu5wU zGvMX?_09j*ZzowEXgiv}oAq3nYA90Mg1io=E!j(<@_O_<+XI#AJdxg$wi7#+f;Pu^ zEU)_uv<1*_3h>YYpn~)CBB7vFS)E2-ZP1NLl{$$!F_{YPZST}hF5s)@R4lWm=3*uv zMd;6c4FwOK$Vy~oF^GqP9+mnniKdi!k`f>+-jN3}-M2X5asqv)fk{JWs<~;o0?sHc zr?^$-zcEGK)}?~yiBwPkHW3Ytwvh`nh$|7ngVTga`)8>Co}4LicFIoF zTyO7k(Jm4Zvq!#IC)@}22vy8kp=swTxKd$54a1x^tkQ7Puatv9n9@VZO)?N`DJ#bm)%|4SnzF78T1TpYPs*2~;n%b%fisftCvAx*WbRpjSZ$DNAOhx@1 zCz2H?n+s*z$;K;|B#@M(;^`@oe0oz#_#qQ(c6=^_>5$G`e0s`{7i7{VlZb$zI65TC zW>g#HwYPvQIr%!^Z5{#Pv4-AKfWF)hnThPwjPEj8Q)N+`GY+L4HU@$8UJu1lyyKLP zHRQeHj*(fT*`u%L#uBl=rn>9qgsKduBcD6UG}(#W5czZ4kH7DnzcwaoI9$j?v{E~( zp8O2%Qv3C#mD*>bkm@+k6qSpQxT%oM7Ge5vZUavASJV}j@9>5%`Bv0I%`tiRmkv7b z5&b+9shhW>NtLowKYH(^T|#o)oz;^l0x>4jo7^f*6)v>s?{CEkoCx_ciBhyFRtW)N z$x<|HGYLQ4r1kwzn9q2qyeQ(%92aq4%^s@7J6ayewu+e{TcgoI7_DV4o43?*)ntZH zQJt?i!9~X>yRj+oz57x(#bI_7t6z*$zAu~BU;^^nFs#hns7<%27R1EIH3pM-Egkc4 z@y$g* zD2YEfN|$gghw3VAWziii#uvUGOq|dG`8VBScXK`8pLw-cid#%07E9b@_J!YzyNH5l zNP}NL^(u7je8t|*ze+t8yi;`OOEEZjsPSxmgm11p$7Gu6xsnj$Tk;ocR(e$tiA#F5 z6-&6eD+x=3rcs*Mb+GWnI$MZ)Z=deAqD$Q(m4jNi^wZ9ily4=!T#Ov*lGrh>hXjtp zrlHT7<+^hD?BIAc(es(0yU}*)&Z~P_ahi-TAtu~0RXBAidrW7N*VL}GIdVT%RV*<* zIY0$*oIyhb-RO1#k&#eNn9e`rcp+ljwPi%KMOwSBVLO2%-J?+UqWNnYyKQZ~D_4$9 ztipE20ow;O5X%PN{@H0=#?xyb-}5vr=QUfX)&rCMliQ3Y5ZDnj$1D~sEIEWl!GYcT zR(gv3-i;xn)E<}LA; zeT~Ez2AC0)-#L7n@@6 zXWgGt37PnbW0_>Gi1Kt9(6Qp zQ`~RQ)Oi~-zXj$e4kh-%DWo0ePU$mK)!i#er*UC!T1W=!%$Rr+?`Oia37$&C0(w@) zgQXOl8-o^poEeT46oWW=6|l$B$`12lg9&Jh(>{bxGEBdZYCqc?*U|_bAqWgaGBg_? z1q6|!mOr;Lx(pxrPZ5N^IRXQ?xph@wxyTrGxLY3{z~jjdda_qew}Gc+=~RPM@xzkL zO;EV{H^SiA3OGV;aH}C}zVl$D&qd9N|NavO?8^RfEwuT|>%LGcr42`2vn@|ca~O|j z&$n-CZ`MMgpdy8yU=`dq@fD1pQ(QAXp?a-6&Qu>77(d=!CZ?ymXausc2?<4-@I12P zg_Zdn(x>7B8K8>O_(-6PhM#B{fZP1b?##`QZSR{o8-pLEs&EhpWTp;6<8pArXIsC^ zP$V!zzOyCF z9;)mfmncALbg`{)vIT1mH0LV$imo;-9`2*B(DD?r{zY7 zcYV-K0_`n<0F5R|Ml~xC7+#`NW4u3A!h860dhNp38NmG9QIx56BlyLz@z&5x;Jlqf z`DM3|b1dbPgqz|AnX};~cpb*186zBQFW9=!3f;6_uPi{8*Znudks^8o-mdc+tBL${ zdke3{*7VlZXB#KgX-v9#da9P;Tb)%Y*3xCVt6}$*Zg>WN(}Hj_PO@FSw$snZ@OLxoSl929|lZ_=KZSOPWN0p9_G8}VQ9L3 zC3NFp7Jpo~t^d$VY5+|k!kQ?O36sa?a1a{VWo>X>*vG4>${ckD=jUJnCbHCt&1w62WFv6u! z@rQ+)S}yp~A9{yR7x|AmgdWE!Z#GDuc%WL@y1{J&W?iU9&(h6iF>{<-D2E+G-t#5b z9MpU@GLA>7BhEZYt=lkHiY|^bTTirt_0_u$i&fUHvKuby(K&uAYWl=Z7j`dsb6ozC zI7;lOfj1MRj}A7`(~~%Hh`^;HjnbE@$B>&_&ZvbmKbDoztDbR*h7{N#zp?U-&!U8l zzXsKlBJw&bK`2C4$b&qBZwn8qHD&1{K=(UD#wt=!P6ZTGp=cITL>hgs+;kpm+oG7* zbJ=?R`AYuECHpqWY8BIFx%t(6waqK2RIf9Ja!@}&;5^Kry*o}$-ryCvPG4<%)V#q{ zoK1Y8_j-bD+2Kdo_N>{c4-HmaudO~N?W)&hmsTVlBrU>~a9s@^cBan)@vPDat@A8m zh|wSYFe6y>dt<%JYN;yK*Gmw3-47HSD4bUb?HMo@d1mPX4}3rXs0FumdEUnh9wRF& zL>7gFKMSIMKZrR83K!ENx6*Uf^NxfUPL zin8A8>r-LcR=Fx@T&&H7`P|Rqawwwwut+Vwz7}pM>pZ-@#+z>2m-tc5h{^RB-y(eH zsYh^$Z30m<(s-9=@kTz?=T5$M<+E=f8T=22bqVl7ZTVT;((@D2$tH_0e#?2oGcaQ- zb`Uy9rN+8t>Egm^4c_x4FmB=~37g?tY`=@^UenRUn6=RT0*Bj$c_fVC zPEVV8$>&r~6NQq`zTR5}GZi4&higgShkQ!c1~+lY7MRa*ao-2H==X$$yQ<90L9kWt zDt*PBQ%6X8+vD5jeZ3ec%UlQBP3+1zRc2tow@__1NTBO=+Ua()O1ipdZ9DV<(NAv8 z)(H_Q-&M+lm#Ax)TPjM8&R?BD(`fRdkku1x1s4ye8dup3W%x8AEad2iK??h&n3Tid za(d|o(QAt%qibRU3(BxNg1C0Kf0;hEe1puLNcso-(gfF5Kid>rFT^Zs)3#8)M}b)C z{9?t=x3uo6sCH|vOyjPENoQ(R@7Rl-bAHwPn~AHl%EtVwioqfK&>wD&YtSqrz#E_PJOL!IHETrfy13S2|m2nh#VXY z{EmHU}SjNbH8&J-&%^4%g)?N@oe5tsYZl48EtQzgw! zej@}(9dI9KPuzyPNbGbrx8_VF;ZS039 zWx~nXdX3S3Jf{7^7Ou(SUKZo`l$*6U-(aBURRH(h`&HMcao^V7GG96ufom+Evx&&l zoAF4ad&qpb$u`PMxro_7IH~Q=_pyD*cRkwf7aHr^8~L`vpj0FvQv2o>y)gnd0lBW1ne%VTsrex@_+2Gy)%<*B&>@)m zj-1?JErb1Q&C=8^!lfGzU9VC9Q*(Y-LDF07gGUZ{n8vLt8Y6lwXbLhs#_soU8Zt$} zi!28n4+|m^T#Nh-gvnf2LOh}SEp9;O`^Xk#DBioB!PydWKnD4ciLpyGwcd0Ifn#<-^9~Tk>wp_d{=> z!1L9}H9n6LjfXr#9qD?I$t%lseVKxMb*zs|v3`3q=n3l)@b9h-B@jf6x6>EnA{;ug z6S^g3y4wzGmLrg@xeW*VxRfLSJNx>s1kH~%lQG&)4}I3Bb;CaB9jiti@#bJ(X6e(1 zjqc~SG|d-cjfXaNa8Qf}%|zR&5bhzq?E{(X#EgvYtt|cY+XrMjH}8kX>*ni2nd!UM zH-diiPkVN0L$%o%ynG10yW-E-FXgp8$vnWHOaC+^#(sa=u2pAPaJkNXhT@CLwd$&e z3c$9(nJQ{gy{blhGEMdg&ooy+r;#xE{@3HRHz|*S_nU}iM~bPkSuKXtxN^$P6X!88@nDm8S@9P3rL~Qm`oNdXm=W*am1(0@Gwajb6 zS#Depz~Bd7T271u5B3FH-_w1ia9Y&xj*h6+W$8eV~Pm&Inn(#$;YRX;!hF;c zQ-Gi7G|3+w>;(r-ms|}=ua1_HN>WJrau~PQjDytN*V(zgdz6F?&U3S6NubAAKq~rD zZ)jVB4`STqEWDvr+lXJ{#<`*2uX){K?pG#Z~?!o?AQ2AmrM)+(q zX4h8?oVuXWklGVXOv(7NMSev*6N-EF2%~MCwUQ;%>@oATm?}A$2~sIeQe=bt!KS3` z+uA^sj#iaX_hy2(w@eSpeX}%8IBko9r9}w^gRy0F6XPgPkjZ=&PF*;$hMM)YL&_^y zP2T)`)+VI>&rmwt;I{h7{gRju&e=bu_rC4_cwHf>=$-x!1=7Jc`z({~(ZsQJpi3k# zoepoWGbceGY~Y?=k4i-I9Q3`}4Jr{Hk^Y@0-pHiv45a}bTs*)x_TmF1Nwv(Nq?n_B{4ATG`7?1FGTreM9NjLyYPPPD1wfD2kGL zJX@%Pf#T4zQlO>s{C*3HL3Qe0_NPQ;0Xyb`bdA}hbNKW!*>F+E@$Gn*2!~Jw-k1fo zyRZ$ihW3wWxM7SCGbS@h-9WMJ_F8xBhK!Xxmd*I1WPHf^8*3&q=H0en+zFZL zdO{1tG$`+L(0mA!NIL5r(xjb8uQDy~qY7#X9JcSS*JTL9sFmxLW|=A~iETL!&TH|E z+tA0V?-NoAtB#R>1Y?#W&L&9Qlz415WQM*uWss&-4G0Ow45X|}b>_SxZcv`>2Rmyy z?6ODKFmxVvP@CyD*jX-3AZW^5QKR`(z8Vog_+BIIn6PMjY!&R}N_?E{pF{S#^iYuPV->oM{>ET?izSBX!-bs`*_N9E|?*%AhOoPbMv8jO#^ zdsbXcxMACmpeb?npmo2|Vdrc|c+&vVBu_Id^hz(ZRfzBJ|H^w-JC*ojd{gsazDF?7 zTBdhQ{vF3sR*a`h6v8q31xl)Yj_GiCl_KClV-cO_-RNU>wTtkTNJaA4I4?9#;8tq) z)7fVi=0F4e1Hpz_d+72s4ny_tNnZ_q!52m0I#N%|Pq@hNyg77%0~sI2ptMX1+2Do= zp?Oo+9qO)g3Kxcf#9Yi|X72(jX(@F=386s|Y4+nd&AE{BhEpDn)iEQkjB8e6FI!?8 zn5_`%SsFs0^<*=Zc;d!oFKK9SbodURZzW5E`VLj{ZiH;9PCtGlH zVxO(4sG0QmMNSJG4o!tWT)4%eVH1 z$eg>#jO*Ut`HbMVTr2D&u5=g;=GVVtY%(F#)xiLgSHgsm9I7jLDkJn(Fbl2E264Qp zI|xcMP;WrOUIN7Lu^m=rDAcMv=8m7F=m82nd?b}@m#=IzC)qZMS=cc>nDdsU=5NGMsw! zK5Tv@03<1yE0V(N|@&hx_lpryU)LYd9Q$Jcl&;NI`ixC{lCKcduSRZj^ZV)9+))6(sN-otqvnSfxnoU0?B*S< zz1nFVutF`p={2#B3Fq}5+%3{i{?F$85m-_4YK9>)MB7!IxiIrHLcL6vz^h+*J`|Lj z-cqi<^o<2+Iwe~@sO0b5iK?6{_&e-g(Y_`8nnqX?jnKDon@wzOsgi(6RINvXY+p`g z$Db1BNX2PbzZM)-v6>>|x=b&?cL+HfsT_1{XF~XJq!m>cP7=uV5-`LLP-ymD`FT^l zHp&ZhP5WQxg&k*xYP?Q6N*c2~88N@tzp{)a`s#quTsk)u&S=P>Sg*b`I1a|oqDQsEH1iN5er5gOM&HSc1ZkB-3j%F_72X)J zDf-cryzD!w+8mXwcbyaNG-vN`2{72F=`?j^3X_~j75#Z2^MrX#1o)q@i0ApEQ|<9D zQx+qt(MLPhvQ4GzOO+G?*bhbInPDeM%Y0-CeH|Op?AGTtg8}^MF5{g`1_t#FJlR(C zUeylRUeZn-1vL|?_PT8mLwI((9a^T>=syLwFT}pif004Bltrxy%ZQ%qGUg!$sK*%3 zeW!^$MY-bE)+EiRSxhNBu^hg+;XAM9G)Psby))$&G`c4!molZ44&$@LNj3uTO^@!d zgc!ONv_JEy7|u&c+kD!{PsSR@rO};SE|Ix)$o6!`UJA*G$yjh%2IXt#LZmTdhn5|C zh}ZztE#ob?7+$erStZDw05gf6MJk&ry;+P!<`4S})KyFs*VJ?Y-S2p~T*|^27BMSg zvc|%Tz_F3G1*SL&JHeO{s?pUIPTaSxyGzi!bGFqiUF;Czl=h&FRGh7;atC_wE8CEk zkLxFzQVxTF_>~yt!J^sC7{w4HRJ+)rSKzyM&e@)KM_9upvi>y+v7>HuKX+A>Lq89& zvOD8a7lNe%Y@v5(A~f+u&zcgF4`tw;CO zG_M>V>W+EGn%m^5J7%}5iuv@DfY$J%66}q&j3)75$2fZbXKC zR2uG2vpbyG^r6Zly>GUXDMsB*r?-hyA(pQpQ|2&9Cf4OSH}9X&S|&NXnNJPRxu5Fh zuA9H|h1;Qipzf3tq2S5;J|A$kk4#&!Xw#i4d!{y;eZb7ei~@N#V*aEPKd|op)AGA1 zbM*SHN^001v>*xZ4p;AJO*Y>2ndLF_#9I7X>`~WPrAV>v_o@1LHo|Gc2JxwUQyiUO z3R{{))JT_QC-sz#+3|oQ(f-DXAY0yL4{f^Kra|Pie1~c$9_vXE-EeZ-my=>n<;!gX zY2%WrxFJg!x}p4?54{K7S&J;ml+(Bp{^ocIXruR9bh)0EOa5S#Lon2I)Ac&Y!D-Sb4!_L z_4IeXO_8nLG&-G!(*qbJ!QxkLy%>Zj#7x8Q$|M{E)Xyq$Xg&)@H5y2>ZTf-t`xcVa z%(q{CT~!iw>I+xm;ExECPl@(`NRz|a@GkxEQ$F~RM*yetaVvOC*f)P%L1kd=>sGa2 z6)uAF6g4xD(6U?eAhxXDS#m=|=95(Cc2QDCv@|$%#F(2v=R}3vEeh+HTbW_!&EULz z$Ok#r1yiO{SLOxV!EIKX{_J@CI4Q^VGG)X=L$pB_4o(KNUB*?(bPs|e0qtW(M_24; z+i4fpTaY~PM}JWU^sz$eyg2b=!LhEffbvniY5{%tsiIkFBL%iEtKoY(xDsdP)sDOE z(3pdRi(=MjaF9puW3f80althtNY8ZTE``>G+l+mMBWA!ilU>w$2-M7^;&0dT zH_E@9jKGS;X^efVZRTI{it$=l-tLvXproVAWuDa2@K9~9gfJA4Op;}7hu%_ks#_ab zfM@UgO|jl{A}AW!K#VPr-*Y57CDxAX1g1rvd}#16G;HXouj!9;+Sb9g_7+GQs#B=( zc!!abo;%=bh*iS6?(!qXHW5=tkV@c_+D7=QNrw`Lcts`g+urz+<9z(W0t99mke5#u&VJ zQLqzG(5iV^cNf1V{#v{lG9=ip|CpYuP3oFTt#CISkJcQD=jDD=Y_Lxgy+cnhmLEe2 z{R_)esAJs^upU<6JN^>z8);R!`f}=*?5es}oVOE0XK4_+7Qso3>WYOmLSq=ES1uTYi3rjV_4TExx_mP2(lhZeAJ| z(Ef?q5e;}gLf?oM{+5(`;?}CP-bXW}S0RfS_b4Vuo#lM7Mqb~17c_-G>)eBIh_3(X z8)Z90%q`pjVcF)UI3i*@x9;8@W?Q}eONT?!(g^wYHh~~BMa{2A9~v5crPV7ShQ}J2_;37Oa=L9r}$kLsBeHIl{pySk8mXym~MDb6!Z1|Hs3^ z_^crkBQKHw?vC;6ezkQr)F_QnL&wP@Nv z-)nKiY_*%XScnRQta>7WxOZb vuRc>EPS7-P)8I9#F{pO=L@RkeKVLcX;BwqNTfDq73neYCAXX}3`1$_>T9x!W literal 0 HcmV?d00001 diff --git a/plugins/SlicerT/bg_no_logo.png b/plugins/SlicerT/bg_no_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b017cc9f28f88c8dec2d517f970c800efbff5596 GIT binary patch literal 16125 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 aMsaqnwE353-F&44O+`UdzF5}s<^KY@cnlB# literal 0 HcmV?d00001 diff --git a/plugins/SlicerT/icon.png b/plugins/SlicerT/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f2c4fabf14facdba53038995a70700bd9a14ad06 GIT binary patch literal 759 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 literal 0 HcmV?d00001 diff --git a/plugins/SlicerT/logo.png b/plugins/SlicerT/logo.png index aead03f79a8a4d5b6be99c7b70d15a1f96022482..08eb67b128186b67fcc509ee7f55d803bfc0b992 100644 GIT binary patch delta 8359 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 delta 36542 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⁡RFA!xA=se!YDD3iIottb(A;zZ~)yzRA!Ht6HBd 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 literal 0 HcmV?d00001 diff --git a/plugins/SlicerT/slide_indicator_bar.png b/plugins/SlicerT/slide_indicator_bar.png new file mode 100644 index 0000000000000000000000000000000000000000..b17a644e8209f839c61ff3c99b33338ef541eef5 GIT binary patch literal 197 zcmeAS@N?(olHy`uVBq!ia0vp^OhD|*=^Qfk1qw-)xJHyX=jZ08=9Mrw7o{eaq^2m8 zXO?6rxO@5rgg5eu0~P6dx;TbtoKJqUV#fFRjg1fGg_)U|ohPI#2@5le^ccBGHqSYD i=)i#w`FFqlO>_ z%)p?h48n{ROYO^mg6t)pzOL-gIR!;I6i!!3cmahZOI#yLobz*YQ}ap~oQqNuOHxx5 z$}>wc6x=<11Hv2m#DR*sJY5_^JdP)CN%H*le&xIa4ZJyf?7AB}8yOO;zNO1Op2$Ai zP=)W_{R0Kh?hCo8v9YnW&5hqJxqgog+w?tu9!VUYmtWf>u}f&lvcLWlW_^>7>|vd` zSn&A40|yR#=wHZulJ~_jmP)ln9fIGEu4oEmUCiYs+M^{AYOpFHs7(8A5T-G@yGywo1mt=?l literal 0 HcmV?d00001 From f2410fd28a643e2e3d235a6a72568ff80b5d8589 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Thu, 21 Sep 2023 17:17:50 +0200 Subject: [PATCH 28/99] fixed cmake --- cmake/modules/PluginList.cmake | 1 + plugins/CMakeLists.txt | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/modules/PluginList.cmake b/cmake/modules/PluginList.cmake index 0a4686fb2c6..fe2e03eee24 100644 --- a/cmake/modules/PluginList.cmake +++ b/cmake/modules/PluginList.cmake @@ -59,6 +59,7 @@ SET(LMMS_PLUGIN_LIST Sf2Player Sfxr Sid + SlicerT SpectrumAnalyzer StereoEnhancer StereoMatrix diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index aaa92089a70..9a71be4b823 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -18,4 +18,3 @@ INCLUDE_DIRECTORIES( FOREACH(PLUGIN ${PLUGIN_LIST}) ADD_SUBDIRECTORY(${PLUGIN}) ENDFOREACH() -ADD_SUBDIRECTORY("SlicerT") From 81977f359736464c619241ccb9f2cd5f20ae4643 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Fri, 22 Sep 2023 23:32:04 +0200 Subject: [PATCH 29/99] Waveform UI finished --- plugins/SlicerT/SlicerT.h | 2 +- plugins/SlicerT/SlicerTUI.cpp | 18 +-- plugins/SlicerT/SlicerTUI.h | 18 +-- plugins/SlicerT/WaveForm.cpp | 241 +++++++++++++++++----------------- plugins/SlicerT/WaveForm.h | 39 ++++-- plugins/SlicerT/bg.png | Bin 19736 -> 20842 bytes 6 files changed, 164 insertions(+), 154 deletions(-) diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 3f4dc482cad..8c0a10a3be9 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -62,7 +62,7 @@ class PhaseVocoder { // timeshift stuff static const int windowSize = 512; - static const int overSampling = 64; + static const int overSampling = 32; // depending on scaleRatio int stepSize = 0; diff --git a/plugins/SlicerT/SlicerTUI.cpp b/plugins/SlicerT/SlicerTUI.cpp index 711470bf7c7..c2115a735bf 100644 --- a/plugins/SlicerT/SlicerTUI.cpp +++ b/plugins/SlicerT/SlicerTUI.cpp @@ -56,7 +56,7 @@ SlicerTUI::SlicerTUI( SlicerT * instrument, m_bpmBox(3, "19green", this), m_resetButton(this, nullptr), m_midiExportButton(this, nullptr), - m_wf(248, 132, instrument, this) + m_wf(248, 128, instrument, this) { setAcceptDrops( true ); setAutoFillBackground( true ); @@ -65,24 +65,24 @@ SlicerTUI::SlicerTUI( SlicerT * instrument, pal.setBrush( backgroundRole(), PLUGIN_NAME::getIconPixmap( "bg" ) ); setPalette( pal ); - m_wf.move(2, 2); + m_wf.move(2, 6); - m_bpmBox.move(7, 153); + m_bpmBox.move(115, 200); m_bpmBox.setToolTip(tr("Original sample BPM")); m_bpmBox.setLabel(tr("BPM")); m_bpmBox.setModel(&m_slicerTParent->m_originalBPM); - m_noteThresholdKnob.move(7, 200); + m_noteThresholdKnob.move(15, 200); m_noteThresholdKnob.setToolTip(tr("Threshold used for slicing")); - // m_noteThresholdKnob.setLabel(tr("Threshold")); + m_noteThresholdKnob.setLabel(tr("Threshold")); m_noteThresholdKnob.setModel(&m_slicerTParent->m_noteThreshold); - m_fadeOutKnob.move(200, 200); + m_fadeOutKnob.move(65, 200); m_fadeOutKnob.setToolTip(tr("FadeOut for notes")); - // m_fadeOutKnob.setLabel(tr("FadeOut")); + m_fadeOutKnob.setLabel(tr("FadeOut")); m_fadeOutKnob.setModel(&m_slicerTParent->m_fadeOutFrames); - m_midiExportButton.move(145, 198); + m_midiExportButton.move(185, 200); m_midiExportButton.setActiveGraphic( embed::getIconPixmap("midi_tab") ); m_midiExportButton.setInactiveGraphic( @@ -90,7 +90,7 @@ SlicerTUI::SlicerTUI( SlicerT * instrument, m_midiExportButton.setToolTip(tr("Copy midi pattern to clipboard")); connect(&m_midiExportButton, SIGNAL( clicked() ), this, SLOT( exportMidi() )); - m_resetButton.move(80, 198); + m_resetButton.move(225, 200); m_resetButton.setActiveGraphic( embed::getIconPixmap("reload") ); m_resetButton.setInactiveGraphic( diff --git a/plugins/SlicerT/SlicerTUI.h b/plugins/SlicerT/SlicerTUI.h index 40ac38cbbe5..118d9682b57 100644 --- a/plugins/SlicerT/SlicerTUI.h +++ b/plugins/SlicerT/SlicerTUI.h @@ -47,16 +47,16 @@ namespace gui class SlicerTKnob : public Knob { public: SlicerTKnob( QWidget * _parent ) : - Knob( KnobType::Styled, _parent ) + Knob( KnobType::Bright26, _parent ) { - setFixedSize( 46, 40 ); - setCenterPointX( 23.0 ); - setCenterPointY( 15.0 ); - setInnerRadius( 3 ); - setOuterRadius( 11 ); - // setTotalAngle( 270.0 ); - setLineWidth( 3 ); - setOuterColor( QColor(178, 115, 255) ); + // setFixedSize( 46, 40 ); + // setCenterPointX( 23.0 ); + // setCenterPointY( 15.0 ); + // setInnerRadius( 3 ); + // setOuterRadius( 11 ); + // // setTotalAngle( 270.0 ); + // setLineWidth( 3 ); + // setOuterColor( QColor(178, 115, 255) ); } }; diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/WaveForm.cpp index 115e24c258e..d59acc8cfdf 100644 --- a/plugins/SlicerT/WaveForm.cpp +++ b/plugins/SlicerT/WaveForm.cpp @@ -25,6 +25,7 @@ #include "WaveForm.h" #include "SlicerT.h" +#include "embed.h" namespace lmms { @@ -34,17 +35,24 @@ namespace gui { WaveForm::WaveForm(int w, int h, SlicerT * instrument, QWidget * parent) : QWidget(parent), - m_sliceEditor(QPixmap(w, h - m_seekerHeight - m_margin)), - m_seeker(QPixmap(w, m_seekerHeight)), - m_seekerWaveform(QPixmap(w, m_seekerHeight)), + // calculate sizes + m_width(w), + m_height(h), + m_seekerWidth(w - m_seekerHorMargin * 2), + m_editorHeight(h - m_seekerHeight - m_middleMargin), + m_editorWidth(w), + + // create pixmaps + m_sliceArrow(PLUGIN_NAME::getIconPixmap( "slide_indicator_arrow" )), + m_seeker(QPixmap(m_seekerWidth, m_seekerHeight)), + m_seekerWaveform(QPixmap(m_seekerWidth, m_seekerHeight)), + m_sliceEditor(QPixmap(w, m_editorHeight)), + + // references to instrument vars + m_slicerTParent(instrument), m_currentSample(instrument->m_originalSample), m_slicePoints(instrument->m_slicePoints) { - m_width = w; - m_height = h; - m_editorHeight = h - m_seekerHeight - m_margin; - - m_slicerTParent = instrument; setFixedSize(m_width, m_height); setMouseTracking( true ); @@ -55,54 +63,18 @@ WaveForm::WaveForm(int w, int h, SlicerT * instrument, QWidget * parent) : SIGNAL(isPlaying(float, float, float)), this, SLOT(isPlaying(float, float, float))); - - connect(m_slicerTParent, SIGNAL(dataChanged()), this, SLOT(updateData())); + connect(m_slicerTParent, SIGNAL(dataChanged()), this, SLOT(updateUI())); updateUI(); } -void WaveForm::drawEditor() -{ - m_sliceEditor.fill(m_waveformBgColor); - QPainter brush(&m_sliceEditor); - - float startFrame = m_seekerStart * m_currentSample.frames(); - float endFrame = m_seekerEnd * m_currentSample.frames(); - - brush.setPen(m_playHighlighColor); - brush.drawLine(0, m_editorHeight/2, m_width, m_editorHeight/2); - - brush.setPen(m_waveformColor); - m_currentSample.visualize( - brush, - QRect( 0, 0, m_width, m_editorHeight ), - startFrame, endFrame); - - - for (int i = 0;i= startFrame && sliceIndex <= endFrame) - { - float xPos = (float)(sliceIndex - startFrame) / (float)(endFrame - startFrame) * (float)m_width; - if (i == m_sliceSelected) - { - brush.setPen(QPen(m_selectedSliceColor, 2)); - } - - brush.drawLine(xPos, 0, xPos, m_editorHeight); - } - } -} - +// graphics void WaveForm::drawSeekerWaveform() { + m_seekerWaveform.fill(m_waveformBgColor); QPainter brush(&m_seekerWaveform); brush.setPen(m_waveformColor); - m_seekerWaveform.fill(m_waveformBgColor); m_currentSample.visualize( brush, QRect( 0, 0, m_seekerWaveform.width(), m_seekerWaveform.height() ), @@ -111,41 +83,74 @@ void WaveForm::drawSeekerWaveform() void WaveForm::drawSeeker() { - m_seeker.fill(QColor(0, 0, 0, 0)); + m_seeker.fill(m_waveformBgColor); QPainter brush(&m_seeker); - brush.setPen(m_waveformColor); - // draw slice points brush.setPen(m_sliceColor); for (int i = 0;im_originalSample.data(), m_slicerTParent->m_originalSample.frames()); - updateUI(); -} - void WaveForm::isPlaying(float current, float start, float end) { m_noteCurrent = current; @@ -170,10 +168,12 @@ void WaveForm::isPlaying(float current, float start, float end) update(); } +// events void WaveForm::mousePressEvent( QMouseEvent * me ) { - float normalizedClick = (float)me->x() / m_width; - + float normalizedClickSeeker = (float)(me->x() - m_seekerHorMargin) / m_seekerWidth; + float normalizedClickEditor = (float)(me->x()) / m_editorWidth; + // reset seeker on middle click if (me->button() == Qt::MouseButton::MiddleButton) { m_seekerStart = 0; @@ -181,44 +181,39 @@ void WaveForm::mousePressEvent( QMouseEvent * me ) return; } - if (me->y() < m_seekerHeight) + if (me->y() < m_seekerHeight) // seeker click { - if (abs(normalizedClick - m_seekerStart) < 0.03) + if (abs(normalizedClickSeeker - m_seekerStart) < distanceForClick) // dragging start { m_currentlyDragging = m_draggingTypes::m_seekerStart; - - } else if (abs(normalizedClick - m_seekerEnd) < 0.03) + } else if (abs(normalizedClickSeeker - m_seekerEnd) < distanceForClick) // dragging end { m_currentlyDragging = m_draggingTypes::m_seekerEnd; - } else if (normalizedClick > m_seekerStart && normalizedClick < m_seekerEnd) + } else if (normalizedClickSeeker > m_seekerStart && normalizedClickSeeker < m_seekerEnd) // dragging middle { m_currentlyDragging = m_draggingTypes::m_seekerMiddle; - m_seekerMiddle = normalizedClick; + m_seekerMiddle = normalizedClickSeeker; } - - } else { + } else { // editor click m_sliceSelected = -1; float startFrame = m_seekerStart * m_currentSample.frames(); float endFrame = m_seekerEnd * m_currentSample.frames(); - + // select slice for (int i = 0;ibutton() == Qt::MouseButton::LeftButton) - { - m_isDragging = true; - } else if (me->button() == Qt::MouseButton::RightButton) + if (me->button() == Qt::MouseButton::RightButton) // erase selected slice { if (m_sliceSelected != -1 && m_slicePoints.size() > 2) { @@ -226,66 +221,66 @@ void WaveForm::mousePressEvent( QMouseEvent * me ) m_sliceSelected = -1; } } - + updateUI(); } void WaveForm::mouseReleaseEvent( QMouseEvent * me ) { - m_isDragging = false; m_currentlyDragging = m_draggingTypes::nothing; updateUI(); } void WaveForm::mouseMoveEvent( QMouseEvent * me ) { - float normalizedClick = (float)me->x() / m_width; + float normalizedClickSeeker = (float)(me->x() - m_seekerHorMargin) / m_seekerWidth; + float normalizedClickEditor = (float)(me->x()) / m_editorWidth; + + float distStart = m_seekerStart - m_seekerMiddle; + float distEnd = m_seekerEnd - m_seekerMiddle; + float startFrame = m_seekerStart * m_currentSample.frames(); + float endFrame = m_seekerEnd * m_currentSample.frames(); // handle dragging events - if (m_isDragging) + switch (m_currentlyDragging) { - if (m_currentlyDragging == m_draggingTypes::m_seekerStart) - { - m_seekerStart = std::clamp(normalizedClick, 0.0f, m_seekerEnd - 0.13f); + case m_draggingTypes::m_seekerStart: + m_seekerStart = std::clamp(normalizedClickSeeker, 0.0f, m_seekerEnd - minSeekerDistance); + break; - } else if (m_currentlyDragging == m_draggingTypes::m_seekerEnd) - { - m_seekerEnd = std::clamp(normalizedClick, m_seekerStart + 0.13f, 1.0f);; - - } else if (m_currentlyDragging == m_draggingTypes::m_seekerMiddle) - { - float distStart = m_seekerStart - m_seekerMiddle; - float distEnd = m_seekerEnd - m_seekerMiddle; + case m_draggingTypes::m_seekerEnd: + m_seekerEnd = std::clamp(normalizedClickSeeker, m_seekerStart + minSeekerDistance, 1.0f); + break; - m_seekerMiddle = normalizedClick; - - if (m_seekerMiddle + distStart >= 0 && m_seekerMiddle + distEnd <= 1) - { - m_seekerStart = m_seekerMiddle + distStart; - m_seekerEnd = m_seekerMiddle + distEnd; - } + case m_draggingTypes::m_seekerMiddle: + m_seekerMiddle = normalizedClickSeeker; - } else if (m_currentlyDragging == m_draggingTypes::slicePoint) + if (m_seekerMiddle + distStart >= 0 && m_seekerMiddle + distEnd <= 1) { - float startFrame = m_seekerStart * m_currentSample.frames(); - float endFrame = m_seekerEnd * m_currentSample.frames(); + m_seekerStart = m_seekerMiddle + distStart; + m_seekerEnd = m_seekerMiddle + distEnd; + } + break; - m_slicePoints[m_sliceSelected] = startFrame + normalizedClick * (endFrame - startFrame); + case m_draggingTypes::m_slicePoint: // TODO: fix this + m_slicePoints[m_sliceSelected] = startFrame + normalizedClickEditor * (endFrame - startFrame); - m_slicePoints[m_sliceSelected] = std::clamp(m_slicePoints[m_sliceSelected], 0, m_currentSample.frames()); + m_slicePoints[m_sliceSelected] = std::clamp(m_slicePoints[m_sliceSelected], 0, m_currentSample.frames()); - std::sort(m_slicePoints.begin(), m_slicePoints.end()); - } - updateUI(); + std::sort(m_slicePoints.begin(), m_slicePoints.end()); + break; + case m_draggingTypes::nothing: + break; } + updateUI(); } void WaveForm::mouseDoubleClickEvent(QMouseEvent * me) { - float normalizedClick = (float)me->x() / m_width; + float normalizedClickEditor = (float)(me->x()) / m_editorWidth; float startFrame = m_seekerStart * m_currentSample.frames(); float endFrame = m_seekerEnd * m_currentSample.frames(); - float slicePosition = startFrame + normalizedClick * (endFrame - startFrame); + float slicePosition = startFrame + normalizedClickEditor * (endFrame - startFrame); for (int i = 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 delta 15495 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 From 5ba792be87c42bb2f89cdf59253862cab920ced0 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sat, 23 Sep 2023 12:41:51 +0200 Subject: [PATCH 30/99] Roxas UI knobs + layout --- plugins/SlicerT/SlicerT.cpp | 27 +++++++++++++++++---------- plugins/SlicerT/SlicerT.h | 5 +++-- plugins/SlicerT/SlicerTUI.cpp | 12 ++++++------ plugins/SlicerT/SlicerTUI.h | 17 ++++++++--------- plugins/SlicerT/bg.png | Bin 20842 -> 23763 bytes 5 files changed, 34 insertions(+), 27 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index d60764b6459..d8f5fdd8fe7 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -25,7 +25,8 @@ // TODO: better onset detection + bpm cleanup // TODO: switch to arrayVector (maybe) // TODO: cleaunp UI classes -// TODO: implment roxas new UI +// TODO: add text when no sample loaded +// TODO: better buttons // TODO: code cleaunp, style and test #include "SlicerT.h" @@ -83,13 +84,13 @@ PhaseVocoder::~PhaseVocoder() { void PhaseVocoder::loadData(std::vector originalData, int sampleRate, float newRatio) { originalBuffer = originalData; originalSampleRate = sampleRate; - scaleRatio = -1; // force update, kinda hacky + m_scaleRatio = -1; // force update, kinda hacky updateParams(newRatio); dataLock.lock(); - // set buffer size + // set buffer sizes m_processedWindows.resize(numWindows, false); lastPhase.resize(numWindows*windowSize, 0); sumPhase.resize(numWindows*windowSize, 0); @@ -132,19 +133,19 @@ void PhaseVocoder::getFrames(std::vector & outData, int start, int frames // adjust pv params and reset buffers void PhaseVocoder::updateParams(float newRatio) { if (originalBuffer.size() < 2048) { return; } - if (newRatio == scaleRatio) { return; } + if (newRatio == m_scaleRatio) { return; } dataLock.lock(); // TODO: remove static stuff from here, like stepsize - scaleRatio = newRatio; + m_scaleRatio = newRatio; stepSize = (float)windowSize / overSampling; numWindows = (float)originalBuffer.size() / stepSize - overSampling - 1; - outStepSize = scaleRatio * (float)stepSize; // float, else inaccurate + outStepSize = m_scaleRatio * (float)stepSize; // float, else inaccurate freqPerBin = originalSampleRate/windowSize; expectedPhaseIn = 2.*M_PI*(float)stepSize/(float)windowSize; expectedPhaseOut = 2.*M_PI*(float)outStepSize/(float)windowSize; - processedBuffer.resize(scaleRatio*originalBuffer.size(), 0); + processedBuffer.resize(m_scaleRatio*originalBuffer.size(), 0); // very slow :( std::fill(m_processedWindows.begin(), m_processedWindows.end(), false); @@ -269,7 +270,7 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) SlicerT::SlicerT(InstrumentTrack * instrumentTrack) : Instrument( instrumentTrack, &slicert_plugin_descriptor ), m_noteThreshold(0.6f, 0.0f, 2.0f, 0.01f, this, tr( "Note threshold" ) ), - m_fadeOutFrames(0.0f, 0.0f, 8192.0f, 4.0f, this, tr("FadeOut")), + m_fadeOutFrames(400.0f, 0.0f, 8192.0f, 1.0f, this, tr("FadeOut")), m_originalBPM(1, 1, 999, this, tr("Original bpm")), m_originalSample(), m_phaseVocoder() @@ -386,6 +387,7 @@ void SlicerT::findBPM() if (m_originalSample.frames() < 2048) { return; } int bpmSnap = 1; // 1 = disabled + // caclulate length of sample float sampleRate = m_originalSample.sampleRate(); float totalFrames = m_originalSample.frames(); float sampleLength = totalFrames / sampleRate; @@ -417,13 +419,18 @@ void SlicerT::writeToMidi(std::vector * outClip) { if (m_originalSample.frames() < 2048) { return; } - int ticksPerBar = DefaultTicksPerBar; - float sampleRate = m_originalSample.sampleRate(); + // update incase bpm changed + const float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo(); + m_phaseVocoder.setScaleRatio(speedRatio); + // calculate how many "beats" are in the sample + float ticksPerBar = DefaultTicksPerBar; + float sampleRate = m_originalSample.sampleRate(); float bpm = Engine::getSong()->getTempo(); float samplesPerBeat = 60.0f / bpm * sampleRate; float beats = (float)m_phaseVocoder.frames() / samplesPerBeat; + // calculate how many ticks in sample float barsInSample = beats / Engine::getSong()->getTimeSigModel().getDenominator(); float totalTicks = ticksPerBar * barsInSample; diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 8c0a10a3be9..25142f41118 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -48,13 +48,14 @@ class PhaseVocoder { void setScaleRatio(float newRatio) { updateParams(newRatio); } void getFrames(std::vector & outData, int start, int frames); int frames() { return processedBuffer.size(); } + float scaleRatio() { return m_scaleRatio; } private: QMutex dataLock; // original data std::vector originalBuffer; int originalSampleRate = 0; - float scaleRatio = -1; // to force on fisrt load + float m_scaleRatio = -1; // to force on fisrt load // output data std::vector processedBuffer; @@ -125,12 +126,12 @@ class dinamicPlaybackBuffer { } } int frames() { return leftChannel.frames(); } + float scaleRatio() { return leftChannel.scaleRatio(); } void setScaleRatio(float newRatio) { leftChannel.setScaleRatio(newRatio); rightChannel.setScaleRatio(newRatio); } private: - float scaleRatio; PhaseVocoder leftChannel; PhaseVocoder rightChannel; }; diff --git a/plugins/SlicerT/SlicerTUI.cpp b/plugins/SlicerT/SlicerTUI.cpp index c2115a735bf..e1af70c2847 100644 --- a/plugins/SlicerT/SlicerTUI.cpp +++ b/plugins/SlicerT/SlicerTUI.cpp @@ -53,7 +53,7 @@ SlicerTUI::SlicerTUI( SlicerT * instrument, m_slicerTParent(instrument), m_noteThresholdKnob(this), m_fadeOutKnob(this), - m_bpmBox(3, "19green", this), + m_bpmBox(3, "21pink", this), m_resetButton(this, nullptr), m_midiExportButton(this, nullptr), m_wf(248, 128, instrument, this) @@ -67,22 +67,22 @@ SlicerTUI::SlicerTUI( SlicerT * instrument, m_wf.move(2, 6); - m_bpmBox.move(115, 200); + m_bpmBox.move(135, 200); m_bpmBox.setToolTip(tr("Original sample BPM")); m_bpmBox.setLabel(tr("BPM")); m_bpmBox.setModel(&m_slicerTParent->m_originalBPM); - m_noteThresholdKnob.move(15, 200); + m_noteThresholdKnob.move(14, 195); m_noteThresholdKnob.setToolTip(tr("Threshold used for slicing")); m_noteThresholdKnob.setLabel(tr("Threshold")); m_noteThresholdKnob.setModel(&m_slicerTParent->m_noteThreshold); - m_fadeOutKnob.move(65, 200); + m_fadeOutKnob.move(80, 195); m_fadeOutKnob.setToolTip(tr("FadeOut for notes")); m_fadeOutKnob.setLabel(tr("FadeOut")); m_fadeOutKnob.setModel(&m_slicerTParent->m_fadeOutFrames); - m_midiExportButton.move(185, 200); + m_midiExportButton.move(190, 200); m_midiExportButton.setActiveGraphic( embed::getIconPixmap("midi_tab") ); m_midiExportButton.setInactiveGraphic( @@ -90,7 +90,7 @@ SlicerTUI::SlicerTUI( SlicerT * instrument, m_midiExportButton.setToolTip(tr("Copy midi pattern to clipboard")); connect(&m_midiExportButton, SIGNAL( clicked() ), this, SLOT( exportMidi() )); - m_resetButton.move(225, 200); + m_resetButton.move(215, 200); m_resetButton.setActiveGraphic( embed::getIconPixmap("reload") ); m_resetButton.setInactiveGraphic( diff --git a/plugins/SlicerT/SlicerTUI.h b/plugins/SlicerT/SlicerTUI.h index 118d9682b57..425e4e7669e 100644 --- a/plugins/SlicerT/SlicerTUI.h +++ b/plugins/SlicerT/SlicerTUI.h @@ -47,16 +47,15 @@ namespace gui class SlicerTKnob : public Knob { public: SlicerTKnob( QWidget * _parent ) : - Knob( KnobType::Bright26, _parent ) + Knob( KnobType::Styled, _parent ) { - // setFixedSize( 46, 40 ); - // setCenterPointX( 23.0 ); - // setCenterPointY( 15.0 ); - // setInnerRadius( 3 ); - // setOuterRadius( 11 ); - // // setTotalAngle( 270.0 ); - // setLineWidth( 3 ); - // setOuterColor( QColor(178, 115, 255) ); + setFixedSize( 46, 40 ); + setCenterPointX( 23.0 ); + setCenterPointY( 15.0 ); + setInnerRadius( 3 ); + setOuterRadius( 11 ); + setLineWidth( 3 ); + setOuterColor( QColor(178, 115, 255) ); } }; diff --git a/plugins/SlicerT/bg.png b/plugins/SlicerT/bg.png index d87224f890c91457ea6a2b1f947101e572171b9d..a9d1c82733c280ac39f31ae30938e18e134f27c4 100644 GIT binary patch delta 14835 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 M#N}a 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 From 648d6b7e4e4d0143016659b6e3b9a2936564abaf Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 24 Sep 2023 19:28:24 +0200 Subject: [PATCH 31/99] Spectral flux onset detection --- plugins/SlicerT/SlicerT.cpp | 74 ++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 26 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index d8f5fdd8fe7..36bab44818e 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -22,7 +22,8 @@ * */ -// TODO: better onset detection + bpm cleanup +// TODO: fix some PV bugs +// TODO: onset detection find valleys // TODO: switch to arrayVector (maybe) // TODO: cleaunp UI classes // TODO: add text when no sample loaded @@ -269,7 +270,7 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) SlicerT::SlicerT(InstrumentTrack * instrumentTrack) : Instrument( instrumentTrack, &slicert_plugin_descriptor ), - m_noteThreshold(0.6f, 0.0f, 2.0f, 0.01f, this, tr( "Note threshold" ) ), + m_noteThreshold(0.3f, 0.0f, 2.0f, 0.01f, this, tr( "Note threshold" ) ), m_fadeOutFrames(400.0f, 0.0f, 8192.0f, 1.0f, this, tr("FadeOut")), m_originalBPM(1, 1, 999, this, tr("Original bpm")), m_originalSample(), @@ -340,41 +341,58 @@ void SlicerT::playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) } } - +// uses the spectral flux to determine the change in magnitude +// resources: +// http://www.iro.umontreal.ca/~pift6080/H09/documents/papers/bello_onset_tutorial.pdf void SlicerT::findSlices() { if (m_originalSample.frames() < 2048) { return; } m_slicePoints = {}; + const int windowSize = 1024; + const int minDist = 2048; - const int window = 1024; - int minWindowsPassed = 1; - int peakIndex = 0; + std::vector leftChannel(m_originalSample.frames(), 0); - float lastPeak = 0; - float currentPeak = 0; + for (int i = 0;i prevMags(windowSize, 0); + std::vector fftIn(windowSize, 0); + fftwf_complex fftOut[windowSize]; - if (sampleValue > currentPeak) - { - currentPeak = sampleValue; - peakIndex = i; + fftwf_plan fftPlan = fftwf_plan_dft_r2c_1d(windowSize, fftIn.data(), fftOut, FFTW_MEASURE); + + int lastPoint = -minDist - 1; + float spectralFlux = 0; + float prevFlux = 0; + float real, imag, magnitude, diff; + + for (int i = 0;i 1+m_noteThreshold.value() && minWindowsPassed <= 0) - { - m_slicePoints.push_back(std::max(0, peakIndex-window/2)); // slight offset - minWindowsPassed = 2; // wait at least one window for a new note - } - lastPeak = currentPeak; - currentPeak = 0; - minWindowsPassed--; + if (spectralFlux / prevFlux > 1.0f+m_noteThreshold.value() && i - lastPoint > minDist) { + m_slicePoints.push_back(i); + lastPoint = i; } + + prevFlux = spectralFlux; + spectralFlux = 0; + } + m_slicePoints.push_back(m_originalSample.frames()); emit dataChanged(); @@ -434,9 +452,11 @@ void SlicerT::writeToMidi(std::vector * outClip) float barsInSample = beats / Engine::getSong()->getTimeSigModel().getDenominator(); float totalTicks = ticksPerBar * barsInSample; + float lastEnd = 0; + for (int i = 0;i * outClip) sliceNote.setPos(sliceStart); sliceNote.setLength(sliceEnd - sliceStart); outClip->push_back(sliceNote); + + lastEnd = sliceEnd; } } From 4d183f2aa698209d54d85b3c07ec3db3e1dee291 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Fri, 29 Sep 2023 16:32:46 +0200 Subject: [PATCH 32/99] build + PV fixes --- plugins/SlicerT/CMakeLists.txt | 4 +++- plugins/SlicerT/SlicerT.cpp | 15 +++++++-------- plugins/SlicerT/WaveForm.cpp | 3 ++- plugins/SlicerT/WaveForm.h | 7 +++---- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/plugins/SlicerT/CMakeLists.txt b/plugins/SlicerT/CMakeLists.txt index b93021e98f9..365a6042537 100644 --- a/plugins/SlicerT/CMakeLists.txt +++ b/plugins/SlicerT/CMakeLists.txt @@ -1,5 +1,7 @@ INCLUDE(BuildPlugin) -BUILD_PLUGIN(slicert SlicerT.cpp SlicerT.h SlicerTUI.cpp SlicerTUI.h WaveForm.cpp WaveForm.h MOCFILES SlicerT.h SlicerTUI.h WaveForm.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png") +INCLUDE_DIRECTORIES(${FFTW3F_INCLUDE_DIRS}) +LINK_LIBRARIES(${FFTW3F_LIBRARIES}) +BUILD_PLUGIN(slicert SlicerT.cpp SlicerT.h SlicerTUI.cpp SlicerTUI.h WaveForm.cpp WaveForm.h MOCFILES SlicerT.h SlicerTUI.h WaveForm.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png") \ No newline at end of file diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 36bab44818e..9cb87fa4043 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -23,6 +23,8 @@ */ // TODO: fix some PV bugs +// make fft size and windowsize seperate, pad window to fit into fft with 0 +// this should produce better quality hopefully // TODO: onset detection find valleys // TODO: switch to arrayVector (maybe) // TODO: cleaunp UI classes @@ -178,7 +180,7 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) fftwf_execute(fftPlan); // analysis step - for (int j = 0; j < windowSize; j++) + for (int j = 0; j < windowSize/2; j++) // only process nyquistic frequency { real = FFTSpectrum[j][0]; imag = FFTSpectrum[j][1]; @@ -192,11 +194,8 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) freq -= (float)j*expectedPhaseIn; // subtract expected phase - // some black magic to get into +/- PI interval, revise later pls - long qpd = freq/M_PI; - if (qpd >= 0) qpd += qpd&1; - else qpd -= qpd&1; - freq -= M_PI*(float)qpd; + // this puts freq in 0-2pi + freq = fmod(freq + M_PI, -2.0f * M_PI) + M_PI; freq = (float)overSampling*freq/(2.*M_PI); // idk @@ -216,7 +215,7 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) // synthesis, all the operations are the reverse of the analysis - for (int j = 0; j < windowSize; j++) + for (int j = 0; j < windowSize/2; j++) { magnitude = allMagnitudes[j]; freq = allFrequencies[j]; @@ -225,7 +224,7 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) deltaPhase /= freqPerBin; - deltaPhase = 2.*M_PI*deltaPhase/overSampling;; + deltaPhase = 2.*M_PI*deltaPhase/overSampling; deltaPhase += (float)j*expectedPhaseOut; diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/WaveForm.cpp index d59acc8cfdf..e6ac38b4bc5 100644 --- a/plugins/SlicerT/WaveForm.cpp +++ b/plugins/SlicerT/WaveForm.cpp @@ -49,9 +49,10 @@ WaveForm::WaveForm(int w, int h, SlicerT * instrument, QWidget * parent) : m_sliceEditor(QPixmap(w, m_editorHeight)), // references to instrument vars - m_slicerTParent(instrument), m_currentSample(instrument->m_originalSample), + m_slicerTParent(instrument), m_slicePoints(instrument->m_slicePoints) + { setFixedSize(m_width, m_height); setMouseTracking( true ); diff --git a/plugins/SlicerT/WaveForm.h b/plugins/SlicerT/WaveForm.h index cf78042ad20..461bc99601a 100644 --- a/plugins/SlicerT/WaveForm.h +++ b/plugins/SlicerT/WaveForm.h @@ -115,6 +115,9 @@ class WaveForm : public QWidget { SampleBuffer & m_currentSample; + SlicerT * m_slicerTParent; + std::vector & m_slicePoints; + void drawEditor(); void drawSeekerWaveform(); void drawSeeker(); @@ -124,10 +127,6 @@ class WaveForm : public QWidget { public: WaveForm(int w, int h, SlicerT * instrument, QWidget * parent); - - private: - SlicerT * m_slicerTParent; - std::vector & m_slicePoints; }; } // namespace gui } // namespace lmms From 35bb114505888decfb5b5b65ac98c962853a34fd Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Fri, 29 Sep 2023 17:24:24 +0200 Subject: [PATCH 33/99] clang-format formatting --- plugins/SlicerT/SlicerT.cpp | 245 +++++++++--------- plugins/SlicerT/SlicerT.h | 252 +++++++++--------- plugins/SlicerT/SlicerTUI.cpp | 131 ++++------ plugins/SlicerT/SlicerTUI.h | 52 ++-- plugins/SlicerT/WaveForm.cpp | 463 +++++++++++++++++----------------- plugins/SlicerT/WaveForm.h | 176 +++++++------ 6 files changed, 642 insertions(+), 677 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 9cb87fa4043..891edb55550 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -30,61 +30,55 @@ // TODO: cleaunp UI classes // TODO: add text when no sample loaded // TODO: better buttons -// TODO: code cleaunp, style and test #include "SlicerT.h" -#include #include +#include #include "Engine.h" -#include "Song.h" #include "InstrumentTrack.h" - #include "PathUtil.h" +#include "Song.h" #include "embed.h" #include "plugin_export.h" +namespace lmms { -namespace lmms -{ - -extern "C" -{ -Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = -{ - LMMS_STRINGIFY( PLUGIN_NAME ), +extern "C" { +Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = { + LMMS_STRINGIFY(PLUGIN_NAME), "SlicerT", - QT_TRANSLATE_NOOP( "PluginBrowser", - "Basic Slicer" ), + QT_TRANSLATE_NOOP("PluginBrowser", "Basic Slicer"), "Daniel Kauss Serna ", 0x0100, Plugin::Type::Instrument, - new PluginPixmapLoader( "icon" ), + new PluginPixmapLoader("icon"), nullptr, nullptr, -} ; +}; } // end extern - -PhaseVocoder::PhaseVocoder() : - FFTInput(windowSize, 0), - IFFTReconstruction(windowSize, 0), - allMagnitudes(windowSize, 0), - allFrequencies(windowSize, 0), - processedFreq(windowSize, 0), - processedMagn(windowSize, 0) +PhaseVocoder::PhaseVocoder() + : FFTInput(windowSize, 0) + , IFFTReconstruction(windowSize, 0) + , allMagnitudes(windowSize, 0) + , allFrequencies(windowSize, 0) + , processedFreq(windowSize, 0) + , processedMagn(windowSize, 0) { fftPlan = fftwf_plan_dft_r2c_1d(windowSize, FFTInput.data(), FFTSpectrum, FFTW_MEASURE); ifftPlan = fftwf_plan_dft_c2r_1d(windowSize, FFTSpectrum, IFFTReconstruction.data(), FFTW_MEASURE); } -PhaseVocoder::~PhaseVocoder() { +PhaseVocoder::~PhaseVocoder() +{ fftwf_destroy_plan(fftPlan); fftwf_destroy_plan(ifftPlan); } -void PhaseVocoder::loadData(std::vector originalData, int sampleRate, float newRatio) { +void PhaseVocoder::loadData(std::vector originalData, int sampleRate, float newRatio) +{ originalBuffer = originalData; originalSampleRate = sampleRate; m_scaleRatio = -1; // force update, kinda hacky @@ -95,13 +89,15 @@ void PhaseVocoder::loadData(std::vector originalData, int sampleRate, flo // set buffer sizes m_processedWindows.resize(numWindows, false); - lastPhase.resize(numWindows*windowSize, 0); - sumPhase.resize(numWindows*windowSize, 0); - freqCache.resize(numWindows*windowSize, 0); - magCache.resize(numWindows*windowSize, 0); + lastPhase.resize(numWindows * windowSize, 0); + sumPhase.resize(numWindows * windowSize, 0); + freqCache.resize(numWindows * windowSize, 0); + magCache.resize(numWindows * windowSize, 0); - for (int i = 0;i originalData, int sampleRate, flo dataLock.unlock(); } -void PhaseVocoder::getFrames(std::vector & outData, int start, int frames) { +void PhaseVocoder::getFrames(std::vector& outData, int start, int frames) +{ if (originalBuffer.size() < 2048) { return; } dataLock.lock(); @@ -119,14 +116,17 @@ void PhaseVocoder::getFrames(std::vector & outData, int start, int frames int endWindow = std::min((float)numWindows, (float)(start + frames) / outStepSize + windowMargin); // this encompases the minimum windows needed to get full quality, // which must be computed - for (int i = startWindow;i & outData, int start, int frames } // adjust pv params and reset buffers -void PhaseVocoder::updateParams(float newRatio) { +void PhaseVocoder::updateParams(float newRatio) +{ if (originalBuffer.size() < 2048) { return; } if (newRatio == m_scaleRatio) { return; } dataLock.lock(); @@ -144,11 +145,11 @@ void PhaseVocoder::updateParams(float newRatio) { stepSize = (float)windowSize / overSampling; numWindows = (float)originalBuffer.size() / stepSize - overSampling - 1; outStepSize = m_scaleRatio * (float)stepSize; // float, else inaccurate - freqPerBin = originalSampleRate/windowSize; - expectedPhaseIn = 2.*M_PI*(float)stepSize/(float)windowSize; - expectedPhaseOut = 2.*M_PI*(float)outStepSize/(float)windowSize; + freqPerBin = originalSampleRate / windowSize; + expectedPhaseIn = 2. * M_PI * (float)stepSize / (float)windowSize; + expectedPhaseOut = 2. * M_PI * (float)outStepSize / (float)windowSize; - processedBuffer.resize(m_scaleRatio*originalBuffer.size(), 0); + processedBuffer.resize(m_scaleRatio * originalBuffer.size(), 0); // very slow :( std::fill(m_processedWindows.begin(), m_processedWindows.end(), false); @@ -170,72 +171,76 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) { // declare vars float real, imag, phase, magnitude, freq, deltaPhase = 0; - int windowStart = (float)windowNum*stepSize; - int windowIndex = (float)windowNum*windowSize; + int windowStart = (float)windowNum * stepSize; + int windowIndex = (float)windowNum * windowSize; - if (!useCache) { // normal stuff - memcpy(FFTInput.data(), originalBuffer.data() + windowStart, windowSize*sizeof(float)); + if (!useCache) + { // normal stuff + memcpy(FFTInput.data(), originalBuffer.data() + windowStart, windowSize * sizeof(float)); // FFT fftwf_execute(fftPlan); // analysis step - for (int j = 0; j < windowSize/2; j++) // only process nyquistic frequency + for (int j = 0; j < windowSize / 2; j++) // only process nyquistic frequency { real = FFTSpectrum[j][0]; imag = FFTSpectrum[j][1]; - magnitude = 2.*sqrt(real*real + imag*imag); - phase = atan2(imag,real); + magnitude = 2. * sqrt(real * real + imag * imag); + phase = atan2(imag, real); freq = phase; - freq = phase - lastPhase[std::max(0, windowIndex + j - windowSize)]; // subtract prev pahse to get phase diference + freq = phase + - lastPhase[std::max(0, windowIndex + j - windowSize)]; // subtract prev pahse to get phase diference lastPhase[windowIndex + j] = phase; - freq -= (float)j*expectedPhaseIn; // subtract expected phase + freq -= (float)j * expectedPhaseIn; // subtract expected phase // this puts freq in 0-2pi freq = fmod(freq + M_PI, -2.0f * M_PI) + M_PI; - freq = (float)overSampling*freq/(2.*M_PI); // idk + freq = (float)overSampling * freq / (2. * M_PI); // idk - freq = (float)j*freqPerBin + freq*freqPerBin; // "compute the k-th partials' true frequency" ok i guess + freq = (float)j * freqPerBin + freq * freqPerBin; // "compute the k-th partials' true frequency" ok i guess allMagnitudes[j] = magnitude; allFrequencies[j] = freq; } // write cache - memcpy(freqCache.data() + windowIndex, allFrequencies.data(), windowSize*sizeof(float)); - memcpy(magCache.data() + windowIndex, allMagnitudes.data(), windowSize*sizeof(float)); - } else { + memcpy(freqCache.data() + windowIndex, allFrequencies.data(), windowSize * sizeof(float)); + memcpy(magCache.data() + windowIndex, allMagnitudes.data(), windowSize * sizeof(float)); + } + else + { // read cache - memcpy(allFrequencies.data(), freqCache.data() + windowIndex, windowSize*sizeof(float)); - memcpy(allMagnitudes.data(), magCache.data() + windowIndex, windowSize*sizeof(float)); + memcpy(allFrequencies.data(), freqCache.data() + windowIndex, windowSize * sizeof(float)); + memcpy(allMagnitudes.data(), magCache.data() + windowIndex, windowSize * sizeof(float)); } - // synthesis, all the operations are the reverse of the analysis - for (int j = 0; j < windowSize/2; j++) + for (int j = 0; j < windowSize / 2; j++) { magnitude = allMagnitudes[j]; freq = allFrequencies[j]; - deltaPhase = freq - (float)j*freqPerBin; + deltaPhase = freq - (float)j * freqPerBin; deltaPhase /= freqPerBin; - deltaPhase = 2.*M_PI*deltaPhase/overSampling; + deltaPhase = 2. * M_PI * deltaPhase / overSampling; - deltaPhase += (float)j*expectedPhaseOut; + deltaPhase += (float)j * expectedPhaseOut; sumPhase[windowIndex + j] += deltaPhase; deltaPhase = sumPhase[windowIndex + j]; // this is the bin phase - if (windowIndex + j + windowSize < sumPhase.size()) { // only if not last window + if (windowIndex + j + windowSize < sumPhase.size()) + { // only if not last window sumPhase[windowIndex + j + windowSize] = deltaPhase; // copy to the next } - FFTSpectrum[j][0] = magnitude*cos(deltaPhase); - FFTSpectrum[j][1] = magnitude*sin(deltaPhase); + FFTSpectrum[j][0] = magnitude * cos(deltaPhase); + FFTSpectrum[j][1] = magnitude * sin(deltaPhase); } // inverse fft @@ -255,35 +260,32 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) // (float)overSampling); // discrete windowing - // dataOut[outIndex] += (float)overSampling/totalWindowOverlap*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); + // dataOut[outIndex] += + // (float)overSampling/totalWindowOverlap*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); // printf("timeshifted in phase: %f\n", m_timeshiftedBufferL[outIndex]); // continuos windowing - float window = -0.5f*cos(2.*M_PI*(float)j/(float)windowSize)+0.5f; - processedBuffer[outIndex] += window*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); + float window = -0.5f * cos(2. * M_PI * (float)j / (float)windowSize) + 0.5f; + processedBuffer[outIndex] += window * IFFTReconstruction[j] / (windowSize / 2.0f * overSampling); } - } - // ################################# SlicerT #################################### -SlicerT::SlicerT(InstrumentTrack * instrumentTrack) : - Instrument( instrumentTrack, &slicert_plugin_descriptor ), - m_noteThreshold(0.3f, 0.0f, 2.0f, 0.01f, this, tr( "Note threshold" ) ), - m_fadeOutFrames(400.0f, 0.0f, 8192.0f, 1.0f, this, tr("FadeOut")), - m_originalBPM(1, 1, 999, this, tr("Original bpm")), - m_originalSample(), - m_phaseVocoder() +SlicerT::SlicerT(InstrumentTrack* instrumentTrack) + : Instrument(instrumentTrack, &slicert_plugin_descriptor) + , m_noteThreshold(0.3f, 0.0f, 2.0f, 0.01f, this, tr("Note threshold")) + , m_fadeOutFrames(400.0f, 0.0f, 8192.0f, 1.0f, this, tr("FadeOut")) + , m_originalBPM(1, 1, 999, this, tr("Original bpm")) + , m_originalSample() + , m_phaseVocoder() { } - - -void SlicerT::playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) +void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) { if (m_originalSample.frames() < 2048) { return; } - const float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo() ; + const float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo(); const int noteIndex = handle->key() - 69; const fpp_t frames = handle->framesLeftForCurrentPeriod(); @@ -294,21 +296,22 @@ void SlicerT::playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) const int totalFrames = m_phaseVocoder.frames(); int sliceStart, sliceEnd; - if (noteIndex > m_slicePoints.size()-2 || noteIndex < 0) + if (noteIndex > m_slicePoints.size() - 2 || noteIndex < 0) { sliceStart = 0; sliceEnd = totalFrames; - } else { + } + else + { sliceStart = m_slicePoints[noteIndex] * speedRatio; - sliceEnd = m_slicePoints[noteIndex+1] * speedRatio; + sliceEnd = m_slicePoints[noteIndex + 1] * speedRatio; } int sliceFrames = sliceEnd - sliceStart; int currentNoteFrame = sliceStart + playedFrames; int noteFramesLeft = sliceFrames - playedFrames; - - if( noteFramesLeft > 0) + if (noteFramesLeft > 0) { int framesToCopy = std::min((int)frames, noteFramesLeft); m_phaseVocoder.getFrames(workingBuffer + offset, currentNoteFrame, framesToCopy); @@ -316,9 +319,9 @@ void SlicerT::playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) // exponential fade out, applyRelease kinda sucks if (noteFramesLeft < m_fadeOutFrames.value()) { - for (int i = 0;iprocessAudioBuffer( workingBuffer, frames + offset, handle ); + instrumentTrack()->processAudioBuffer(workingBuffer, frames + offset, handle); // calculate absolute for the waveform float absoluteCurrentNote = (float)currentNoteFrame / totalFrames; float absoluteStartNote = (float)sliceStart / totalFrames; float abslouteEndNote = (float)sliceEnd / totalFrames; emit isPlaying(absoluteCurrentNote, absoluteStartNote, abslouteEndNote); - } else { - emit isPlaying(-1, 0, 0); } + else { emit isPlaying(-1, 0, 0); } } // uses the spectral flux to determine the change in magnitude @@ -352,7 +354,8 @@ void SlicerT::findSlices() std::vector leftChannel(m_originalSample.frames(), 0); - for (int i = 0;i 1.0f+m_noteThreshold.value() && i - lastPoint > minDist) { + if (spectralFlux / prevFlux > 1.0f + m_noteThreshold.value() && i - lastPoint > minDist) + { m_slicePoints.push_back(i); lastPoint = i; } prevFlux = spectralFlux; spectralFlux = 0; - } m_slicePoints.push_back(m_originalSample.frames()); @@ -432,7 +437,7 @@ void SlicerT::findBPM() m_originalBPM.setInitValue(bpm); } -void SlicerT::writeToMidi(std::vector * outClip) +void SlicerT::writeToMidi(std::vector* outClip) { if (m_originalSample.frames() < 2048) { return; } @@ -453,7 +458,7 @@ void SlicerT::writeToMidi(std::vector * outClip) float lastEnd = 0; - for (int i = 0;igetTempo() ; - m_phaseVocoder.loadSample(m_originalSample.data(), - m_originalSample.frames(), - m_originalSample.sampleRate(), - speedRatio); + float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo(); + m_phaseVocoder.loadSample( + m_originalSample.data(), m_originalSample.frames(), m_originalSample.sampleRate(), speedRatio); emit dataChanged(); } @@ -490,7 +493,7 @@ void SlicerT::updateSlices() findSlices(); } -void SlicerT::saveSettings(QDomDocument & document, QDomElement & element) +void SlicerT::saveSettings(QDomDocument& document, QDomElement& element) { element.setAttribute("src", m_originalSample.audioFile()); if (m_originalSample.audioFile().isEmpty()) @@ -501,7 +504,7 @@ void SlicerT::saveSettings(QDomDocument & document, QDomElement & element) element.setAttribute("totalSlices", (int)m_slicePoints.size()); - for (int i = 0;igetTempo() ; - m_phaseVocoder.loadSample(m_originalSample.data(), - m_originalSample.frames(), - m_originalSample.sampleRate(), - speedRatio); + float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo(); + m_phaseVocoder.loadSample( + m_originalSample.data(), m_originalSample.frames(), m_originalSample.sampleRate(), speedRatio); emit dataChanged(); - } QString SlicerT::nodeName() const { - return( slicert_plugin_descriptor.name ); + return (slicert_plugin_descriptor.name); } -gui::PluginView * SlicerT::instantiateView( QWidget * parent ) +gui::PluginView* SlicerT::instantiateView(QWidget* parent) { - return( new gui::SlicerTUI( this, parent ) ); + return (new gui::SlicerTUI(this, parent)); } - -extern "C" -{ +extern "C" { // necessary for getting instance out of shared lib -PLUGIN_EXPORT Plugin * lmms_plugin_main( Model *m, void * ) +PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* m, void*) { - return( new SlicerT( static_cast( m ) ) ); + return (new SlicerT(static_cast(m))); } } // extern } // namespace lmms - diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 25142f41118..ebb0fee0f61 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -25,156 +25,164 @@ #ifndef SLICERT_H #define SLICERT_H -#include "SlicerTUI.h" - #include -#include "Note.h" +#include "AutomatableModel.h" #include "Instrument.h" #include "InstrumentView.h" -#include "AutomatableModel.h" +#include "Note.h" #include "SampleBuffer.h" +#include "SlicerTUI.h" - -namespace lmms -{ +namespace lmms { // takes one audio-channel and timeshifts it -class PhaseVocoder { - public: - PhaseVocoder(); - ~PhaseVocoder(); - void loadData(std::vector originalData, int sampleRate, float newRatio); - void setScaleRatio(float newRatio) { updateParams(newRatio); } - void getFrames(std::vector & outData, int start, int frames); - int frames() { return processedBuffer.size(); } - float scaleRatio() { return m_scaleRatio; } - private: - QMutex dataLock; - // original data - std::vector originalBuffer; - int originalSampleRate = 0; - - float m_scaleRatio = -1; // to force on fisrt load - - // output data - std::vector processedBuffer; - std::vector m_processedWindows; // marks a window processed - - // timeshift stuff - static const int windowSize = 512; - static const int overSampling = 32; - - // depending on scaleRatio - int stepSize = 0; - int numWindows = 0; - float outStepSize = 0; - float freqPerBin = 0; - float expectedPhaseIn = 0; - float expectedPhaseOut = 0; - - // buffers - fftwf_complex FFTSpectrum[windowSize]; - std::vector FFTInput; - std::vector IFFTReconstruction; - std::vector allMagnitudes; - std::vector allFrequencies; - std::vector processedFreq; - std::vector processedMagn; - std::vector lastPhase; - std::vector sumPhase; - - // cache - std::vector freqCache; - std::vector magCache; - - // fftw plans - fftwf_plan fftPlan; - fftwf_plan ifftPlan; - - void updateParams(float newRatio); - void generateWindow(int windowNum, bool useCache); +class PhaseVocoder +{ +public: + PhaseVocoder(); + ~PhaseVocoder(); + void loadData(std::vector originalData, int sampleRate, float newRatio); + void setScaleRatio(float newRatio) { updateParams(newRatio); } + void getFrames(std::vector& outData, int start, int frames); + int frames() { return processedBuffer.size(); } + float scaleRatio() { return m_scaleRatio; } + +private: + QMutex dataLock; + // original data + std::vector originalBuffer; + int originalSampleRate = 0; + + float m_scaleRatio = -1; // to force on fisrt load + + // output data + std::vector processedBuffer; + std::vector m_processedWindows; // marks a window processed + + // timeshift stuff + static const int windowSize = 512; + static const int overSampling = 32; + + // depending on scaleRatio + int stepSize = 0; + int numWindows = 0; + float outStepSize = 0; + float freqPerBin = 0; + float expectedPhaseIn = 0; + float expectedPhaseOut = 0; + + // buffers + fftwf_complex FFTSpectrum[windowSize]; + std::vector FFTInput; + std::vector IFFTReconstruction; + std::vector allMagnitudes; + std::vector allFrequencies; + std::vector processedFreq; + std::vector processedMagn; + std::vector lastPhase; + std::vector sumPhase; + + // cache + std::vector freqCache; + std::vector magCache; + + // fftw plans + fftwf_plan fftPlan; + fftwf_plan ifftPlan; + + void updateParams(float newRatio); + void generateWindow(int windowNum, bool useCache); }; // simple helper class that handles the different audio channels -class dinamicPlaybackBuffer { - public: - dinamicPlaybackBuffer() : - leftChannel(), - rightChannel() - {} - void loadSample(const sampleFrame * outData, int frames, int sampleRate, float newRatio) { - std::vector leftData(frames, 0); - std::vector rightData(frames, 0); - for (int i = 0;i leftOut(frames, 0); // not a huge performance issue - std::vector rightOut(frames, 0); - - leftChannel.getFrames(leftOut, startFrame, frames); - rightChannel.getFrames(rightOut, startFrame, frames); - - for (int i = 0;i leftData(frames, 0); + std::vector rightData(frames, 0); + for (int i = 0; i < frames; i++) + { + leftData[i] = outData[i][0]; + rightData[i] = outData[i][1]; } - int frames() { return leftChannel.frames(); } - float scaleRatio() { return leftChannel.scaleRatio(); } - void setScaleRatio(float newRatio) { - leftChannel.setScaleRatio(newRatio); - rightChannel.setScaleRatio(newRatio); + leftChannel.loadData(leftData, sampleRate, newRatio); + rightChannel.loadData(rightData, sampleRate, newRatio); + } + void getFrames(sampleFrame* outData, int startFrame, int frames) + { + std::vector leftOut(frames, 0); // not a huge performance issue + std::vector rightOut(frames, 0); + + leftChannel.getFrames(leftOut, startFrame, frames); + rightChannel.getFrames(rightOut, startFrame, frames); + + for (int i = 0; i < frames; i++) + { + outData[i][0] = leftOut[i]; + outData[i][1] = rightOut[i]; } - private: - PhaseVocoder leftChannel; - PhaseVocoder rightChannel; + } + int frames() { return leftChannel.frames(); } + float scaleRatio() { return leftChannel.scaleRatio(); } + void setScaleRatio(float newRatio) + { + leftChannel.setScaleRatio(newRatio); + rightChannel.setScaleRatio(newRatio); + } + +private: + PhaseVocoder leftChannel; + PhaseVocoder rightChannel; }; -class SlicerT : public Instrument{ +class SlicerT : public Instrument +{ Q_OBJECT - public: - SlicerT(InstrumentTrack * instrumentTrack); - ~SlicerT() override = default; +public: + SlicerT(InstrumentTrack* instrumentTrack); + ~SlicerT() override = default; - void playNote( NotePlayHandle * handle, sampleFrame * workingBuffer ) override; + void playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) override; - void saveSettings( QDomDocument & document, QDomElement & element ) override; - void loadSettings( const QDomElement & element ) override; + void saveSettings(QDomDocument& document, QDomElement& element) override; + void loadSettings(const QDomElement& element) override; - QString nodeName() const override; - gui::PluginView * instantiateView( QWidget * parent ) override; + QString nodeName() const override; + gui::PluginView* instantiateView(QWidget* parent) override; - void writeToMidi(std::vector * outClip); + void writeToMidi(std::vector* outClip); - public slots: - void updateFile(QString file); - void updateSlices(); +public slots: + void updateFile(QString file); + void updateSlices(); - signals: - void isPlaying(float current, float start, float end); +signals: + void isPlaying(float current, float start, float end); - private: - FloatModel m_noteThreshold; - FloatModel m_fadeOutFrames; - IntModel m_originalBPM; +private: + FloatModel m_noteThreshold; + FloatModel m_fadeOutFrames; + IntModel m_originalBPM; - SampleBuffer m_originalSample; - dinamicPlaybackBuffer m_phaseVocoder; + SampleBuffer m_originalSample; + dinamicPlaybackBuffer m_phaseVocoder; - std::vector m_slicePoints; + std::vector m_slicePoints; - void findSlices(); - void findBPM(); + void findSlices(); + void findBPM(); - friend class gui::SlicerTUI; - friend class gui::WaveForm; + friend class gui::SlicerTUI; + friend class gui::WaveForm; }; } // namespace lmms #endif // SLICERT_H diff --git a/plugins/SlicerT/SlicerTUI.cpp b/plugins/SlicerT/SlicerTUI.cpp index e1af70c2847..c0d79ae8e0e 100644 --- a/plugins/SlicerT/SlicerTUI.cpp +++ b/plugins/SlicerT/SlicerTUI.cpp @@ -23,47 +23,40 @@ */ #include "SlicerTUI.h" -#include "SlicerT.h" -#include #include +#include -#include "StringPairDrag.h" #include "Clipboard.h" -#include "Track.h" #include "DataFile.h" - #include "Engine.h" -#include "Song.h" #include "InstrumentTrack.h" - +#include "SlicerT.h" +#include "Song.h" +#include "StringPairDrag.h" +#include "Track.h" #include "embed.h" -namespace lmms -{ - - -namespace gui -{ +namespace lmms { +namespace gui { -SlicerTUI::SlicerTUI( SlicerT * instrument, - QWidget * parent ) : - InstrumentViewFixedSize( instrument, parent ), - m_slicerTParent(instrument), - m_noteThresholdKnob(this), - m_fadeOutKnob(this), - m_bpmBox(3, "21pink", this), - m_resetButton(this, nullptr), - m_midiExportButton(this, nullptr), - m_wf(248, 128, instrument, this) +SlicerTUI::SlicerTUI(SlicerT* instrument, QWidget* parent) + : InstrumentViewFixedSize(instrument, parent) + , m_slicerTParent(instrument) + , m_noteThresholdKnob(this) + , m_fadeOutKnob(this) + , m_bpmBox(3, "21pink", this) + , m_resetButton(this, nullptr) + , m_midiExportButton(this, nullptr) + , m_wf(248, 128, instrument, this) { - setAcceptDrops( true ); - setAutoFillBackground( true ); + setAcceptDrops(true); + setAutoFillBackground(true); QPalette pal; - pal.setBrush( backgroundRole(), PLUGIN_NAME::getIconPixmap( "bg" ) ); - setPalette( pal ); + pal.setBrush(backgroundRole(), PLUGIN_NAME::getIconPixmap("bg")); + setPalette(pal); m_wf.move(2, 6); @@ -83,20 +76,16 @@ SlicerTUI::SlicerTUI( SlicerT * instrument, m_fadeOutKnob.setModel(&m_slicerTParent->m_fadeOutFrames); m_midiExportButton.move(190, 200); - m_midiExportButton.setActiveGraphic( - embed::getIconPixmap("midi_tab") ); - m_midiExportButton.setInactiveGraphic( - embed::getIconPixmap("midi_tab")); + m_midiExportButton.setActiveGraphic(embed::getIconPixmap("midi_tab")); + m_midiExportButton.setInactiveGraphic(embed::getIconPixmap("midi_tab")); m_midiExportButton.setToolTip(tr("Copy midi pattern to clipboard")); - connect(&m_midiExportButton, SIGNAL( clicked() ), this, SLOT( exportMidi() )); + connect(&m_midiExportButton, SIGNAL(clicked()), this, SLOT(exportMidi())); m_resetButton.move(215, 200); - m_resetButton.setActiveGraphic( - embed::getIconPixmap("reload") ); - m_resetButton.setInactiveGraphic( - embed::getIconPixmap("reload") ); + m_resetButton.setActiveGraphic(embed::getIconPixmap("reload")); + m_resetButton.setInactiveGraphic(embed::getIconPixmap("reload")); m_resetButton.setToolTip(tr("Reset Slices")); - connect(&m_resetButton, SIGNAL( clicked() ), m_slicerTParent, SLOT( updateSlices() )); + connect(&m_resetButton, SIGNAL(clicked()), m_slicerTParent, SLOT(updateSlices())); } // copied from piano roll @@ -104,74 +93,60 @@ void SlicerTUI::exportMidi() { using namespace Clipboard; - DataFile dataFile( DataFile::Type::ClipboardData ); - QDomElement note_list = dataFile.createElement( "note-list" ); - dataFile.content().appendChild( note_list ); + DataFile dataFile(DataFile::Type::ClipboardData); + QDomElement note_list = dataFile.createElement("note-list"); + dataFile.content().appendChild(note_list); std::vector notes; m_slicerTParent->writeToMidi(¬es); - if (notes.size() == 0) - { - return; - } + if (notes.size() == 0) { return; } - TimePos start_pos( notes.front().pos().getBar(), 0 ); - for( Note note : notes ) + TimePos start_pos(notes.front().pos().getBar(), 0); + for (Note note : notes) { - Note clip_note( note ); - clip_note.setPos( clip_note.pos( start_pos ) ); - clip_note.saveState( dataFile, note_list ); + Note clip_note(note); + clip_note.setPos(clip_note.pos(start_pos)); + clip_note.saveState(dataFile, note_list); } - copyString( dataFile.toString(), MimeType::Default ); + copyString(dataFile.toString(), MimeType::Default); } // all the drag stuff is copied from AudioFileProcessor -void SlicerTUI::dragEnterEvent( QDragEnterEvent * dee ) +void SlicerTUI::dragEnterEvent(QDragEnterEvent* dee) { - // For mimeType() and MimeType enum class + // For mimeType() and MimeType enum class using namespace Clipboard; - if( dee->mimeData()->hasFormat( mimeType( MimeType::StringPair ) ) ) + if (dee->mimeData()->hasFormat(mimeType(MimeType::StringPair))) { - QString txt = dee->mimeData()->data( - mimeType( MimeType::StringPair ) ); - if( txt.section( ':', 0, 0 ) == QString( "clip_%1" ).arg( - static_cast(Track::Type::Sample) ) ) + QString txt = dee->mimeData()->data(mimeType(MimeType::StringPair)); + if (txt.section(':', 0, 0) == QString("clip_%1").arg(static_cast(Track::Type::Sample))) { dee->acceptProposedAction(); } - else if( txt.section( ':', 0, 0 ) == "samplefile" ) - { - dee->acceptProposedAction(); - } - else - { - dee->ignore(); - } - } - else - { - dee->ignore(); + else if (txt.section(':', 0, 0) == "samplefile") { dee->acceptProposedAction(); } + else { dee->ignore(); } } + else { dee->ignore(); } } -void SlicerTUI::dropEvent( QDropEvent * de ) +void SlicerTUI::dropEvent(QDropEvent* de) { - QString type = StringPairDrag::decodeKey( de ); - QString value = StringPairDrag::decodeValue( de ); - if( type == "samplefile" ) + QString type = StringPairDrag::decodeKey(de); + QString value = StringPairDrag::decodeValue(de); + if (type == "samplefile") { - m_slicerTParent->updateFile( value ); + m_slicerTParent->updateFile(value); // castModel()->setAudioFile( value ); // de->accept(); // set m_wf wave file return; } - else if( type == QString( "clip_%1" ).arg( static_cast(Track::Type::Sample) ) ) + else if (type == QString("clip_%1").arg(static_cast(Track::Type::Sample))) { - DataFile dataFile( value.toUtf8() ); - m_slicerTParent->updateFile( dataFile.content().firstChild().toElement().attribute( "src" ) ); + DataFile dataFile(value.toUtf8()); + m_slicerTParent->updateFile(dataFile.content().firstChild().toElement().attribute("src")); de->accept(); return; } @@ -179,7 +154,7 @@ void SlicerTUI::dropEvent( QDropEvent * de ) de->ignore(); } -void SlicerTUI::paintEvent(QPaintEvent * pe) +void SlicerTUI::paintEvent(QPaintEvent* pe) { } diff --git a/plugins/SlicerT/SlicerTUI.h b/plugins/SlicerT/SlicerTUI.h index 425e4e7669e..d83f679932f 100644 --- a/plugins/SlicerT/SlicerTUI.h +++ b/plugins/SlicerT/SlicerTUI.h @@ -25,38 +25,35 @@ #ifndef SLICERT_UI_H #define SLICERT_UI_H -#include "WaveForm.h" - #include #include "Instrument.h" #include "InstrumentView.h" #include "Knob.h" -#include "PixmapButton.h" #include "LcdSpinBox.h" +#include "PixmapButton.h" +#include "WaveForm.h" - -namespace lmms -{ +namespace lmms { class SlicerT; -namespace gui -{ +namespace gui { -class SlicerTKnob : public Knob { - public: - SlicerTKnob( QWidget * _parent ) : - Knob( KnobType::Styled, _parent ) - { - setFixedSize( 46, 40 ); - setCenterPointX( 23.0 ); - setCenterPointY( 15.0 ); - setInnerRadius( 3 ); - setOuterRadius( 11 ); - setLineWidth( 3 ); - setOuterColor( QColor(178, 115, 255) ); - } +class SlicerTKnob : public Knob +{ +public: + SlicerTKnob(QWidget* _parent) + : Knob(KnobType::Styled, _parent) + { + setFixedSize(46, 40); + setCenterPointX(23.0); + setCenterPointY(15.0); + setInnerRadius(3); + setOuterRadius(11); + setLineWidth(3); + setOuterColor(QColor(178, 115, 255)); + } }; class SlicerTUI : public InstrumentViewFixedSize @@ -64,22 +61,21 @@ class SlicerTUI : public InstrumentViewFixedSize Q_OBJECT public: - SlicerTUI( SlicerT * instrument, - QWidget * parent ); + SlicerTUI(SlicerT* instrument, QWidget* parent); ~SlicerTUI() override = default; protected slots: void exportMidi(); - //void sampleSizeChanged( float _new_sample_length ); + // void sampleSizeChanged( float _new_sample_length ); protected: - virtual void dragEnterEvent( QDragEnterEvent * _dee ); - virtual void dropEvent( QDropEvent * _de ); + virtual void dragEnterEvent(QDragEnterEvent* _dee); + virtual void dropEvent(QDropEvent* _de); - virtual void paintEvent(QPaintEvent * pe); + virtual void paintEvent(QPaintEvent* pe); private: - SlicerT * m_slicerTParent; + SlicerT* m_slicerTParent; SlicerTKnob m_noteThresholdKnob; SlicerTKnob m_fadeOutKnob; diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/WaveForm.cpp index e6ac38b4bc5..28e568de3fa 100644 --- a/plugins/SlicerT/WaveForm.cpp +++ b/plugins/SlicerT/WaveForm.cpp @@ -23,284 +23,277 @@ */ #include "WaveForm.h" -#include "SlicerT.h" +#include "SlicerT.h" #include "embed.h" -namespace lmms +namespace lmms { + +namespace gui { +WaveForm::WaveForm(int w, int h, SlicerT* instrument, QWidget* parent) + : QWidget(parent) + , + // calculate sizes + m_width(w) + , m_height(h) + , m_seekerWidth(w - m_seekerHorMargin * 2) + , m_editorHeight(h - m_seekerHeight - m_middleMargin) + , m_editorWidth(w) + , + + // create pixmaps + m_sliceArrow(PLUGIN_NAME::getIconPixmap("slide_indicator_arrow")) + , m_seeker(QPixmap(m_seekerWidth, m_seekerHeight)) + , m_seekerWaveform(QPixmap(m_seekerWidth, m_seekerHeight)) + , m_sliceEditor(QPixmap(w, m_editorHeight)) + , + + // references to instrument vars + m_currentSample(instrument->m_originalSample) + , m_slicerTParent(instrument) + , m_slicePoints(instrument->m_slicePoints) + { + setFixedSize(m_width, m_height); + setMouseTracking(true); + m_sliceEditor.fill(m_waveformBgColor); + m_seekerWaveform.fill(m_waveformBgColor); -namespace gui -{ -WaveForm::WaveForm(int w, int h, SlicerT * instrument, QWidget * parent) : - QWidget(parent), - // calculate sizes - m_width(w), - m_height(h), - m_seekerWidth(w - m_seekerHorMargin * 2), - m_editorHeight(h - m_seekerHeight - m_middleMargin), - m_editorWidth(w), - - // create pixmaps - m_sliceArrow(PLUGIN_NAME::getIconPixmap( "slide_indicator_arrow" )), - m_seeker(QPixmap(m_seekerWidth, m_seekerHeight)), - m_seekerWaveform(QPixmap(m_seekerWidth, m_seekerHeight)), - m_sliceEditor(QPixmap(w, m_editorHeight)), - - // references to instrument vars - m_currentSample(instrument->m_originalSample), - m_slicerTParent(instrument), - m_slicePoints(instrument->m_slicePoints) - - { - setFixedSize(m_width, m_height); - setMouseTracking( true ); - - m_sliceEditor.fill(m_waveformBgColor); - m_seekerWaveform.fill(m_waveformBgColor); - - connect(m_slicerTParent, - SIGNAL(isPlaying(float, float, float)), - this, - SLOT(isPlaying(float, float, float))); - connect(m_slicerTParent, SIGNAL(dataChanged()), this, SLOT(updateUI())); - - updateUI(); - } + connect(m_slicerTParent, SIGNAL(isPlaying(float, float, float)), this, SLOT(isPlaying(float, float, float))); + connect(m_slicerTParent, SIGNAL(dataChanged()), this, SLOT(updateUI())); + + updateUI(); +} // graphics void WaveForm::drawSeekerWaveform() { - m_seekerWaveform.fill(m_waveformBgColor); - QPainter brush(&m_seekerWaveform); - brush.setPen(m_waveformColor); - - m_currentSample.visualize( - brush, - QRect( 0, 0, m_seekerWaveform.width(), m_seekerWaveform.height() ), - 0, m_currentSample.frames()); + m_seekerWaveform.fill(m_waveformBgColor); + QPainter brush(&m_seekerWaveform); + brush.setPen(m_waveformColor); + + m_currentSample.visualize( + brush, QRect(0, 0, m_seekerWaveform.width(), m_seekerWaveform.height()), 0, m_currentSample.frames()); } void WaveForm::drawSeeker() { - m_seeker.fill(m_waveformBgColor); - QPainter brush(&m_seeker); - // draw slice points - brush.setPen(m_sliceColor); - for (int i = 0;ix() - m_seekerHorMargin) / m_seekerWidth; - float normalizedClickEditor = (float)(me->x()) / m_editorWidth; - // reset seeker on middle click - if (me->button() == Qt::MouseButton::MiddleButton) - { - m_seekerStart = 0; - m_seekerEnd = 1; - return; - } - - if (me->y() < m_seekerHeight) // seeker click - { - if (abs(normalizedClickSeeker - m_seekerStart) < distanceForClick) // dragging start - { - m_currentlyDragging = m_draggingTypes::m_seekerStart; - } else if (abs(normalizedClickSeeker - m_seekerEnd) < distanceForClick) // dragging end - { - m_currentlyDragging = m_draggingTypes::m_seekerEnd; - - } else if (normalizedClickSeeker > m_seekerStart && normalizedClickSeeker < m_seekerEnd) // dragging middle - { - m_currentlyDragging = m_draggingTypes::m_seekerMiddle; - m_seekerMiddle = normalizedClickSeeker; - } - } else { // editor click - m_sliceSelected = -1; - float startFrame = m_seekerStart * m_currentSample.frames(); - float endFrame = m_seekerEnd * m_currentSample.frames(); - // select slice - for (int i = 0;ibutton() == Qt::MouseButton::RightButton) // erase selected slice - { - if (m_sliceSelected != -1 && m_slicePoints.size() > 2) - { - m_slicePoints.erase(m_slicePoints.begin() + m_sliceSelected); - m_sliceSelected = -1; - } - } - updateUI(); + float normalizedClickSeeker = (float)(me->x() - m_seekerHorMargin) / m_seekerWidth; + float normalizedClickEditor = (float)(me->x()) / m_editorWidth; + // reset seeker on middle click + if (me->button() == Qt::MouseButton::MiddleButton) + { + m_seekerStart = 0; + m_seekerEnd = 1; + return; + } + + if (me->y() < m_seekerHeight) // seeker click + { + if (abs(normalizedClickSeeker - m_seekerStart) < distanceForClick) // dragging start + { + m_currentlyDragging = m_draggingTypes::m_seekerStart; + } + else if (abs(normalizedClickSeeker - m_seekerEnd) < distanceForClick) // dragging end + { + m_currentlyDragging = m_draggingTypes::m_seekerEnd; + } + else if (normalizedClickSeeker > m_seekerStart && normalizedClickSeeker < m_seekerEnd) // dragging middle + { + m_currentlyDragging = m_draggingTypes::m_seekerMiddle; + m_seekerMiddle = normalizedClickSeeker; + } + } + else + { // editor click + m_sliceSelected = -1; + float startFrame = m_seekerStart * m_currentSample.frames(); + float endFrame = m_seekerEnd * m_currentSample.frames(); + // select slice + for (int i = 0; i < m_slicePoints.size(); i++) + { + int sliceIndex = m_slicePoints[i]; + float xPos = (float)(sliceIndex - startFrame) / (float)(endFrame - startFrame); + + if (abs(xPos - normalizedClickEditor) < distanceForClick) + { + m_currentlyDragging = m_draggingTypes::m_slicePoint; + m_sliceSelected = i; + } + } + } + + if (me->button() == Qt::MouseButton::RightButton) // erase selected slice + { + if (m_sliceSelected != -1 && m_slicePoints.size() > 2) + { + m_slicePoints.erase(m_slicePoints.begin() + m_sliceSelected); + m_sliceSelected = -1; + } + } + updateUI(); } -void WaveForm::mouseReleaseEvent( QMouseEvent * me ) +void WaveForm::mouseReleaseEvent(QMouseEvent* me) { - m_currentlyDragging = m_draggingTypes::nothing; - updateUI(); + m_currentlyDragging = m_draggingTypes::nothing; + updateUI(); } -void WaveForm::mouseMoveEvent( QMouseEvent * me ) +void WaveForm::mouseMoveEvent(QMouseEvent* me) { - float normalizedClickSeeker = (float)(me->x() - m_seekerHorMargin) / m_seekerWidth; - float normalizedClickEditor = (float)(me->x()) / m_editorWidth; - - float distStart = m_seekerStart - m_seekerMiddle; - float distEnd = m_seekerEnd - m_seekerMiddle; - float startFrame = m_seekerStart * m_currentSample.frames(); - float endFrame = m_seekerEnd * m_currentSample.frames(); - - // handle dragging events - switch (m_currentlyDragging) - { - case m_draggingTypes::m_seekerStart: - m_seekerStart = std::clamp(normalizedClickSeeker, 0.0f, m_seekerEnd - minSeekerDistance); - break; - - case m_draggingTypes::m_seekerEnd: - m_seekerEnd = std::clamp(normalizedClickSeeker, m_seekerStart + minSeekerDistance, 1.0f); - break; - - case m_draggingTypes::m_seekerMiddle: - m_seekerMiddle = normalizedClickSeeker; - - if (m_seekerMiddle + distStart >= 0 && m_seekerMiddle + distEnd <= 1) - { - m_seekerStart = m_seekerMiddle + distStart; - m_seekerEnd = m_seekerMiddle + distEnd; - } - break; - - case m_draggingTypes::m_slicePoint: // TODO: fix this - m_slicePoints[m_sliceSelected] = startFrame + normalizedClickEditor * (endFrame - startFrame); - - m_slicePoints[m_sliceSelected] = std::clamp(m_slicePoints[m_sliceSelected], 0, m_currentSample.frames()); - - std::sort(m_slicePoints.begin(), m_slicePoints.end()); - break; - case m_draggingTypes::nothing: - break; - } - updateUI(); + float normalizedClickSeeker = (float)(me->x() - m_seekerHorMargin) / m_seekerWidth; + float normalizedClickEditor = (float)(me->x()) / m_editorWidth; + + float distStart = m_seekerStart - m_seekerMiddle; + float distEnd = m_seekerEnd - m_seekerMiddle; + float startFrame = m_seekerStart * m_currentSample.frames(); + float endFrame = m_seekerEnd * m_currentSample.frames(); + + // handle dragging events + switch (m_currentlyDragging) + { + case m_draggingTypes::m_seekerStart: + m_seekerStart = std::clamp(normalizedClickSeeker, 0.0f, m_seekerEnd - minSeekerDistance); + break; + + case m_draggingTypes::m_seekerEnd: + m_seekerEnd = std::clamp(normalizedClickSeeker, m_seekerStart + minSeekerDistance, 1.0f); + break; + + case m_draggingTypes::m_seekerMiddle: + m_seekerMiddle = normalizedClickSeeker; + + if (m_seekerMiddle + distStart >= 0 && m_seekerMiddle + distEnd <= 1) + { + m_seekerStart = m_seekerMiddle + distStart; + m_seekerEnd = m_seekerMiddle + distEnd; + } + break; + + case m_draggingTypes::m_slicePoint: // TODO: fix this + m_slicePoints[m_sliceSelected] = startFrame + normalizedClickEditor * (endFrame - startFrame); + + m_slicePoints[m_sliceSelected] = std::clamp(m_slicePoints[m_sliceSelected], 0, m_currentSample.frames()); + + std::sort(m_slicePoints.begin(), m_slicePoints.end()); + break; + case m_draggingTypes::nothing: + break; + } + updateUI(); } -void WaveForm::mouseDoubleClickEvent(QMouseEvent * me) +void WaveForm::mouseDoubleClickEvent(QMouseEvent* me) { - float normalizedClickEditor = (float)(me->x()) / m_editorWidth; - float startFrame = m_seekerStart * m_currentSample.frames(); - float endFrame = m_seekerEnd * m_currentSample.frames(); - - float slicePosition = startFrame + normalizedClickEditor * (endFrame - startFrame); - - for (int i = 0;ix()) / m_editorWidth; + float startFrame = m_seekerStart * m_currentSample.frames(); + float endFrame = m_seekerEnd * m_currentSample.frames(); + + float slicePosition = startFrame + normalizedClickEditor * (endFrame - startFrame); + + for (int i = 0; i < m_slicePoints.size(); i++) + { + if (m_slicePoints[i] < slicePosition) + { + m_slicePoints.insert(m_slicePoints.begin() + i, slicePosition); + break; + } + } + + std::sort(m_slicePoints.begin(), m_slicePoints.end()); } -void WaveForm::paintEvent( QPaintEvent * pe) +void WaveForm::paintEvent(QPaintEvent* pe) { - QPainter p( this ); - p.drawPixmap(m_seekerHorMargin, 0 ,m_seekerWaveform); - p.drawPixmap(m_seekerHorMargin, 0, m_seeker); - p.drawPixmap(0, m_seekerHeight + m_middleMargin, m_sliceEditor); + QPainter p(this); + p.drawPixmap(m_seekerHorMargin, 0, m_seekerWaveform); + p.drawPixmap(m_seekerHorMargin, 0, m_seeker); + p.drawPixmap(0, m_seekerHeight + m_middleMargin, m_sliceEditor); } } // namespace gui } // namespace lmms \ No newline at end of file diff --git a/plugins/SlicerT/WaveForm.h b/plugins/SlicerT/WaveForm.h index 461bc99601a..79bd236dbae 100644 --- a/plugins/SlicerT/WaveForm.h +++ b/plugins/SlicerT/WaveForm.h @@ -33,100 +33,96 @@ #include "SampleBuffer.h" - -namespace lmms -{ +namespace lmms { class SlicerT; -namespace gui -{ +namespace gui { - -class WaveForm : public QWidget { - Q_OBJECT - - protected: - virtual void mousePressEvent(QMouseEvent * me); - virtual void mouseReleaseEvent(QMouseEvent * me); - virtual void mouseMoveEvent(QMouseEvent * me); - virtual void mouseDoubleClickEvent(QMouseEvent * me); - - virtual void paintEvent(QPaintEvent * pe); - - private: - // vars used to control structure and colors - int m_width; - int m_height; - - int m_seekerHorMargin = 5; - int m_seekerHeight = 38; // used to calcualte all hor sizes - int m_seekerWidth; - - int m_middleMargin = 6; - int m_editorHeight; - int m_editorWidth; - - - QColor m_waveformBgColor = QColor(255, 255, 255, 0); - QColor m_waveformColor = QColor(123, 49, 212); - // QColor m_waveformColorDark = QColor(39, 15, 67); - - // QColor m_waveformColor = QColor(255, 161, 247); // logo color - QColor m_playColor = QColor(255, 255, 255, 200); - QColor m_playHighlighColor = QColor(255, 255, 255, 70); - QColor m_sliceColor = QColor(218, 193, 255); - QColor m_selectedSliceColor = QColor(178, 153, 215); - QColor m_seekerColor = QColor(178, 115, 255); - QColor m_seekerHighlightColor = QColor(178, 115, 255, 100); - QColor m_seekerShadowColor = QColor(0, 0, 0, 120); - - // interaction vars - float distanceForClick = 0.03f; - float minSeekerDistance = 0.13f; - - // dragging vars - enum class m_draggingTypes - { - nothing, - m_seekerStart, - m_seekerEnd, - m_seekerMiddle, - m_slicePoint, - }; - m_draggingTypes m_currentlyDragging; - - // seeker vars - float m_seekerStart = 0; - float m_seekerEnd = 1; - float m_seekerMiddle = 0.5f; - int m_sliceSelected = 0; - - // playback highlight vars - float m_noteCurrent; - float m_noteStart; - float m_noteEnd; - - // pixmaps - QPixmap m_sliceArrow; - QPixmap m_seeker; - QPixmap m_seekerWaveform; // only stores waveform graphic - QPixmap m_sliceEditor; - - SampleBuffer & m_currentSample; - - SlicerT * m_slicerTParent; - std::vector & m_slicePoints; - - void drawEditor(); - void drawSeekerWaveform(); - void drawSeeker(); - public slots: - void updateUI(); - void isPlaying(float current, float start, float end); - - public: - WaveForm(int w, int h, SlicerT * instrument, QWidget * parent); +class WaveForm : public QWidget +{ + Q_OBJECT + +protected: + virtual void mousePressEvent(QMouseEvent* me); + virtual void mouseReleaseEvent(QMouseEvent* me); + virtual void mouseMoveEvent(QMouseEvent* me); + virtual void mouseDoubleClickEvent(QMouseEvent* me); + + virtual void paintEvent(QPaintEvent* pe); + +private: + // vars used to control structure and colors + int m_width; + int m_height; + + int m_seekerHorMargin = 5; + int m_seekerHeight = 38; // used to calcualte all hor sizes + int m_seekerWidth; + + int m_middleMargin = 6; + int m_editorHeight; + int m_editorWidth; + + QColor m_waveformBgColor = QColor(255, 255, 255, 0); + QColor m_waveformColor = QColor(123, 49, 212); + // QColor m_waveformColorDark = QColor(39, 15, 67); + + // QColor m_waveformColor = QColor(255, 161, 247); // logo color + QColor m_playColor = QColor(255, 255, 255, 200); + QColor m_playHighlighColor = QColor(255, 255, 255, 70); + QColor m_sliceColor = QColor(218, 193, 255); + QColor m_selectedSliceColor = QColor(178, 153, 215); + QColor m_seekerColor = QColor(178, 115, 255); + QColor m_seekerHighlightColor = QColor(178, 115, 255, 100); + QColor m_seekerShadowColor = QColor(0, 0, 0, 120); + + // interaction vars + float distanceForClick = 0.03f; + float minSeekerDistance = 0.13f; + + // dragging vars + enum class m_draggingTypes + { + nothing, + m_seekerStart, + m_seekerEnd, + m_seekerMiddle, + m_slicePoint, + }; + m_draggingTypes m_currentlyDragging; + + // seeker vars + float m_seekerStart = 0; + float m_seekerEnd = 1; + float m_seekerMiddle = 0.5f; + int m_sliceSelected = 0; + + // playback highlight vars + float m_noteCurrent; + float m_noteStart; + float m_noteEnd; + + // pixmaps + QPixmap m_sliceArrow; + QPixmap m_seeker; + QPixmap m_seekerWaveform; // only stores waveform graphic + QPixmap m_sliceEditor; + + SampleBuffer& m_currentSample; + + SlicerT* m_slicerTParent; + std::vector& m_slicePoints; + + void drawEditor(); + void drawSeekerWaveform(); + void drawSeeker(); +public slots: + void updateUI(); + void isPlaying(float current, float start, float end); + +public: + WaveForm(int w, int h, SlicerT* instrument, QWidget* parent); }; } // namespace gui } // namespace lmms From 0077806788214aea6b26738402e44c108960f187 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sat, 30 Sep 2023 17:42:14 +0200 Subject: [PATCH 34/99] slice snap + better defaults --- plugins/SlicerT/SlicerT.cpp | 59 +++++++++++++++++++++++++---------- plugins/SlicerT/SlicerT.h | 1 + plugins/SlicerT/SlicerTUI.cpp | 5 +++ plugins/SlicerT/SlicerTUI.h | 2 ++ plugins/SlicerT/WaveForm.cpp | 10 ++++++ 5 files changed, 61 insertions(+), 16 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 891edb55550..ff497288c7c 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -25,10 +25,8 @@ // TODO: fix some PV bugs // make fft size and windowsize seperate, pad window to fit into fft with 0 // this should produce better quality hopefully -// TODO: onset detection find valleys // TODO: switch to arrayVector (maybe) // TODO: cleaunp UI classes -// TODO: add text when no sample loaded // TODO: better buttons #include "SlicerT.h" @@ -273,12 +271,21 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) SlicerT::SlicerT(InstrumentTrack* instrumentTrack) : Instrument(instrumentTrack, &slicert_plugin_descriptor) - , m_noteThreshold(0.3f, 0.0f, 2.0f, 0.01f, this, tr("Note threshold")) + , m_noteThreshold(0.6f, 0.0f, 2.0f, 0.01f, this, tr("Note threshold")) , m_fadeOutFrames(400.0f, 0.0f, 8192.0f, 1.0f, this, tr("FadeOut")) , m_originalBPM(1, 1, 999, this, tr("Original bpm")) + , m_sliceSnap(this, tr("Slice snap")) , m_originalSample() , m_phaseVocoder() { + m_sliceSnap.addItem("None"); + m_sliceSnap.addItem("1/1"); + m_sliceSnap.addItem("1/2"); + m_sliceSnap.addItem("1/4"); + m_sliceSnap.addItem("1/8"); + m_sliceSnap.addItem("1/16"); + m_sliceSnap.addItem("1/32"); + m_sliceSnap.setValue(0); } void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) @@ -291,8 +298,8 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) const fpp_t frames = handle->framesLeftForCurrentPeriod(); const f_cnt_t offset = handle->noteOffset(); const int playedFrames = handle->totalFramesPlayed(); - m_phaseVocoder.setScaleRatio(speedRatio); + m_phaseVocoder.setScaleRatio(speedRatio); // check if bpm changed const int totalFrames = m_phaseVocoder.frames(); int sliceStart, sliceEnd; @@ -349,34 +356,35 @@ void SlicerT::findSlices() { if (m_originalSample.frames() < 2048) { return; } m_slicePoints = {}; - const int windowSize = 1024; - const int minDist = 2048; - std::vector leftChannel(m_originalSample.frames(), 0); + const int windowSize = 512; + const int minDist = 2048; // this value should probably be calculated through samplerate or something + + std::vector singleChannel(m_originalSample.frames(), 0); for (int i = 0; i < m_originalSample.frames(); i++) { - leftChannel[i] = m_originalSample.data()[i][0]; + singleChannel[i] = (m_originalSample.data()[i][0] + m_originalSample.data()[i][1]) / 2; } - std::vector prevMags(windowSize, 0); + std::vector prevMags(windowSize / 2, 0); std::vector fftIn(windowSize, 0); fftwf_complex fftOut[windowSize]; fftwf_plan fftPlan = fftwf_plan_dft_r2c_1d(windowSize, fftIn.data(), fftOut, FFTW_MEASURE); - int lastPoint = -minDist - 1; + int lastPoint = -minDist - 1; // to always store 0 first float spectralFlux = 0; float prevFlux = 0; float real, imag, magnitude, diff; - for (int i = 0; i < leftChannel.size() - windowSize; i += windowSize) + for (int i = 0; i < singleChannel.size() - windowSize; i += windowSize) { - memcpy(fftIn.data(), leftChannel.data() + i, windowSize * sizeof(float)); + memcpy(fftIn.data(), singleChannel.data() + i, windowSize * sizeof(float)); fftwf_execute(fftPlan); - for (int j = 0; j < windowSize / 2; j++) - { // only use niquistic frequencies + for (int j = 0; j < windowSize / 2; j++) // only use niquistic frequencies + { real = fftOut[j][0]; imag = fftOut[j][1]; magnitude = sqrt(real * real + imag * imag); @@ -399,6 +407,25 @@ void SlicerT::findSlices() m_slicePoints.push_back(m_originalSample.frames()); + int noteLock = m_sliceSnap.value(); // 1 / notelock; is to which note is locked + int timeSignature = Engine::getSong()->getTimeSigModel().getNumerator(); + int samplesPerBar = 60.0f * timeSignature / m_originalBPM.value() * m_originalSample.sampleRate(); + int sliceLock = samplesPerBar / pow(2, noteLock + 1); + if (noteLock == 0) { sliceLock = 1; } // disable notelock + + for (int i = 0; i < m_slicePoints.size(); i++) + { + m_slicePoints[i] += sliceLock / 2; + m_slicePoints[i] -= m_slicePoints[i] % sliceLock; + } + + // set to fit + m_slicePoints[0] = 0; + m_slicePoints[m_slicePoints.size()] = m_originalSample.frames(); + + // remove duplicates + m_slicePoints.erase(std::unique(m_slicePoints.begin(), m_slicePoints.end()), m_slicePoints.end()); + emit dataChanged(); } @@ -466,7 +493,7 @@ void SlicerT::writeToMidi(std::vector* outClip) Note sliceNote = Note(); sliceNote.setKey(i + 69); sliceNote.setPos(sliceStart); - sliceNote.setLength(sliceEnd - sliceStart); + sliceNote.setLength(sliceEnd - sliceStart + 1); // + 1 needed for whatever reason outClip->push_back(sliceNote); lastEnd = sliceEnd; @@ -478,8 +505,8 @@ void SlicerT::updateFile(QString file) m_originalSample.setAudioFile(file); if (m_originalSample.frames() < 2048) { return; } - findSlices(); findBPM(); + findSlices(); float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo(); m_phaseVocoder.loadSample( diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index ebb0fee0f61..e276cacbd3a 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -172,6 +172,7 @@ public slots: FloatModel m_noteThreshold; FloatModel m_fadeOutFrames; IntModel m_originalBPM; + ComboBoxModel m_sliceSnap; SampleBuffer m_originalSample; dinamicPlaybackBuffer m_phaseVocoder; diff --git a/plugins/SlicerT/SlicerTUI.cpp b/plugins/SlicerT/SlicerTUI.cpp index c0d79ae8e0e..12fccdd638e 100644 --- a/plugins/SlicerT/SlicerTUI.cpp +++ b/plugins/SlicerT/SlicerTUI.cpp @@ -47,6 +47,7 @@ SlicerTUI::SlicerTUI(SlicerT* instrument, QWidget* parent) , m_noteThresholdKnob(this) , m_fadeOutKnob(this) , m_bpmBox(3, "21pink", this) + , m_snapSetting(this, "Slice snap") , m_resetButton(this, nullptr) , m_midiExportButton(this, nullptr) , m_wf(248, 128, instrument, this) @@ -60,6 +61,10 @@ SlicerTUI::SlicerTUI(SlicerT* instrument, QWidget* parent) m_wf.move(2, 6); + m_snapSetting.setGeometry(14, 150, 55, ComboBox::DEFAULT_HEIGHT); + m_snapSetting.setToolTip(tr("Set slice snapping for detection")); + m_snapSetting.setModel(&m_slicerTParent->m_sliceSnap); + m_bpmBox.move(135, 200); m_bpmBox.setToolTip(tr("Original sample BPM")); m_bpmBox.setLabel(tr("BPM")); diff --git a/plugins/SlicerT/SlicerTUI.h b/plugins/SlicerT/SlicerTUI.h index d83f679932f..b028fc7c2de 100644 --- a/plugins/SlicerT/SlicerTUI.h +++ b/plugins/SlicerT/SlicerTUI.h @@ -27,6 +27,7 @@ #include +#include "ComboBox.h" #include "Instrument.h" #include "InstrumentView.h" #include "Knob.h" @@ -80,6 +81,7 @@ protected slots: SlicerTKnob m_noteThresholdKnob; SlicerTKnob m_fadeOutKnob; LcdSpinBox m_bpmBox; + ComboBox m_snapSetting; PixmapButton m_resetButton; PixmapButton m_midiExportButton; diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/WaveForm.cpp index 28e568de3fa..275e7c11d9f 100644 --- a/plugins/SlicerT/WaveForm.cpp +++ b/plugins/SlicerT/WaveForm.cpp @@ -81,6 +81,8 @@ void WaveForm::drawSeeker() { m_seeker.fill(m_waveformBgColor); QPainter brush(&m_seeker); + + // if (m_currentSample.frames() < 2048) { return; } // draw slice points brush.setPen(m_sliceColor); for (int i = 0; i < m_slicePoints.size(); i++) @@ -115,6 +117,14 @@ void WaveForm::drawEditor() m_sliceEditor.fill(m_waveformBgColor); QPainter brush(&m_sliceEditor); + if (m_currentSample.frames() < 2048) + { + brush.setPen(m_playHighlighColor); + brush.drawText( + m_editorWidth / 2 - 100, m_editorHeight / 2 - 100, 200, 200, Qt::AlignCenter, tr("Drag sample to load")); + return; + } + float startFrame = m_seekerStart * m_currentSample.frames(); float endFrame = m_seekerEnd * m_currentSample.frames(); float numFramesToDraw = endFrame - startFrame; From 0ed6237de1a808e07ecdc4d0c73e36fbb3024ec8 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sat, 30 Sep 2023 17:46:36 +0200 Subject: [PATCH 35/99] windows build fixes --- include/Clipboard.h | 59 +++++++++++++++++++------------------ plugins/SlicerT/SlicerT.cpp | 14 +++++---- 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/include/Clipboard.h b/include/Clipboard.h index c6ae66ee873..03793fa2ce4 100644 --- a/include/Clipboard.h +++ b/include/Clipboard.h @@ -25,46 +25,47 @@ #ifndef LMMS_CLIPBOARD_H #define LMMS_CLIPBOARD_H -#include #include +#include + +#include "lmms_export.h" class QMimeData; -namespace lmms::Clipboard -{ +namespace lmms::Clipboard { - enum class MimeType - { - StringPair, - Default - }; +enum class MimeType +{ + StringPair, + Default +}; - // Convenience Methods - const QMimeData * getMimeData(); - bool hasFormat( MimeType mT ); +// Convenience Methods +const QMimeData* getMimeData(); +bool hasFormat(MimeType mT); - // Helper methods for String data - void copyString( const QString & str, MimeType mT ); - QString getString( MimeType mT ); +// Helper methods for String data +void copyString(const QString& str, MimeType mT); +QString getString(MimeType mT); - // Helper methods for String Pair data - void copyStringPair( const QString & key, const QString & value ); - QString decodeKey( const QMimeData * mimeData ); - QString decodeValue( const QMimeData * mimeData ); +// Helper methods for String Pair data +void copyStringPair(const QString& key, const QString& value); +QString decodeKey(const QMimeData* mimeData); +QString decodeValue(const QMimeData* mimeData); - inline const char * mimeType( MimeType type ) +inline const char* mimeType(MimeType type) +{ + switch (type) { - switch( type ) - { - case MimeType::StringPair: - return "application/x-lmms-stringpair"; - break; - case MimeType::Default: - default: - return "application/x-lmms-clipboard"; - break; - } + case MimeType::StringPair: + return "application/x-lmms-stringpair"; + break; + case MimeType::Default: + default: + return "application/x-lmms-clipboard"; + break; } +} } // namespace lmms::Clipboard diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index ff497288c7c..442facadd00 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -33,12 +33,14 @@ #include #include +#include #include "Engine.h" #include "InstrumentTrack.h" #include "PathUtil.h" #include "Song.h" #include "embed.h" +#include "lmms_constants.h" #include "plugin_export.h" namespace lmms { @@ -144,8 +146,8 @@ void PhaseVocoder::updateParams(float newRatio) numWindows = (float)originalBuffer.size() / stepSize - overSampling - 1; outStepSize = m_scaleRatio * (float)stepSize; // float, else inaccurate freqPerBin = originalSampleRate / windowSize; - expectedPhaseIn = 2. * M_PI * (float)stepSize / (float)windowSize; - expectedPhaseOut = 2. * M_PI * (float)outStepSize / (float)windowSize; + expectedPhaseIn = 2. * F_PI * (float)stepSize / (float)windowSize; + expectedPhaseOut = 2. * F_PI * (float)outStepSize / (float)windowSize; processedBuffer.resize(m_scaleRatio * originalBuffer.size(), 0); @@ -196,9 +198,9 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) freq -= (float)j * expectedPhaseIn; // subtract expected phase // this puts freq in 0-2pi - freq = fmod(freq + M_PI, -2.0f * M_PI) + M_PI; + freq = fmod(freq + F_PI, -2.0f * F_PI) + F_PI; - freq = (float)overSampling * freq / (2. * M_PI); // idk + freq = (float)overSampling * freq / (2. * F_PI); // idk freq = (float)j * freqPerBin + freq * freqPerBin; // "compute the k-th partials' true frequency" ok i guess @@ -226,7 +228,7 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) deltaPhase /= freqPerBin; - deltaPhase = 2. * M_PI * deltaPhase / overSampling; + deltaPhase = 2. * F_PI * deltaPhase / overSampling; deltaPhase += (float)j * expectedPhaseOut; @@ -262,7 +264,7 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) // (float)overSampling/totalWindowOverlap*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); // printf("timeshifted in phase: %f\n", m_timeshiftedBufferL[outIndex]); // continuos windowing - float window = -0.5f * cos(2. * M_PI * (float)j / (float)windowSize) + 0.5f; + float window = -0.5f * cos(2. * F_PI * (float)j / (float)windowSize) + 0.5f; processedBuffer[outIndex] += window * IFFTReconstruction[j] / (windowSize / 2.0f * overSampling); } } From c91e4a42a1074c736574973b1cacaab00c930491 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 1 Oct 2023 01:05:06 +0200 Subject: [PATCH 36/99] windows build fixes part 2 --- include/Clipboard.h | 55 +++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/include/Clipboard.h b/include/Clipboard.h index 03793fa2ce4..491f8ebf748 100644 --- a/include/Clipboard.h +++ b/include/Clipboard.h @@ -32,40 +32,41 @@ class QMimeData; -namespace lmms::Clipboard { - -enum class MimeType +namespace lmms::Clipboard { - StringPair, - Default -}; -// Convenience Methods -const QMimeData* getMimeData(); -bool hasFormat(MimeType mT); + enum class MimeType + { + StringPair, + Default + }; + + // Convenience Methods + const QMimeData * getMimeData(); + bool hasFormat( MimeType mT ); -// Helper methods for String data -void copyString(const QString& str, MimeType mT); -QString getString(MimeType mT); + // Helper methods for String data + void LMMS_EXPORT copyString( const QString & str, MimeType mT ); + QString getString( MimeType mT ); -// Helper methods for String Pair data -void copyStringPair(const QString& key, const QString& value); -QString decodeKey(const QMimeData* mimeData); -QString decodeValue(const QMimeData* mimeData); + // Helper methods for String Pair data + void copyStringPair( const QString & key, const QString & value ); + QString decodeKey( const QMimeData * mimeData ); + QString decodeValue( const QMimeData * mimeData ); -inline const char* mimeType(MimeType type) -{ - switch (type) + inline const char * mimeType( MimeType type ) { - case MimeType::StringPair: - return "application/x-lmms-stringpair"; - break; - case MimeType::Default: - default: - return "application/x-lmms-clipboard"; - break; + switch( type ) + { + case MimeType::StringPair: + return "application/x-lmms-stringpair"; + break; + case MimeType::Default: + default: + return "application/x-lmms-clipboard"; + break; + } } -} } // namespace lmms::Clipboard From 7f6d7fd391af41958e0664d15f174d92effd3cc3 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 1 Oct 2023 20:29:56 +0200 Subject: [PATCH 37/99] Fixed slice bug + Waveform code cleanup --- plugins/SlicerT/SlicerT.cpp | 20 +++++++++++--------- plugins/SlicerT/WaveForm.cpp | 29 +++++++++++++++++------------ 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 442facadd00..6e70e649268 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -22,11 +22,6 @@ * */ -// TODO: fix some PV bugs -// make fft size and windowsize seperate, pad window to fit into fft with 0 -// this should produce better quality hopefully -// TODO: switch to arrayVector (maybe) -// TODO: cleaunp UI classes // TODO: better buttons #include "SlicerT.h" @@ -421,13 +416,20 @@ void SlicerT::findSlices() m_slicePoints[i] -= m_slicePoints[i] % sliceLock; } - // set to fit - m_slicePoints[0] = 0; - m_slicePoints[m_slicePoints.size()] = m_originalSample.frames(); - // remove duplicates m_slicePoints.erase(std::unique(m_slicePoints.begin(), m_slicePoints.end()), m_slicePoints.end()); + // fit to sample size + m_slicePoints[0] = 0; + m_slicePoints[m_slicePoints.size()-1] = m_originalSample.frames(); + + for (int i = 0; i < m_slicePoints.size(); i++) { + printf("%i \n", m_slicePoints[i]); + } + + + printf("total frames: %i\n", m_originalSample.frames()); + emit dataChanged(); } diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/WaveForm.cpp index 275e7c11d9f..3f74bfb1dc5 100644 --- a/plugins/SlicerT/WaveForm.cpp +++ b/plugins/SlicerT/WaveForm.cpp @@ -91,25 +91,31 @@ void WaveForm::drawSeeker() brush.drawLine(xPos, 0, xPos, m_seekerHeight); } - // this is a complete mess, TODO: cleanup + // seeker vars + float seekerStartPosX = m_seekerStart * m_seekerWidth; + float seekerEndPosX = m_seekerEnd * m_seekerWidth; + float seekerMiddleWidth = (m_seekerEnd - m_seekerStart) * m_seekerWidth; + + // note playback vars + float noteCurrentPosX = m_noteCurrent * m_seekerWidth; + float noteStartPosX = m_noteStart * m_seekerWidth; + float noteEndPosX = (m_noteEnd - m_noteStart) * m_seekerWidth; + // draw current playBack brush.setPen(m_playColor); - brush.drawLine(m_noteCurrent * m_seekerWidth, 0, m_noteCurrent * m_seekerWidth, m_seekerHeight); - brush.fillRect( - m_noteStart * m_seekerWidth, 0, (m_noteEnd - m_noteStart) * m_seekerWidth, m_seekerHeight, m_playHighlighColor); + brush.drawLine(noteCurrentPosX, 0, noteCurrentPosX, m_seekerHeight); + brush.fillRect(noteStartPosX, 0, noteEndPosX, m_seekerHeight, m_playHighlighColor); // highlight on selected area - brush.fillRect(m_seekerStart * m_seekerWidth, 0, (m_seekerEnd - m_seekerStart) * m_seekerWidth, m_seekerHeight, - m_seekerHighlightColor); + brush.fillRect(seekerStartPosX, 0, seekerMiddleWidth, m_seekerHeight, m_seekerHighlightColor); // shadow on not selected area - brush.fillRect(0, 0, m_seekerStart * m_seekerWidth, m_seekerHeight, m_seekerShadowColor); - brush.fillRect(m_seekerEnd * m_seekerWidth + 1, 0, m_seekerWidth + 1, m_seekerHeight, m_seekerShadowColor); + brush.fillRect(0, 0, seekerStartPosX, m_seekerHeight, m_seekerShadowColor); + brush.fillRect(seekerEndPosX + 1, 0, m_seekerWidth + 1, m_seekerHeight, m_seekerShadowColor); // draw border around selection brush.setPen(QPen(m_seekerColor, 1)); - brush.drawRoundedRect(m_seekerStart * m_seekerWidth, 0, (m_seekerEnd - m_seekerStart) * m_seekerWidth - 1, - m_seekerHeight - 1, 4, 4); // -1 needed + brush.drawRoundedRect(seekerStartPosX, 0, seekerMiddleWidth - 1, m_seekerHeight - 1, 4, 4); // -1 needed } void WaveForm::drawEditor() @@ -231,6 +237,7 @@ void WaveForm::mousePressEvent(QMouseEvent* me) void WaveForm::mouseReleaseEvent(QMouseEvent* me) { m_currentlyDragging = m_draggingTypes::nothing; + std::sort(m_slicePoints.begin(), m_slicePoints.end()); updateUI(); } @@ -269,8 +276,6 @@ void WaveForm::mouseMoveEvent(QMouseEvent* me) m_slicePoints[m_sliceSelected] = startFrame + normalizedClickEditor * (endFrame - startFrame); m_slicePoints[m_sliceSelected] = std::clamp(m_slicePoints[m_sliceSelected], 0, m_currentSample.frames()); - - std::sort(m_slicePoints.begin(), m_slicePoints.end()); break; case m_draggingTypes::nothing: break; From ea75dccd0407223a48c05908419366513c4613f0 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 1 Oct 2023 21:48:40 +0200 Subject: [PATCH 38/99] UI button text + reorder + file cleanup --- plugins/SlicerT/SlicerT.cpp | 2 -- plugins/SlicerT/SlicerTUI.cpp | 22 ++++++++++++++-------- plugins/SlicerT/WaveForm.cpp | 1 + plugins/SlicerT/bg_no_logo.png | Bin 16125 -> 0 bytes plugins/SlicerT/copyMidi.png | Bin 0 -> 4249 bytes plugins/SlicerT/knob_center.png | Bin 6455 -> 0 bytes plugins/SlicerT/resetSlices.png | Bin 0 -> 493 bytes plugins/SlicerT/slide_indicator_bar.png | Bin 197 -> 0 bytes plugins/SlicerT/slide_indicator_full.png | Bin 290 -> 0 bytes 9 files changed, 15 insertions(+), 10 deletions(-) delete mode 100644 plugins/SlicerT/bg_no_logo.png create mode 100644 plugins/SlicerT/copyMidi.png delete mode 100644 plugins/SlicerT/knob_center.png create mode 100644 plugins/SlicerT/resetSlices.png delete mode 100644 plugins/SlicerT/slide_indicator_bar.png delete mode 100644 plugins/SlicerT/slide_indicator_full.png diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 6e70e649268..4f46f12653a 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -22,8 +22,6 @@ * */ -// TODO: better buttons - #include "SlicerT.h" #include diff --git a/plugins/SlicerT/SlicerTUI.cpp b/plugins/SlicerT/SlicerTUI.cpp index 12fccdd638e..e91953dbb78 100644 --- a/plugins/SlicerT/SlicerTUI.cpp +++ b/plugins/SlicerT/SlicerTUI.cpp @@ -61,11 +61,11 @@ SlicerTUI::SlicerTUI(SlicerT* instrument, QWidget* parent) m_wf.move(2, 6); - m_snapSetting.setGeometry(14, 150, 55, ComboBox::DEFAULT_HEIGHT); + m_snapSetting.setGeometry(190, 200, 55, ComboBox::DEFAULT_HEIGHT); m_snapSetting.setToolTip(tr("Set slice snapping for detection")); m_snapSetting.setModel(&m_slicerTParent->m_sliceSnap); - m_bpmBox.move(135, 200); + m_bpmBox.move(135, 203); m_bpmBox.setToolTip(tr("Original sample BPM")); m_bpmBox.setLabel(tr("BPM")); m_bpmBox.setModel(&m_slicerTParent->m_originalBPM); @@ -80,15 +80,15 @@ SlicerTUI::SlicerTUI(SlicerT* instrument, QWidget* parent) m_fadeOutKnob.setLabel(tr("FadeOut")); m_fadeOutKnob.setModel(&m_slicerTParent->m_fadeOutFrames); - m_midiExportButton.move(190, 200); - m_midiExportButton.setActiveGraphic(embed::getIconPixmap("midi_tab")); - m_midiExportButton.setInactiveGraphic(embed::getIconPixmap("midi_tab")); + m_midiExportButton.move(205, 155); + m_midiExportButton.setActiveGraphic(PLUGIN_NAME::getIconPixmap("copyMidi")); + m_midiExportButton.setInactiveGraphic(PLUGIN_NAME::getIconPixmap("copyMidi")); m_midiExportButton.setToolTip(tr("Copy midi pattern to clipboard")); connect(&m_midiExportButton, SIGNAL(clicked()), this, SLOT(exportMidi())); - m_resetButton.move(215, 200); - m_resetButton.setActiveGraphic(embed::getIconPixmap("reload")); - m_resetButton.setInactiveGraphic(embed::getIconPixmap("reload")); + m_resetButton.move(25, 155); + m_resetButton.setActiveGraphic(PLUGIN_NAME::getIconPixmap("resetSlices")); + m_resetButton.setInactiveGraphic(PLUGIN_NAME::getIconPixmap("resetSlices")); m_resetButton.setToolTip(tr("Reset Slices")); connect(&m_resetButton, SIGNAL(clicked()), m_slicerTParent, SLOT(updateSlices())); } @@ -161,6 +161,12 @@ void SlicerTUI::dropEvent(QDropEvent* de) void SlicerTUI::paintEvent(QPaintEvent* pe) { + QPainter brush(this); + brush.setPen(QColor(255, 255, 255)); + brush.setFont(QFont(brush.font().family(), 7.5f, -1, false)); + brush.drawText(205, 170, 20, 20, Qt::AlignCenter, tr("Midi")); + brush.drawText(22, 170, 25, 20, Qt::AlignCenter, tr("Reset")); + brush.drawText(190, 217, 55, 20, Qt::AlignCenter, tr("Snap")); } } // namespace gui diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/WaveForm.cpp index 3f74bfb1dc5..934756f306e 100644 --- a/plugins/SlicerT/WaveForm.cpp +++ b/plugins/SlicerT/WaveForm.cpp @@ -126,6 +126,7 @@ void WaveForm::drawEditor() if (m_currentSample.frames() < 2048) { brush.setPen(m_playHighlighColor); + brush.setFont(QFont(brush.font().family(), 9.0f, -1, false)); brush.drawText( m_editorWidth / 2 - 100, m_editorHeight / 2 - 100, 200, 200, Qt::AlignCenter, tr("Drag sample to load")); return; diff --git a/plugins/SlicerT/bg_no_logo.png b/plugins/SlicerT/bg_no_logo.png deleted file mode 100644 index b017cc9f28f88c8dec2d517f970c800efbff5596..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16125 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 aMsaqnwE353-F&44O+`UdzF5}s<^KY@cnlB# diff --git a/plugins/SlicerT/copyMidi.png b/plugins/SlicerT/copyMidi.png new file mode 100644 index 0000000000000000000000000000000000000000..e2ef15199bc9cf6d1c16938d9187bb18f23770a2 GIT binary patch literal 4249 zcmeHKd2AGA6rZI^OQDJtl|yhcXhMP6Ird<7+yzRv$7;9KQlTYkIy>{tc3^jBc4pdc zNl>{IOd&u(C4w;o!zDyef(R+$iWiCzFH96L@g|^xQUQZ+X1CjIBr#3ozs}~{dGo&a z{oZ@O?|pA~metl&78Q;x#4xNVR28g)`!H)14219H%SIQ$?PJssl0%{KSOGkMrQ;UN zVcEcN2bX1Q7qg9X{b-UdT? zHtnkuWa}!hKV2Z@!|1+OhQhTt>ttxB%MI&k*2Ob4Pct}6^ByPop{%f$9@|9)Q&;NpIrUTkIlQrHff{Ydh2}i`(w0yc^&={yG!=h zt$ew*;-2zUw*&8DgG6xG(&=4^90RXs{EKA(@G zS&C&zupo{0gej!RgfZ5FNMi(%A?k`|Dry3^FolTPVg?8T=JD?M;#xSIg-;k67Jv^b zC1@1mq^WqE%3WcY<*fjc3Fw9^jQVyBQFX{rTXYeXx1xkOHW$J=L0JSVrKl3u?4*Fa zUd|*jYp1p7G22EGDHKC-05u>!rkA~`$US8Bip`p_)5#44+-325S$F4d8-rCi%m-Dm z#c~=71_&!&UQ$Ix;%!l6Bu4Of+@y>4BGN5Qa*;m7a3rW1#QMBUgky7{LJ7kZ5+bso z0Nkknj_h^O5n1w*5!&q`-KcIFwUTH_^_bQx zaWumeEYcV+WW{;i9+vg`Je;q`XeQDPC^!q#ZjttkB=Qx2C_v#TaUqH*EfKXRtZm^T z86Z}$>J*S|J>LBT}eE!&6dyZi72n&s(qUKU(f zl@z)CzpSmpgZsfAw!BI)z<;}~N}s41D4BjrKgJaMDB-w$D0o3kr(g)JNV4Mutn`%F zEF_``x<{s5yY28T+Y!apJcPa+(aR zOY5L{K`*4T%`n3m#CtQZ%wF6~7dYPUq)+_z)74K`pBU(qaesC7)72*i`efW+UH=-=vn}#Q!zGL0R>u_m_>WGwRIx7DAvp53xw r>{nA8__7-H%cB)Xw1VN>00(y4=6i=EUwv*JAYh^Ln&8f9O$+}4hIpt@ literal 0 HcmV?d00001 diff --git a/plugins/SlicerT/knob_center.png b/plugins/SlicerT/knob_center.png deleted file mode 100644 index f74e202309025859bc4de2bd7498f0f20154d9c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6455 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 diff --git a/plugins/SlicerT/resetSlices.png b/plugins/SlicerT/resetSlices.png new file mode 100644 index 0000000000000000000000000000000000000000..15de6cdc18fccc8bc7b0e7e01fe4b44dea8be677 GIT binary patch literal 493 zcmV-vmXaVUnmzyy0|Ltik~Q2Lw1U7OI!%|$-36MPWU}Jl{r2D8nGNcVCS^vY zR2opt6B1`vfjwYd6X02M5Ju+Jn;VCbi2bm0Rr3rBXxy3?Eqci3OE8x(UE%!`vCUC$ULOu@9aXIWx@Pt z11njA|H`{S4d@ra5@%!JEKm23+|qGW;0%4uI8XN-`lYdSlwR0A&@YWYMdIvc;;baj zK4t-*bnN`Y(1DJ_EZ`k51-gl|?LWBN0Cs@yOrZAW2@n9cMbO0A7I4q3N>0Pbyv?=1 z|GqL>ug{|OzJN7#b*N^BVPv-b&4ToQsL&p>uBqjioC4>-Wf+;yLZAmue_s{gMe{n0 j%zM_lT;Q@%dSU+nYpk5`^k80=00000NkvXXu0mjf#CFZh literal 0 HcmV?d00001 diff --git a/plugins/SlicerT/slide_indicator_bar.png b/plugins/SlicerT/slide_indicator_bar.png deleted file mode 100644 index b17a644e8209f839c61ff3c99b33338ef541eef5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 197 zcmeAS@N?(olHy`uVBq!ia0vp^OhD|*=^Qfk1qw-)xJHyX=jZ08=9Mrw7o{eaq^2m8 zXO?6rxO@5rgg5eu0~P6dx;TbtoKJqUV#fFRjg1fGg_)U|ohPI#2@5le^ccBGHqSYD i=)i#w`FFqlO>_ z%)p?h48n{ROYO^mg6t)pzOL-gIR!;I6i!!3cmahZOI#yLobz*YQ}ap~oQqNuOHxx5 z$}>wc6x=<11Hv2m#DR*sJY5_^JdP)CN%H*le&xIa4ZJyf?7AB}8yOO;zNO1Op2$Ai zP=)W_{R0Kh?hCo8v9YnW&5hqJxqgog+w?tu9!VUYmtWf>u}f&lvcLWlW_^>7>|vd` zSn&A40|yR#=wHZulJ~_jmP)ln9fIGEu4oEmUCiYs+M^{AYOpFHs7(8A5T-G@yGywo1mt=?l From dd60d6953d433a17a66fa58d65a78c9f74efd837 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 1 Oct 2023 23:21:01 +0200 Subject: [PATCH 39/99] Added knob colors --- data/themes/default/style.css | 8 ++++++++ plugins/SlicerT/SlicerT.cpp | 2 +- plugins/SlicerT/SlicerTUI.h | 4 ---- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/data/themes/default/style.css b/data/themes/default/style.css index 092332eee65..2b875f47ebe 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -946,6 +946,14 @@ lmms--gui--SidInstrumentView lmms--gui--Knob { qproperty-lineWidth: 2; } +lmms--gui--SlicerTUI lmms--gui--Knob { + color: rgb(162, 128, 226); + qproperty-outerColor: rgb( 162, 128, 226 ); + qproperty-innerRadius: 1; + qproperty-outerRadius: 11; + qproperty-lineWidth: 3; +} + lmms--gui--WatsynView lmms--gui--Knob { qproperty-innerRadius: 1; qproperty-outerRadius: 7; diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 4f46f12653a..2830c63d5d6 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -273,7 +273,7 @@ SlicerT::SlicerT(InstrumentTrack* instrumentTrack) , m_originalSample() , m_phaseVocoder() { - m_sliceSnap.addItem("None"); + m_sliceSnap.addItem("Off"); m_sliceSnap.addItem("1/1"); m_sliceSnap.addItem("1/2"); m_sliceSnap.addItem("1/4"); diff --git a/plugins/SlicerT/SlicerTUI.h b/plugins/SlicerT/SlicerTUI.h index b028fc7c2de..7bced0b2e04 100644 --- a/plugins/SlicerT/SlicerTUI.h +++ b/plugins/SlicerT/SlicerTUI.h @@ -50,10 +50,6 @@ class SlicerTKnob : public Knob setFixedSize(46, 40); setCenterPointX(23.0); setCenterPointY(15.0); - setInnerRadius(3); - setOuterRadius(11); - setLineWidth(3); - setOuterColor(QColor(178, 115, 255)); } }; From 79d6f47c3e739c1ba2662a3b97993d820a2ec4c6 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Mon, 2 Oct 2023 22:43:57 +0200 Subject: [PATCH 40/99] comments + code cleanup --- plugins/SlicerT/SlicerT.cpp | 101 ++++++++++++++++++---------------- plugins/SlicerT/SlicerT.h | 9 ++- plugins/SlicerT/SlicerTUI.cpp | 10 ++++ plugins/SlicerT/SlicerTUI.h | 5 +- plugins/SlicerT/WaveForm.cpp | 18 +++--- plugins/SlicerT/WaveForm.h | 10 ++-- 6 files changed, 89 insertions(+), 64 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 2830c63d5d6..6dc5edc674f 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -87,11 +87,12 @@ void PhaseVocoder::loadData(std::vector originalData, int sampleRate, flo freqCache.resize(numWindows * windowSize, 0); magCache.resize(numWindows * windowSize, 0); + // maybe limit this to a set amount of windows to reduce initial lag spikes for (int i = 0; i < numWindows; i++) { if (!m_processedWindows[i]) { - generateWindow(i, false); + generateWindow(i, false); // first pass, no cache m_processedWindows[i] = true; } } @@ -113,7 +114,7 @@ void PhaseVocoder::getFrames(std::vector& outData, int start, int frames) { if (!m_processedWindows[i]) { - generateWindow(i, true); + generateWindow(i, true); // theses should use the cache m_processedWindows[i] = true; } } @@ -126,14 +127,13 @@ void PhaseVocoder::getFrames(std::vector& outData, int start, int frames) dataLock.unlock(); } -// adjust pv params and reset buffers +// adjust pv params buffers to a new scale ratio void PhaseVocoder::updateParams(float newRatio) { if (originalBuffer.size() < 2048) { return; } - if (newRatio == m_scaleRatio) { return; } + if (newRatio == m_scaleRatio) { return; } // nothing changed dataLock.lock(); - // TODO: remove static stuff from here, like stepsize m_scaleRatio = newRatio; stepSize = (float)windowSize / overSampling; numWindows = (float)originalBuffer.size() / stepSize - overSampling - 1; @@ -147,7 +147,8 @@ void PhaseVocoder::updateParams(float newRatio) // very slow :( std::fill(m_processedWindows.begin(), m_processedWindows.end(), false); std::fill(processedBuffer.begin(), processedBuffer.end(), 0); - // somehow this works if this is commented, idk why but its faster + // this can be commented, since the start phase is not importante to the PV + // and the sum phase will grow linearly anyway // std::fill(lastPhase.begin(), lastPhase.end(), 0); // std::fill(sumPhase.begin(), sumPhase.end(), 0); @@ -183,19 +184,25 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) magnitude = 2. * sqrt(real * real + imag * imag); phase = atan2(imag, real); + // calculate difference in phase with prev window freq = phase; freq = phase - lastPhase[std::max(0, windowIndex + j - windowSize)]; // subtract prev pahse to get phase diference lastPhase[windowIndex + j] = phase; freq -= (float)j * expectedPhaseIn; // subtract expected phase + // at this point, freq is the difference in phase + // between the last phase, having removed the expected phase at this point in the sample - // this puts freq in 0-2pi + // this puts freq in 0-2pi. Since the phase difference is proportional to the deviation in bin frequency, + // with this we can better estimate the true frequency freq = fmod(freq + F_PI, -2.0f * F_PI) + F_PI; - freq = (float)overSampling * freq / (2. * F_PI); // idk + // convert phase difference into bin freq mulitplier + freq = (float)overSampling * freq / (2. * F_PI); - freq = (float)j * freqPerBin + freq * freqPerBin; // "compute the k-th partials' true frequency" ok i guess + // add to the expected freq the change in freq calculated from the phase diff + freq = (float)j * freqPerBin + freq * freqPerBin; allMagnitudes[j] = magnitude; allFrequencies[j] = freq; @@ -217,14 +224,19 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) magnitude = allMagnitudes[j]; freq = allFrequencies[j]; + // difference in freq deltaPhase = freq - (float)j * freqPerBin; + // scaled to 1 deltaPhase /= freqPerBin; + // difference in phase deltaPhase = 2. * F_PI * deltaPhase / overSampling; + // add the expected phase deltaPhase += (float)j * expectedPhaseOut; + // sum this phase to the total, to keep track of the out phase along the sample sumPhase[windowIndex + j] += deltaPhase; deltaPhase = sumPhase[windowIndex + j]; // this is the bin phase if (windowIndex + j + windowSize < sumPhase.size()) @@ -245,18 +257,7 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) float outIndex = windowNum * outStepSize + j; if (outIndex >= frames()) { break; } - // calculate windows overlapping at index - // float startWindowOverlap = ceil(outIndex / outStepSize + 0.00000001f); - // float endWindowOverlap = ceil((float)(-outIndex + dataOut.size()) / outStepSize + 0.00000001f); - // float totalWindowOverlap = std::min( - // std::min(startWindowOverlap, endWindowOverlap), - // (float)overSampling); - - // discrete windowing - // dataOut[outIndex] += - // (float)overSampling/totalWindowOverlap*IFFTReconstruction[j]/(windowSize/2.0f*overSampling); - // printf("timeshifted in phase: %f\n", m_timeshiftedBufferL[outIndex]); - // continuos windowing + // hann windowing float window = -0.5f * cos(2. * F_PI * (float)j / (float)windowSize) + 0.5f; processedBuffer[outIndex] += window * IFFTReconstruction[j] / (windowSize / 2.0f * overSampling); } @@ -280,25 +281,26 @@ SlicerT::SlicerT(InstrumentTrack* instrumentTrack) m_sliceSnap.addItem("1/8"); m_sliceSnap.addItem("1/16"); m_sliceSnap.addItem("1/32"); - m_sliceSnap.setValue(0); + m_sliceSnap.setValue(0); // no snap by default } void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) { if (m_originalSample.frames() < 2048) { return; } + // update current speed ratio, in case bpm changed const float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo(); + m_phaseVocoder.setScaleRatio(speedRatio); + // current playback status + const int totalFrames = m_phaseVocoder.frames(); const int noteIndex = handle->key() - 69; const fpp_t frames = handle->framesLeftForCurrentPeriod(); const f_cnt_t offset = handle->noteOffset(); const int playedFrames = handle->totalFramesPlayed(); - m_phaseVocoder.setScaleRatio(speedRatio); // check if bpm changed - const int totalFrames = m_phaseVocoder.frames(); - int sliceStart, sliceEnd; - if (noteIndex > m_slicePoints.size() - 2 || noteIndex < 0) + if (noteIndex > m_slicePoints.size() - 2 || noteIndex < 0) // full sample if ouside range { sliceStart = 0; sliceEnd = totalFrames; @@ -309,6 +311,7 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) sliceEnd = m_slicePoints[noteIndex + 1] * speedRatio; } + // slice vars int sliceFrames = sliceEnd - sliceStart; int currentNoteFrame = sliceStart + playedFrames; int noteFramesLeft = sliceFrames - playedFrames; @@ -352,16 +355,18 @@ void SlicerT::findSlices() if (m_originalSample.frames() < 2048) { return; } m_slicePoints = {}; + // computacion params const int windowSize = 512; - const int minDist = 2048; // this value should probably be calculated through samplerate or something + const int minDist = 2048; + // copy vector into one vector, averaging channels std::vector singleChannel(m_originalSample.frames(), 0); - for (int i = 0; i < m_originalSample.frames(); i++) { singleChannel[i] = (m_originalSample.data()[i][0] + m_originalSample.data()[i][1]) / 2; } + // buffers std::vector prevMags(windowSize / 2, 0); std::vector fftIn(windowSize, 0); fftwf_complex fftOut[windowSize]; @@ -375,21 +380,25 @@ void SlicerT::findSlices() for (int i = 0; i < singleChannel.size() - windowSize; i += windowSize) { + // fft memcpy(fftIn.data(), singleChannel.data() + i, windowSize * sizeof(float)); fftwf_execute(fftPlan); + // calculate spectral flux in regard to last window for (int j = 0; j < windowSize / 2; j++) // only use niquistic frequencies { real = fftOut[j][0]; imag = fftOut[j][1]; magnitude = sqrt(real * real + imag * imag); + // using L2-norm (euclidean distance) diff = sqrt(pow(magnitude - prevMags[j], 2)); spectralFlux += diff; prevMags[j] = magnitude; } + // detect increases in flux if (spectralFlux / prevFlux > 1.0f + m_noteThreshold.value() && i - lastPoint > minDist) { m_slicePoints.push_back(i); @@ -402,11 +411,12 @@ void SlicerT::findSlices() m_slicePoints.push_back(m_originalSample.frames()); - int noteLock = m_sliceSnap.value(); // 1 / notelock; is to which note is locked + // snap slices to notes + int noteSnap = m_sliceSnap.value(); int timeSignature = Engine::getSong()->getTimeSigModel().getNumerator(); int samplesPerBar = 60.0f * timeSignature / m_originalBPM.value() * m_originalSample.sampleRate(); - int sliceLock = samplesPerBar / pow(2, noteLock + 1); - if (noteLock == 0) { sliceLock = 1; } // disable notelock + int sliceLock = samplesPerBar / pow(2, noteSnap + 1); // lock to note: 1 / noteSnap² + if (noteSnap == 0) { sliceLock = 1; } // disable noteSnap for (int i = 0; i < m_slicePoints.size(); i++) { @@ -421,13 +431,7 @@ void SlicerT::findSlices() m_slicePoints[0] = 0; m_slicePoints[m_slicePoints.size()-1] = m_originalSample.frames(); - for (int i = 0; i < m_slicePoints.size(); i++) { - printf("%i \n", m_slicePoints[i]); - } - - - printf("total frames: %i\n", m_originalSample.frames()); - + // update UI emit dataChanged(); } @@ -436,7 +440,6 @@ void SlicerT::findSlices() void SlicerT::findBPM() { if (m_originalSample.frames() < 2048) { return; } - int bpmSnap = 1; // 1 = disabled // caclulate length of sample float sampleRate = m_originalSample.sampleRate(); @@ -446,7 +449,7 @@ void SlicerT::findBPM() // this assumes the sample has a time signature of x/4 float bpmEstimate = 240.0f / sampleLength; - // deal with samlpes that are not 1 bar long + // get into 100 - 200 range while (bpmEstimate < 100) { bpmEstimate *= 2; @@ -457,13 +460,8 @@ void SlicerT::findBPM() bpmEstimate /= 2; } - // snap bpm - int bpm = bpmEstimate; - bpm += (float)bpmSnap / 2; - bpm -= bpm % bpmSnap; - - m_originalBPM.setValue(bpm); - m_originalBPM.setInitValue(bpm); + m_originalBPM.setValue(bpmEstimate); + m_originalBPM.setInitValue(bpmEstimate); } void SlicerT::writeToMidi(std::vector* outClip) @@ -487,6 +485,7 @@ void SlicerT::writeToMidi(std::vector* outClip) float lastEnd = 0; + // write to midi for (int i = 0; i < m_slicePoints.size() - 1; i++) { float sliceStart = lastEnd; @@ -524,6 +523,7 @@ void SlicerT::updateSlices() void SlicerT::saveSettings(QDomDocument& document, QDomElement& element) { + // save sample element.setAttribute("src", m_originalSample.audioFile()); if (m_originalSample.audioFile().isEmpty()) { @@ -531,13 +531,14 @@ void SlicerT::saveSettings(QDomDocument& document, QDomElement& element) element.setAttribute("sampledata", m_originalSample.toBase64(s)); } + // save slice points element.setAttribute("totalSlices", (int)m_slicePoints.size()); - for (int i = 0; i < m_slicePoints.size(); i++) { element.setAttribute(tr("slice_%1").arg(i), m_slicePoints[i]); } + // save knobs m_fadeOutFrames.saveSettings(document, element, "fadeOut"); m_noteThreshold.saveSettings(document, element, "threshold"); m_originalBPM.saveSettings(document, element, "origBPM"); @@ -545,6 +546,7 @@ void SlicerT::saveSettings(QDomDocument& document, QDomElement& element) void SlicerT::loadSettings(const QDomElement& element) { + // load sample if (!element.attribute("src").isEmpty()) { m_originalSample.setAudioFile(element.attribute("src")); @@ -561,6 +563,7 @@ void SlicerT::loadSettings(const QDomElement& element) m_originalSample.loadFromBase64(element.attribute("srcdata")); } + // load slices if (!element.attribute("totalSlices").isEmpty()) { int totalSlices = element.attribute("totalSlices").toInt(); @@ -571,10 +574,12 @@ void SlicerT::loadSettings(const QDomElement& element) } } + // load knobs m_fadeOutFrames.loadSettings(element, "fadeOut"); m_noteThreshold.loadSettings(element, "threshold"); m_originalBPM.loadSettings(element, "origBPM"); + // create dinamic buffer float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo(); m_phaseVocoder.loadSample( m_originalSample.data(), m_originalSample.frames(), m_originalSample.sampleRate(), speedRatio); diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index e276cacbd3a..fc860ad3968 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -42,9 +42,12 @@ class PhaseVocoder public: PhaseVocoder(); ~PhaseVocoder(); + void loadData(std::vector originalData, int sampleRate, float newRatio); void setScaleRatio(float newRatio) { updateParams(newRatio); } + void getFrames(std::vector& outData, int start, int frames); + int frames() { return processedBuffer.size(); } float scaleRatio() { return m_scaleRatio; } @@ -54,10 +57,10 @@ class PhaseVocoder std::vector originalBuffer; int originalSampleRate = 0; - float m_scaleRatio = -1; // to force on fisrt load + float m_scaleRatio = -1; // to force on first load // output data - std::vector processedBuffer; + std::vector processedBuffer; // final output std::vector m_processedWindows; // marks a window processed // timeshift stuff @@ -169,11 +172,13 @@ public slots: void isPlaying(float current, float start, float end); private: + // models FloatModel m_noteThreshold; FloatModel m_fadeOutFrames; IntModel m_originalBPM; ComboBoxModel m_sliceSnap; + // sample buffers SampleBuffer m_originalSample; dinamicPlaybackBuffer m_phaseVocoder; diff --git a/plugins/SlicerT/SlicerTUI.cpp b/plugins/SlicerT/SlicerTUI.cpp index e91953dbb78..0f5fe93a7ba 100644 --- a/plugins/SlicerT/SlicerTUI.cpp +++ b/plugins/SlicerT/SlicerTUI.cpp @@ -52,40 +52,49 @@ SlicerTUI::SlicerTUI(SlicerT* instrument, QWidget* parent) , m_midiExportButton(this, nullptr) , m_wf(248, 128, instrument, this) { + // window settings setAcceptDrops(true); setAutoFillBackground(true); + // render background QPalette pal; pal.setBrush(backgroundRole(), PLUGIN_NAME::getIconPixmap("bg")); setPalette(pal); + // move editor and seeker m_wf.move(2, 6); + // snap combo box m_snapSetting.setGeometry(190, 200, 55, ComboBox::DEFAULT_HEIGHT); m_snapSetting.setToolTip(tr("Set slice snapping for detection")); m_snapSetting.setModel(&m_slicerTParent->m_sliceSnap); + // bpm spin box m_bpmBox.move(135, 203); m_bpmBox.setToolTip(tr("Original sample BPM")); m_bpmBox.setLabel(tr("BPM")); m_bpmBox.setModel(&m_slicerTParent->m_originalBPM); + // threshold knob m_noteThresholdKnob.move(14, 195); m_noteThresholdKnob.setToolTip(tr("Threshold used for slicing")); m_noteThresholdKnob.setLabel(tr("Threshold")); m_noteThresholdKnob.setModel(&m_slicerTParent->m_noteThreshold); + // fadeout knob m_fadeOutKnob.move(80, 195); m_fadeOutKnob.setToolTip(tr("FadeOut for notes")); m_fadeOutKnob.setLabel(tr("FadeOut")); m_fadeOutKnob.setModel(&m_slicerTParent->m_fadeOutFrames); + // midi copy button m_midiExportButton.move(205, 155); m_midiExportButton.setActiveGraphic(PLUGIN_NAME::getIconPixmap("copyMidi")); m_midiExportButton.setInactiveGraphic(PLUGIN_NAME::getIconPixmap("copyMidi")); m_midiExportButton.setToolTip(tr("Copy midi pattern to clipboard")); connect(&m_midiExportButton, SIGNAL(clicked()), this, SLOT(exportMidi())); + // slcie reset button m_resetButton.move(25, 155); m_resetButton.setActiveGraphic(PLUGIN_NAME::getIconPixmap("resetSlices")); m_resetButton.setInactiveGraphic(PLUGIN_NAME::getIconPixmap("resetSlices")); @@ -159,6 +168,7 @@ void SlicerTUI::dropEvent(QDropEvent* de) de->ignore(); } +// display button text void SlicerTUI::paintEvent(QPaintEvent* pe) { QPainter brush(this); diff --git a/plugins/SlicerT/SlicerTUI.h b/plugins/SlicerT/SlicerTUI.h index 7bced0b2e04..adcc1d0ef2b 100644 --- a/plugins/SlicerT/SlicerTUI.h +++ b/plugins/SlicerT/SlicerTUI.h @@ -41,6 +41,7 @@ class SlicerT; namespace gui { +// style knob, defined in data/themes/default/style.css#L949 class SlicerTKnob : public Knob { public: @@ -59,11 +60,9 @@ class SlicerTUI : public InstrumentViewFixedSize public: SlicerTUI(SlicerT* instrument, QWidget* parent); - ~SlicerTUI() override = default; protected slots: void exportMidi(); - // void sampleSizeChanged( float _new_sample_length ); protected: virtual void dragEnterEvent(QDragEnterEvent* _dee); @@ -74,11 +73,13 @@ protected slots: private: SlicerT* m_slicerTParent; + // lmms UI SlicerTKnob m_noteThresholdKnob; SlicerTKnob m_fadeOutKnob; LcdSpinBox m_bpmBox; ComboBox m_snapSetting; + // buttons PixmapButton m_resetButton; PixmapButton m_midiExportButton; diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/WaveForm.cpp index 934756f306e..9a6fc7072aa 100644 --- a/plugins/SlicerT/WaveForm.cpp +++ b/plugins/SlicerT/WaveForm.cpp @@ -54,19 +54,21 @@ WaveForm::WaveForm(int w, int h, SlicerT* instrument, QWidget* parent) , m_slicePoints(instrument->m_slicePoints) { + // window config setFixedSize(m_width, m_height); setMouseTracking(true); + // draw backgrounds m_sliceEditor.fill(m_waveformBgColor); m_seekerWaveform.fill(m_waveformBgColor); + // connect to playback connect(m_slicerTParent, SIGNAL(isPlaying(float, float, float)), this, SLOT(isPlaying(float, float, float))); connect(m_slicerTParent, SIGNAL(dataChanged()), this, SLOT(updateUI())); updateUI(); } -// graphics void WaveForm::drawSeekerWaveform() { m_seekerWaveform.fill(m_waveformBgColor); @@ -82,7 +84,6 @@ void WaveForm::drawSeeker() m_seeker.fill(m_waveformBgColor); QPainter brush(&m_seeker); - // if (m_currentSample.frames() < 2048) { return; } // draw slice points brush.setPen(m_sliceColor); for (int i = 0; i < m_slicePoints.size(); i++) @@ -123,6 +124,7 @@ void WaveForm::drawEditor() m_sliceEditor.fill(m_waveformBgColor); QPainter brush(&m_sliceEditor); + // draw text if no sample loaded if (m_currentSample.frames() < 2048) { brush.setPen(m_playHighlighColor); @@ -132,6 +134,7 @@ void WaveForm::drawEditor() return; } + // editor boundaries float startFrame = m_seekerStart * m_currentSample.frames(); float endFrame = m_seekerEnd * m_currentSample.frames(); float numFramesToDraw = endFrame - startFrame; @@ -158,7 +161,6 @@ void WaveForm::drawEditor() } } -// slots void WaveForm::updateUI() { drawSeekerWaveform(); @@ -172,7 +174,7 @@ void WaveForm::isPlaying(float current, float start, float end) m_noteCurrent = current; m_noteStart = start; m_noteEnd = end; - drawSeeker(); // only update seeker, else horrible performance + drawSeeker(); // only update seeker, else horrible performance because of waveform redraw update(); } @@ -205,8 +207,8 @@ void WaveForm::mousePressEvent(QMouseEvent* me) m_seekerMiddle = normalizedClickSeeker; } } - else - { // editor click + else // editor click + { m_sliceSelected = -1; float startFrame = m_seekerStart * m_currentSample.frames(); float endFrame = m_seekerEnd * m_currentSample.frames(); @@ -239,6 +241,7 @@ void WaveForm::mouseReleaseEvent(QMouseEvent* me) { m_currentlyDragging = m_draggingTypes::nothing; std::sort(m_slicePoints.begin(), m_slicePoints.end()); + updateUI(); } @@ -273,9 +276,8 @@ void WaveForm::mouseMoveEvent(QMouseEvent* me) } break; - case m_draggingTypes::m_slicePoint: // TODO: fix this + case m_draggingTypes::m_slicePoint: m_slicePoints[m_sliceSelected] = startFrame + normalizedClickEditor * (endFrame - startFrame); - m_slicePoints[m_sliceSelected] = std::clamp(m_slicePoints[m_sliceSelected], 0, m_currentSample.frames()); break; case m_draggingTypes::nothing: diff --git a/plugins/SlicerT/WaveForm.h b/plugins/SlicerT/WaveForm.h index 79bd236dbae..ede26749d52 100644 --- a/plugins/SlicerT/WaveForm.h +++ b/plugins/SlicerT/WaveForm.h @@ -52,27 +52,28 @@ class WaveForm : public QWidget virtual void paintEvent(QPaintEvent* pe); private: - // vars used to control structure and colors + // sizes int m_width; int m_height; int m_seekerHorMargin = 5; - int m_seekerHeight = 38; // used to calcualte all hor sizes + int m_seekerHeight = 38; // used to calcualte all vertical sizes int m_seekerWidth; int m_middleMargin = 6; int m_editorHeight; int m_editorWidth; + // colors QColor m_waveformBgColor = QColor(255, 255, 255, 0); QColor m_waveformColor = QColor(123, 49, 212); - // QColor m_waveformColorDark = QColor(39, 15, 67); - // QColor m_waveformColor = QColor(255, 161, 247); // logo color QColor m_playColor = QColor(255, 255, 255, 200); QColor m_playHighlighColor = QColor(255, 255, 255, 70); + QColor m_sliceColor = QColor(218, 193, 255); QColor m_selectedSliceColor = QColor(178, 153, 215); + QColor m_seekerColor = QColor(178, 115, 255); QColor m_seekerHighlightColor = QColor(178, 115, 255, 100); QColor m_seekerShadowColor = QColor(0, 0, 0, 120); @@ -117,6 +118,7 @@ class WaveForm : public QWidget void drawEditor(); void drawSeekerWaveform(); void drawSeeker(); + public slots: void updateUI(); void isPlaying(float current, float start, float end); From 2a06101726c1eb78f5814337dfc6a3be622135ad Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Mon, 2 Oct 2023 23:02:05 +0200 Subject: [PATCH 41/99] var names fit convention --- plugins/SlicerT/SlicerT.cpp | 150 +++++++++++++++++------------------ plugins/SlicerT/SlicerT.h | 76 +++++++++--------- plugins/SlicerT/WaveForm.cpp | 10 +-- plugins/SlicerT/WaveForm.h | 4 +- 4 files changed, 120 insertions(+), 120 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 6dc5edc674f..9ce4f499232 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -53,42 +53,42 @@ Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = { } // end extern PhaseVocoder::PhaseVocoder() - : FFTInput(windowSize, 0) - , IFFTReconstruction(windowSize, 0) - , allMagnitudes(windowSize, 0) - , allFrequencies(windowSize, 0) - , processedFreq(windowSize, 0) - , processedMagn(windowSize, 0) + : m_FFTInput(s_windowSize, 0) + , m_IFFTReconstruction(s_windowSize, 0) + , m_allMagnitudes(s_windowSize, 0) + , m_allFrequencies(s_windowSize, 0) + , m_processedFreq(s_windowSize, 0) + , m_processedMagn(s_windowSize, 0) { - fftPlan = fftwf_plan_dft_r2c_1d(windowSize, FFTInput.data(), FFTSpectrum, FFTW_MEASURE); - ifftPlan = fftwf_plan_dft_c2r_1d(windowSize, FFTSpectrum, IFFTReconstruction.data(), FFTW_MEASURE); + m_fftPlan = fftwf_plan_dft_r2c_1d(s_windowSize, m_FFTInput.data(), m_FFTSpectrum, FFTW_MEASURE); + m_ifftPlan = fftwf_plan_dft_c2r_1d(s_windowSize, m_FFTSpectrum, m_IFFTReconstruction.data(), FFTW_MEASURE); } PhaseVocoder::~PhaseVocoder() { - fftwf_destroy_plan(fftPlan); - fftwf_destroy_plan(ifftPlan); + fftwf_destroy_plan(m_fftPlan); + fftwf_destroy_plan(m_ifftPlan); } void PhaseVocoder::loadData(std::vector originalData, int sampleRate, float newRatio) { - originalBuffer = originalData; - originalSampleRate = sampleRate; + m_originalBuffer = originalData; + m_originalSampleRate = sampleRate; m_scaleRatio = -1; // force update, kinda hacky updateParams(newRatio); - dataLock.lock(); + m_dataLock.lock(); // set buffer sizes - m_processedWindows.resize(numWindows, false); - lastPhase.resize(numWindows * windowSize, 0); - sumPhase.resize(numWindows * windowSize, 0); - freqCache.resize(numWindows * windowSize, 0); - magCache.resize(numWindows * windowSize, 0); + m_processedWindows.resize(m_numWindows, false); + m_lastPhase.resize(m_numWindows * s_windowSize, 0); + m_sumPhase.resize(m_numWindows * s_windowSize, 0); + m_freqCache.resize(m_numWindows * s_windowSize, 0); + m_magCache.resize(m_numWindows * s_windowSize, 0); // maybe limit this to a set amount of windows to reduce initial lag spikes - for (int i = 0; i < numWindows; i++) + for (int i = 0; i < m_numWindows; i++) { if (!m_processedWindows[i]) { @@ -97,17 +97,17 @@ void PhaseVocoder::loadData(std::vector originalData, int sampleRate, flo } } - dataLock.unlock(); + m_dataLock.unlock(); } void PhaseVocoder::getFrames(std::vector& outData, int start, int frames) { - if (originalBuffer.size() < 2048) { return; } - dataLock.lock(); + if (m_originalBuffer.size() < 2048) { return; } + m_dataLock.lock(); - int windowMargin = overSampling / 2; // numbers of windows before full quality - int startWindow = std::max(0.0f, (float)start / outStepSize - windowMargin); - int endWindow = std::min((float)numWindows, (float)(start + frames) / outStepSize + windowMargin); + int windowMargin = s_overSampling / 2; // numbers of windows before full quality + int startWindow = std::max(0.0f, (float)start / m_outStepSize - windowMargin); + int endWindow = std::min((float)m_numWindows, (float)(start + frames) / m_outStepSize + windowMargin); // this encompases the minimum windows needed to get full quality, // which must be computed for (int i = startWindow; i < endWindow; i++) @@ -121,41 +121,41 @@ void PhaseVocoder::getFrames(std::vector& outData, int start, int frames) for (int i = 0; i < frames; i++) { - outData[i] = processedBuffer[start + i]; + outData[i] = m_processedBuffer[start + i]; } - dataLock.unlock(); + m_dataLock.unlock(); } // adjust pv params buffers to a new scale ratio void PhaseVocoder::updateParams(float newRatio) { - if (originalBuffer.size() < 2048) { return; } + if (m_originalBuffer.size() < 2048) { return; } if (newRatio == m_scaleRatio) { return; } // nothing changed - dataLock.lock(); + m_dataLock.lock(); m_scaleRatio = newRatio; - stepSize = (float)windowSize / overSampling; - numWindows = (float)originalBuffer.size() / stepSize - overSampling - 1; - outStepSize = m_scaleRatio * (float)stepSize; // float, else inaccurate - freqPerBin = originalSampleRate / windowSize; - expectedPhaseIn = 2. * F_PI * (float)stepSize / (float)windowSize; - expectedPhaseOut = 2. * F_PI * (float)outStepSize / (float)windowSize; + m_stepSize = (float)s_windowSize / s_overSampling; + m_numWindows = (float)m_originalBuffer.size() / m_stepSize - s_overSampling - 1; + m_outStepSize = m_scaleRatio * (float)m_stepSize; // float, else inaccurate + m_freqPerBin = m_originalSampleRate / s_windowSize; + m_expectedPhaseIn = 2. * F_PI * (float)m_stepSize / (float)s_windowSize; + m_expectedPhaseOut = 2. * F_PI * (float)m_outStepSize / (float)s_windowSize; - processedBuffer.resize(m_scaleRatio * originalBuffer.size(), 0); + m_processedBuffer.resize(m_scaleRatio * m_originalBuffer.size(), 0); // very slow :( std::fill(m_processedWindows.begin(), m_processedWindows.end(), false); - std::fill(processedBuffer.begin(), processedBuffer.end(), 0); + std::fill(m_processedBuffer.begin(), m_processedBuffer.end(), 0); // this can be commented, since the start phase is not importante to the PV // and the sum phase will grow linearly anyway // std::fill(lastPhase.begin(), lastPhase.end(), 0); // std::fill(sumPhase.begin(), sumPhase.end(), 0); - dataLock.unlock(); + m_dataLock.unlock(); } -// time shifts one window from originalBuffer and writes to processedBuffer +// time shifts one window from originalBuffer and writes to m_processedBuffer // resources: // http://blogs.zynaptiq.com/bernsee/pitch-shifting-using-the-ft/ // https://sethares.engr.wisc.edu/vocoders/phasevocoder.html @@ -165,21 +165,21 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) { // declare vars float real, imag, phase, magnitude, freq, deltaPhase = 0; - int windowStart = (float)windowNum * stepSize; - int windowIndex = (float)windowNum * windowSize; + int windowStart = (float)windowNum * m_stepSize; + int windowIndex = (float)windowNum * s_windowSize; if (!useCache) { // normal stuff - memcpy(FFTInput.data(), originalBuffer.data() + windowStart, windowSize * sizeof(float)); + memcpy(m_FFTInput.data(), m_originalBuffer.data() + windowStart, s_windowSize * sizeof(float)); // FFT - fftwf_execute(fftPlan); + fftwf_execute(m_fftPlan); // analysis step - for (int j = 0; j < windowSize / 2; j++) // only process nyquistic frequency + for (int j = 0; j < s_windowSize / 2; j++) // only process nyquistic frequency { - real = FFTSpectrum[j][0]; - imag = FFTSpectrum[j][1]; + real = m_FFTSpectrum[j][0]; + imag = m_FFTSpectrum[j][1]; magnitude = 2. * sqrt(real * real + imag * imag); phase = atan2(imag, real); @@ -187,10 +187,10 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) // calculate difference in phase with prev window freq = phase; freq = phase - - lastPhase[std::max(0, windowIndex + j - windowSize)]; // subtract prev pahse to get phase diference - lastPhase[windowIndex + j] = phase; + - m_lastPhase[std::max(0, windowIndex + j - s_windowSize)]; // subtract prev pahse to get phase diference + m_lastPhase[windowIndex + j] = phase; - freq -= (float)j * expectedPhaseIn; // subtract expected phase + freq -= (float)j * m_expectedPhaseIn; // subtract expected phase // at this point, freq is the difference in phase // between the last phase, having removed the expected phase at this point in the sample @@ -199,67 +199,67 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) freq = fmod(freq + F_PI, -2.0f * F_PI) + F_PI; // convert phase difference into bin freq mulitplier - freq = (float)overSampling * freq / (2. * F_PI); + freq = (float)s_overSampling * freq / (2. * F_PI); // add to the expected freq the change in freq calculated from the phase diff - freq = (float)j * freqPerBin + freq * freqPerBin; + freq = (float)j * m_freqPerBin + freq * m_freqPerBin; - allMagnitudes[j] = magnitude; - allFrequencies[j] = freq; + m_allMagnitudes[j] = magnitude; + m_allFrequencies[j] = freq; } // write cache - memcpy(freqCache.data() + windowIndex, allFrequencies.data(), windowSize * sizeof(float)); - memcpy(magCache.data() + windowIndex, allMagnitudes.data(), windowSize * sizeof(float)); + memcpy(m_freqCache.data() + windowIndex, m_allFrequencies.data(), s_windowSize * sizeof(float)); + memcpy(m_magCache.data() + windowIndex, m_allMagnitudes.data(), s_windowSize * sizeof(float)); } else { // read cache - memcpy(allFrequencies.data(), freqCache.data() + windowIndex, windowSize * sizeof(float)); - memcpy(allMagnitudes.data(), magCache.data() + windowIndex, windowSize * sizeof(float)); + memcpy(m_allFrequencies.data(), m_freqCache.data() + windowIndex, s_windowSize * sizeof(float)); + memcpy(m_allMagnitudes.data(), m_magCache.data() + windowIndex, s_windowSize * sizeof(float)); } // synthesis, all the operations are the reverse of the analysis - for (int j = 0; j < windowSize / 2; j++) + for (int j = 0; j < s_windowSize / 2; j++) { - magnitude = allMagnitudes[j]; - freq = allFrequencies[j]; + magnitude = m_allMagnitudes[j]; + freq = m_allFrequencies[j]; // difference in freq - deltaPhase = freq - (float)j * freqPerBin; + deltaPhase = freq - (float)j * m_freqPerBin; // scaled to 1 - deltaPhase /= freqPerBin; + deltaPhase /= m_freqPerBin; // difference in phase - deltaPhase = 2. * F_PI * deltaPhase / overSampling; + deltaPhase = 2. * F_PI * deltaPhase / s_overSampling; // add the expected phase - deltaPhase += (float)j * expectedPhaseOut; + deltaPhase += (float)j * m_expectedPhaseOut; // sum this phase to the total, to keep track of the out phase along the sample - sumPhase[windowIndex + j] += deltaPhase; - deltaPhase = sumPhase[windowIndex + j]; // this is the bin phase - if (windowIndex + j + windowSize < sumPhase.size()) + m_sumPhase[windowIndex + j] += deltaPhase; + deltaPhase = m_sumPhase[windowIndex + j]; // this is the bin phase + if (windowIndex + j + s_windowSize < m_sumPhase.size()) { // only if not last window - sumPhase[windowIndex + j + windowSize] = deltaPhase; // copy to the next + m_sumPhase[windowIndex + j + s_windowSize] = deltaPhase; // copy to the next } - FFTSpectrum[j][0] = magnitude * cos(deltaPhase); - FFTSpectrum[j][1] = magnitude * sin(deltaPhase); + m_FFTSpectrum[j][0] = magnitude * cos(deltaPhase); + m_FFTSpectrum[j][1] = magnitude * sin(deltaPhase); } // inverse fft - fftwf_execute(ifftPlan); + fftwf_execute(m_ifftPlan); // windowing - for (int j = 0; j < windowSize; j++) + for (int j = 0; j < s_windowSize; j++) { - float outIndex = windowNum * outStepSize + j; + float outIndex = windowNum * m_outStepSize + j; if (outIndex >= frames()) { break; } // hann windowing - float window = -0.5f * cos(2. * F_PI * (float)j / (float)windowSize) + 0.5f; - processedBuffer[outIndex] += window * IFFTReconstruction[j] / (windowSize / 2.0f * overSampling); + float window = -0.5f * cos(2. * F_PI * (float)j / (float)s_windowSize) + 0.5f; + m_processedBuffer[outIndex] += window * m_IFFTReconstruction[j] / (s_windowSize / 2.0f * s_overSampling); } } diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index fc860ad3968..46d9093a129 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -48,51 +48,51 @@ class PhaseVocoder void getFrames(std::vector& outData, int start, int frames); - int frames() { return processedBuffer.size(); } + int frames() { return m_processedBuffer.size(); } float scaleRatio() { return m_scaleRatio; } private: - QMutex dataLock; + QMutex m_dataLock; // original data - std::vector originalBuffer; - int originalSampleRate = 0; + std::vector m_originalBuffer; + int m_originalSampleRate = 0; float m_scaleRatio = -1; // to force on first load // output data - std::vector processedBuffer; // final output + std::vector m_processedBuffer; // final output std::vector m_processedWindows; // marks a window processed // timeshift stuff - static const int windowSize = 512; - static const int overSampling = 32; + static const int s_windowSize = 512; + static const int s_overSampling = 32; // depending on scaleRatio - int stepSize = 0; - int numWindows = 0; - float outStepSize = 0; - float freqPerBin = 0; - float expectedPhaseIn = 0; - float expectedPhaseOut = 0; + int m_stepSize = 0; + int m_numWindows = 0; + float m_outStepSize = 0; + float m_freqPerBin = 0; + float m_expectedPhaseIn = 0; + float m_expectedPhaseOut = 0; // buffers - fftwf_complex FFTSpectrum[windowSize]; - std::vector FFTInput; - std::vector IFFTReconstruction; - std::vector allMagnitudes; - std::vector allFrequencies; - std::vector processedFreq; - std::vector processedMagn; - std::vector lastPhase; - std::vector sumPhase; + fftwf_complex m_FFTSpectrum[s_windowSize]; + std::vector m_FFTInput; + std::vector m_IFFTReconstruction; + std::vector m_allMagnitudes; + std::vector m_allFrequencies; + std::vector m_processedFreq; + std::vector m_processedMagn; + std::vector m_lastPhase; + std::vector m_sumPhase; // cache - std::vector freqCache; - std::vector magCache; + std::vector m_freqCache; + std::vector m_magCache; // fftw plans - fftwf_plan fftPlan; - fftwf_plan ifftPlan; + fftwf_plan m_fftPlan; + fftwf_plan m_ifftPlan; void updateParams(float newRatio); void generateWindow(int windowNum, bool useCache); @@ -103,8 +103,8 @@ class dinamicPlaybackBuffer { public: dinamicPlaybackBuffer() - : leftChannel() - , rightChannel() + : m_leftChannel() + , m_rightChannel() { } void loadSample(const sampleFrame* outData, int frames, int sampleRate, float newRatio) @@ -116,16 +116,16 @@ class dinamicPlaybackBuffer leftData[i] = outData[i][0]; rightData[i] = outData[i][1]; } - leftChannel.loadData(leftData, sampleRate, newRatio); - rightChannel.loadData(rightData, sampleRate, newRatio); + m_leftChannel.loadData(leftData, sampleRate, newRatio); + m_rightChannel.loadData(rightData, sampleRate, newRatio); } void getFrames(sampleFrame* outData, int startFrame, int frames) { std::vector leftOut(frames, 0); // not a huge performance issue std::vector rightOut(frames, 0); - leftChannel.getFrames(leftOut, startFrame, frames); - rightChannel.getFrames(rightOut, startFrame, frames); + m_leftChannel.getFrames(leftOut, startFrame, frames); + m_rightChannel.getFrames(rightOut, startFrame, frames); for (int i = 0; i < frames; i++) { @@ -133,17 +133,17 @@ class dinamicPlaybackBuffer outData[i][1] = rightOut[i]; } } - int frames() { return leftChannel.frames(); } - float scaleRatio() { return leftChannel.scaleRatio(); } + int frames() { return m_leftChannel.frames(); } + float scaleRatio() { return m_leftChannel.scaleRatio(); } void setScaleRatio(float newRatio) { - leftChannel.setScaleRatio(newRatio); - rightChannel.setScaleRatio(newRatio); + m_leftChannel.setScaleRatio(newRatio); + m_rightChannel.setScaleRatio(newRatio); } private: - PhaseVocoder leftChannel; - PhaseVocoder rightChannel; + PhaseVocoder m_leftChannel; + PhaseVocoder m_rightChannel; }; class SlicerT : public Instrument diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/WaveForm.cpp index 9a6fc7072aa..db5f6815710 100644 --- a/plugins/SlicerT/WaveForm.cpp +++ b/plugins/SlicerT/WaveForm.cpp @@ -193,11 +193,11 @@ void WaveForm::mousePressEvent(QMouseEvent* me) if (me->y() < m_seekerHeight) // seeker click { - if (abs(normalizedClickSeeker - m_seekerStart) < distanceForClick) // dragging start + if (abs(normalizedClickSeeker - m_seekerStart) < m_distanceForClick) // dragging start { m_currentlyDragging = m_draggingTypes::m_seekerStart; } - else if (abs(normalizedClickSeeker - m_seekerEnd) < distanceForClick) // dragging end + else if (abs(normalizedClickSeeker - m_seekerEnd) < m_distanceForClick) // dragging end { m_currentlyDragging = m_draggingTypes::m_seekerEnd; } @@ -218,7 +218,7 @@ void WaveForm::mousePressEvent(QMouseEvent* me) int sliceIndex = m_slicePoints[i]; float xPos = (float)(sliceIndex - startFrame) / (float)(endFrame - startFrame); - if (abs(xPos - normalizedClickEditor) < distanceForClick) + if (abs(xPos - normalizedClickEditor) < m_distanceForClick) { m_currentlyDragging = m_draggingTypes::m_slicePoint; m_sliceSelected = i; @@ -259,11 +259,11 @@ void WaveForm::mouseMoveEvent(QMouseEvent* me) switch (m_currentlyDragging) { case m_draggingTypes::m_seekerStart: - m_seekerStart = std::clamp(normalizedClickSeeker, 0.0f, m_seekerEnd - minSeekerDistance); + m_seekerStart = std::clamp(normalizedClickSeeker, 0.0f, m_seekerEnd - m_minSeekerDistance); break; case m_draggingTypes::m_seekerEnd: - m_seekerEnd = std::clamp(normalizedClickSeeker, m_seekerStart + minSeekerDistance, 1.0f); + m_seekerEnd = std::clamp(normalizedClickSeeker, m_seekerStart + m_minSeekerDistance, 1.0f); break; case m_draggingTypes::m_seekerMiddle: diff --git a/plugins/SlicerT/WaveForm.h b/plugins/SlicerT/WaveForm.h index ede26749d52..4d858523756 100644 --- a/plugins/SlicerT/WaveForm.h +++ b/plugins/SlicerT/WaveForm.h @@ -79,8 +79,8 @@ class WaveForm : public QWidget QColor m_seekerShadowColor = QColor(0, 0, 0, 120); // interaction vars - float distanceForClick = 0.03f; - float minSeekerDistance = 0.13f; + float m_distanceForClick = 0.03f; + float m_minSeekerDistance = 0.13f; // dragging vars enum class m_draggingTypes From c2948c0bc50774c7babf4b5ee4c696cde61465be Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Wed, 4 Oct 2023 21:46:45 +0200 Subject: [PATCH 42/99] PV better windowing --- plugins/SlicerT/SlicerT.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 9ce4f499232..5d82a6b7a2d 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -161,6 +161,7 @@ void PhaseVocoder::updateParams(float newRatio) // https://sethares.engr.wisc.edu/vocoders/phasevocoder.html // https://dsp.stackexchange.com/questions/40101/audio-time-stretching-without-pitch-shifting/40367#40367 // https://www.guitarpitchshifter.com/ +// https://en.wikipedia.org/wiki/Window_function void PhaseVocoder::generateWindow(int windowNum, bool useCache) { // declare vars @@ -257,9 +258,17 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) float outIndex = windowNum * m_outStepSize + j; if (outIndex >= frames()) { break; } - // hann windowing - float window = -0.5f * cos(2. * F_PI * (float)j / (float)s_windowSize) + 0.5f; - m_processedBuffer[outIndex] += window * m_IFFTReconstruction[j] / (s_windowSize / 2.0f * s_overSampling); + // blackman-harris window + float a0 = 0.35875f; + float a1 = 0.48829f; + float a2 = 0.14128f; + float a3 = 0.01168f; + + float piN2 = 2.0f * F_PI * j; + float window = a0 - (a1 * cos(piN2 / s_windowSize)) + (a2 * cos(2.0f * piN2 / s_windowSize)) - (a3 * cos(3.0f * piN2)); + + // inverse fft magnitudes are windowsSize times bigger + m_processedBuffer[outIndex] += window * (m_IFFTReconstruction[j] / s_windowSize / s_overSampling); } } From 8457d9682a2838fa12a11df6aa0b109f76886f0e Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Wed, 4 Oct 2023 23:29:05 +0200 Subject: [PATCH 43/99] waveform zoom --- plugins/SlicerT/WaveForm.cpp | 15 ++++++++++++++- plugins/SlicerT/WaveForm.h | 5 +++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/WaveForm.cpp index db5f6815710..7073ab51839 100644 --- a/plugins/SlicerT/WaveForm.cpp +++ b/plugins/SlicerT/WaveForm.cpp @@ -145,7 +145,9 @@ void WaveForm::drawEditor() // draw waveform brush.setPen(m_waveformColor); - m_currentSample.visualize(brush, QRect(0, 0, m_editorWidth, m_editorHeight), startFrame, endFrame); + float zoomOffset = ((float)m_editorHeight - m_zoomLevel * m_editorHeight) / 2; + m_currentSample.visualize( + brush, QRect(0, zoomOffset, m_editorWidth, m_zoomLevel * m_editorHeight), startFrame, endFrame); // draw slicepoints brush.setPen(QPen(m_sliceColor, 2)); @@ -188,6 +190,7 @@ void WaveForm::mousePressEvent(QMouseEvent* me) { m_seekerStart = 0; m_seekerEnd = 1; + m_zoomLevel = 1; return; } @@ -306,6 +309,16 @@ void WaveForm::mouseDoubleClickEvent(QMouseEvent* me) std::sort(m_slicePoints.begin(), m_slicePoints.end()); } +void WaveForm::wheelEvent(QWheelEvent* _we) +{ + // m_zoomLevel = _we-> / 360.0f * 2.0f; + m_zoomLevel += _we->angleDelta().y() / 360.0f * m_zoomSensitivity; + m_zoomLevel = std::max(0.0f, m_zoomLevel); + + drawEditor(); + update(); +} + void WaveForm::paintEvent(QPaintEvent* pe) { QPainter p(this); diff --git a/plugins/SlicerT/WaveForm.h b/plugins/SlicerT/WaveForm.h index 4d858523756..3e74b995f9e 100644 --- a/plugins/SlicerT/WaveForm.h +++ b/plugins/SlicerT/WaveForm.h @@ -48,6 +48,7 @@ class WaveForm : public QWidget virtual void mouseReleaseEvent(QMouseEvent* me); virtual void mouseMoveEvent(QMouseEvent* me); virtual void mouseDoubleClickEvent(QMouseEvent* me); + virtual void wheelEvent(QWheelEvent* _we); virtual void paintEvent(QPaintEvent* pe); @@ -81,6 +82,7 @@ class WaveForm : public QWidget // interaction vars float m_distanceForClick = 0.03f; float m_minSeekerDistance = 0.13f; + float m_zoomSensitivity = 0.5f; // dragging vars enum class m_draggingTypes @@ -104,6 +106,9 @@ class WaveForm : public QWidget float m_noteStart; float m_noteEnd; + // editor vars + float m_zoomLevel = 1.0f; + // pixmaps QPixmap m_sliceArrow; QPixmap m_seeker; From b2cd8085720a4e2b5ffad63461607caae6dc0bca Mon Sep 17 00:00:00 2001 From: Katherine Pratt Date: Thu, 5 Oct 2023 01:09:36 -0400 Subject: [PATCH 44/99] Minor style fixes. --- plugins/SlicerT/SlicerT.cpp | 2 +- plugins/SlicerT/SlicerT.h | 13 ++++++++----- plugins/SlicerT/SlicerTUI.cpp | 18 +++++++++++------- plugins/SlicerT/WaveForm.cpp | 3 ++- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 5d82a6b7a2d..fc43af4ca21 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -588,7 +588,7 @@ void SlicerT::loadSettings(const QDomElement& element) m_noteThreshold.loadSettings(element, "threshold"); m_originalBPM.loadSettings(element, "origBPM"); - // create dinamic buffer + // create dynamic buffer float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo(); m_phaseVocoder.loadSample( m_originalSample.data(), m_originalSample.frames(), m_originalSample.sampleRate(), speedRatio); diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 46d9093a129..7543821bc60 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -99,14 +99,14 @@ class PhaseVocoder }; // simple helper class that handles the different audio channels -class dinamicPlaybackBuffer +class DynamicPlaybackBuffer { public: - dinamicPlaybackBuffer() + DynamicPlaybackBuffer() : m_leftChannel() , m_rightChannel() - { - } + {} + void loadSample(const sampleFrame* outData, int frames, int sampleRate, float newRatio) { std::vector leftData(frames, 0); @@ -119,6 +119,7 @@ class dinamicPlaybackBuffer m_leftChannel.loadData(leftData, sampleRate, newRatio); m_rightChannel.loadData(rightData, sampleRate, newRatio); } + void getFrames(sampleFrame* outData, int startFrame, int frames) { std::vector leftOut(frames, 0); // not a huge performance issue @@ -133,8 +134,10 @@ class dinamicPlaybackBuffer outData[i][1] = rightOut[i]; } } + int frames() { return m_leftChannel.frames(); } float scaleRatio() { return m_leftChannel.scaleRatio(); } + void setScaleRatio(float newRatio) { m_leftChannel.setScaleRatio(newRatio); @@ -180,7 +183,7 @@ public slots: // sample buffers SampleBuffer m_originalSample; - dinamicPlaybackBuffer m_phaseVocoder; + DynamicPlaybackBuffer m_phaseVocoder; std::vector m_slicePoints; diff --git a/plugins/SlicerT/SlicerTUI.cpp b/plugins/SlicerT/SlicerTUI.cpp index 0f5fe93a7ba..6f601552c77 100644 --- a/plugins/SlicerT/SlicerTUI.cpp +++ b/plugins/SlicerT/SlicerTUI.cpp @@ -94,7 +94,7 @@ SlicerTUI::SlicerTUI(SlicerT* instrument, QWidget* parent) m_midiExportButton.setToolTip(tr("Copy midi pattern to clipboard")); connect(&m_midiExportButton, SIGNAL(clicked()), this, SLOT(exportMidi())); - // slcie reset button + // slice reset button m_resetButton.move(25, 155); m_resetButton.setActiveGraphic(PLUGIN_NAME::getIconPixmap("resetSlices")); m_resetButton.setInactiveGraphic(PLUGIN_NAME::getIconPixmap("resetSlices")); @@ -139,8 +139,14 @@ void SlicerTUI::dragEnterEvent(QDragEnterEvent* dee) { dee->acceptProposedAction(); } - else if (txt.section(':', 0, 0) == "samplefile") { dee->acceptProposedAction(); } - else { dee->ignore(); } + else if (txt.section(':', 0, 0) == "samplefile") + { + dee->acceptProposedAction(); + } + else + { + dee->ignore(); + } } else { dee->ignore(); } } @@ -151,10 +157,8 @@ void SlicerTUI::dropEvent(QDropEvent* de) QString value = StringPairDrag::decodeValue(de); if (type == "samplefile") { - m_slicerTParent->updateFile(value); - // castModel()->setAudioFile( value ); - // de->accept(); // set m_wf wave file + m_slicerTParent->updateFile(value); return; } else if (type == QString("clip_%1").arg(static_cast(Track::Type::Sample))) @@ -180,4 +184,4 @@ void SlicerTUI::paintEvent(QPaintEvent* pe) } } // namespace gui -} // namespace lmms \ No newline at end of file +} // namespace lmms diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/WaveForm.cpp index 7073ab51839..c6f8ad72177 100644 --- a/plugins/SlicerT/WaveForm.cpp +++ b/plugins/SlicerT/WaveForm.cpp @@ -30,6 +30,7 @@ namespace lmms { namespace gui { + WaveForm::WaveForm(int w, int h, SlicerT* instrument, QWidget* parent) : QWidget(parent) , @@ -327,4 +328,4 @@ void WaveForm::paintEvent(QPaintEvent* pe) p.drawPixmap(0, m_seekerHeight + m_middleMargin, m_sliceEditor); } } // namespace gui -} // namespace lmms \ No newline at end of file +} // namespace lmms From b7c7def78fed86d2bd69d8c45597f2e2f147afb4 Mon Sep 17 00:00:00 2001 From: Katherine Pratt Date: Thu, 5 Oct 2023 02:49:43 -0400 Subject: [PATCH 45/99] Initial artistic rebalancing of the plugin artwork. --- plugins/SlicerT/SlicerTUI.cpp | 22 +++++++++++----------- plugins/SlicerT/SlicerTUI.h | 6 +++--- plugins/SlicerT/bg.png | Bin 23763 -> 28021 bytes plugins/SlicerT/bg.xcf | Bin 0 -> 92565 bytes 4 files changed, 14 insertions(+), 14 deletions(-) create mode 100644 plugins/SlicerT/bg.xcf diff --git a/plugins/SlicerT/SlicerTUI.cpp b/plugins/SlicerT/SlicerTUI.cpp index 6f601552c77..6fe9159a4e9 100644 --- a/plugins/SlicerT/SlicerTUI.cpp +++ b/plugins/SlicerT/SlicerTUI.cpp @@ -65,37 +65,37 @@ SlicerTUI::SlicerTUI(SlicerT* instrument, QWidget* parent) m_wf.move(2, 6); // snap combo box - m_snapSetting.setGeometry(190, 200, 55, ComboBox::DEFAULT_HEIGHT); + m_snapSetting.setGeometry(185, 200, 55, ComboBox::DEFAULT_HEIGHT); m_snapSetting.setToolTip(tr("Set slice snapping for detection")); m_snapSetting.setModel(&m_slicerTParent->m_sliceSnap); // bpm spin box - m_bpmBox.move(135, 203); + m_bpmBox.move(130, 203); m_bpmBox.setToolTip(tr("Original sample BPM")); m_bpmBox.setLabel(tr("BPM")); m_bpmBox.setModel(&m_slicerTParent->m_originalBPM); // threshold knob - m_noteThresholdKnob.move(14, 195); + m_noteThresholdKnob.move(9, 195); m_noteThresholdKnob.setToolTip(tr("Threshold used for slicing")); m_noteThresholdKnob.setLabel(tr("Threshold")); m_noteThresholdKnob.setModel(&m_slicerTParent->m_noteThreshold); // fadeout knob - m_fadeOutKnob.move(80, 195); - m_fadeOutKnob.setToolTip(tr("FadeOut for notes")); - m_fadeOutKnob.setLabel(tr("FadeOut")); + m_fadeOutKnob.move(75, 195); + m_fadeOutKnob.setToolTip(tr("Fade Out for notes")); + m_fadeOutKnob.setLabel(tr("Fade Out")); m_fadeOutKnob.setModel(&m_slicerTParent->m_fadeOutFrames); // midi copy button - m_midiExportButton.move(205, 155); + m_midiExportButton.move(215, 150); m_midiExportButton.setActiveGraphic(PLUGIN_NAME::getIconPixmap("copyMidi")); m_midiExportButton.setInactiveGraphic(PLUGIN_NAME::getIconPixmap("copyMidi")); m_midiExportButton.setToolTip(tr("Copy midi pattern to clipboard")); connect(&m_midiExportButton, SIGNAL(clicked()), this, SLOT(exportMidi())); // slice reset button - m_resetButton.move(25, 155); + m_resetButton.move(19, 150); m_resetButton.setActiveGraphic(PLUGIN_NAME::getIconPixmap("resetSlices")); m_resetButton.setInactiveGraphic(PLUGIN_NAME::getIconPixmap("resetSlices")); m_resetButton.setToolTip(tr("Reset Slices")); @@ -178,9 +178,9 @@ void SlicerTUI::paintEvent(QPaintEvent* pe) QPainter brush(this); brush.setPen(QColor(255, 255, 255)); brush.setFont(QFont(brush.font().family(), 7.5f, -1, false)); - brush.drawText(205, 170, 20, 20, Qt::AlignCenter, tr("Midi")); - brush.drawText(22, 170, 25, 20, Qt::AlignCenter, tr("Reset")); - brush.drawText(190, 217, 55, 20, Qt::AlignCenter, tr("Snap")); + brush.drawText(212, 165, 25, 20, Qt::AlignCenter, tr("Midi")); + brush.drawText(14, 165, 30, 20, Qt::AlignCenter, tr("Reset")); + brush.drawText(185, 217, 55, 20, Qt::AlignCenter, tr("Snap")); } } // namespace gui diff --git a/plugins/SlicerT/SlicerTUI.h b/plugins/SlicerT/SlicerTUI.h index adcc1d0ef2b..4b914507049 100644 --- a/plugins/SlicerT/SlicerTUI.h +++ b/plugins/SlicerT/SlicerTUI.h @@ -48,8 +48,8 @@ class SlicerTKnob : public Knob SlicerTKnob(QWidget* _parent) : Knob(KnobType::Styled, _parent) { - setFixedSize(46, 40); - setCenterPointX(23.0); + setFixedSize(50, 40); + setCenterPointX(24.0); setCenterPointY(15.0); } }; @@ -87,4 +87,4 @@ protected slots: }; } // namespace gui } // namespace lmms -#endif // SLICERT_UI_H \ No newline at end of file +#endif // SLICERT_UI_H diff --git a/plugins/SlicerT/bg.png b/plugins/SlicerT/bg.png index a9d1c82733c280ac39f31ae30938e18e134f27c4..0e00e4c17d3cd942aaf10549a2d2650d44facee9 100644 GIT binary patch delta 24063 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 literal 23763 zcmeFZby!@>vo<=oTY%saAZYMG1`Te(Ex64f!QCZ5AP_7_aJLYGLvRle2pZhog1f^V zVteoJ$ajC|Ip_XypR;%tt5$b))%#XgcQ1nwB?T!ARAN*B0DvJQEv^DRUf%D>h|u4! zx3vWU00JAxa~V?^8Abpyv)-FhFbF@6AD}YHFS#5l)Bn=BZ_f^Givc~>L3a+Qj0Zh_f$kQ;KmE=?Wjv@cn zJ|H&$77!GT1R#R8x#td(@fTj`>7VW2dO`u*0)Aq;=k_};_XK~(IpR|DLoIv6E=1r5J*7>66EC& zfJTSo-Uti|3JN+JIw1xIA;%-qM;!n1cGm*HLB7WX2SWvb#esptfw}t(pn&2;fcdfg zqyjyMg@Z>xL_$VEMT06-Vc$mz4i+8`0RbK!TI&t12f*VXJb1(|f{3eZghXYJ$MN!Q zCNi~XSu4KE&^`^Pv4cMf>O%rTBI3uibo302KrU_`UJ##{xP+vXw2Z8(n!1MOb1iKX zFvQf%+~S3!le3Gfo4ZFq;H#kE*CC^YY&pd?>G|tol@4Q(M>8 z{<)*G>q~dfx8ae|vGIw?sl}z`mDRQNjm_@|heyXJr)TFEm-oExdH&vhGW$Px;Xrx8 z!o$PCBi-|YfpxtnjsuVIh#m2Ph%%CqJuVf;OJqFJx0z+FDAb%P`}oEVL#PjFfQye0 z?y3D?_WzEU|9^?uA7X#;ngXE1!9Y6?4hJ9%IH<3$4{S))SW?QI0FWN^Y2FT{-}>a< z0h;=VBd@)6?*QYHx6Yz|tiI+0cK~6PJHWhYVCT&jJfpzLqifyH$8vrLvt^gDN9v) z`>#cffGG}-=x&ZJd@G?E=>@`j*;=uxr;(+)!?#1wW;yrD6jLg`10(5YLmzwS@2f^` zzx~^)y8p9EtEw)wI1|wwHJ*qhQeX`Kg+=pLQS&@Se=)~eUFmQwX7(L`LR0u;s5w=+ zaO$tlk}Rd6&k6P2ru?n#7&79 zWsyOdUOXH9I;L={g#xqwvUKPn!_=w6huml30W}^sFDPi&uiij&7waG&Iv^+>_%`^a zN~v7nRy9+c!wwq!sm<=Ly=7&1Zvm0AhjU z^j&D83wK6`vXT$%-2wcFF%OIX)E54#O_m@X+P=L5oWo@7F>d{0ligGI8*ioRT4ehC z9RR=*?5=6>3$fn#PsH+B5#|>+cYuA^4cB<*-%Y_1dHo(VTISqu zdvbr8_Rz2E`}$r3>L|+pfP(z;`YbErSIfxBKTiLswmE@pLG2N zZkRqh)%^$JRQ8(t*m@F?f_n>AOgEV|IqY)1L-@!-bD7#<1Fc$8gmf|`Sp?<-?MF%kb47XQdGOl$n*u* ztH+VIH$&^J(5l@Ci5ZXIj;{XMSn;Pu=3Z%@+|8a7r@PJ|Gp2a=`AM+4?8T)h^EY9!MNK@6?cFj=f*ogW>VMfb@lnk(Iav;OpDPCAFgvL$AuECRBgQ% zV>{x71aYJgMbLUlmFfBX;_86EHE=8B1iARt_f^qHdJSeNgE>L=Ef4GvWeO2H57a8O z74XobO?W>nEcjz0mne?{t@VT+=xpuzqJ+wae!<&g{ETjv?=eL_uv3p$G;;WzX-I%> z-&OxWGOc>=iMDrkSavVU6G3}Dx@*pky&Lo~;j_)=&hmBfG*(&C(BoO*h0(dprCFV) zky@WLhqFT^(|aBpxDCJw80YM-x5_o+n0LR=YqYQ-)-pMqSpz3X$5nIveRVK{qx}%kIaFgQ33{+WGZ}0i!V=T#0dZRPn0v$61v|~w$Pi30T zij<8_Y)iBhDM=~A!frt&!f#r&{YHwnS;?4fm~|`*kNVUE&&Kt^`5nt?{Mn0It}@?hy$ zWeHex6_uf5qSp){V0s! z#XaPuo_Pl_H#je!ewOTRK=m}SMbO>OS9a5PQR>|6HWVTM=7{1BK>AUym^$g8tDR&X z%N(Ux%aWmR^Ra*B+wOWBH>AO^gY$8ngT33QbHvcA5a?B-2tH4!$vzavEuP}S4y&e~ z?9!p(4yB>!VqC~FB~}!=S))@E$NpY$gL|F@d*9MD8b!G{?*qw|Xr#h0<;k*%iztBQ z2E`*5A9!<@!>}&q(GP4Vh-!%C$qRe0C|}LEt#wMO)4-an-vOx98%?xI%g-sSZ7qB) zACEQ+ZHGXhSHe#MQfmjSMrHCX0nMabjf&5_nVx=IO`03{GIuM&)JzBfyFeG^gSa=! zKIJ_^<-}+7&dn%Zs{1=R#7#NCRMSzHi?E(%-G>VSp*_BV)cSA*6++X9m(cT`hW4(iH)z6frvYL%AL^Q4m&!`%Y)MUU&m!R@6h50TQE!<{KG zo;dlUxP9#)R(13Lwlp*Sl)By>2ksrZf{-yL=az0~@P3!b0eRYG?s(YF6G+mMp`j5G z<3mX$cl!~5hik603)M-)%Peyt?)|e<+Yn~1_;>5@B79#=E)?m|EqtC#S;o&}+fWT{ zv#@;9k?c28nRd|m7CqmRgyny&nE66uj-_IR)3t06Pbt7+C2B4CfVykyQ^4|7;x*?- z(YNx}gl`w6^E4={9WYR4HxRG@8$x_PBKkhVYOBWz_9bFDA=w1_63ipYWtKCIst0x|5!Y6tSrGE z1$4Mz+RV#ZgX+B*Dw+Kl7|iovjiFjFLpyWzN7VvLB{x}Z?1v&Xk;+<)^nCc$oqQ35xzUA!dX|M2#+qPrBGTmGdHRUBs;I%NQeN!>R0~}h^=JZ>b!!Vd< z`Qb~-%Pylm3xj-+@lb-tQsiGwI};RQn%hgqDkCnu-^Dtf zk3880ew}1yQOYSx!P(kVmy|a?cA>!nW-P_y8NUIThO%pWkOy;Ddzfh8v=Yt(`-UDS zy|MX%UT(ouC2uJ+{FuA$VN`WFCh?MuD}~XFO81hwVd+*Ad7yY#de)~pm#;DhLJ)cTJKZ&L*b<)@RVS=aG?DunHyPfsa8sUKmBR) zc74lcl@|nz?eAn6ocs4oDb^gKUoEF^>YTwi&lBqrDQtO)m8q7(h55J_n~5|N`_!8` z-2q%ch~6)kg^Qf(5``!Zyy6vv2W^8+oHql;Pi~zn4R&umVn6Xch4sS&AOTRO3aBKpGBcOI{*6D!Zsdoe7#z^336@ZIf0YOUlc z)q%#Wl*o{C5Gc+3xG{M9Zi z`dtOK=5t+(bj&5TGA(DKX-`+ie8Nk6-BjnXAz!O=_-WD1Vy8{5(=+;+XkpF^jmo-d zPYe=bq2!0DCd7=9?0zga-Gwo&_`S?{U&sgz{h2t$#~w9Rh`YniP{PEfH7%9s8%$5M zeN&`*50~f8^^Ykve_@O_Rf@v8_>zLLC)2x%)!6C1;oob+vmp62Iq`iE>`W#GeJ zH-L|S&5Qp0R*Li)xk;>CgmD5nqf?(A@#jIH1>@h$&CZC=NRft`@$@!|0-`j7FB}`VJ8J?gdCq6brA1HJ`N> z))DE4mYp(1UM#cT?3-WVcU>PqO#$^_wBO}l;X$AL{Ahj?x_s;Q6?zd(toc+k>!bId zQ=ty~JAeT6kxLT>e+JVx*Fs_An%~}|CKZ3zF=a3fX$W-)|1{ogoc_xii(iSv^FL1i zGVA(OqYzu=Ce-%_tQXIVO!Z5iq%WH_xF{2I!No z`?%yfA$Tx$P2~UP5?S_#3&B&({CwYBx!*yv_-e*GunXC|X_pwJxSIqVL~DgZA0`@k zs6BU5F>l!ojE?o+JFk=SD{*B{TlL)?7%Ev~MuZn%;^lmn z>=xi=I?Tmk&ijmrY!Fijt;Qm4^I zd`MEb5FIm#J9=nVq+(L!c}Ng+blJl(ZV^EhBJo5dD*|ssNfLqExbl0>bLnB9wl3kN z%f}83D~theO<>f`qx6K&DXw?N7@x^|$SlGS-YoG_m^>X|8T`oKe^#~(RP=MZ657LbK0Vz_)bJHnzH&%(tRmb50I>Khpnd;b{u!T%tu>1g*wz@r z;%064;|a5Xu$!Hci6z8|(imc9VIxSjSKmxUX#p0b(&Un7leZItm|IADI6zcA6x2*S zEKNXQDq$g10XIIV0c(hp5v7~8m5n2xn;_K>yL?djzL=GY@`s9(r6ARF=sOcJTL%aw zkOj!X#w_7x;le>Bgi0yk05;`Q5tsaB0(vG$W$xr;$H&U*>gvkk%E@BuV8+T00)bfB zI9NG2n4ucXj_x*2MsCbDj@0)ie%cU+IGQ*>Gugt{hVtH~k+H3_lOPopw4U-eK5IL9 z`9J1u9DhXt8V^=CBRf`h7B*IEYu3NFaCDMzfg1S*=-+MOs0JNEtSS&kTW1Foh=dEo z#)hNbp8$m>h6#Ie`x>a{zDn6B`?n>ZfoLv?^H%ykm?>UAK2E!0?hYAgs_`H zc#MoWn0eT^xtM|MM!d`*pb3bXjh%-R#K~dGVam(|o-aGP*KI3Pxxe>b7%U;)iaBdfoox;F*BH^l)2 zLqOc@%p68Q4rU+-%*M>i3kEZDm~cXkvGW@77_t9AbKehqB1$rXR2(d9f0Za%89ABS zI#>%*J+rWJcKfSB&B7X@>SS~uG~xvHyAY^VG`X$55iA{4pr_j7)w~a5Qp(fPdhG+WJ{#Vs2z(27%5V zztZ)0xy8Se3cT!KHVBx91KMl6M$lB? z$N?f^28|;$R?zhP@ivx{Uem$`Z0qXC@J}?Z=8*dif`*Kl4aCgOqsGC(2L$nPa#FGW z&(j038v(gNCY;R1CTu*+K%fzXnHOTh3r$ZW4mLIuHg;2X(|^MKkLmqL(EpSi2gf}* zb|4>+<5xcXe?XB9$Y}~4AneRw5C@nU$N}bI=4E38GIJS$K%AV$>}))o|D6;$f2Tap zKPd{Z-p~5Kb5?-$-(>Yqg}*Hm(BA)923>xjs~PK`UsC?x?D}sD7wTU(#1I?kb*wA&cDHy{4jX#2 zjA$$;B@Vc||H){~kAaq;*hyg-A{^@)AhvC#@^X;GJOH2X)fD#}hE~4f(wc~B!r>7pbTYW0Xl^=~q^dA0!w4Wgu z58u2>LR_Ln<0+oC*C&n_OhP-0Rh42#q}WIsJ`?o)yoN0(I8njJsunm_8AR}{fgm{e z=%_N4f)6u|bCH}o-g%z=jq7uK>L0Nf)t$HII6qc2HwHDHYCCP)+ZrOns{(pSz0R5e zh@34S8}}A1=kmj-v0nqI5OUjCsm^SNi7){(j%U8|?K;#)2eeJ(-ZilY)yK=Z7!N|d zKMa|LlJZ-%l}w-RQHtf9tZ4yEQmVc{KEJL>_3D-j-jio zD+xZ>R=hUDz83gSD%o} z)D4%A>FEhjhyvfs_Z{$1aJt{8_}q-dI}J5_b*-OOsP5^0+v(r&i^MGe0Gx{LI?I9S z@csl;YjxrjWMueS$neH>zc{>aio7Lo9Kex5h97K5_Nt zM9T7v#R9D`k2?G8Uc3%r`cD1Br22LJhLl@WU1AjYVV5*3XDiq59<(vd>YicZZqE&{ zk~n=?Bj%75-(0-oNPk`y5>6%cwfy1xZe?+NBEm&MY~?%}LUcByaxlvsj>@W}ahm5U zZI>xOeG4U^|8sHbF4MLFxxAKfa)AGX-S$S|9gJrz`_f??lg@zY0g{nG*O0|b6qZL@a+cR9=n?~au z?K%IoIc=X9IfjikkvR_gFo0z98lAr+6?jG~0UN9Ge%#e$>zzEy5MAdMVLBmNNbzLl za$Z>C?p>kEL)@8*B69RWD#?d$IbNsbjWT5<>HsJ8n2R&;L>Ta+gcz|9Gn~n^EzjOm z$)U0wc)ZS#uATR8b9H#;@V?c1wdj>{b7Mkj{RIHzs%QnQ4}m8cccpfG)#M zts}eyy2dHMIDU2Tqw?A7{;B%A2L#-&37-*%;#E?;R|xH<19`papaxN&1$Lqk3we!M zDK|OKrxuBwH_SKIW3shAit>Cxi*3n0{3bu{tI12D&sU`0&({Lv)ZHm1k> z!OsXpbGNS~)z7~`3Pm&v5dnb)20;e?RjN^D!#pjrT zryCt?@RDKRl_V{ILz0B#VMYP5ETuqjao&Xhsb2$C_;uXor0W5n8?Qwok52eMWA=$SFb zrKijhm~Uq&t527)be$kCslbuizMnqj$KvQsJXm@iiNIvk`B=c66AK}5G4EKltKBYE zkqrz#I#E-7eK{6x?5}Q4o7?clL8eGMdX>_yc-F$8p4Q8*D2;DesCGKDJKG~t&>~v+ zcT%HMt*mLa(N1Cy5K6~5;X_rv9wN!&nj$uLT@@X{Ml$v6F zeCz%K#n~~cSVMWo7;rvqN%=|!=~bdl9W0F^oq$fD0`bcWP;ebpD6gl#p+ERhQ^ zofsdu$Z@5Ry!=icKP*|i%IolG^?6QO=z~T^aQ<_`;tef%>#f}3IK@-H zC;R0D-~637Ypz3Z_lGX34LiRv1N_J$`#Uw$J-olJ(1r>qm*bN?&cjYDkY0-ha2)gEVW@+7rP*9$Lz358!*@o1{Bo(7^<-e?W?KsFLzE5iV_)-M@bRdxGRa%;m6FyaC=k6tZ0+Gpbz0x|L zq|0v0JF@AFs?HG+2W~PFTowlZ4>)yoF9=DdhphT)6f958FM`J+$!Yrn(*~~k`Qgmc zF38cbuuS?>K<`Iiyo$KEJ@W<3*B;VQ0-9@X85?$;yI=q|^*d+0w-~mpABR{kgwtCl z&C(G7Zrf#D_MbDmXdhfL=aav2ZNAcb-gGTNNK7nhU|?YEOo(e@V)C4Kufved$7_ej zF2%J7z-DPk0SGLjR$Qy=k5LrDXl`0n(Dj^k{kXi+y0JmeZ8hQNyp`5Ds(FX3>2qYJ zBx_Xa3*b_3XJF;0kUJHq++1&PUPrv%=zqh`!RNojU!A>+-(y!jnq|FNzeDMqBuwrs z8?#U;+7uC!+9Mkg;(OY&ryEq=xKr7-!g6e$fSxXBtrh6_rA5Nlm!Rn`pk!fUyy;4lRa2AD6a5ClHw3Jm%2H$@Cbx%#x`zd}PZ`d9UMN@&1SFRE412Jek>S&V z2)rbT=J-3(vxXTjrW9RJfp(xPm*p(40nfS6VFgc12ATn4JMlHsgg|CVjMopp`1#E> zvCJH9r7tc7V6k{wRq;JY;^~WPW3ct+94qoh6s|kW`&QgX3sU6h2Cd?=$bXmF+Epls zteFL~)-whNLrDO6BbY)%LQpayP|x(gA$J%G7hjNmUb>5^Wn8gmv@Hkq;SB5NJ66}T;Nm_748y9S-c z?$s<~owrHEzkIzgQnyG?Ha@7ylr8$ezy*7J?$*gIwFDUVH9;J(m)iR0G!?SmVU%|qtNlI z@e<#M>DMc-&NoM!8No(&QPvNQjg6^jwyd#$BP{_pa2t+@Y_0=Dl&H*9dS+I-oZt5O z(sWONHjNhw*PDu1%WaC@=zvngRyM>n&fQu|^*RV{B2coc+NAxoYV0O4DP>yLVV5`a zjRJ>4B3zAExvIadK6%HDNixoOUvhP2$c#)Jn1P0us~Ge9-A*s(9ipC|Ua@KM_(7tg zfm*_fM3F@9%m+V zll|P)z-NC1=wHf}_?$4QhV&%zK-Z0+>oFm zEwrr;)qqX=2*u?`*#>#2<3`+RV~Y5>#Vo^jvZqU(yEI0s6!a|VY@<&?n)aHmz>(n4 zc}=D0$qxr(=-suen*0*qd-)eWnID&TD=>grHS^(GLh&`16HN5OcXw^J8_(uyu8QZE zL+H{z@6O^95zCq^cgjDnwjpb}N!?p)MWV3H!8K90qZhU6sin4@YjAHJ@NKwzX-u;o zg(r?$VTSIHKHr>2x?N$&09cUI^vYVqvAw&YC|%OwF&sZPSF#(%@Z0H(84#E>Y^lHz zGVV=a$G%>@=u3WEb(@wW5wH|)_|-{q-I(dEO=zuOtjQBsA&x=rtaz!@u}_(6ymi&@ zd`?11?0eZYm%})3i6fQ`cV}u7_+4#wsT;TS%oc)$uOA6rzx9>L2X#0C8b)^7s>jF2 z2@u0O(Mel z$4SpPpz|nFbIJ8lPvFeXHcnqzOlGImdWo4A?fGTe0(M?yT8v zJ=Jw-#&MuS-;??C;Goj(>Ze3bv!(5dfzALdp+H-ot)$q@r>`IHdeoQt87Bk3pLp0= zS0T&9sU!>u!*O1k)=ZkrHf}i7IhsYnv`m~AI~*gwVn;^QvgNcrVbym=ZaVLHGc-Gk z0l4px;@d@5PUk0&qXeGIlp@brnScD)@;q23?GQLMBVR>Su8LIYuEx7NUfpzK;hJDk$DpwsldjGCGnh5=Lfnr+7A z>y7-|DXgeOT35ZjQy>sJ+0W-8y|e7Vp7ZRt;N5F*REcC`8;ra-T1Pr-?>(O!@R=)3 zqo8BpNF95@Bv)hS^^xx8NDi)eX1#?B%2P9MB-d8yCML$8xiiiLx&^cLsw z8+5<=C$TPlG9(yqJ27H~xG-qz%+Lj~$}+8Ua4<^hxSic|KL{~!EaAN(pjYyB=~{PC!L88No`N({Rj@o zpUQ(e>F`Xkb{8*K&xHKMFa?nT`{nV?cqJY;a(2_qXT&+=T@w>5=3^6Oj!2(&W`aB# z^;K458!ReT5H1b{o1xRYu3c-_L3oLN%w{K~)6vk(Q}x44J{|9}>uBv)WI35>xo2SA zxJGyh>!vggScd%Oo6nTI+Ti16oWQj^~8{FJsR5F3Jm8KJ}UU(PQH6{>RHBi)1hp`2n9md!c z{dCAAI~<`OZME^RnT_2N66=Oea=tShYiVj7k7vC)TbQb=tFs%^+H0M;2mqz&v~g1V z#SW2v^T3k#jFy@m-1xZbbSi4q*T3x&B*Se#eP!1Ri+9B`iR-wAj(_Rp>r0`3@j-K8 zyP_Xmf4bBtn?SR4U9ro4|BxC>vU(Ves-@s0Baf6Rcl7f+`Du1;J{(T@>YWMLK4R*D zotCqvE5VezOe|Qy>5QWg%fqP*=b^KSi~&-LvmVZ-T9I~W*qS9e!K6P8ulyfm1e zo42*#vhR!eY;}%QLrz$t_IOx+zSsV&MswHhZe9EeNPs5>O{Bti9jGZzZO{qw$h^sY ziZ@=R#}XeQgX3`~AL(m36e->qS0hOj(WTFzdR6XDNW9(yD+X%>o*6s1wceC~#2e1c zFsH%nNFmg_gXC)&W`pLX>6&Y~d5OlR>HSqA%XBnV0oLpDX5&pCIs0!ibg%#q50Cv$ z9`Z<@rh{|cb?Bnnel3U@)_B{a*+g}IA$)fv>_E0PyXV7Ne6?x&Ai&;8*j|?A+TH91 zcdaiucsarm&63pNOY8XsYjImhS9%(+`vhYE)w=4gn+l(K{fQ`*r^@IYchc_6!dFVp z+>52uO!vf^t^RuZJ|5dnp>{cTprcLZ>c@m;ZHo3Jv+<1n`U;fO{-cA&J#UKa?Cdx+ zjY}J%)#qFSCw*p1O;@{UKEg|{sqJPlG$CZ%pALc>9)9gPCvS+{_YY>2{00* zTWbHJU@yPNJd0SzqsN?3203~taQVfgX6ev&X|MmrDHCs~eE%(${%ci55ZSnaFPuh+ ze!$zeZzm+<6ru!VBJlEVxl**}4^GdPpr7+^w`E80J>Z%1Qil^3Jp3>|JiD`{FDB0r zMdegibO}aWQpu+0L|P}x%{g2_pbJjQ>ewW0A`ExByBFJh_2^@!0pi@;wa+xOEK9_b>a8$V5nt5TSrSr^ z9C#lC!&7~uCSQH0Kf7wTPLyPWaVcvne}g&2UxwT`d-ICh8a{;9aGTiCv7vfzCc>I} zI>Wgl*@28LoojOVqW zy(u}A6WXNB_3G&v8UNw)m!>;B<%$|$ktbS~rScxj^QM>biB(-mvmvX|9;DeO*@L=W zr>WDW7FS1u+9U`R>gSDtDBZGI3=YoH9M5>u--Jjn*mN zjuT)3a79EAz^(*NC%}j=>#{F%BMfAnN^0fL&Dp2D;2iotf(Vm9bk(el-s{`~&CBlF z#e4l_2vnjD2}up&7ZSv{;MGuMa{Ow1=zWgppLjFyhkh|18D&s@5zTGz#^ zl@crD)JdM-ruWQ#ku!pLq2%wvnz#uHu&eDB3V%2M7O6Yt+Qm}?t;$p7&`N_Lv&JCtAtXIzN9h(UDXBbK zc__0pzBC4u1@mOkcMGyoBabUVXjqyVY%i=oM0u+KJ(_}EuD+cO&$wJMbp){oyHvk> z+3@Z2TDg8#_BMv84Xn=B;l-^H9Oks%&W+1jO_K~@x#zHDj7ZIlNS)ud-&kq4zDOHE zEvea|f?nPuinP=(Gp+d1g8I(f1vx;ilTIe1ayaNW_-~&&d?#%-DoMvpJUCS;pEk0I z&HU6!6#Yd(3B2uBNIIFFo6RTcsD$)S7CF+kCRB4d#tz~gSPpBX>gZLhl48Aw>7G=$k$9D3mAq%K6=zaIa)CA? za>Zs+)aU`W708pN^r$E(N`UuBuU!Av&Nj7#KF_COy6FuUaH?o z8ADK3kDhMjig-X1`9!dsQ}y#WkG}-_Y|<<;{imJ4Wa~vsL;`*~Urt<>x{n%bpk9DM zc{n%9ZfNc#o$Ez7TAi~36Q^tyN}Bw6&5&HktoTw;Gy6eW9-A=4}MakJHi zsLH$6f0BFmFh9kVpXdy1>ynB2;cqMU!`8f*#$=KYG*X_yt6^L=v8E}ofV63`vRL)|eiuxepE;#bu_||=m-mst6Q)WsK`HCIx zW6TlX8=OY-(Xr0+`E~sOPz6C&la9$rg>Rdt&tW7X8HvSttrnaZ?|ST;QuEt4%8q#4 z)>^@eJ~um`FxTC(yWXSEz-&5CJ$bcqF%xWv?KXZkc~o4rvcD&Mfh_5=W4`eOy)Pl) zNTGHjy1lE(MLS4?wSMYNL7!gvxEgcZYxfWvEmVDBE|t;oH5r=}c^Az^b8cB5`$Dq= z+Njq{=FuA#MIK26m+J$~>TA}Og<-|nX@l@sPjvUNDah$UhN2kgoK~JY&^Vf zk5}AB3|AQ3z_ArDPtD1gl07u1#C|x*dNC;fj&xW53h%-v@9a(jhiG5`SA@%~$YWN&0yY7cjs zGp>s@>wzU)&6KfNQ{Y|WhWEI!`5GIyjaG1^_s+_fBqyhfNCD2qhv%ywFie?M8c+7g zg}iqaVL`h4qt!VHckb~MeglodvF%;WF4{Ws=i(K!gAIKT1gFSG`Do>BqMtEznZi4(GwXzG6U_hk#P6gEyt%p_lfck6N#wca|w`@8pEe3!h@f>wEi5KRc(Xm>q91 zf4iZ|jvODn-8#(m>|*T8?gPQE!iv-$Mb%~tqFtM(EN$owRSJjRLGg<@nkjIm3s*ee zs9WgSbPWV@51G3bTChp;NyOp1Hanr>L*@wzBo+`nmViUGlpUG*p1jW3(mq^7 zJ9$96+eXOz@&(yI#}n*vux3Vr!5k+ zPwusnHj&6}qaWUm)N}9A zd~IwNIsKRC-A{VnMl0yC9mWh?I&|xoD2kKS)J#sJ_VX^LD{p-;>T|^E@H!;eamAkV z?wQZ3&KEu+Y!tlVJsQDU&bYzEBAFIj?pLoiJaNRz98+(fPjNEposqY-c!rU^28kx6 zl*2p|I3%esg~Ob>Q%Z6F9_-MwC9k7Yfjo&TuesP^Cy ztv&yX>28HblW{zWIHS59s?URFoR|{v>IpRb$qb7 z6rjSn_`W@&iu${u*M-!2wvcky{$O+s_gVaaK)QfVOf{>)P;C2nNh75Th7|c_lBfgk z;o$gZ{pRZ~ijeL{b=9dlRQ!+Mg=anR_>^eVs&?GvD9zG4K0w94O6iWKsAS)ZM=v$8 z$9IseglPnw-K@wa!|ZA-Qe4_R!&_7H?6=3caQk^T;l{2$?M(UiQEz3EzH*#9o|HZT z;tws2!1#Anx$9ys#-;eQP0DvKlF_A^G}?9*lf5LJ^ZZ6<3_j~Kwb}HE`OwJiOB1s& z^sHtxqYu}F6a`#zfg%O^($9=XrMy#D=1tH*2q;}kjAMIuQ3l?-8Jy-+8ccDTpPuIQ zgCn4@x`CW`s+&SuJ9VBHELt^Yo&YD_eiR-KFual*oIwVg{zk=rQoSex;I0cNUz^U`|->8+~wN zxk~_l%T>LZR-DdgPRxv82?Nq_xw$&#?t1&YJV?AG6CYPdZLSmXol1|SL=}iJd3^3+ zdB9Uf-Qt{Q<<)5xs^yYcoWsz^2rz&%VTVEj@xlP*u9V3m9X_$+qqCw*3_wPHHLKZv zks}Qf_Ls#-eYS7o-&emp@;W~&spE*wVTQMnm zoYF)(W~*&-(ZAZ=8h2MGuc*CVQ9@^>jDMQk*PJ>!qa`_#qe~4jx>?QRaBY$2DyQ4? zIm&4zHPb+wUR1M$=V7vRyYraxJlrl*Oh82LocP@T4j$~226&R>3H`&fb*4G1 zm($XR`*nU7p4*Vd5>im$`m|+#Q|CpreB)r?x-PBP2KRJ0X2GgR1m#vZw#y5zN9UUb z*_{&swlADL&k%W5(|JUW*&KFPH{hG9Ez&H_!@_r90S(~9#wVXFjw+Z@9$z;wg|x>v zc9NafOC}$G{W_%`d+lz38BDSNVypEXb+DbpVkx8x-a-0oCltC2So4a%*7CZ|e(k*9 zM$JQFQ8x5eY^dgyvdsGfHu)Fi4;@5Sn*~xoG!l`w!nJWi-#@?r9`-?R#v-LO?+PA^ z)TU$EnjMsqq>!Xe%vBonTT_7ma+tK_axxZuZ@M(@CMN<6zdrK(7Wx9A-#U{p^{vkM zhNO~h9m3)>4yoqe=sAttK=U<2wseQ>w^8g2H3p2&)6kn^*&mtNSff@|b91@*pSrlN zxsfai%9nZEv;+J)(6A~)>RcejMFk-_L?o@*mYP~J>~GfU;I0euDOjF4uxyr@KGMPr zUef;4sL)XNmX8qpts>au7LPgNLh{TV)EEh_>J9; zY3OWqeJ4Yp(a?Ct2)vPxi?0cOYcj>3IP>ZBblw&-rNrQlosBaHF^JuFO?2yt=Byj& zbxp=YLtW=(&5uT9>)R59NwstdTpBZnuVggPy4P>#!U+%@2#HfnZg%K+yz?1A*XMbA zE#}VyFDeM&9csR75>OYmI4R@KX$$9T9IaBnZ0s3MRz*pDX4+3#QC-$`Ra?CEe!Nj$ zc#2hR(b{9^#1Jk_OS2W&8z;44h}B$uqm8hlDe@LyF5B@*X&$b;Blj0Fc^R(E+K6@T zk{}1Xxw|nr6RL)rv}8vt2dd7UvzhHn;uUxq_Vu)DcY3MC+VycmFJmtiL{VzrV5RMh?L zQe@4+mS(;LM3$uz;GGAcm%Z;!*XphyUXn3r~)Z z4W2vJ&AdV3?z);5abD8jYtX8muD1S+^Ho1sE5H#@RQAHU=ITZJJ2U%%_G*)hZ|y^? zPG(63n)+w96Q!#lOU>aBsb(Zr-vg3XXTy#XmbQw466?{7C+0XSC870@El#)Y@?|zl zJ4}~x*Tn;8tRRtE=48A1^u5pJkCAv{+p$XS_|oRfyAR7Eg};|DgFenTbRN!@G&P&SR=J}~~y^$-O-D6y@?2~*FCT5USr7YbxZ%ICME>ea=mMmUr zgwH0Grxo#Df!o_Ly>93+ST-RCB?q8RJ+ig@-dRVy#FH~5!ivd?<>TG)QRH&zjX`*3 zqNj0pTY&v~K=bGR1F|tsa<>&0tLY{jg+38YO@bFat;Gj80`>M7yY3sVH7CusuutDB zGgDs7v>K(Hyc=k!lP_Z4)b?(FpL+v+*a zl-lcfXFbqbFly{jbCx-6f7RHG0j#|v4MJRB`pDx;8`ZApdxdXOKs&`rTgVRI@u{LN zpkrQ$IJyCuVF!jBQ{&F9Rog=b(W?-Q8DSJP`SAhn<4TFO1y1S5j@!h%qn}EtcfjMC z=0J^@*9rjvo_4MS09xAb@At-DZYrsHnk_~*HzHJ+_II4VT|aosN->Uy;N7esviOGL zx|xu40#2ve<}>sXS@sQFc4Aw6-)PO<_Ej;)ie2lfElNZ#23Co*MEZd5!sSy;08HB z;gS{SB}d8=wYxD${MI#*U{ZXEr^)qm<4S@v=h%)~ZbUo5cP`{NhtY7Y*>i6>sP4*~ zcrgXj(>YdDB575;rLjI^aEN<)1~4|h8})iUFi)xh<5t^JDW=)&xn*wa%<$1etBDk$rfNZ@S5;Gm{Br;?61hY-%=SWd6DKI>B+bm`{1rcn>5_6hrjR^V=uFgu#>X%p zKCF^bJPFDEPQeJw!<#U#JMMlMgiIuJq!Ms+eqhd?R60t7*3w#wz(FO^Y^9jVB5PSx zj1Sr}2_Lk0CV)8X)@B*Q?jKV%Pi{?|Qi?d}QI`@#GhNA$mkCRQsbuD;OKvSPRz;_= z6_-h8Kv_!IsWi!vw^B@JA5kYJi1xHfY@pqZp%+AzeNu6eA!mMutu7`EcKgIdy4I%! z!R%dI)D)SkW~CEJYj+k+_b`Ob%1j@5!aLsKVSR{+j z*TiAQ7ssLo95d15nwhg4JREp`mHt&RPN6-~MB)eIW z#$PukrBSZ~ia7Mya}hnX24GkRW{ zFfyekzfw$NLbmJ3+%c`ri5EadK+SdIxp)>EMYmFVz9xs|O1Q~h-&2p|>5+9RSRkd@ z>EEdib;6{WW-7%@ZEbFASbLCk#%ZC1rx6W&1wr||Q+Lo;&C7%%7(28mOdItBZ#11W zd*k)-PP(H`HE13t#Z=EP@4RgjR(I><#6=O?iegP7!&+w4K$9E4ddVBLn)iH}p;>`Q z#|iI8>ZzsjXqEHGlVUra9^2034{NG`U!K=ArVdHPjDj;-PwZ)IolvX*M10E0Ranw& z`(~7(Qhu=nBGlN;ch_2{5+2O1hUckBa;_A~Cz@5~q!Y3udv`%ueD)>$rqE{&?hI0ft zT_ZhbGmcL&H=;3516xfc+N1T&b+KMuE4oN#5$`dGa;-3n?k4 zDAkj^^kxaqiBn1bRSh!RHX2uwc0G~x;e=C!b=?8B2v2Tpb7E)>Kr@)Mo48ZifL$5O zXYI8E9XlM>dB6OH9(>E=t+%Gr)AyejbCD==HH~5i31y3o`g6h6ap=ptgeQ=4gLQ6& z8;!aqqqsxW2w%?fSyB3Cj|WC{9mOH5OQr%rr(SefCVRD$VyGenkDBl0xFu1isX0#( zjs11X=l=Ml_X!VwPYt^YK(jk8weq%5>*^3b@5Yq!?S}YrXbvAeQu3V?z&dvDGX3)E zC&ic}hbEEg&F*O9hH;VBx8`yof8BicS%i0QkD$O{Vkmaac%=z-VsJ$WpB-7JNedaL(Ssp8uStS?}8qO#Zzy zxB=liu?F9G`K(&w$J6LujWCaXngC)7qMz<|LLCdn6hCQ|lVZ~LT2mz+73cQQxS`Wj zrgfGcoQuTuskk&_xjgI6lZTjzAk@oF%u zh-cP(@U3co`Q#*=Hmf_c40q0ye))2alD!>CNA4uyKbJJZYhq}x6Deyp%0-n;`1UTJ zl}Bz$^3j7S)TSkAFqB7lw^>7I&gbp1UA3f`EI~!v3RjEl@%Vc=^~K7ZjN&FdHT2DM zPbm!uuQWlE@(AxO$9endTPJ)qmd}zh8Zre-ltA>@MJx9}&x+W1$uv*C9w+fA?#5wX zlwLqYgSqBCJ)J|l$Nlo^CB@`5i>qbEvvsAMNN(!bWR>i%#~MPEeZ~~RD}(TE^9i?P zb5KIsw@h;wMcK<|lkJUja$0PPo^oO@QmDl32dMyJXv3VhoAgCJ7vCR;QKq@dNip0J z9oTY6E_gPa@N6e*6(jOg3Fw63mP&YLC>8qf1O-h-Q&7L{7^m-rH0XT)Q3rKa5(qy(GUOK~AFHZ5rCvONE zgC^*y7MkzjIOO(Bh2$^6dxZCw+=xIm&xh9pW{oevYwsh*)|x$ENDAf~N|qg@Ema`!I)Twf-J@uW2|3$?bGQ#Toh{|cn=zZANT-EEO%*^#lnIJ*`Zou7& z&TZ4^i9tZi%W?H`ja?;0+V$Nv*E}VKSd$1O80=N4TgZ)`zdzr4n}h&{18D3V+2Ywg z)D%hB>VSnvP>7}gfnZxA6~9i7JmhMalX2bPwptFGGdA15ttDb4bbJZQS$W@mDjfYf~13r)y1!v6L0000h5EjKLjDGZ@o?X$DO1JtU;(E}f7I zqyR}S?Jk!dY6uWIq!D^C;Qw#-$-=oyzPtOrKZ&IGc4l_x&D%0NGdmvLI$@G=Mq`U{ znk_2oPxyDo{-Ntl_^9y_2=L{Gj}tz#IecmGan0V?`xAX|t;I)#PYymF_G`j(|I?r3 zpWs(M8tqsb+a^qCp4fr=Kd~CK+nehktWx3@MC8r8ZZWJ<@(@y*7ziI%pO7E4q8lrf=_ zke1PnZR6Y8jUnvE(muMO-WX-H;S&`KaKrC{0p!6mrcA1DY@ImTm|D-%(HHcplmE)btq>#v7+K zw@+zpo0t|HWwiwxnP07+^e;Q)!znA%Z4+DO7nGv~}`ZL#K) zn5_@!+v9n$?UQ@s@q>^d971A+S+HByPZkzC)%Pr_5`r!_bI zAEfhpwERG9G7pIS9|>D0PU)zh*w{?ZNMdWn113f_*_xXYqAiJuiS?G)mc$rKQbR(E zB{4pxAvz&Grll$2|0Md~<8O+6A;=sJd7=}LYDOK>7*g3fv8iqPl+gc?@9ATjF(XpX zu-K9;Q3++y(aEt%$uWO881QiY7>oXY5W0i*e-*le@b`qC6jdJ^pVSy*X=t=1SYl)A zn=OgWjfs}%#`n9}AsLH~*!`9o79-i$LWF@3XO!ha+9{NEYi@y$)P`j~_!OG2#8h5;TGZK+SHkF#J!iEoT)j&81x z`45v9CjZf`6YIzKpCW1cXu|XnA06A&oD?5riLQ^0w!|hi*)WkbHK8ZRp#G@D`h@!5 zUE}j7bu?NtCrujP+Q`(=Xe-U$c4fK$SpU6Y@bAR5U_Kz`XNPDDndP@M6hJ){mF6J4i$ z`sru*b){>3h(c_?#?PP3{jPocT}%318~a^L`(4ZWUCaAjD{)Qj(cX0#J}&rB+Xj3B z>AJVK_db3D?AN{c_3u~Sw+~K7S-*eEN&Rm6BtZ4zpYjp@_nFi7-%I>2`|iWVjgt1+ z_-9_j|E4$azoSa{-|fXBV!C}s@=GHh4HWw{Jh5VnGq`}6p&ieDK3brXPUGBXOX z;wAya1mF|``8`04lDlYG&OpQvKtel6&sQfQD)s;>rN|EQd%1|0xrgG8oIRO4%J0k2*Vw!Dz@0#SU!|lA6bo7Ho6EEl2_5>nn)dzjNu?4@u;)tnr!MR`M| zcGG$pBT?+B;N&8Z5Xe<=MX6G`X+3m$a|i)F0Cd5JfKG~El`6HyL+57-43CVC1K1rP zF+Si-&MB@d)ml$qQ(#zRR16nKT}F#~C3d(#aY?21H3eHDqoQM?qaq_1oY>*OszCmW z!p+MZ5^k|Xgog(C8}$GO0wj|CB3Ej?{R~Ef!Oz#*(_I5VFoYYtCSuY-=huw#uUuoY}Lcj;k$BkMj3&L(4+ESdo-N=NGUUK(08Y)cAy?46SeP>{`&(HNOkrv!~P# zO$qVV(3~ukx&ZkO`YxV_WkL;M++1^l5FmOQ1fPn=S4egzCJ4R-O>6Dl(om~au zYZ6A)N9LrstW>y zH95NVF&a22!#n0otIJEUMA+hzQj+3q5tf9!QB&u1R3{tN5?9Wp*HuWQGPzK34;%KZzLocWG&{N<_F6ihFP)k%WmQ5`ma=#T|xw-`zch98Vu2mPvX6 zyNf&A2KT=eN&?052T&|+xD|*6JhGF-ELKQ3agQsJI!laVrQ^;;V!$03CXlPdPps;@ z5lei;Zop#@iNss1k=zkW2u&*S61z)!q;xHlc;Nbu6wfD>$|de%t>m^;p`$y6L<^c* zQn`Bw-6|z+0N#?Rd=uzaB~b%Lqa!`$&3Zeb)rQ;w+Ww zBqFhnXgvGwydH4x4Y7CCr80~}ov$$Mxi9> z$yHjN-an8Fz8UDR*J)KiP~qJ$1cl_HRHkzG35bYK%gi2_otYLN5#ZyllA#-k#67GV z$|dJX6oIz1ys{B>4UG+TBg*p9Y=Pcxa&${4OctyvndGcgq19W`hE&%#k8WvcZfP0a zTwgsT&7#*Tq)rlvnBx(pl2br5M(0%2H8qT^EE$qNq@;3WLsMNvPP9>r0Vx+_(9+XO zBq!0)j4}B&jrG+9St+qLt1UJqtDw5Ru_iypsF6bmPOh97%RZrtC7037_0jnw8f%AU zM27_hasihEgTkUShSoNY$dA^mrI>Fxu~Q%V37N{J&W2we0Q;bZK2jIMO&;oLG;#)k zzK6A{Ct~oU?*n@QEOdZ<`>0)cYA|dxiX33?KCqa;kbut1XmoObbq~M-{cSuXft?*- z&pxo!aEqwDbw;C$1M0y)mY-gVqB|rZqtVp?)%MZK4Y!#Xj7G5o=mua9dn!BB$7qx| zpz1zS1tazKGa9K0ULLAGc_>gM@Vn=4G|C)6MIWUKMR!e+Mx)#TllKv-40rT~pj4xg zYTzrC_R+Z+dQ9QDMx)XJlCYEb)JNrR zxEqvWG-@0WQ6Gm?#JJbe1$6^qACVT!`{@IWM)y7tM_m~m#GN1{D-ZN_)^&8={Q!`# zyMfc+14QV}dN|c^4+KhsD+GDN*9UJ9-PBL-K>&XPiiHND-qlawEAqKZUC;y1X+ysi z`bqR+Kh9T(!FUhl8p9o#o?gcT*86kS7@j75Zp_b@`G;Yzi3h~68{s%u_Ura|J_YVjR3<$uYX22VB1Bg1vO#;&!FgP1~ z40>OqQ4I)ng6llQ*ALPb8hY@4sgDZTtHSdzfz#6q>@Wq`@nq~kYHZ3BQb(fkAkynl zDS9xP;zh;)G5~x*(FYLv-2=K5V(~+1uby6?xpcyQw-lYdZ{CDImUi?oCV`2#_UitGq# z!;g4bA(8RvgN76q6%HAc9v>MJV8UF(v0iZx{gy*JPH0DLxM=hbwx#4(w6xFcoYOh8 zy`>^Q#TIPFlqm#9LP&#S*dkE77|!4|Cl0Qi(!J%`mtNiZ>PydV>7G(MI59kcCU4f| zeC~UL;uafA5C~P{Y`A6$u%=hGFMo2!t~YjbyMEsN#;zStE^n_)w*;`o0vw5SH@tK@ z8i07g9E9s+_}&x{nKf$O_E%ot`TX|v%et4Xf9(04ufOu>yir*eGZqaU=jG&i-AhL_ zdMqE1y~yyrF~FKt-~IH?9Z#&7HL-DcNCR)-d>z1=W)f;!_yOu zQTG5L(VZ~*ho_I~esRnR4qSd`_PSB5fpaUwE(JB`xFHn6H(MlIrsMB<9Q}ilbTm-JS#r!sflZPu@ zaetTYo#bH(@q?RCg~a8ytC%1%p*&O}b-BgvaLdWD%f}Y7*HsF90ky+QC5n64jSJmq zlc@$&&etZ9a^TTb0TY8(C-k}Nr6n^755~>`ERzMn8T)*-hcPVDW{V6ndZ=OaaZ;+F zs|d(+fi^{oOG=f8Id0I?b+wyWD&u3tFbk zoH?a=u+_(nQwhNtr@7^(mPlmSq)R=HX_PRAxVyVSXT?rWp;YU_2er&vxNuf;ZkW!^ zP2=IE6M74Mu6ujAOBL>ZfuRu=7wa`kM6j>c(-abF_Ef<{q4-gyH6@g{FIX_8BEjUL z)foaYoFYZGYmwGagUUNJY0%KJ3YW^O6=g%>gTm7b3o}A>Y7utLYF$`P%iMY6@-4nz zhVaCkqOyugQPtH-;Q81JS~{1kUbo(9!{znsR?Vx;9x{Hxf(e5{(IioNh2>9Lvwlul zv?(y5uxUp3s&(r{8?LTfIcJ2ewDa+||MB&KgHDHjJ$T@Yx0g1xJ@)RqPmE17X|VhK zQK``<)NgwCqZd1JV=_k0+rI0gFZLf49=djL|7W|q2S4=km*1be;Ck`$`S1U^Y2xgi zhYtO9dQN~l2!F;yrj7ga{-f`74a;j;v+Ju9XD>KiymH~(@sHLwKJxjwA5I_p_K5T0 z%Ll*QyL{~2H;$ipYu2D3BK$$6HK(_|c=XhVi))+K>^pk?!s+8jj~o#mzIyQUH^*hv)9tgyHVYdC4TtMTjVR47ob~4SCmrN!B7gbVnZK`o=&>)) zpZ)xW(Jv){b5cHw;C-2HQ4ov6u69^BWk2l}?k&6AOy{bci| zkI$ZYe`Rf2xIg9;&Uh8m(BS%qKRtEo>kr@AE8O$Z+i$%3%$kn!VV!)-!9=chkU#wB zmlwX@IU_&9K>7jJE>mDsVg8I4K0kKixbv}(zWesT$Is1fock6%bAGUgS_NBJU(?$j z-+%GgUnXRR7_gw41A{{&Y$;`tC9@uT^W)FI5`HcC=A#4Oo;>;4lQR~+&69honE@^^ zbMni_&VB;&3&);o8yFc98g79<6p;ZL6~4A1Ewh%aS}R<)fBmNIJ3c#oe9!V_drqSD z!J#^>M(ghB6P(4!AAaQX^WXoqvotL}EsV|&wRLkOi@-^N>XA%Tx?8CVv$#%Eg>~M zGgFxLaduw$)Mt;J{qo^8drzKvXF)}JLR@@8VsdK6&}lnBzIpj;M=u=Sv8bgqFFRvE zYI1T?VoFh1*0Aak&b1$pY?!$8ugA}Qwq^C+lV?6yH@UHHR9!8%ZQBS+TOj;JoL7&LL|hRx1fe%`w6sa;>4JN^Eem3vQL_~x%qKC*3_ z@R1+3u9>~)wc}_0zH0oOmk*pfbKu>VpV+o_E4St5rnL(vjM@Id*9Tn=Up{j5`|}qL zzSOmB&zVauYGsw!zHz&7VkQE_WbGN-(pd6asAOGp+AAfMV_|y4QU;Smx%Liy2=8Rgh zeejU;;cJKXfBv@@SB|Ugc>3f0E{A?Nc;L%-w{~sY_x|%!a-&nLAKLob2W-?|#i-xa zJ)~{v`psLmZWTU!b?cT*Yr3b78k{|B^3rv#8-85BdSPqT*sku5qDWIf?BM#T3s=%a zb93Fw*&|X4Yw8=Fnyxf9)Q_qu%S*O~MGq{gaIN~Wvb<)b}t6xZ=JC z_j1_PnO{Z+gQ!5&gOR3$9TbLsv5*#5tgUiyfH+kz<~$J$(D$X-IrlJ#pMp~gU>294 z5a@iN83j_sb(jrZ+43ca6(06+Sn%YE8&t|xUfA2o63xlAR3YqYtR*dRYB#)V6*qy+ zFYj)k7Ra2Kp;;)wwr;uyaThi zyBCx{T1%OMMkWJ-0Bs1RBC>SLekLQ0F_`9>XqzS2sM9D>+ACKpVAhbGS7^NbBa?E6 zqAyjH4$V!9^!L^%Pzl@#P)A@sJ|k0lm~81o`x%XgrrS&&um;KHSWNIp*HvD@$z&*= zzo3hof4ytL{Lc3Jp~=BI6({$F>`uc_qzlWawi|})M%Iny4a3zLVT5B;oWz=)RogKK zHsJCh*=Z?h*~|t!r=u1&V3izp`70`~kc`@CbEdbHX2wTZxrocwsQAp%mg&5?S1H#k zzQ?kbT-`Be>ZrWPuH@f{_R;oByWLr3y}q}?S-T&bDN1u$xo?xchj8ZgArSL(B?^B290S~0c^}?q6i&Uu#OCM5PJR}`< zZy9wRAyl;+3h*jZYTP|MTs#FHH?*W&X|-xO-tkXTg-1|oQB_q@YLJJ5OqA}Pp3Yv^ zJ>Atzu?i!_>S~{26=L#GV7&dzTe7OE2FDsS(ER)YLL-D$L8QQX9W#Vd*QZ!raVS>l zp`MHohF{x|`o>}LMy=M5pMad8f0Yi7>!(-!+F!2<@gTeRk1(|bW{v4yHhrj79}r*I zIDHYD@1R)CuIZ;(9e$u#Jw7(gs3ylI+KeByao-2ePsxc&A368Yeu~wu2a46v-zZiZ z5R#cUb=eEcz3SKcx_){Us5$8ma!`|6_CT-d zXsH`1ti3W~czMOY=~cT)!6swtw zm|_K07XEmJlGjDge)8~R-$1dNkQr(+n*9TVLqfwW3B^7R#p=+%C|3C)(98O%bz8T6 ze*VO34;95*T*9wg;)l99l&W8Tt5nS?uoyU>Bfff5_&}zTZP`Fd)tbgZNinuaD_ojk z;n71p1Abqr+P9!QEiNWDHZDFfwV(q!*(V#9yn6Kf;a3(k*p;&QxVWT(uuLdbPPKxO zzy4OMn$*CJ6pXr9H>$RFCN#2tteLs>6KGX0u7CnQvX&chv!=SN+^$t!;zrl z{D0`{zinT=6Kd7snvsiN|2MVjo73k>tvYw=;F~KZ&fnEft@`uomku1oC>%HvYSnj4 zt-3~P)wR}Vp9diVKdEmf1B`Ur~EMW$HIXNpyhEv4$Atvf&XeE-3N z+@b4Cv&x&eWZlM1+-AWR!RBk5Hmq9EK73$m(WE77dDUu6Woy@>jzX*69Ft!M)oSf} z;f5RQX|9PKR548|q+vxu!fTSATS&V~i*+?3d1`}$)W>uf!P zIW#mZJS;3EI1rXFvVQA4wQ2?2NgkfAUINdX?rv}#VTQ)~dWU9g4^Jl)U)HLOygA<; zE^ak+Dq8V5QY@s3yEIT(33H!|DO!bBtyn~g47v#?QNx2n3%(LAW;kMm@a_0v(Gp<| zS3$+$ux!yiDL5*?M7Y<(b%})y*8;H#iU{05P^1Lngnqvq?tMY;g)%Zq_`omYX}{J< zTyRbIfYNZ zs7&B_hvBh8vN{_L5>^{;9;DhhlsXw)U;*vb%K*Cx0@MtX9&FU8Mr62eG>QE0!3s)F zhF(uXV(h~5>q~}i!(F3)2(*m55inb-485N6-(ZM{Jd{p{XEUju>$Fylv#+Ijqnqe_@GWNn?DDaZ$1YY-m3wtPd{@Gv(iY=MB<=N+- ze`eFPqUbE-oGz6#!`p3_lqCEdyItz4+4FF*(-Yu$Tcr8(vc* z9C5X#YG_(O+|V(zm#$gA!D-{K8`iJxs>>~!xNuS15SzaNqWZyT3Q4M(|J1W9>(ecf zX(gi{TD*Gw#*N&js~b10o;ND4e9jYZfAaMK=R=pFQ0-aXGWqd$-+OX=R+vc$v$HuY zy=B{npX``XkepNB_1Nx@zdUeIbm;nl&vq{WT$ezc2)A7Z>ZG~*PM!PgvDxEWnwy19hZ^fgmgg3C zf_g6&XtH{M+5)S`v9teJJNt>R&Y%0@#g)^?HZ=(AuhxyM9zJ*Psk49I++J71RST+q ztt>0bO-n4WlUq$p!~sN33-pRNPFy_v^8C7jtTgA;Yw0CZcYb&7o0rygcg^SK9h^IR z`h?mc0}2@}3@&D}JHwRD({9}ib?SrF_1Ur32rgU@am5muSlqbnv(u-(`RJX!d$~P_ z-+p7~vumeTmd_^E?es7!SqCwL&9?1dUHBeaNnAJ=ayc|S!V(#iT2}DTi@c%ggYUjQ z`1j}LG|zvFj6H>su!X@-6d1|VKKkT=i^rayG$_VmvD%_z6OvNX^J?SDIv;-vhN`c> z7Jl<98LB>i@}cgx8M)1$GaiA{qL2Iu*tuS4FGxv9Oo7j6U`}pcQC(nM5&Grob;9+( zY}mB@RT!%Fu2?~)rg=qCft>kg|GU71Zj3 zVloTMtA}$nf)O9pjq85(*qMKBg1zVz)PwklP^_C4TVf$>P3Jy+Wb21#&VIakQgv~` z;QYK?#1CX-SD3=#|H>LD%K12VNY%7Ie|zrBZEML=wP<+GfV2S_nQ#YG&f0n6+^5^t z>^gSg$ZN|d)|M6x$p>{-W_GD9r@UsQ)2NT?>YLh@!BqA6*0uXio%wJ>$LOY}mNBhk z$F+{_`s;~v|J>a5*r#XDeYIoltcjzWV1}u$tF0bBWYV&YTb#Gl{Q|Ladbwtf3!k3as{qZ<}3d;P@OPu5QEdgb8xvj_jS{1ZBRU`c>xVE{i4I+( z(O)uU#iob1J@Tk%`_)Gu-ne|$=(3{W)0VH_=)CFZ#tmz`$B&q>aLM%2ga~WOP*}!k z!r8QGBPN`=qXtzrwvHPwoN#&kxUpke8fr>%lH*ecR}2@|{9HY3V9LN@WrGvKC|V*d zACsI@&F$fpMX7lO#oVx;ONxsM@&^rsJ2ENO8X6oL85{4K@I!oDOhl+9Ce{`TccOm~ zn@|%HMDb@)l9MwqD|0|hi z_wy0i;~6xDn*ft8;Rim%a29huP7WB6vK3kZPo&7lnEDilD>@eJ{hs90ZF7wpg%18c~93 zppt+WCm*rz1wUh8L`+&venDYDe$If{h(II1sH+Te9~a+qh5$?Apz`L)GdgE?&Y0X> zJ}3!}O#~O{^-2ThCxhevg0CSsE^kEpqAkxb$LTX$7PXJaiw`k!e!W0KS3sNB{ET6# z6%IG)b-SCiA}!oRXe@#QF#$rqQ>F+wf*yVO^;e(UzJ6)<()HVU$LPq+2os*p2e0+C zDLk`o5xGH^KQy7Cx}v&a!mJffkQ=lv6V+i6J7EePFme$&Jg1K;&KsDSkvTB0q;5Lz z@JtIaA_&27+!&O=+?z{UOS00l^9zR-9-OiVduc5K#d;`07rPf&eU*1X0HA=6gLUHLV6_OiCR}$^1%MNxcM&yBxK!i} zQ>TtFs8u(QUUw0!zCMfuaJ7fPT?iwdcOQnzLv;~r8BINI*OhqOEt=+FjH{gKmcyVaEu`)pP8YFNCsi3S&O{go9I3LI|G`36~|qGRLW$4N;Wke->9 zm7bgs7lU}T^0w~f^Xlv8t-}8J*7{ZR>nhn6`lofPx*uvON(hV0C>#dYY1QzW5hH}P zCx@40CD=j(&HB!#KKSR`t5&`B`2p39`lD>I=KJKkKTBE?ueYcQIlspG^?{y zIES14)2wNuhr|Z!5zf7A*U?j7Ji6_l-=7y=ya5$++p0InPT=%|;3C${6JPCITwghM zR@b6M-HVqkU$$g%clW|Mh_s+TF*_f3u|_D=|JkDZ6G?_uPj3l=#@F zFpvusA2P*oepNZdSUYmms1cPT7wkEC>fNRFMS~E)kTtM){3BnVJM!e{v=EaqD86jQ zqS>`+p(eerPNSlz>gzN!o#hpBi4L+_S-*? zP4oBi)JF}Qv9Ply$?QepniSN1W6s-XY}KMa@7}}h`+DzNFRkuaOpe95#Ys_-R%>KT zR>P(*&K`MsOp4K6>uW0^@h6LQoh8aXl$DuL#OO9Wy!W%O_a8WX@~6ue{;_RrW*~gX z59qYSD(f0u)s;21!jT7HQ>v!z%ibkZ7p-2mVe^x39sc3M7f-e4h3lw6Tgh}hBS}Yz zY3N25>yz!>>o*Ixe6xA|;&!-;zuz+thF)~S@hf&6Ie+4B8%JjZdNA?WNtt_0r1)?d zKN$9Waq!UL!-p^$HmrL4`0=-97vYq7R9trb%D28f`|X}pqtKehC$f0jf?2~8O&(-c z!VLF;eHeZMUG(C`3+KQ8^pVxO4j=mKjJyb=udg9Ay=m>fZ%=&n%ABGIoz}-%)IN85 zWrEq0&FgUDeekctf6waqPrdi<P z1UaX;5taF1y1QCgUYH!0J#=VRgs)aDR321&2E^wN8#W|0Sf`;gFA+9tfFH%XvIPr$ z8uMyIsKG$f>{V-+*%)HA2IH&*)qD`a+xp-LOQ^|9L+hM33?aVIilDpkjgzMh6S~Gz z=i-fsCg^fLnA4Sbrk|CF4F?RXB3nPP7U+az(<4f(hFEU!rG)QO;C2lw3&NUop76Ay zAKXW9H$10UWoZp2``|5}2!|uEDmg2!Vy&b_8|yI$TogjAPi&R;^b`YI;BgPnCZn@F zz=L^*$CKdL5L*sCJf#c^%cKJd>l3StN5Z3eddh)x-@~1u;7AabGady8#FV$Ogpz)s z08Xz54!tO^g4_N$j*y0T@HD zdEQ|MBM<>c3MA>W+FS7dFa?kExWfji!VRGYcL8;C*9c^n;1MN61S;SZ51pV;!u;Js zL~4=!^t_gsgERpMS4Rgs=c| z5YsscBGS0=UV9F9Y(i!=tKE9l&T!`Y@MzGRRfw7h1Om^AlEqL2*(*`26)es#h@G!M z1!zu68Ym`D96iM`75If&y%Nv~R1|>F3!Aw>qGYiY0vuLjV3C^$;TiWeh}mQA19m13 zeA1~k99*Oaz>!2A7x8-n=lWb#1nyI%rEIdT74~cNH-i`>-;}%|$GZCQydJnkpl?)$Uh16st469INx!~(T zQ5i+Uh9o8qDXS2|uj+_myRMQKrPQkbt6CrsebgO(f;77nc!QH&8`p>PYB z%O-X&pG)fNI;gLR<*Tb`TfEj~!_V+Pch77tj14e{$B@#SIv}0P;4)99C)>gTjW|}= z`Q&f(*Uw3RCAZTr2ls#R(HoD=sZNW_EGQXPT2@xhRdD4$m6Z&N4f2B$s_d`8UWWqv z@G3rj$QhA+XHR_j>cZhUgUhOhLt*Ad_Kx6ceyT3dk1=_``y=aHvPplv#`IU-xdvOP z;M;>tgMEH#$>5=Aw!Er#)X3_JveIEi*%o$iPTKVrufcBE!Zg^W?Tg+%bph6=?ZQXD z+5Y4U`wm?={o&H847jOM((@`uR1e8aO-_op8Z-#<)??Hd;!CPQ>`qXp0gn(Mx8( zm1UKp>O+;o@{8eJ|98bT5>X+tz7`qXEt2k@ImOvdSqC#S(u;6=>fH@np|^hH&|5S7 z;pOU2hI5-Y`5pnq)v5MKO=T5sPwrhjbrHf{AE>Pe`gqWsyFy4Wg!RMa?cM93wH{_# z>szF?R?{}IcKq@_tu;W)96QU55_6HhoZ-9xj~V0gZO8wJ^+6>86h@tSC+n z^rU?Z^M$_e81(!3P3xZ3^PYV7-N(lc@JD#3+A}D##jdqR`>54E)|`U8ShJ@aW?iD^ zB-cwFv(0*L!w2i2w^p_-TreRo%nPU95F!;gfY)2o1GGw|yD>60D$pA$9#$3NUm~&f zo8}O*OxyILqRcRFwOlAYrEoXL<&_K@l0pxxbo0dd18;XMS|CUBob;x*IW#<&pIWcN zPM{Brh=6C4adr{zN#CIG@DKw+dua`Vs)n@?bN?l395_&1J=|3)Oz2Kt*F3fEUOqma zZirigRB@_JrSMviAE#Z_8{pak*@fRp43f#}V!1=dXr znmddJQD*`KJ_+c&wi>ndxY3H?LdCKkwrEI!ORJee#%iI{h0u`+ON>K4Pr~2Z5zm_^N3G!b^fz3qVcq zU-_nCBXhtV%-P|Lb-bZI`o8MjH6*Xgvfsj;A8Bbqfi=({!NH-2#x4w!ZhwqE9zE>{^pg7_P{^lUgoM$@gwVl+2#0pqfH47y=l^vCF9QwIcg4Xst=V1g7RJC}9@#)Qb@7rUr1 zsQb9oX>LRRLAXMQD6|K`wEiXyd<0~wa}OqO0U7y3AwA4hp}`q3=rch<+Tcs%DkQAn z;2^{~zo#vNsZ>2^KPsJNy>{9F4<4HKNc2JK>C#7u`~w zZo$1lLL4Mhz}bL9UdA5cFGv+E2ojJxA1-n_fGF*wRt4V=3Z&yi{BboeM`McMdqIH# zbeOFlgxU*|GYB2)#CbX#&ALT0Ab>RZZV--w32|tLA`QGAKqS5+XF+qjN%ZX45Ev%LXwC#+!W2fO4U|VO;nXxB((dI#Qm zT#9jdFTf1C<6s7I4~AEfVIvgPSnK(3eJu7KYX+?2*dU4foVa=`KRmJJagUW`x)*KjKRXQXi zHax_dl9NBAplE2xu(EQpn+`88g!3^gBeiPsl9gReO7ZFto^ic z!|ElSt)=OS14m5gm_GBN&e?P33g`VacUD{V;LK!ruI4=b!KZJpS@ZT62ZV=iz?;9i z>zR-DlP~YUfiFLP^NFs8()tCD{`vVAUVQ20S6+Gf<(FQ1;i=VQ1}DOo7Pw;f(bHc( z_Nd+5dh*jp*1+8Q%TLZf?7w*7?Dt>)b;ZQVYhQT%jW^%g^Uk~P?tS~MH+Q|fxiu#W zs-J1ao>LdTdUV6yFUga5{m@tYHmrfU^~aOPoQ@v&?)d2o=MTKHVCu5R|MJ3%FaLGl z`|s}B@zRUWJ+ZuQKse6185X{M^6W>erY_s`u<(&<+cvM5zGUy|iwE9#?rGsuC!hZF zj`t2w^3FD_dG;8+4O*{AQdUVO$&Y4r1a&3Wdyf2&!o$uX6qw1PR!zBx!Y)eZw z`RcdLo7z5k@}$XAW^Z`&_=Wx3#*`F6i>sga!dp9*jj1Ru%+CNhVsG_iliRnTc6jY5 zqzc7Z@%ok}`%azy+p4kErNu)F3koYHZHK+}>2bLUF|jGd)Bp0u%L}U0%?xQ_yxt1hjPW`zA2K--xOjQSHH+ilTCo{d-%U`p1zIefHKjmQ*DK z8GlRZVz`Buj6FbkzEj?zLAg1@7~Q6A`^esUgxOmknUHJ6F=+dP&P9ue^tnNOA-0tAB8jHqv0xf|Bj_cxEtwFa2{5qa_K?ic3{iw%U6*V%9b;SCqQdIUoO z8@K%jzudQR&7Kn{-kw{Uo`jT`(o;3~WVg3QuLG91%dI6-tX3XVIh8xS- z2z-Q%z;Di<{B--8-G>jqKC3V;JR~?YDrfZicfS4ps~yw>0xUUgkMDSPRxyGSX(7Oi z!N~t--^l;^z!!Vhc0K*xdrwZxjSTWPn}ckF#%_4)laK$pvKApn;pt7Qw{L7491|J{ zY9pr18~taTuQse%&|K5r-95P|E`&~^ngVSD8|JK7F|Q>jGQdA5Dz9ncnBpW$2qt<0 zbB3FVx%QdPspi9~lFZcnva-CmP#WEI%Gi=xSW!`&X$!(!85Wy0XdvdB5bVhrwZRY! z*F8VMif}>+@h8y<3DKcA-YPO2Hu{I!Vv&Rp(=4KNkrFS;5(X!}gV^Mcl>^l}2ML02 z1cih}L?A;D3G){ttrBQ~t)?&rgoTPit}!~ui&j#h(b;s3kNw5$heBFGXo(bZX~-Q~kpuj3{Q38gsJ38Ph=47C42Mo< zhxV_CR@>lUIdHJ9$`IWKw)>;dT8_0cSP6{YAUgthvDbmT0$_%HLBZ}kl<^$i2X(R69sGX)Gr959_LlpoogJKcgz(P~i9#8; zZEu^w8EXzW1lAvJ=gpHRXS_WCJ2{x^gOj86U*L>8&k#EM4Xp*u3mwgi9PD`k&fYJY z{}(u8&kkqp_lMir(}f@*h4Bunz8}5N!5<;;^@oIs9PKkW<4+et?tZe~^R>pK46H{8N=g3eKkr0 z3NK@O2ScMFG$A`K07d{K4uxID_7g)cBs#Mo4e6**Mu)oYVk>9ithZuP2gdW5RC)yu zX|sj}1zY2(N=A+ii6Y3X_x3Rc>0uM~#`gD`Lg@{^7|zLoLoGJMLL?o8?V4=KuuJ0$ zWypC3?-GZ+T?)BbAw$M683G!R(+s~x#DMoAsap{Hb;7A}P9DYVvk!hKC0{R{FlWD! zfYN;=?uEbkMUFB$1QJQ9=;*=<%pUvIeS0LiRq=aQEF~drkSHai9Gh_3*MRvj#dTE0 zb~$*UFvUIGaT1uo$rTrBzr~InJ{mqv8=Z`!WKpajh#?4J#+K9W6)1uBlIv2r!z1T! zFT>fXK1Z8MX|@+$mCEhzHzKE0Y(Ie=I!Y&JemQ%+2E`4=Cj)Tk^#bw+5Q)A&LR%4FO|#s@yD_kLI`#c`R)2xccUbC4?yIuW4=UUUS)>~#rGgQ zWgmz=7~htNLCAq;cR<>cH=;rwqz1^rpv+z$4-#nwcB>9v9o&|xUbP4XIL(Ox5G22c zktc*X=QbJBU{EsSz#GPil1_>ehFlgx3AQk7=oZf)4VGL*7gRXzL$DXW!RL%4a45eu zCuBN0N4M80PZnxdz^+Bu*Vw&6sqw^-yI>UDr*U{ojcfzHu!mAM(5tYWz|Z3w8WZBJ zl2OSOnF59i@4&dMSTk(FL1Y?Iz+Yju4j7Ue3hOIhLfcA9Qg*CA!+|YK5$JEiaiG{> z)Dui*ErlQE1stsNVU6Qi6cmmD!yfKKa83{hLE%u6!o0?qy(@#6o|ACwNx*0{WoF>N z5 zPPR`6KOS_(m#?3vAs!@-KuSaDsXM8GwZ!cH(FIPM$ zcjL=>J}KLv9bX75%^lwA~LR`s?Bm(G$iP^Ad@4{ij zg*-F}CB+#I^qF41z{R)gaL)JW20MJ<%9 z%K}_VuZBn&3(Lc1za(@7{q~|1mp-_>hYW0C#51rt-Cb|I)KH!^-1d0Wb~S{xf@tC% zZ`y#eTS_l>#~WS71Xb}eF^DK-oj3Xxx>g|S`?ar#wu96;{S7}8(};Wl9>1AIvDxq= zv1BiG6eIlm20PlvB0ss1n|vonH3k4!)l`iTTemyZpQ&pg?HK_$ulqW0(F1@V%W;Nw z8NkU7K-0eLF)5M=1kNa@kwKh#696?afQubKLq-PxHNgO`0NkRsiG5S z{TZF8F52iiIF{R6VGy@|49n~f$pCZZ^Li;Ol34LI@)AW2ZJRrzE+;xXGC8NPtfH(i zCpj`KYT&5pbJ~g`b#kQlMotLmkI*ed3LjQgA2>*6je5>Yjn0X zws6$wR@bptT1Ss6jJ0Hs?s}-y=7S^+(tfBh6|?8pr9~u`HjH7YV;V{m!&B?#&#s8} zRVx^xtKzEKH>PsV{E^Aw$>oh>#A7eFj%h4U3Qw+`Kc_Or4-QC%=ZpmF3YAYZ)tDM? zuMwz@8e@F>P$^+L_GU_*t&}*m#1xEbLGzbeTSgVc5T~=sq8!NBa>;)o9m_MK2!a@5 zvu0N_LHr6qRA*al1(Oy`9cuMY$cYL~EJt|>Jq}Ubp>+4i za_TB@`PSJ*toF}noHMsI-x_GkEUsofwwkPQ*1@fF=QL#odGe{M?~u3C)m7-iIUjSC z>1-vF=gl0M5?~BX$SN3CKCB=sA=DU{I&$Xx$tBT#IAV(pF^=aV{jrntF*IUIshK)^ zd|oK>XNK6~65?zjMxA$9{)E|6Yf=Lc3roqodyoPj&*6N+MXC+TsF~1=u825FZ$G^s zcI_&U(A<^@BQlYf3p(sBQOqh&bM7i8!EXvE zB@HX^0$C3u^XV(-BXJZ6T<$QH_O~=*)apR_7aC7TyS}IuoO_@dR6^4aN@OT;=Sw6g z@xxAxwWB5!VPk}T%DE^UOdAz9$R|y`*TqHlpi0_Lvp=way*3EM zeKk`W7J>XKL`S`zCnL$R2P9*LKzwWa z>nb@0dqiMbMPu#YxFA@~lQMJjax;@`L8hR%!L^MQX#rXp+7gNJ=Kv_tudC!+nF?7W z>gtCk2ARVWa|%jI%SuZMauUN#K?y_a>xS68mGtN$u?tV(0x!E<8<1ArglsnEu$26g z@=6XzO3O>~Q^HKhdec;$=C6?najc!S;DTMHQWud|-%t?kAC!m_aa?upaIX3i;(ii> z%+Uo6^?4Cq3WxxI1A$m_rZ54uKEA9O3DNy+IVF|V&ciQPSC$O4nZvS1HkZcfF}3vJ zUPd^Gf7*!Al_^2NNd@IquEVcXl@}xh1tnLGu1Pg(Wc@L3e@9)Y(&%~L9NCv0dhOH`0)%gAiVbRd)IuS}-?Vc8>F%Hs9NKEopm zk>KDvxmQGfLt|l#e^3J6SXFiP@apO+_Qw2U3L6{p`4jv-G@P(M=3Fmf6$uzn(^8Wa zZVFB2`@|*c6Um{b@GO)MAin8cfC71=U5`nWK2e1Yjl+@xV744sP>gO@Tre;p)DVy~ ztg)de+E*o0O3(m0hKuVlF+`GH)jTTS>gVGhnV5lIosk&n@8f69AJtryj@&3nvyIAu z?Rr9@@V4aE)R*C4hr5qC)Dme4HT$@`n-a_GYjUkVDw#?GdlN$yOV#?Q+_IuL1YyB= zLV@ZC`b5y#(DFgidN<1NeU~N@iR%@y)Xgs5Wo-ADQD) zhf)JdAtf(d;xE0BY&~&l;x9eWgo)mdR4jH!u~|aPFJ!90jX!YksK_FPn*hl@%*8PG zh!waCKlTXypfYgG3SQcgUX2~t+ zd?pyx8{qMx+z;t3TXsD^t0Z&ixY>)AI4`}pWYO%gg=s?{dT!U|rqm!`7F^6okYSmT zAPGSGGjD94ke^*Mt!ptzep#}(YwGaK-0|DrcxKYTFar*&k{=u!ci1g1<9P5MO-iVauHk%hB&zP_kDy{K*8VurbRUR&XS@vz92V=z(z@YockBk=nL`ObYqvL};3ax=a>j0aZRe`GRHO+ZsxMg%d-7YsXQOrZ3Crtv z{PpJ-RVG9x53Xz+H*s8J<=|x7|3}`tfXQ`LcfMVx-lwWgbyrnazq|US?pC*2ZP}I| zQcJQVTeh(+LJZivv5m2jdDsjd!dM^$CJZKgUVXY`}?nbs=8#gWRWj(bBC)>)#seO)_$II_StLgwf^g>+u!?Z zpMCtnn|F+%Qvp<3xcAhOD1W=sHFWuHKlvYi>lg0bw{?8`Rr_yv!43Pbnw^-s<_#bF ztzUZ2%dW8xE5~NfvQ)DVOV#DcY0v-l4}lJ2^?yvwnl;Z@*;+wgJYw8)ryS!W1VM&#x+nIo0M*J(l~OFnWe{3fz_ZApXQW9Gp@+m}rV=IgL@88= zpNFOgm(nesC}XejSV~mOM#>V|%+vjR*}5nQCN*DW*9U1pm--hK)g#42SG7{X=cA=Y zOVqu#+^!0o_Xk&cG~yMMqEV^x0{=mA`3S#vQ;!eLU|29M>g34`D}C|oXi;alGI6R){$$JF*+*MgaK>n+y;SK3gx z^tSi6s#kVQU3Q>Vy|TlpS4J!B?Y3xBrY<`~@D8*oKQL{|Wm8w&Ds9SaFCW65;4d2b zll|Lf58Q^B-J(C)w%_z8L%>dh`MJ5$fKttygU)2MWcZQ!!oq#l{Mx>Y^0ZI7u(`ik1U?PSxsG;sOt zXh(L>NIT*m`2ot2UE6lOTFQ|=hyGaEq_FRsW~5mdYawL7a-(tYxu@AU>2Gene7k8y z5Lw~^5-UfY{nsdlF1;jD4E+yN49)H^eaK9AAng-ndTcNUZWOP7X%9nLI+q8VZ=)3KRGqr)sCF&A*q&NA>dFq3-kiZLY(1BT|lKNL>xk6*CiujwB@pPjMMEh|B#w;*_w71ci z>HSTy-Ze&IeIAjhv)L;|`beK~wn*=|G(a_^`PUqrevULxXgQPSbBLauHsCW7jeGBT< z7U_Lxi=_8kklu5~90T+4+epCUCcMuM_K!7%_p$!L*+_UV5c`aQe)?D5_qrSR z9(ds^UjL^1-}L%dNNRnZNv$^s%&o8Xt>MXCCZWFJ#=X}gp&oS->dUU*d*d5E^jp92 z{#RZ-&XTD|0x4$I2&8X+(cbG{vY4oC6Yl89JoD_ zLBEd-dhCJQ4;*;S2R`+iKlPH!M+r-Wi^8+RTO+aann>*Y0b=KUFWmQI4Z{9w{CNA}s#n>#+36|fNV0rU3R^UxDFp4vUzeujp$p2)GG`aGWLAn^ z$^&ok2LRwq?Yrj}KKH9fe|-N7k2u-#?7lXN8y`fH zeCWmd4!!dJri6L_D-Z3v^&!&Vb^{?H65u0o50GRB$QPo2_}0+K*16Yw@Kc}p$bC2L zzV@~w51Nem;E~&|-F5wak}+Sii)JQ!L%^)oSSc5Ox%p+U zd*fT)__~+fyyx=WFEjD-rrBYZ_lS62FB#ZZ56L7w%+)V@&oBMf$BrDF-FE5jgD<%G z1=sGrY}@t&M@+)JYMkxeC>Ifsde{5d-v1W3nb*AbJszO*oCW$n6$;{-IvFU4G z|Dlh+=QaDLh9!}+7a#VddfN%TGdi>Hwj-~<5%HGw{0K1!v7H{fg% zv_gfXdch6DJXpADVn_}UqHG9eSvc_Ypb{clml=^LKoyWcAFHhDwuMiJyR9H@cWu$lrW73mId~CO@0(bNDM)blhQ;<}Y zoZwo*otmXIWlxf3mKOpzQ^m@y>P^Vww#nnlrK1>vsxVcGoz|Bz=5(ztptNC$ge0ac z&{(FCs6BU5U4CDwgM%X)j1-vxa-}ALt_>OJ8r7_-i?!U4B5sYy>_@gev08pe@}9%M zFvN*goCcY>8aPN#cWMK1BGI?vkR0NmNjR{GUg!;=hh!_+W#5$SHFNiM3qd17yh!#v z<4TuZqeI;gy@_{`?0dFB*9sfKw;yt&@>DC;AUpcI8^{?*=o{oHFPRe?Fc+!5Q&HJb zzXlxDVj7XTNp;;4wNY`p?{t*ajmV(UI!XfZNcXK8&FfgTCJlBAi6iQ7l(8(dF*<|c zPTVy}s2b_MGs-DDX^Hr?Zp?(6Rc1Db$~YhkPFBK1!ZuWo3{fL2uVn@AqifzaKpn71 zrmqgQ(<|4Nw;gJ#3SFMFhTUSh3DN8_qZKFkR=hJ_QFvc@N-~&P714$AY#oF+5PBVp ztVJ;g1;|2d^gi1MzMxcFSaGMU6tN^qadz?M5k@06`Wow9?l#8Gl-1?I<#i<3H(Zd= zO7tnlB->jDy+$fKTGQZ23wj7K6)#W)StZP2_ZlThH5ONE*j`L&s~rzy;ZawsS+kul zM-`;layA-6SFU6`QK(^6AH%9XiD9sZGb-DW*ru1#Br{|(W%V#Wp2zyb#uQ;A3;Iu3 z_9`o~b1yB%#UcS%h!vv6Xz$?n}2jS(zHMg zdN-0#5`jx^?Du)RSPMFptV&SDH6!v6AgwC=bgbV%B_lIunnm^{sMT(k1E`~!MbI+g z`)$o8=n8b@uA+}iicIxr-yFFgastWIku^hF39T%>)SxFgVi68?Y~ZvhyBdAyfhA`0 ztJPv(@7Tb+Q^ZA%16{_mgavQ4hpA10I8STBYYvsiM!q7?h{eG3q@p$!snw!VEaVF; zJc!a|N`L!YmaYbQkgu&b_^c{v>2aspsbNNYCtF5qXOm87oS-C39lY-QS%iu1`s!K$ zxJvu7jkVT%QRUVJJwl}pFdH?_1VvO%0$SRe!dN>`Zw9e~&+^mUJZbC`pS_TIK#4$ThG5tCrU;F+gl)|MnBmb zPTi^Mv#xD2gR;yK)v7+5s)qHKBkyokovN-{=6Hl1FaWVxZ>oCAh{!LRlS(B~sTMQq za%Z}Fx=B=%1|ynE-JrP!4VJZkh^x0Fx+*bkQ9d#{q78Jqdd8Kim%UV~L2L9gS4B8SE*Rr z6P}R0(o&Ln(9x~dtY#Cyv&ik0;^a<2_m~A^GUc{8H7bKiEbM5|1-Ch$H@h{>gBvWM zlvx!X2yzKiK*eMq&TLt#_?>mVPH~{jREJ~8If$0~ZlT z4Dt!I4z_dRShqea=NAUH(e|MhdeYI=rY3#R=u9=_Pj5o+Z_<1CNXDq27@MsH^y=-h zAVNL)&FBiI9?^r%=nCd8I{Py1t@K6*J-_6>uY9FT#&D*!0cGyYa`X|e(Ec;F`(fV| zkaqZ3E_2(|xi0cTYcD!VQPBS zvUTGNgQaJCQ{#wP~J3C)U4PoE~EVQ*WhXVX;Z7VPco%2u0{484s1u0}aM zea2-J2*w0N2^kG<%1nbO90@fwHZc#Vu3+uQ=u2k0uDsr;m5UqZ#5^BusMe&+^t`BJ zE^qr}b|N`GuYD`%j3K#O=~<)43FN*YwTHECDjQWWR#){NEQ#^NmN*eTypaVKQevYW z+37{AiAIj6s3k!u@zMB5a#&2@RJ+E;rj~&x$6IJidfcNF$C4U~^)9g!Yr~P9s&B>Z z+8ax4iBU^pB!OcEA}kz47?RU+c55B8^RX-~@P;$m2ku+8c8rf_Cst+PZMhQib$~)F z%aJjF&tqeFK>!ev!j9bQTXMrRGLjihjf8j+47o%b+o9}mY-ACyCQQZ6f?W|b@CzW~ zM_srZ-Z;J&Mq+2vNOC@q@5Qr%TgQ9b48X~X9L$nXRJmT)H4NU=qB@8P{4j>=Dpkup zS)F{}ZYtm$L`*Uk8LA6D-K_{E3){nn54bmTCSo#ccI~}NSWXZbnOi?8e2?ujio66c zoQ&(;}ueY-~tG~>bv+i^*-S4|!eCJ|TC zm=?EzJ#S1NX7`RMW=sSGo!Op-=@bxUbj+BnXO3$To!8ieaLhBaUJFxXr%xPj$>9wi z|6Cr$IAnwA5@(SBoZN78j5n$>Kg?bu zX#gT$Yg(T#Nm;fs)n3sh{3Td9s*x^5?zKq_KQ~ecrb$Y^ zC0B=~bVNB6CNq&QMOTV-n#h-`(s>+WT(gm$+epU}hxEvEF~f2` z;>VQ7JSJ_WEEyn5$1-TclahzMk&d0wMmj=iBfZz9>jlWMnbb`0BRzWdNe;6q*UUas z)|hCZ{qK$BxWlb8i-bmMPdORYX|H4wBh57o(VYZlKFqwuS&Y+piE}1c30q;s%?H}I zlk;w}oM}p2=8!M5;FOUvO*x7;j}3NkW|5PW$%BFr6$wyrEO;?@#S1zP@`4kb(hAq+ ze3_LXVM$1{YgjnMc_=bDyj(?=sD$6GPLn3xaI(W)|BD<7u67+16F0b2vsv*H<_-Zc z6lR!&gQ}{Zf`RZ|n^-;?yM~Mu#NRQ#h z*f;4-oQ9xCfX*6gBRxjI8aW1<0*(G5ko9&?Ch@Q(+8DbgN=Hx0rcwtB-Gsv1kZkM- zM+tVf!hSzmp~l8m_-5Az*;w&<6YLwX%|NEc%2W$M3#}rOym_@~ZLCP+AmkV^ZsB4S zi!HQeZ(~6!TQNCiQurGC4O+W}LvdD^iD*rlGMh3YsMsEZmVTkFm=lT4^h_d7)yo?9$FrCc0K+ z9qqVeqie9`?3!IATaHS-=IFL-qGq}XI?CdTRhk@RRh2fQRF%@)ep0eO*OEqf8mdqB z7Y3TewMSVyqHGF=r=m*Hwah)$f{L{;G=FH1Yqq+y?TV4AI}oPfC=GCt^}t? z=$u1af6KYwaz4~@o@+VJx11MR&WoI9LjG~ozcfG9t@5`-=N#I`bRO}IwwzaV&QJU; z|9)w?<)YDh@$v7IDwlu1@?doQNq4lr4PGuj`B|6#$v=#a--wQX9UbT0@t2d)u`@bG z<^S@|==!?o_>$;&w>$nyME{kD{wopvS0Z{_8#qHh+%GuZcC7XH|91YwZEaz2RaCX0DJcQr8k;;)*se z+{uHrlOx=uz-@HnZXL^uIWLU09c%esfE4-UfRk z_Z=7WzsMW8PQ7t{7kne3s|TCCkzJ&>{QK3PZndE)=^fEgb64YEgO}@H zyWS=K+HKMCp6K}Y==krV<42?8C!=Fj{;z%2UH``yy5nzXR^i{L4n)Ui-SN}v6aPNl z5gm0e?BBom8|fe8t=Y?OcbgaNYD)xHw-Er0-_?4X5{KH3iNlA(hr@@%hr@@1BX3TF zqXtI}jv5>_IBIZkSL*qysY0CyX+tLB=OK`01qitB#w)Oax zHe{@}SZ}gvMAh6+Je`C4bc781RPNoW~^{LjUS~^O# zS*p!at-6?{T7F@cYR~y?q%SM!MHWwde3rZ4Cku6YX!Tk7_w@R?F0s))W?6wIS7pRBJbbYD0>*8>O4o z`rAnNLaIH7PkEI-*A};%?CRsusZvUnTV9=MBYYgkcw1N*=9aX@gCt1{Y|RlIq^+%5 zO}1?fJd*0|ZKV=VNPDcbs|}g9}f{|ua--f^5n%yUnBS%^F2Fd@ixw@ zJhR=_jqYQKpW>0BQzbe@+Y%)@m7`N7I#r}oejDzmBrPD}v6t6IKj@TCr*MI83-og3 zV>AlUKcG{ybZRu14M*wJEb?+Yotllc`#zllOr1{g%lLGP&soA=J}TYxNT+-{#Zwy= z=oH6 zsvDEdPT50D`5~PO=~PIkLOK<;#kow|b5C!054>;<($%Ez;zvh#L!OA8)8O?f-#8GbZ2Ou`vqhgOYBW}Fby!hj7b=;sjF6RYFRKm7IWrnkC zB^GRJOI8v*jkLM4-c~u5J=1nf+(fP~z+FPq)0Qa0UCI_?KHNpPeYlJ3ZN(<{#Msy{ z+!Kj0xX0k0$c_=Hye&BfcMa~b^|rAFcRakjEy=1`1C0;I^tvoI3 z7Oi3UcfPgDN1$<^?aQBg<6G`|%g|fze&bv2QkI5D@h`rBp)Hs5`#FATep+b!up&i6 zcp|I*edcC&w7-oZh!!xP`3Xn+lRt@$-`<3mq|ZF&(w|V<`1fBV{@6c^e&qcO_CX=N z45S6>FVu}g&C~!W45dn$SqQ?{CLal3Mlh}@PZ4|#QO$)X2;UKZBz${bI29_eJ-wd6 zkluFo3qk$8k390;0y(i}0mh;|y_U&BmEH(2&GknfO}dF|GQj*cH+3B$i7&Y)xcvoQSa_Vg*oiegY71oX4U zTPpo_7}oF0P%c1zzW=*n_*b=J`iz*>fB>vRsd6Lc?~q)|1ZN!c9m`?(4^O7PQvdRA zedMxu`m|+|nhuS0N4$~ebQpewJPZg;_p^_!=EL=8KkB7d#m=L^enBgHJq+)*Dja*} z$bb8Xx*smA-sf&f0o}AT-p%4^!@cl>4PwP-{PgMC{ofA5uSfg|+eN^bBkIBr!tm#} zweaWD3y0}OCadN+ZNB6E_O|U`55up`weS|wiwhx-t7Cnd@O^{B+x~qRerIm(1jG0f zEvyv#>M%T&^~D~Lr;{}A78NDvi#?BVSWASZ+$dA7jDp#rSOKld$9WkrnX(aD?7KcYv;^# z4R8<*G@cuc)@SFI!J*qWJ=o6MrhA-4X$}-rd74aTcJ16YHGoHl-Ucw5EUQqWlEHd; zI_U?(#MSH{$sFBW=T zbuh9ql5>+qKKv|?jK64{HdSPnd9uC)Q$j9dv=EM*oD@aaK&lOv|fJ%bpFodPaVc*K>CEF$U*tQ9*j=-TV;8 zO08>nbnA>aJO8NhU8Q$ke4)MY@74>S)m-w`Yp%WSh8HAnTDWRfoTDQ>AM!$Dw70!$ z_zS$vA07RK-u8mtc1Pn?|D&1N+27Zzp8vRB^#ZSY;D`2}|KOV+j?ac0#AG47KJWpg z>+Bi^%OyLzG6ODuH+xbVylla5v^P7u46x+La918^PaYMY{lN_IH^byv00CyWi?3^z zSHR_y^H$Pd4~El?e13vgO8ZSQc3TlM*gAupbQN~x1jtgh%%aL?)tg=3K4p!OS6r6d zokmYuZUKxctr|TKDv@AOwUvRsp01v*pt_1_S1%hBQgzF2xTirXT3kXo0b$01K3V1^ z;jbs33?KK|puyFxx4XSsS8lIwy&quDOfCGg_>-X?{>>+!{HE02 z7@4ps)I$>pF5b1E7GS|cH3-h%jssAE{g|V7m0*Z!q=v;r-*oenYm*=V(g(hF@WwMUNr1uzgth&7LB*7>xh;e z5vu{BBvHE0ixaK-7N!xJ5V8Kg79KCp-FSM$n(!s8;rg^&!xn+HVmSO93y<17Bf^+; z+iX%@_yTVz$O@PkE6&cm-h|=dmolCFow4@y_jPwOp?tQzx(Mr+Wle5d0p76jj*NVk zx8}|1+8IWXaq8hU_^a_^^^BOUSkwN;Mj~!aD~ zD4#~$M}}o(=;`UoRnLsSm4(NjI{xHfUB!D1Zl;@$&B5(@&{e3Oei5DM>Q>qIY+B(F z``q6DD-54Jyult+HT^j51c;+-j*M>Kq4mLc4rxtdEsgzGb8=+O@|LZ0sZHO^++T} z)=wH@ zT)OGdb#uFBYB;^EF}PXwCo{FR(cO^F+;rXC)zdw^%{*y=%eg*0l&Y`IC&DXu-cXZkRRAq0ZKUKo6e?qf3IV;FC_!CHv>T3@=l;wrQ) zJXM+;q0*qX$}`<<<Og~GHq8N?_YteKN|M5 z4Tb$}UExGq2Xb^F+|gFVI7G%V;1Oa+T}cx-Qy~^SkTS5PF$?-EP9!P(>$}Y>NZwkB zY7)y2S$(A8wyYf`v}h)H$mb>m7d1fFu)b94_<|S4f^4|_DhZg9$ zTYtnz_2u9Fz>Cv2EMC{q&eQyrAjn?7&=u(P$6oZ3mnB~Qk8k_cGhz5QJAqT)v6a9s zA9&=EyZzEK%T#!tAi@N-PyZh8O=fOw*Un>J7+!-3Sg{>G#mxmTP<)BTirAa0{{hV2 z-059AqlM|;gyFkVj4L^`JU)%dm$FbS>;LrJBK%)sm0QG7XEpdn82Q>Jy6nfr(4_ zNiSUg`T(K`+e#GJBMt(y-3?}{Q=8wzTORsteHAR9z% z;dsw*dE@0ngqr8w#D@-rFou5QTvX1kE*1?Lqg(J35(q{v1{?-)IFMsOikB@=Y!o>Y zC>DA|=2GM&i0(#Pqv(S%O(1aLZ9z>PKnvcX3i4#5!f2a}LOT9#j z$R*MKpaFt+zR*1~IyE!fm?H}L&vPq%%@IBeTbm>Ne6{=L%w4*FXOT5W{2EK*A9J*P zYgWw>KVu}#5x1tVU%U=F69YPlLkkrN-e0--mLL1^#I65$|1WEf*t&HBxI=u%Uvt-8 zcjG3KU(LbuMAl!T_F*2xFJ)%eX12bQ8^13pi!1oVh72aYL}NYe&DMFRzU0jkfz=kd zzRw)-PPt00MOn_|RQ(eai@cDYU4s9MTAKqE$_(*~V2tMAlnLTuF0a+dmu6=(UtRvd z)=?&lCy+BWK}1}s!l@#n2?fonS{TE{Pnllf2_Ntmn;~A8_U9>(;M&HRnq3j=)~%Uv zK4JxanpOSkAx#h&e+6a=G?_s)7JLbRPvn^$R^04>fB;Ww_RNf1$XkCRwt|}+vi@mU zW@l%nMn`}P!p_ddDPjtuxxwmsLr>8_erQ6(BSqH}f#8gZB=aH74MRxFl*im~Q)Uk? zK`?xoTe)Z6Zsik#ts$(8n;H5TNR6N#7>2?eIbmWl?)5EhYVhZc9bF_t+oLP|fE*KkOU{oh54H;`$$*`vx^f#R z25(oq)JHlFMNzs0Gwq;Yu*=>_uOfrs|H5RbOohm~jHpayBgv6UBmt9}qZ_VMjwBbQ zO2tTsl;+A*BK*6YrRG&&lh(+VC})TdAoC>&TRukjEOq1_GM=1BK#_eLc49^aRene= z70HPtf>UyE1xqm2+=>uNNI=|8;uA6n6JnTF)&Q(IhLri`vrHQGh=E3MQbKtSv$3P1 zAtk6dX)Mb_6YK&_><~xFR#j$oC8PmsNU=6QynSx(^*0~>LbTo(8P@ziSYtA|b-*n+ zzWP9Ic-!nJGEeG`4~rg)$eK)ki`DF-=g;&~!lTcY;ee4W3N zIlOk$p?}L{avf%IL$LX*I9D87B3i`XB&b9?V%x0 zr0xE#Fr|D@Py=gEJo#j5$Uw#pbgnI_C6`O~j zIDD7P?LBmJ>hOyA4;^Ba^65e27gFWwO@Eik^aT*@k5FeOi6| zYvG58?}1AQ7dR{d!T9v_vW-ZVFM6Xa;l0|*&uW_4maQ$qz4V_Re$lnprVhqxs`U@K z%?Ic0Hh&TKmto|G@RxbA{*(Z2YOJ0m?t^=l=3i^oZ;^2(5Ag441u_=h<0d1p3E42> zR7l?;9&t@Gf0V*GgnRp$lWqaxN^RjKs3W}VmuQmtGSMH0hT*o3$tqC3ELj0=;BO;N z;({pZ@0M|F1XEDvxfmk?3t>oQaXGXGV|>C{1uCpcUCiD@-O(xBe6R4_!Mf4jvphwZ zGY?VxRD^}1o0y2pMU5L0L(n>7{yO9lN5On^iu6DNp#?1Gdi zAdEN^2GI&pGcMdTjBlSG8{GOQ+qOc?BBp+FQw_Mn> zcBZvqHV6+Trq{H*!m${v%?TPGmE2q@6|jciE!PjM>DPCI zzMBccU0S1syUQd160kl;Wb*a7GAi#qovex|ugbEGJiujAfvo_|SGWV^`!d*!av5P& zh%ZQ8r}>h1D2&Dxp1H0Zl$ z@!}s??`4A{-0ES|FK<zs4r^qiIvIA_U^m>=H%h^>xj3# zdbuvrx=7YChwGr84KqPKx$@aPmtv}&;dJ%4w~USs^BH`y{@ctKer@gU0|&hQ>$?dR z8s6@W*F`2o$?b|WRIJr++cJz{|E|U|@Kg*Ip{UT~MpT1ExUzHXyLtYZL*@&Fx!CwfgPjikn3EQH;Ks3Cv!7nD?Hz z+stC{7G@$`SSV=Ud}j{U3d zIC+aZUhUc%yMD=Czv7S``E$+wD~qZ2@VtLh#VeEH{$%kg{w9i7|H%AxJ#xLf@CfV; z!u@;ccjLEj@7`i~5x;8=!gtQkYCLGQAGGoO5&7$S3hdQ;U9I|E|vVr~)`RjV*diPHp8t%m*?nU#r?+EweB7O=*yl=k;{E=Vs z%ehJp1@u4l>R9%8@}HXb?0ow$3I0El`}Q95ju-bGWWkA*+wr815v9i{hdcnA;-G@W z(U-+RD5Pj`N5f9#sYeS7!pncKbV>Z_Ai)vw+) zw>MSAeYLyLQ^1Gqhdo!&{H-^B>R{}^;{I#)(Z0FeyOX=>yXW>^bG~O`>QL;u zrE3o!*uNisZ;v+@+f(1S{~#Mh$>A27_R6%m|m2mARZyaAV1kDpXe`kIMo+Er$)?lBJth`WWnaVl797m~!3-qsovWC?f@_ z90ypXEW@fHYmP0B0{l}{T21D6l!CI9aRss3**J>Q%`jVpf)U!A1Ya2{jdUC- zq72wza2J|{Qj?HYhR#L?yd|OxW;ee&>B3p1}?g{DyV#eSs!NysPxRpHe zlo%x@NhDyed4zSj#00~XBr#O)%T`M_CQ|s119cSRt}m9S=jNi?g+uLd!E$mtNzA!s z3G`V3gOmZNthohqHL*NG0P`{CQU%4R7Zu7E>XytX3(YhSn=avQ#dvgGu@&+OyG-Uo zz=4s+rB9HC#U(cK1R#piCYx!BUF7(-T3X!ru-LQqOj&sjVGR^IOWUS_j!@U)Q5yS; z7C%Y#xL`((SBPRY7Z*t|Z!S^s!WD`+osaIOPY*{ZA)R*&3SB-1Lo&K%53``wF)HU< zEOzbfj#c+`#)3WA7;4@d=)VH%g5{u_IWGvVU`AXnbaoI^CU*sPQ>%r}_CjukNa~9? z;}t4g_}g%Q0Wmnk6|M@?c+3Dq99{WScDceOn}{BuM#PrE&rnxHi)pz^_*?LBXwZ`W zSj$z)$IP%vOZ!8(HZ(XhevPYGcGb$v`U5xuHLiTW567Vu+DUaezZa(l$CmfI2?XTM z3w{-626rC#oz3&24;p-w-tL!hZg4yuen<1XT7lYy1 zSv$W>J|Br$*Q-l$@mMey52Njh8gahkG=&!;o^3QV*F5*+F=b_Jr$bn?+4(Hz>F6Ao zGVsz|`uWP=NZ)*UetA)$0oMEO-E+^*K1`j69}u!9Fazj+3|#Z|3b1f1<6wqv*)lyI zLwh%`)AFdGxB9m7X~zy}=nQcjdn>~ji4(15vUe3H6e58P&DZ2a(K$@~*uJTry8Jzo z_#BYfRRyIB?5;_$yA%v2iHC+u9+q)^??lfkJ|+XCpQ2Ep^G6~OuyHE}uaQ$SI;WH< zO5|9SAx-2+d`*m?@REJKBT*HZ+ERm}kR&1vyEb8%gj0i>gl&tX)S}`bx$1{7XK|zf zSKffXK}trX{wNouMpdbZ)El)T2Z@fVE`wU56+KZdk*G|hOq8q3NHX!q0Z4>wMChcr zy4+`)6hER=qSDy%9<(gEh){~!-QFOu18<|y5#?wS3K5~*4NnId`c*mRZWhBrc8aK73fi=dj!iaLmZcn3b*OJ~|d5U2nc6ia8JjBu&vB=_SeSoe2+ct<{0y(2hVih^4x_pDg9fC6_ z?6U^O9pQ9TW*N&4N+6x`EFKn~WsVTX*vTr2Q1S2u6QM*u3BKDXN#V9EU5+K4RL(G3 zyAm$Bh&f!Bu_u&ncyB5upknThDpqS3X3`EsB}!NfZ+p!Fshpv zP6I~BMZ+=}5s+l;F%$Njau3;v5QWL>H-38;DQH9wu2pZzS=v9|sMnMZF*erbvZbA6 z!cnqLJ{c}u6sAEoTs|M7!Q#j9J~c!GuoH|4EAi(?GcaeJ(Hk|RKa zyE7%nYL3bm1~kZ)7A}Zqu%L2FrSD+&?w96`wbfa96EuS;2Tp|_$r2didB6-J?mFH$ zX>-|mJ~V?U16H*6dZlNb-_G3J^I;mCa@{aY19S8t)QUhBcuF@xHBcQ9szG{g0XLqA z`D|DNV%REpF2>H!StVEux)2Uv_=5cEKh z82CwD0P#Th%*!>IC7uuLKt5lw3j!X<*+&?|JQIm%UPF`?_JYv9>4to^u!M~`gvFA} z_Xc5m!VLmo!4Wau095D=){7X)kXN2UhLBF_UZeDWi>I=N;wS~4wJR934=Z>O$y6$( zBqFvzS3q1zXMlsqBWWg)jn_y5NP#xEz`rKw#t^F45CuIqD=SbH9z>u+EJ%Arxn!>z zXWTReg5F+--Js!x5-!bxA*7=w0KtV!Hr*F!pxkww8Uzv)aAl~QPGuy;V+<;N1nAzm zWo2S$pzcUCz9UP}Pw&GOa;}0su21Zxx(?C2f4zg-a^5?1&;B;z!eccUxE*V0gt?SKIDps(LMr&HN3LK ze1719!1{y_@Ta{dFGD3n>=?IcYfRoeO-Si38>71&}YWct0=f1rBPn!q_+$@&XNR zw`uJ!2J520%dSOjujV3FoobXO0GB3|4L8FTNlU}m&s2m?4x@VE*vGO1ERW?{38al( z4>B2GWe+5N44r;R46qn;ZY+Cc%h9uPfVUw{YN9>@Ry59g{Is2iOh8@CidAM>ol6zf z$+^e5R8(dESnL-3#p6XEZ2JE8efMN~;|eRT;K0C=iCEph4(xsj;rQgjvCx%V|Z4IsE^lL>$f#Q3h)Tm#ddL%2W{__EkB(e1e%H#aNBeEc@51k)%M80qcc$&M^s5OZWf#g2^F2VrF1Snpz1z5wD#ltYLk*^$NJ=K&mvxcM&Jn>Non z$A1CDkthd^xX&YpIoBONd|}*?Q?3_=JK}uttS9G39HNW#w|9$W~D1o7ZPU?i)z$j=V( z;KGn3IeaHWyzYdoaz0p+5QXLX1#n5S%t{70av@j};qWgG9DY7j5(n46GK*&IypSZC z?@+g)^Scl*iRL<~((34o3H;MQL9zIwLO1cO=W@1`gbui=I2 zSQG(*$tV%wqzsrsj*tbO@p#hcG2wv^)1$~m5$K9VWMHlWF4%%eGvzsks2)Xdz(3Mx z=)u$o&t_~U_g*=IN^9INwbx*#p+pBa0azo`aS@-|6kO=s(6{$^U3Fo`bV*_5LR_~4 zcBBg3S445MP>V&1w2s<5dP1;Wthj=_Moq!#Io&f6ANm&|8|D&?OyRz&xaT3EH&Ea6 z@hR2TP;h(QPW3NUs5EZ9rhYYYtZ$jY*OyB1zPTId_C$|i^-SajYUo>1{d|o0nkp{1 zIus}RddzgD?X?X(S{!5V1ZL5OxR8|O^j8|brk$gHTrMRKfj3h=^ z6wGa?R~wMTup_B(8K; zx(G08r&z$R7)nNa8D*AeWguftA%Ak(83n+sNGt?hqFfc;WeAvrd=slE`QR#7Q%ICj z?rBq5=rpD3M3kWfCQYi)$CZlfPJaMSOV9;Z=-=5a7L zge<0Pi)ZV6M-}C$0d;d)%@)*+Ig%#kl9GxY)(wb{ayvzPurmzLk8~3}vLIhmST;wy zfiC~kv*{a%H^C#%z8k018*ca^0B`VKJw09u0#HgW2zvt@3D76FxIQZvSNMr6e39@s z3xdCK9*kv)SbTwAe>b(69Pxn-N0x%+NRJq zRtZm`cM-rh*2@m;7w|l90jb#>{AR_5x_ln)O#yJ^l+3GicIg*_z;W)&n?vBt^Fo0{ zV{Xl9Zp?XcaAJ|ivqF!Zi-+@!iZMK#$k$ma{(^vQ1YC12ERKAom*gugN9^`S}o)(8y(K||15z#SDX+ZiEUWX!VkI%{m)!p?{w=j!Vt==X%ZAA;Es0`UPLZ7ZEw~^hW2U%!K)EsY6 z75F~FKq_}jZlhhE-*ep&@Y zx`Ppma0Rfi0{-c=8cH8~4PGEIEBtSV0JY)}piswb<+FDdz*8&!Q*hUU00^GT(!_v* zE>HsKo8mwPcRhPM){xgfKg${-C+_Xgk>3&!X3%=i_n%eH=Rt>B6tCZneMwDQmz-37J ziRS}|T7`20AP7$&uk8J8iV#(YX*}aE3fXE?d?;fmG$5Z0VyX!XbvA8LL-|}dsAeNB z0uE}$#sM&BebQQ>n>L4pT6UwRaH^ZIKNlAYU^9AZNEO_Vvol->8p>f>oC^)*ke(dQ zMdzmYP)P%};QcS37bdC+gi$&_T2zy#Lp+?XMZbk$xqB4bdIjN%|o8clOVb-q)8D~_mU1ytKn7CP5y zEA}j^HA+-X(Uj{|S6p7%FE9x+-IpRN1-DQeZE{qrwuKtug_X*HtGM2Pa^nh48xoir zRd!TWdp6Y?)u2*sld8zUu2GfpG2EN`(BTfD!;*RDQXlXq#=M* zLdjQ6iqyObE?N$>nfUoo(N>AKKF&x)2sEJ6Y=Vq-h6V{4 z&2VWFhm)IvqoKVLI9m4b@*%#F=Z8iMo?H-)4LqB}qd^zA&Drz~h_v9zXN7ok%Pkj) zkhTJTm0{zY-aT5Y>Hb{^DQ#8YSm5V8EBqYTha>jqM@(B%MxpJX%fC3-v}JM#mnR9X z+{GZLt%3w5_@4QT15Ohfnt}Wzo*y+0fS=TWC#>WP;ifH7qQL|u1{Q^>Yy-JDa#|f$ zP=w;_c9=-IDQucrXb?jg$8-VAv}LifA;Xr#MPR0hTVO@(zwo(yKIF6&-E$3WlX?C8 z=xN4$`<51Fto52mYtKPXTQu}Ep(N?8-vm5O2xwct9YaZ$V$#?gKrI1C5;1p&qo}7$ z5@*AxQA-4)7U3;nCW)kQUMRJQ%d5!{@4R?w2_T!61lgpGvGYNyr9p675rR|g{FrK) z2H;aWsB0r*6C0>&%TdF#aIAq+ho$1sJRDo68n|o`{96u&_3dsijQC`1Ua{FXZJ7u*_O*e^AvpOOg zbxu#aCTE)&9nlKiAVx@ZrO86nxUJswN+YLA78 zsx#gwRzyB^6B7}U;hSm_F$PuFJc!cqp3edE%DHe)P?;)+6(LZE?P+8{;<^WY#munB zCTd#yq8Q(Hq4gaeET*N(U* zE*52OKm?AEhO|nv3?XpbN?T>Qi3^ZW0_AxV&o33)D}kT_r=ept#2T_v;Brbsk%zrf z=orUK31d7s3&k1=Cqe)zm{JAcbj-Q1QG=<%X@@(-U(alq){14Qd8xE7p=B^*YNGrbXw>E9?lc=7D$_9%MOjT`KQ0mYB~y!xG1(A&H)SwnNdAux)}Io#vub z!evFL2%*nCy+rO_-~)=IIO{H!b;0eQ;S!IWC0$C*ye%-7FT0ikWI8~wm9>=^q!MB9 zo@q(3q^-fzSa?NDk&bUN_(5dAMMiukjBz6PM-}EuQ&0^{-D1JA8p%ya#P;IoS%kP@ zB}&Cz98nmoW34BLr!05C&z2xIsy8PzKJXWSfe_ zeh5_3C~R~0V!vmG`1EYzP9h>?S(L|u3Esan_7SncafQtu9t=`!_r%I|DeOnEz~G(j z36h9Ih-GLaw(yeGgO1KZqT~tD9_zsTcy2XcXzwhI zD7Qryp1>$)1;7kNaFjdzWu0<%slTeT!ex?%ILTcW<+=Pq$DmR$I>()UJ^%cN94u6! zC+>MpV%fTQaDlqe$%=S#xl8;73>tOLE4&@!n+118$MB1bW#`?c>H4<$r?3GsW|OwL z6*AU2V=261%CpsyPP?Qi4W%7yrz{N>reox0llY8FY^LV6Sn{eRHy|@r?ilt%UdE-f z$5@IUj}h1#$q6O9!iQUu^%zFULoLavjpSNOvh)NI_aIdgLdu$wCAop#*#~n^NESE4;~v@8mYQ$eU?p^l{PbjIA zRuQ}Kearm=LlxjsiQ;NYTklm zyM(Yst>@+wXX$HH=Jl+dbG{&U%nk<2L)$H?Qgk&kJR=*329dS@HXI14+%fC^PS>8b z%$6H4Xx)ydt$Nup43-5DJvuZQnM-ufD{a!DMo%;n&sbuVI^IYn_|0Hh2(k_~c1(sG zy*(H|D7^tEJ2GreK^p{spCb`!7;hmpHOi5UU}!L znGc>m`sj&AKls3lCxgisKkz}$kH&+)p*R_;6Bf<>%K+Z8XwjVyR)6Niqd!TEkTV1C ze)Po8sAa|)^Jz%n_~KfEe)Pmg_W~Qem!okc)6d`?B|Cjqs*cZtZ|!GJd`MpHOZnV~ zPJED0PAZMTcN#WgM=v`*YF#^e;-ksnnM+=D=e>8n=o0nU&wlj8Q7;{);<#M-zM1*s zX<#|n`JQ?7(Y=hrov9;#cPG4=%-o}oZcU}qatX-#dH&myU626`)%#AoJ07fG@y5)N z^*8SH(od&;^2GaK))$cW^1tB-&t`J_V~-9H>*b9;ylF4>Y~|6%V6}x{fn_<-Upgt2 zCcp8-2La?R+?hWT-WgAw`rrwIjM8pta`PK(%O5!L03W^W!I5yrOP#*|#80R3SfY6> z-y1IZ>^6Sn#Ea?l3*ma9ms))BiH~Ho>(g}jx98v!C$`YlJK?$0OD&C^_ynIbRzcCA zC<-g<@W)PQH137zUN5zpKJl1O%vLvSX+yTi`ky#4%D~7iZV;oB=I88IS zkF+P9N*xZ4EFFf2_T(Gb1+tRCVF8zoHM-`d!YP=h82PuJ_&^ijz>|XpK;GPrRO-zq zekPs@@5monx+9fNXMg6zn~Hc+xI1a@LM$Cm)iaMj)(_i#{*lG|V9N|W_IRZTlt{74 zrlvg*l$K1b{q%{Sil^3h+?RoGM+!IY_n!FacB%{H{%?a6d6?D1O@)S5c7pk8LO zw?6*ZoUjyD@VIA);^|DP{_YbW?@gqhnL2#Oy>}d*N>N&N;NvIW-2q#@9OSZII-gqm zSvsGJ;e+CT> z5(Zp?KrviGADPTe1)C(}=yB!q=zw!!DSeGX8Z74|^fpRuWKkhRN0kGPE2G<$%Cpu; zxyVBFQZ%weh0wH(6#Azp7%@{rk?v}=wV%OZ*fQzPM%xB>o=xQL56a^KO+@wKBw7Bq^VUGc5Z zlK=D_4;_E#{@V@=`hy2_U*?XB%bN&?1RVOPTNu}s1?kUPnR(e)bsx1Z*BKi-+KIh;>nU#mi-MXZbo+; ze=zAUPhNZL?YCY#N#xG#+JndMkSBtwN<^iwg*)=l(OFzGZcV*BypN9^<@}|7iZ3yo+8%WRsNtv?1Q`?llzVK_kc172p0y14^3MC`&L+^nhbnQ#pH81$DP zfAe$bFFpQ1oC?%=Imx~G_=_71dJ2*x9XncuBO1PX&(UMWc7NHbz&DH=O%-p5XV$lJ zCl+6^HN!0WhU0JIkrE3G*-U-r=&}6?DzQ7tW3&I*(d`}niZKw-qW=2h|E4pMd1mYY zkHc!^X`frZEzGP+Oa5h?d>+uKW+Qt*}fI+1&I~Ozc&bL3c`HvJ4|0RQkG%bx0UC|2F&9B>=(ZD^>2Rfn_vIZFA(X1c(M5T!^3~C2mA*yDll%owszegd@ply?R$T49a83? z1Q!*BtPv+lZePC`6-Y{04!PQ2{pNR1e)p@7y<<;p&pRIbD(Amxo<9gQiiDRm=bt`< z9w1%&DC_}$?c`IRog$R$CBO63$*;06P3c|P>N94tt@snSRm&14EA6|S{ z1uMuuI{9(lj%UTFhsi|o&dzwbUj5!vGcdl-e|YhIdXO%A>M8cj#8^Y>wa1b266Lio zo&0>fyuSZ^85s9hx~tt^IQgY^de}Jx$dElPot0F%e%n(|y*)w2sfQQdML()tZ~xx+ zZf6${gp_`;e6+_?$&~A#JNf6+iSjeM?mqh7qj&ErSGuZQvwwc_a~-_zCF`kI$(7gs z%gO(iW8JgDBc?*s)REKy9%yB)1a=xxHEBU~RrC{3o73o`u532oKiI=z-#KvK+aMDr>b< zh7Bd94>L(8_q;jaL$zl`P)5QWMQ~NE0J!Le!|>cxAb> z%kB_aMNJPa>B-d->2=Ckhk0NGt!U@yNMblKbV{}AdBsS1V4yZOv{o4#^+(Ro9=#V# zh{PrzS;HPHM_4U3{|Fm}VJs`}Isp0ycJDW2X15=xHx?SUW! z8&v!}&Y~g{D%KNz`p$`#pi+_mf()NgMIen~J{!vk;xB|Fi9ROAk5HNUrZy+$aCrte{IW&Pq@(o zVB)b8Qp}hRgr1*xjI|K8$cv+-Y-40Y`w`v&U?kbxT}c{sjoQ2*MDGzEhDvP?xiZe! zft4wRM-Wa2N0z4%0#SI#&@DvMWK`)n0_i!{@1Xx^zTV^zAb>hWiN{`T`{R$*)G~A& z;eGLZefY7*!5>=UISSA$(KsnL1dMtq+Kq+#c7T17N4o)7#Im$%*bNimZ4SkTfv2L{ z2=7SbSESfb95S;Zgr^+*6?%;U3BdVj(`-!VRgyFtj6R$)^@9<}PIMbyAV2nOp058a zJLuebMKOim87vHZ?4wo=nP~0Le{3j0$M9*-`~A{6dbu*L;VB+2uC=9{Bf%rVb}4hY zP6|#oSgTOIF zMDVOca7&fo9cdjr^Z*Fn86Y?7xnoDWn%W9q;&vb%O9(E43EKYplnL5Xz<2ow+Nddw z=!EP@ZGtvMGB!#H5i$Hm!mcL#hJ=m!%J;jFu;b`4cn!f}kc7>b0r@&Xu63WJdiCuO zRs))Vbe+~SgZQ}aXLbVd8SFsaInPBck$b#7K4rFjA_9$t| z+Iek5$n>IOW#X&w#s8i4i^-djWI5M27fF^RTaCnIG#Ea)`obx3a@^}p_MO=-fs!P( zauBi6BwWdm9;1@#TQD)w$ZjvS-jCGiA^*mqt|B$6fl~IKai`lgZ(2zUN<4?+K$XPG9n((#=6-?cM&{-*KdgwHNCFiGR*Tk(_zU z%wq@W%)G@DmXyomDE7HRD#L@jKm7cj-5%!M%`c&o{q4_xNSOsvTjs9L3wG=C$DgrQ zb8Nepe-&-|XY|#gBj8#-2n-c$M7FS4b$9=Q4}wl&zy9EoFEQ6O0kO3CXR2Y>-Tf1% zv1rp!FIldDOO0N~+;>=YRQiCh4q{rr;xn z-q+aSS@zycy$|fU^Zc(c_2k0f`H53b=s;dHA$$O<&jhfF+W8ZPpCScCUsv7xG@CQI z>b;9|<@dk)TDtHqz*K+y_IJPkmH>CsP13bFeA4+6YYr!pRT(HSf*05liq2W%B(0?5 zY-sd(@#BLg!C^iN5k{eZPatzQn zlx2@jX8hRrzN1pFSt3%d8}l}7iU}?#uO-ahvd~Pp=e#zH&UJwAW-Kw(hSf~}+p%ql z0`Q6@NxKx?O#fWMFskZnCTlkD z_(t$+^|-P`R)m{VwkucGp_|S;!NMhlB=d*<9#bL-nt}B&P7ufXoC98~Aq-z;WSPh( z5f%cw=@VEueAG-PNn>_t!3p99vqrkEr8R_V-Bt>fE_qLRZnpBf>5Y0eya&u}=#!Um z=;A>3MfO$=rSwquv9A%0UkJui>@Eb|;dnOhrFW`?2hDY%3Jt88a4(WoJJ3{jNm?TB z;CP}V(~#&>)leGsh>l#HMVrt6oqSFZ?m|373Q~2|6vi5GUZFgV_d9*23GVeHZlp0$ zhx{qgZ*0M?!GISZE0kJVdtKsf$;Ylp)`%d#8FpgKX~uOl4f*qo7Z zokd)RdpIC0JHTY+Vg^kZNy@Hy1i0ylh#V~YxPN#I?ZIb9RfnOk@~p4xA-G7MI~)Gw zpP#&F1`dc+2aPca+niFf{AX;+N|>yhn3J+PTmAnMiZ|W7!PXvqrkIy);Aa0n^?Vcj zqt0DS1yRjcVD^<7PVk+^OYKhSUE$P>MKSNUwBsnkwk`(a8ua5K8oz>d>_YG#d1QF5 zeq5|b*PL&&e&@(v&(_xuEk$$)&T7V|XZyy_b)2Cq5h)c$QzGHm9t6wn3JD6J?l~5+gaTraU&#LPPT17zhc03vWHhk-I!h zFhnOFW$kaHM&xv;5nfAFo-;?}oQ}GM*hbMaZbQB1P((c{xi!=)Re*>sLB0G=so97- zI{a|AVe472=e#xxxpG}PtrCyYbqy`Fa;R}$o72y3s<3?BR3#%(XQ5II$F1p~OAw_P z&m7Q8tw1%@U<(kTt?4J<(vM~_f)y_1_-?kQ4+e%kzBg(pP`)=rwoA+nBz2G`uwAIC znzV9|jlw~M!)OQwIi{_aN_%>VG%z=<*{E5Jpfi0O6GQ{K#I&f8NoRT`1W%QgNA8<2 zt`e{o4q!~<0wDfS^7GLP&U8Pd?(|xn@jaxkWr1i!CE_LlngIR`O)rwVL{1O)aA?wK zJn9bdK>^v~?@n)k^m`;e_AX!PU{8@EIEZ_6JMMX0nlud8R~!!-2j~R0h$qci0B^xQ z+lhhy$T#>B;GuUFknbz(?YCCk0 zaHlOBDRh!FTHNWFiwPU+jMUP>nSJ|2>6o7)r6e1TQf*-DVz=ps&38ukgWq0f^m%UL z*tmd36s*zx?qR6I1r!L$9G+lY@C2I$iE!dDXo6dqaR?_HN2)|(2`Yc6aR>_#YGMua z>P%_rBsL0(D?~K7T)TT`4m7*H^E<0_^162#(?fV19Yy#I!c{oG z#Z!8)MiF0$>=GjJ1Sb)dl*F$49ZXo*%rBIr2VZSjWem z6A^{hXb4A!xT^;XXXh)84&a@#wyasV~ zM6QuzTdZd$_szzz6(k=6pKwp9f|e0;3k{ipx+?R7M+p;i(0Mxlq(@86j<;{meVqh9 zto%McXYwDZusMHsaq;e*uX1R2=qPwhLNF{?>?c%-E`oQrr2RM*#hxL^-ayi=lzc-E z;a5RXLpePTtOGfJT@CVv#MHPpF{dz67_3C_SJb%R((SDpnW=fj&;dd8%U%(UP z6~*6mXmdFH0+nHM=dm%^rPB+!%+8Q3AAD8RaB*={PX3f+&&MeGE@3iZuAM#bN*o_f z)(?6M&-1r;=Vw~h$7Z62)#?Ek}x&HzMle^D=)^ z-9=<~#hP!&&hf2YXqp7(CK5XY3j*Iy4adb4;V4oXf>ym8)l$mZR>rkc!>M_bc72e5 zb&hw*&i|uYLnSC^a3x4x2zR4U#+6ZC`PBa1DgUip2g2(EOel+YIu?0Q3|dMycDQ{4 zc}1CSI%(|mw#wucX()c8R!=vQHg@`3GDcQ5q%RwwZcu4pulav!x?=w6{8w(;`@V{L z<_Z{_1u{X>48nM=%-x4N`Jzl3Ot;ED<;yWdj!DI(mH zM_O4pTAsPOgJcgxfAm{}zj^=k;k)l;6N-YYmt}$3S5T0ZmCUoDb;o8=tYl=K$eh%Q zcdOzV$L_J`H}|XJ3AKyjJ!JY$h*T?!i6Jc6(C%Uf$C4h_RMfSl4c|LSB?}dGw`oyq zw^};FcE46UsdU*zMKMOiPoGxBJGEj0ISUnaQ)^M1FwIg$aUP0tLP5(}#OqR)r_ZyTo%2)60ipbuQla>s z%6m~cCmN=77xPu}hK7Zub3${~^Z5^OEKu%T6ObC5B=-f|E)}q{&eyU@2K9;{x5hxzx@z= z_x$}29k{=ab4--3LQ)HsuTm2USRWG6TL*SZ(lOD$bzs-Cix0WHQgq?ik&gSmSn24D zM(oQ6u6(`HRh<&-%l8Y~@rWZ^${Vhm{Zi3=*S#+vm3rDuqR92YxYUzG0qUsfI+_$} z>SD>Nm>dRTJ0FqJisD64dsY;aoJxc8oy?t*NA6P9s(j(KIkX#EH7egAb=okUO((YH zw8=^>{T;D$59A1T8K~IqnrII!0WL+$CZ+)Q{Jdze+(f6>Y=SM+U@>pI{?kqK^jgER zt8tm*c>a_fjK&+1=e@onIl}4y_djt!A^xjJjKT4)w!xGGR-Du9m|)p(=;XuCjKm%&z9l?d0CK z&UJ3zusa2+re)xSt`Vq;tEdZZa4p2^<^^4|N^D4_lha0pvN-K(9Sy|pAEj=`#SHj4 zP|6DBNbG)5$~(jZ3p3sQY9^;mhTb7pt}uzJ++BaG_>X?h4BLs7WDeZ`i7C+;mP+=# z#$y}A6FDhdu8laGP$KGy@>CFFrimFjAzHF3-7Ui=+q!|i5(S$B+?qZdaz=OCTUV<2sm1b0t9+ncMsUBj6s}v&& zx3bbksU=s?+5_Oy4fUdhsz?`}_YxtY#acuzD~a-$$Y58PT9`jE+~^Q5)nvU6@r@i< y;6;oTdCZX)`LEdtv6E*fq#P;V{t*25{L|n6diVI{)vH&(^nd*4)p!2y)&Br Date: Thu, 5 Oct 2023 18:12:53 +0200 Subject: [PATCH 46/99] PV phase ghosting fix --- plugins/SlicerT/SlicerT.cpp | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 5d82a6b7a2d..a39707b86d6 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -83,10 +83,14 @@ void PhaseVocoder::loadData(std::vector originalData, int sampleRate, flo // set buffer sizes m_processedWindows.resize(m_numWindows, false); m_lastPhase.resize(m_numWindows * s_windowSize, 0); - m_sumPhase.resize(m_numWindows * s_windowSize, 0); + m_sumPhase.resize((m_numWindows + 1) * s_windowSize, 0); m_freqCache.resize(m_numWindows * s_windowSize, 0); m_magCache.resize(m_numWindows * s_windowSize, 0); + // clear phase buffers + std::fill(m_lastPhase.begin(), m_lastPhase.end(), 0); + std::fill(m_sumPhase.begin(), m_sumPhase.end(), 0); + // maybe limit this to a set amount of windows to reduce initial lag spikes for (int i = 0; i < m_numWindows; i++) { @@ -108,6 +112,13 @@ void PhaseVocoder::getFrames(std::vector& outData, int start, int frames) int windowMargin = s_overSampling / 2; // numbers of windows before full quality int startWindow = std::max(0.0f, (float)start / m_outStepSize - windowMargin); int endWindow = std::min((float)m_numWindows, (float)(start + frames) / m_outStepSize + windowMargin); + + // discard previous phaseSum if not processed + if (!m_processedWindows[startWindow]) + { + std::fill_n(m_sumPhase.data() + startWindow * s_windowSize, s_windowSize, 0); + } + // this encompases the minimum windows needed to get full quality, // which must be computed for (int i = startWindow; i < endWindow; i++) @@ -147,10 +158,6 @@ void PhaseVocoder::updateParams(float newRatio) // very slow :( std::fill(m_processedWindows.begin(), m_processedWindows.end(), false); std::fill(m_processedBuffer.begin(), m_processedBuffer.end(), 0); - // this can be commented, since the start phase is not importante to the PV - // and the sum phase will grow linearly anyway - // std::fill(lastPhase.begin(), lastPhase.end(), 0); - // std::fill(sumPhase.begin(), sumPhase.end(), 0); m_dataLock.unlock(); } @@ -225,10 +232,10 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) magnitude = m_allMagnitudes[j]; freq = m_allFrequencies[j]; - // difference in freq + // difference to bin freq mulitplier deltaPhase = freq - (float)j * m_freqPerBin; - // scaled to 1 + // convert to phase difference deltaPhase /= m_freqPerBin; // difference in phase @@ -239,11 +246,10 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) // sum this phase to the total, to keep track of the out phase along the sample m_sumPhase[windowIndex + j] += deltaPhase; - deltaPhase = m_sumPhase[windowIndex + j]; // this is the bin phase - if (windowIndex + j + s_windowSize < m_sumPhase.size()) - { // only if not last window - m_sumPhase[windowIndex + j + s_windowSize] = deltaPhase; // copy to the next - } + deltaPhase = m_sumPhase[windowIndex + j]; // final bin phase + + m_sumPhase[windowIndex + j + s_windowSize] = deltaPhase; // copy to the next + m_FFTSpectrum[j][0] = magnitude * cos(deltaPhase); m_FFTSpectrum[j][1] = magnitude * sin(deltaPhase); @@ -256,7 +262,6 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) for (int j = 0; j < s_windowSize; j++) { float outIndex = windowNum * m_outStepSize + j; - if (outIndex >= frames()) { break; } // blackman-harris window float a0 = 0.35875f; From 1080e5c54fac97759037a242fb9e8aaf91881e52 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Thu, 5 Oct 2023 18:18:45 +0200 Subject: [PATCH 47/99] Use base note as keyboard slice start --- plugins/SlicerT/SlicerT.cpp | 5 +++-- plugins/SlicerT/SlicerT.h | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index a39707b86d6..01e3f5254cf 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -287,6 +287,7 @@ SlicerT::SlicerT(InstrumentTrack* instrumentTrack) , m_sliceSnap(this, tr("Slice snap")) , m_originalSample() , m_phaseVocoder() + , m_parentTrack(instrumentTrack) { m_sliceSnap.addItem("Off"); m_sliceSnap.addItem("1/1"); @@ -308,7 +309,7 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) // current playback status const int totalFrames = m_phaseVocoder.frames(); - const int noteIndex = handle->key() - 69; + const int noteIndex = handle->key() - m_parentTrack->baseNote(); const fpp_t frames = handle->framesLeftForCurrentPeriod(); const f_cnt_t offset = handle->noteOffset(); const int playedFrames = handle->totalFramesPlayed(); @@ -506,7 +507,7 @@ void SlicerT::writeToMidi(std::vector* outClip) float sliceEnd = (float)m_slicePoints[i + 1] / m_originalSample.frames() * totalTicks; Note sliceNote = Note(); - sliceNote.setKey(i + 69); + sliceNote.setKey(i + m_parentTrack->baseNote()); sliceNote.setPos(sliceStart); sliceNote.setLength(sliceEnd - sliceStart + 1); // + 1 needed for whatever reason outClip->push_back(sliceNote); diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 46d9093a129..3e1b8d4992a 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -184,6 +184,8 @@ public slots: std::vector m_slicePoints; + InstrumentTrack* m_parentTrack; + void findSlices(); void findBPM(); From 290ef8d5774490201d3ed4dcc9c9a6a024c7fd00 Mon Sep 17 00:00:00 2001 From: Katherine Pratt Date: Thu, 5 Oct 2023 16:58:52 -0400 Subject: [PATCH 48/99] Good draft of Artwork, renamed bg to artwork --- plugins/SlicerT/SlicerTUI.cpp | 2 +- plugins/SlicerT/artwork.png | Bin 0 -> 23467 bytes plugins/SlicerT/{bg.xcf => artwork.xcf} | Bin plugins/SlicerT/bg.png | Bin 28021 -> 0 bytes 4 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 plugins/SlicerT/artwork.png rename plugins/SlicerT/{bg.xcf => artwork.xcf} (100%) delete mode 100644 plugins/SlicerT/bg.png diff --git a/plugins/SlicerT/SlicerTUI.cpp b/plugins/SlicerT/SlicerTUI.cpp index 6fe9159a4e9..2f4a1dc0b9d 100644 --- a/plugins/SlicerT/SlicerTUI.cpp +++ b/plugins/SlicerT/SlicerTUI.cpp @@ -58,7 +58,7 @@ SlicerTUI::SlicerTUI(SlicerT* instrument, QWidget* parent) // render background QPalette pal; - pal.setBrush(backgroundRole(), PLUGIN_NAME::getIconPixmap("bg")); + pal.setBrush(backgroundRole(), PLUGIN_NAME::getIconPixmap("artwork")); setPalette(pal); // move editor and seeker diff --git a/plugins/SlicerT/artwork.png b/plugins/SlicerT/artwork.png new file mode 100644 index 0000000000000000000000000000000000000000..82a80897b2294502197ce840c02b63c65ecfc767 GIT binary patch literal 23467 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 Date: Fri, 6 Oct 2023 17:20:38 -0400 Subject: [PATCH 49/99] Removed soft glow. --- plugins/SlicerT/artwork.png | Bin 23467 -> 14209 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/plugins/SlicerT/artwork.png b/plugins/SlicerT/artwork.png index 82a80897b2294502197ce840c02b63c65ecfc767..76dfbc5c5cd0bdb45e5ee85051f7b2898bb3a1cc 100644 GIT binary patch literal 14209 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 Date: Sat, 7 Oct 2023 12:57:55 +0200 Subject: [PATCH 50/99] Fixed load crashes + findSlices cleanup --- plugins/SlicerT/SlicerT.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 01e3f5254cf..c30d2b00ca1 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -72,12 +72,14 @@ PhaseVocoder::~PhaseVocoder() void PhaseVocoder::loadData(std::vector originalData, int sampleRate, float newRatio) { + m_dataLock.lock(); + m_originalBuffer = originalData; m_originalSampleRate = sampleRate; m_scaleRatio = -1; // force update, kinda hacky + m_dataLock.unlock(); // stupid, but QRecursiveMutex is too expensive to have in updateParas and getFrames updateParams(newRatio); - m_dataLock.lock(); // set buffer sizes @@ -372,13 +374,24 @@ void SlicerT::findSlices() // computacion params const int windowSize = 512; - const int minDist = 2048; + const float minBeatLength = 0.05f; // in seconds, ~ 1/4 length at 220 bpm + + int sampleRate = m_originalSample.sampleRate(); + int minDist = sampleRate * minBeatLength; // copy vector into one vector, averaging channels + float maxMag = -1; std::vector singleChannel(m_originalSample.frames(), 0); for (int i = 0; i < m_originalSample.frames(); i++) { singleChannel[i] = (m_originalSample.data()[i][0] + m_originalSample.data()[i][1]) / 2; + maxMag = std::max(maxMag, singleChannel[i]); + } + + // normalize + for (int i = 0; i < singleChannel.size(); i++) + { + singleChannel[i] /= maxMag; } // buffers From 3e0419f20cdad0b5a0ec578f189804ed18701656 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sat, 7 Oct 2023 15:21:31 +0200 Subject: [PATCH 51/99] Added sync button --- plugins/SlicerT/SlicerT.cpp | 12 ++++++++++-- plugins/SlicerT/SlicerT.h | 1 + plugins/SlicerT/SlicerTUI.cpp | 6 ++++++ plugins/SlicerT/SlicerTUI.h | 2 ++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index c30d2b00ca1..dbd47cf56f2 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -111,6 +111,12 @@ void PhaseVocoder::getFrames(std::vector& outData, int start, int frames) if (m_originalBuffer.size() < 2048) { return; } m_dataLock.lock(); + if (m_scaleRatio == 1) { // directly copy original data + memcpy(outData.data(), m_originalBuffer.data() + start, frames*sizeof(float)); + m_dataLock.unlock(); + return; + } + int windowMargin = s_overSampling / 2; // numbers of windows before full quality int startWindow = std::max(0.0f, (float)start / m_outStepSize - windowMargin); int endWindow = std::min((float)m_numWindows, (float)(start + frames) / m_outStepSize + windowMargin); @@ -287,6 +293,7 @@ SlicerT::SlicerT(InstrumentTrack* instrumentTrack) , m_fadeOutFrames(400.0f, 0.0f, 8192.0f, 1.0f, this, tr("FadeOut")) , m_originalBPM(1, 1, 999, this, tr("Original bpm")) , m_sliceSnap(this, tr("Slice snap")) + , m_enableSync(true, this, tr("BPM sync")) , m_originalSample() , m_phaseVocoder() , m_parentTrack(instrumentTrack) @@ -306,7 +313,8 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) if (m_originalSample.frames() < 2048) { return; } // update current speed ratio, in case bpm changed - const float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo(); + float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo(); + if (!m_enableSync.value()) { speedRatio = 1; } // disable timeshift m_phaseVocoder.setScaleRatio(speedRatio); // current playback status @@ -497,7 +505,7 @@ void SlicerT::writeToMidi(std::vector* outClip) if (m_originalSample.frames() < 2048) { return; } // update incase bpm changed - const float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo(); + float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo(); m_phaseVocoder.setScaleRatio(speedRatio); // calculate how many "beats" are in the sample diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 3e1b8d4992a..871a7e80bc6 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -177,6 +177,7 @@ public slots: FloatModel m_fadeOutFrames; IntModel m_originalBPM; ComboBoxModel m_sliceSnap; + BoolModel m_enableSync; // sample buffers SampleBuffer m_originalSample; diff --git a/plugins/SlicerT/SlicerTUI.cpp b/plugins/SlicerT/SlicerTUI.cpp index 0f5fe93a7ba..bf305cb3084 100644 --- a/plugins/SlicerT/SlicerTUI.cpp +++ b/plugins/SlicerT/SlicerTUI.cpp @@ -48,6 +48,7 @@ SlicerTUI::SlicerTUI(SlicerT* instrument, QWidget* parent) , m_fadeOutKnob(this) , m_bpmBox(3, "21pink", this) , m_snapSetting(this, "Slice snap") + , m_syncToggle("Sync", this, "SyncToggle", LedCheckBox::LedColor::Green) , m_resetButton(this, nullptr) , m_midiExportButton(this, nullptr) , m_wf(248, 128, instrument, this) @@ -69,6 +70,11 @@ SlicerTUI::SlicerTUI(SlicerT* instrument, QWidget* parent) m_snapSetting.setToolTip(tr("Set slice snapping for detection")); m_snapSetting.setModel(&m_slicerTParent->m_sliceSnap); + // sync toggle + m_syncToggle.move(135, 187); + m_syncToggle.setToolTip(tr("Enable BPM sync")); + m_syncToggle.setModel(&m_slicerTParent->m_enableSync); + // bpm spin box m_bpmBox.move(135, 203); m_bpmBox.setToolTip(tr("Original sample BPM")); diff --git a/plugins/SlicerT/SlicerTUI.h b/plugins/SlicerT/SlicerTUI.h index adcc1d0ef2b..1bd180adb0d 100644 --- a/plugins/SlicerT/SlicerTUI.h +++ b/plugins/SlicerT/SlicerTUI.h @@ -32,6 +32,7 @@ #include "InstrumentView.h" #include "Knob.h" #include "LcdSpinBox.h" +#include "LedCheckBox.h" #include "PixmapButton.h" #include "WaveForm.h" @@ -78,6 +79,7 @@ protected slots: SlicerTKnob m_fadeOutKnob; LcdSpinBox m_bpmBox; ComboBox m_snapSetting; + LedCheckBox m_syncToggle; // buttons PixmapButton m_resetButton; From 6990944ec5bac854a4bb1afb9cb629fc48532463 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Tue, 10 Oct 2023 22:49:18 +0200 Subject: [PATCH 52/99] added pitch shifting, sometimes crashes --- plugins/SlicerT/SlicerT.cpp | 41 ++++++++++++++++++++++++++++--------- plugins/SlicerT/SlicerT.h | 2 +- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 43f677e0c4f..994bf5bc570 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -312,18 +312,23 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) { if (m_originalSample.frames() < 2048) { return; } - // update current speed ratio, in case bpm changed - float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo(); - if (!m_enableSync.value()) { speedRatio = 1; } // disable timeshift - m_phaseVocoder.setScaleRatio(speedRatio); - - // current playback status - const int totalFrames = m_phaseVocoder.frames(); + // playback parameters + // const float freq = handle->frequency(); + const float pitchRatio = pow(2, m_parentTrack->pitchModel()->value() / 1200); + const float inversePitchRatio = 1.0f / pitchRatio; const int noteIndex = handle->key() - m_parentTrack->baseNote(); + const int playedFrames = handle->totalFramesPlayed(); const fpp_t frames = handle->framesLeftForCurrentPeriod(); const f_cnt_t offset = handle->noteOffset(); - const int playedFrames = handle->totalFramesPlayed(); + const int bpm = Engine::getSong()->getTempo(); + // update scaling parameters + float speedRatio = (float)m_originalBPM.value() / bpm; + if (!m_enableSync.value()) { speedRatio = 1; } // disable timeshift + m_phaseVocoder.setScaleRatio(speedRatio); + speedRatio *= inversePitchRatio; // adjust for pitch bend + + int totalFrames = inversePitchRatio * m_phaseVocoder.frames(); // adjust frames played with regards to pitch int sliceStart, sliceEnd; if (noteIndex > m_slicePoints.size() - 2 || noteIndex < 0) // full sample if ouside range { @@ -343,8 +348,23 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) if (noteFramesLeft > 0) { - int framesToCopy = std::min((int)frames, noteFramesLeft); - m_phaseVocoder.getFrames(workingBuffer + offset, currentNoteFrame, framesToCopy); + // load sample segmengt, with regards to pitch settings + // TODO: THIS CRASHES OFTEN + std::vector prePitchBuffer(pitchRatio * frames + 1, {0.0f, 0.0f}); // round the frames up + int framesToCopy = std::min((int)(pitchRatio * frames) + 1, noteFramesLeft); // same here + int framesIndex = std::min((int)(pitchRatio * currentNoteFrame), m_phaseVocoder.frames() - framesToCopy); + m_phaseVocoder.getFrames(prePitchBuffer.data(), framesIndex, framesToCopy); + + // resample for pitch bend + SRC_DATA resamplerData; + + resamplerData.data_in = (float*)prePitchBuffer.data(); // wtf + resamplerData.data_out = (float*)workingBuffer + offset; // wtf is this + resamplerData.input_frames = prePitchBuffer.size(); + resamplerData.output_frames = frames; + resamplerData.src_ratio = inversePitchRatio; + + src_simple(&resamplerData, SRC_SINC_MEDIUM_QUALITY, 2); // only 2 channels // exponential fade out, applyRelease kinda sucks if (noteFramesLeft < m_fadeOutFrames.value()) @@ -367,6 +387,7 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) float absoluteCurrentNote = (float)currentNoteFrame / totalFrames; float absoluteStartNote = (float)sliceStart / totalFrames; float abslouteEndNote = (float)sliceEnd / totalFrames; + printf("curr: %f, start: %f, end: %f\n", absoluteCurrentNote, absoluteStartNote, abslouteEndNote); emit isPlaying(absoluteCurrentNote, absoluteStartNote, abslouteEndNote); } else { emit isPlaying(-1, 0, 0); } diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 312d911484c..3b207d4f1b4 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -106,7 +106,7 @@ class DynamicPlaybackBuffer : m_leftChannel() , m_rightChannel() {} - + void loadSample(const sampleFrame* outData, int frames, int sampleRate, float newRatio) { std::vector leftData(frames, 0); From 0cc28bd713984ccc2db8ba53a1dc29a6657dc357 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Wed, 11 Oct 2023 18:59:31 +0200 Subject: [PATCH 53/99] pitch fixes --- plugins/SlicerT/SlicerT.cpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 994bf5bc570..4edad0f54f1 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -313,14 +313,13 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) if (m_originalSample.frames() < 2048) { return; } // playback parameters - // const float freq = handle->frequency(); - const float pitchRatio = pow(2, m_parentTrack->pitchModel()->value() / 1200); - const float inversePitchRatio = 1.0f / pitchRatio; const int noteIndex = handle->key() - m_parentTrack->baseNote(); const int playedFrames = handle->totalFramesPlayed(); const fpp_t frames = handle->framesLeftForCurrentPeriod(); const f_cnt_t offset = handle->noteOffset(); const int bpm = Engine::getSong()->getTempo(); + const float pitchRatio = pow(2, m_parentTrack->pitchModel()->value() / 1200); + const float inversePitchRatio = 1.0f / pitchRatio; // update scaling parameters float speedRatio = (float)m_originalBPM.value() / bpm; @@ -348,11 +347,13 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) if (noteFramesLeft > 0) { - // load sample segmengt, with regards to pitch settings - // TODO: THIS CRASHES OFTEN - std::vector prePitchBuffer(pitchRatio * frames + 1, {0.0f, 0.0f}); // round the frames up - int framesToCopy = std::min((int)(pitchRatio * frames) + 1, noteFramesLeft); // same here + // round the frames up, the more there are the less artyfacts are on each window end + // but its also more expensive, so just deal with it if you pitch shift + int framesToCopy = pitchRatio * frames + 1; int framesIndex = std::min((int)(pitchRatio * currentNoteFrame), m_phaseVocoder.frames() - framesToCopy); + + // load sample segmengt, with regards to pitch settings + std::vector prePitchBuffer(framesToCopy, {0.0f, 0.0f}); m_phaseVocoder.getFrames(prePitchBuffer.data(), framesIndex, framesToCopy); // resample for pitch bend @@ -364,7 +365,9 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) resamplerData.output_frames = frames; resamplerData.src_ratio = inversePitchRatio; - src_simple(&resamplerData, SRC_SINC_MEDIUM_QUALITY, 2); // only 2 channels + // if pitch is changed, interpolate, else just copy + if (pitchRatio != 1.0f) { src_simple(&resamplerData, SRC_SINC_MEDIUM_QUALITY, 2); } + else { memcpy(workingBuffer + offset, prePitchBuffer.data(), frames * sizeof(sampleFrame)); } // exponential fade out, applyRelease kinda sucks if (noteFramesLeft < m_fadeOutFrames.value()) From 5a56aea2d09713cac58d071c9fb007b653450b3d Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Wed, 11 Oct 2023 20:33:25 +0200 Subject: [PATCH 54/99] MacOs build fixes --- plugins/SlicerT/CMakeLists.txt | 5 ++++- plugins/SlicerT/SlicerT.cpp | 26 +++++++++++++------------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/plugins/SlicerT/CMakeLists.txt b/plugins/SlicerT/CMakeLists.txt index 365a6042537..00a425ac69b 100644 --- a/plugins/SlicerT/CMakeLists.txt +++ b/plugins/SlicerT/CMakeLists.txt @@ -1,7 +1,10 @@ INCLUDE(BuildPlugin) INCLUDE_DIRECTORIES(${FFTW3F_INCLUDE_DIRS}) - LINK_LIBRARIES(${FFTW3F_LIBRARIES}) +INCLUDE_DIRECTORIES(${SAMPLERATE_INCLUDE_DIRS}) +LINK_DIRECTORIES(${SAMPLERATE_LIBRARY_DIRS}) +LINK_LIBRARIES(${SAMPLERATE_LIBRARIES}) + BUILD_PLUGIN(slicert SlicerT.cpp SlicerT.h SlicerTUI.cpp SlicerTUI.h WaveForm.cpp WaveForm.h MOCFILES SlicerT.h SlicerTUI.h WaveForm.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png") \ No newline at end of file diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 4edad0f54f1..02297f87fe1 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -349,24 +349,25 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) { // round the frames up, the more there are the less artyfacts are on each window end // but its also more expensive, so just deal with it if you pitch shift - int framesToCopy = pitchRatio * frames + 1; + int framesToCopy = pitchRatio * frames + 128; int framesIndex = std::min((int)(pitchRatio * currentNoteFrame), m_phaseVocoder.frames() - framesToCopy); // load sample segmengt, with regards to pitch settings std::vector prePitchBuffer(framesToCopy, {0.0f, 0.0f}); m_phaseVocoder.getFrames(prePitchBuffer.data(), framesIndex, framesToCopy); - // resample for pitch bend - SRC_DATA resamplerData; - - resamplerData.data_in = (float*)prePitchBuffer.data(); // wtf - resamplerData.data_out = (float*)workingBuffer + offset; // wtf is this - resamplerData.input_frames = prePitchBuffer.size(); - resamplerData.output_frames = frames; - resamplerData.src_ratio = inversePitchRatio; - - // if pitch is changed, interpolate, else just copy - if (pitchRatio != 1.0f) { src_simple(&resamplerData, SRC_SINC_MEDIUM_QUALITY, 2); } + // if pitch is changed, resample, else just copy + if (pitchRatio != 1.0f) + { // resample for pitch bend + SRC_DATA resamplerData; + + resamplerData.data_in = (float*)prePitchBuffer.data(); // wtf + resamplerData.data_out = (float*)workingBuffer + offset; // wtf is this + resamplerData.input_frames = prePitchBuffer.size(); + resamplerData.output_frames = frames; + resamplerData.src_ratio = inversePitchRatio; + src_simple(&resamplerData, SRC_SINC_MEDIUM_QUALITY, 2); + } else { memcpy(workingBuffer + offset, prePitchBuffer.data(), frames * sizeof(sampleFrame)); } // exponential fade out, applyRelease kinda sucks @@ -390,7 +391,6 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) float absoluteCurrentNote = (float)currentNoteFrame / totalFrames; float absoluteStartNote = (float)sliceStart / totalFrames; float abslouteEndNote = (float)sliceEnd / totalFrames; - printf("curr: %f, start: %f, end: %f\n", absoluteCurrentNote, absoluteStartNote, abslouteEndNote); emit isPlaying(absoluteCurrentNote, absoluteStartNote, abslouteEndNote); } else { emit isPlaying(-1, 0, 0); } From 441052c516af95b97a5cf9fb4ec4a418e0b78622 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Wed, 11 Oct 2023 22:19:33 +0200 Subject: [PATCH 55/99] use linear interpolation --- plugins/SlicerT/SlicerT.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 02297f87fe1..e503ce97be8 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -347,9 +347,7 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) if (noteFramesLeft > 0) { - // round the frames up, the more there are the less artyfacts are on each window end - // but its also more expensive, so just deal with it if you pitch shift - int framesToCopy = pitchRatio * frames + 128; + int framesToCopy = pitchRatio * frames + 1; // just in case int framesIndex = std::min((int)(pitchRatio * currentNoteFrame), m_phaseVocoder.frames() - framesToCopy); // load sample segmengt, with regards to pitch settings @@ -358,15 +356,16 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) // if pitch is changed, resample, else just copy if (pitchRatio != 1.0f) - { // resample for pitch bend + { SRC_DATA resamplerData; - resamplerData.data_in = (float*)prePitchBuffer.data(); // wtf - resamplerData.data_out = (float*)workingBuffer + offset; // wtf is this + resamplerData.data_in = (float*)prePitchBuffer.data(); // wtf + resamplerData.data_out = (float*)(workingBuffer + offset); // wtf is this resamplerData.input_frames = prePitchBuffer.size(); resamplerData.output_frames = frames; resamplerData.src_ratio = inversePitchRatio; - src_simple(&resamplerData, SRC_SINC_MEDIUM_QUALITY, 2); + + src_simple(&resamplerData, SRC_LINEAR, 2); } else { memcpy(workingBuffer + offset, prePitchBuffer.data(), frames * sizeof(sampleFrame)); } From 9185d2d6d10b3aaf212e534cc1ba5ec7747672c2 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Thu, 12 Oct 2023 15:07:55 +0200 Subject: [PATCH 56/99] copyright + div by 0 fixes --- plugins/SlicerT/SlicerT.cpp | 8 ++++---- plugins/SlicerT/SlicerT.h | 2 +- plugins/SlicerT/SlicerTUI.cpp | 2 +- plugins/SlicerT/SlicerTUI.h | 2 +- plugins/SlicerT/WaveForm.cpp | 4 +++- plugins/SlicerT/WaveForm.h | 2 +- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index e503ce97be8..8ec3f5616e7 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -1,7 +1,7 @@ /* * SlicerT.cpp - simple slicer plugin * - * Copyright (c) 2006-2008 Andreas Brandmaier + * Copyright (c) 2006-2008 Daniel Kauss Serna * * This file is part of LMMS - https://lmms.io * @@ -43,7 +43,7 @@ Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = { LMMS_STRINGIFY(PLUGIN_NAME), "SlicerT", QT_TRANSLATE_NOOP("PluginBrowser", "Basic Slicer"), - "Daniel Kauss Serna ", + "Daniel Kauss Serna ", 0x0100, Plugin::Type::Instrument, new PluginPixmapLoader("icon"), @@ -434,7 +434,7 @@ void SlicerT::findSlices() int lastPoint = -minDist - 1; // to always store 0 first float spectralFlux = 0; - float prevFlux = 0; + float prevFlux = 1E-10; // small value, no divison by zero float real, imag, magnitude, diff; for (int i = 0; i < singleChannel.size() - windowSize; i += windowSize) @@ -465,7 +465,7 @@ void SlicerT::findSlices() } prevFlux = spectralFlux; - spectralFlux = 0; + spectralFlux = 1E-10; // again for no divison by zero } m_slicePoints.push_back(m_originalSample.frames()); diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 3b207d4f1b4..350cda7bbf9 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -1,7 +1,7 @@ /* * SlicerT.h - declaration of class SlicerT * - * Copyright (c) 2006-2008 Andreas Brandmaier + * Copyright (c) 2006-2008 Daniel Kauss Serna * * This file is part of LMMS - https://lmms.io * diff --git a/plugins/SlicerT/SlicerTUI.cpp b/plugins/SlicerT/SlicerTUI.cpp index 8795437cf22..8bea9eb5f6b 100644 --- a/plugins/SlicerT/SlicerTUI.cpp +++ b/plugins/SlicerT/SlicerTUI.cpp @@ -1,7 +1,7 @@ /* * SlicerTUI.cpp - controls the UI for slicerT * - * Copyright (c) 2006-2008 Andreas Brandmaier + * Copyright (c) 2006-2008 Daniel Kauss Serna * * This file is part of LMMS - https://lmms.io * diff --git a/plugins/SlicerT/SlicerTUI.h b/plugins/SlicerT/SlicerTUI.h index bf84bc4d67b..8fc0493b23f 100644 --- a/plugins/SlicerT/SlicerTUI.h +++ b/plugins/SlicerT/SlicerTUI.h @@ -1,7 +1,7 @@ /* * SlicerTUI.h - declaration of class SlicerTUI * - * Copyright (c) 2006-2008 Andreas Brandmaier + * Copyright (c) 2006-2008 Daniel Kauss Serna * * This file is part of LMMS - https://lmms.io * diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/WaveForm.cpp index c6f8ad72177..23966583926 100644 --- a/plugins/SlicerT/WaveForm.cpp +++ b/plugins/SlicerT/WaveForm.cpp @@ -1,7 +1,7 @@ /* * WaveForm.cpp - slice editor for SlicerT * - * Copyright (c) 2006-2008 Andreas Brandmaier + * Copyright (c) 2006-2008 Daniel Kauss Serna * * This file is part of LMMS - https://lmms.io * @@ -73,6 +73,7 @@ WaveForm::WaveForm(int w, int h, SlicerT* instrument, QWidget* parent) void WaveForm::drawSeekerWaveform() { m_seekerWaveform.fill(m_waveformBgColor); + if (m_currentSample.frames() < 2048) { return; } QPainter brush(&m_seekerWaveform); brush.setPen(m_waveformColor); @@ -83,6 +84,7 @@ void WaveForm::drawSeekerWaveform() void WaveForm::drawSeeker() { m_seeker.fill(m_waveformBgColor); + if (m_currentSample.frames() < 2048) { return; } QPainter brush(&m_seeker); // draw slice points diff --git a/plugins/SlicerT/WaveForm.h b/plugins/SlicerT/WaveForm.h index 3e74b995f9e..2183ceca021 100644 --- a/plugins/SlicerT/WaveForm.h +++ b/plugins/SlicerT/WaveForm.h @@ -1,7 +1,7 @@ /* * WaveForm.h - declaration of class WaveForm * - * Copyright (c) 2006-2008 Andreas Brandmaier + * Copyright (c) 2006-2008 Daniel Kauss Serna * * This file is part of LMMS - https://lmms.io * From fbbe9f04cf92941338a4c54ebab34f63f487e55a Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Fri, 13 Oct 2023 13:29:13 +0200 Subject: [PATCH 57/99] Fixed rare crash + name changes + license --- data/themes/classic/style.css | 8 ++++ plugins/SlicerT/CMakeLists.txt | 2 +- plugins/SlicerT/SlicerT.cpp | 15 ++++--- plugins/SlicerT/SlicerT.h | 6 +-- .../{SlicerTUI.cpp => SlicerTView.cpp} | 26 +++++------- .../SlicerT/{SlicerTUI.h => SlicerTView.h} | 8 ++-- plugins/SlicerT/WaveForm.cpp | 40 +++++++++---------- plugins/SlicerT/WaveForm.h | 20 +++++----- 8 files changed, 65 insertions(+), 60 deletions(-) rename plugins/SlicerT/{SlicerTUI.cpp => SlicerTView.cpp} (91%) rename plugins/SlicerT/{SlicerTUI.h => SlicerTView.h} (89%) diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index ac22a14ec21..6e39fbcc310 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -902,6 +902,14 @@ lmms--gui--SidInstrumentView lmms--gui--Knob { qproperty-lineWidth: 2; } +lmms--gui--SlicerTUI lmms--gui--Knob { + color: rgb(162, 128, 226); + qproperty-outerColor: rgb( 162, 128, 226 ); + qproperty-innerRadius: 1; + qproperty-outerRadius: 11; + qproperty-lineWidth: 3; +} + lmms--gui--WatsynView lmms--gui--Knob { qproperty-innerRadius: 1; qproperty-outerRadius: 7; diff --git a/plugins/SlicerT/CMakeLists.txt b/plugins/SlicerT/CMakeLists.txt index 00a425ac69b..8bec64edaff 100644 --- a/plugins/SlicerT/CMakeLists.txt +++ b/plugins/SlicerT/CMakeLists.txt @@ -7,4 +7,4 @@ INCLUDE_DIRECTORIES(${SAMPLERATE_INCLUDE_DIRS}) LINK_DIRECTORIES(${SAMPLERATE_LIBRARY_DIRS}) LINK_LIBRARIES(${SAMPLERATE_LIBRARIES}) -BUILD_PLUGIN(slicert SlicerT.cpp SlicerT.h SlicerTUI.cpp SlicerTUI.h WaveForm.cpp WaveForm.h MOCFILES SlicerT.h SlicerTUI.h WaveForm.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png") \ No newline at end of file +BUILD_PLUGIN(slicert SlicerT.cpp SlicerT.h SlicerTView.cpp SlicerTView.h WaveForm.cpp WaveForm.h MOCFILES SlicerT.h SlicerTView.h WaveForm.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png") \ No newline at end of file diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 8ec3f5616e7..a2abe65e08c 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -1,7 +1,7 @@ /* * SlicerT.cpp - simple slicer plugin * - * Copyright (c) 2006-2008 Daniel Kauss Serna + * Copyright (c) 2023 Daniel Kauss Serna * * This file is part of LMMS - https://lmms.io * @@ -118,8 +118,11 @@ void PhaseVocoder::getFrames(std::vector& outData, int start, int frames) } int windowMargin = s_overSampling / 2; // numbers of windows before full quality - int startWindow = std::max(0.0f, (float)start / m_outStepSize - windowMargin); - int endWindow = std::min((float)m_numWindows, (float)(start + frames) / m_outStepSize + windowMargin); + int startWindow = (float)start / m_outStepSize - windowMargin; + int endWindow = (float)(start + frames) / m_outStepSize + windowMargin; + + startWindow = std::clamp(startWindow, 0, m_numWindows - 1); + endWindow = std::clamp(endWindow, 0, m_numWindows - 1); // discard previous phaseSum if not processed if (!m_processedWindows[startWindow]) @@ -379,8 +382,8 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) fadeValue = std::clamp(fadeValue, 0.0f, 1.0f); fadeValue = pow(fadeValue, 2); - workingBuffer[i][0] *= fadeValue; - workingBuffer[i][1] *= fadeValue; + workingBuffer[i + offset][0] *= fadeValue; + workingBuffer[i + offset][1] *= fadeValue; } } @@ -653,7 +656,7 @@ QString SlicerT::nodeName() const gui::PluginView* SlicerT::instantiateView(QWidget* parent) { - return (new gui::SlicerTUI(this, parent)); + return (new gui::SlicerTView(this, parent)); } extern "C" { diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 350cda7bbf9..04babfc5371 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -1,7 +1,7 @@ /* * SlicerT.h - declaration of class SlicerT * - * Copyright (c) 2006-2008 Daniel Kauss Serna + * Copyright (c) 2023 Daniel Kauss Serna * * This file is part of LMMS - https://lmms.io * @@ -32,7 +32,7 @@ #include "InstrumentView.h" #include "Note.h" #include "SampleBuffer.h" -#include "SlicerTUI.h" +#include "SlicerTView.h" namespace lmms { @@ -193,7 +193,7 @@ public slots: void findSlices(); void findBPM(); - friend class gui::SlicerTUI; + friend class gui::SlicerTView; friend class gui::WaveForm; }; } // namespace lmms diff --git a/plugins/SlicerT/SlicerTUI.cpp b/plugins/SlicerT/SlicerTView.cpp similarity index 91% rename from plugins/SlicerT/SlicerTUI.cpp rename to plugins/SlicerT/SlicerTView.cpp index 8bea9eb5f6b..8ba28f631da 100644 --- a/plugins/SlicerT/SlicerTUI.cpp +++ b/plugins/SlicerT/SlicerTView.cpp @@ -1,7 +1,7 @@ /* - * SlicerTUI.cpp - controls the UI for slicerT + * SlicerTView.cpp - controls the UI for slicerT * - * Copyright (c) 2006-2008 Daniel Kauss Serna + * Copyright (c) 2023 Daniel Kauss Serna * * This file is part of LMMS - https://lmms.io * @@ -22,7 +22,7 @@ * */ -#include "SlicerTUI.h" +#include "SlicerTView.h" #include #include @@ -41,7 +41,7 @@ namespace lmms { namespace gui { -SlicerTUI::SlicerTUI(SlicerT* instrument, QWidget* parent) +SlicerTView::SlicerTView(SlicerT* instrument, QWidget* parent) : InstrumentViewFixedSize(instrument, parent) , m_slicerTParent(instrument) , m_noteThresholdKnob(this) @@ -109,7 +109,7 @@ SlicerTUI::SlicerTUI(SlicerT* instrument, QWidget* parent) } // copied from piano roll -void SlicerTUI::exportMidi() +void SlicerTView::exportMidi() { using namespace Clipboard; @@ -133,7 +133,7 @@ void SlicerTUI::exportMidi() } // all the drag stuff is copied from AudioFileProcessor -void SlicerTUI::dragEnterEvent(QDragEnterEvent* dee) +void SlicerTView::dragEnterEvent(QDragEnterEvent* dee) { // For mimeType() and MimeType enum class using namespace Clipboard; @@ -145,19 +145,13 @@ void SlicerTUI::dragEnterEvent(QDragEnterEvent* dee) { dee->acceptProposedAction(); } - else if (txt.section(':', 0, 0) == "samplefile") - { - dee->acceptProposedAction(); - } - else - { - dee->ignore(); - } + else if (txt.section(':', 0, 0) == "samplefile") { dee->acceptProposedAction(); } + else { dee->ignore(); } } else { dee->ignore(); } } -void SlicerTUI::dropEvent(QDropEvent* de) +void SlicerTView::dropEvent(QDropEvent* de) { QString type = StringPairDrag::decodeKey(de); QString value = StringPairDrag::decodeValue(de); @@ -179,7 +173,7 @@ void SlicerTUI::dropEvent(QDropEvent* de) } // display button text -void SlicerTUI::paintEvent(QPaintEvent* pe) +void SlicerTView::paintEvent(QPaintEvent* pe) { QPainter brush(this); brush.setPen(QColor(255, 255, 255)); diff --git a/plugins/SlicerT/SlicerTUI.h b/plugins/SlicerT/SlicerTView.h similarity index 89% rename from plugins/SlicerT/SlicerTUI.h rename to plugins/SlicerT/SlicerTView.h index 8fc0493b23f..c624dc68708 100644 --- a/plugins/SlicerT/SlicerTUI.h +++ b/plugins/SlicerT/SlicerTView.h @@ -1,7 +1,7 @@ /* - * SlicerTUI.h - declaration of class SlicerTUI + * SlicerTView.h - declaration of class SlicerTView * - * Copyright (c) 2006-2008 Daniel Kauss Serna + * Copyright (c) 2023 Daniel Kauss Serna * * This file is part of LMMS - https://lmms.io * @@ -55,12 +55,12 @@ class SlicerTKnob : public Knob } }; -class SlicerTUI : public InstrumentViewFixedSize +class SlicerTView : public InstrumentViewFixedSize { Q_OBJECT public: - SlicerTUI(SlicerT* instrument, QWidget* parent); + SlicerTView(SlicerT* instrument, QWidget* parent); protected slots: void exportMidi(); diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/WaveForm.cpp index 23966583926..7bcc70f337c 100644 --- a/plugins/SlicerT/WaveForm.cpp +++ b/plugins/SlicerT/WaveForm.cpp @@ -1,7 +1,7 @@ /* * WaveForm.cpp - slice editor for SlicerT * - * Copyright (c) 2006-2008 Daniel Kauss Serna + * Copyright (c) 2023 Daniel Kauss Serna * * This file is part of LMMS - https://lmms.io * @@ -60,8 +60,8 @@ WaveForm::WaveForm(int w, int h, SlicerT* instrument, QWidget* parent) setMouseTracking(true); // draw backgrounds - m_sliceEditor.fill(m_waveformBgColor); - m_seekerWaveform.fill(m_waveformBgColor); + m_sliceEditor.fill(s_waveformBgColor); + m_seekerWaveform.fill(s_waveformBgColor); // connect to playback connect(m_slicerTParent, SIGNAL(isPlaying(float, float, float)), this, SLOT(isPlaying(float, float, float))); @@ -72,10 +72,10 @@ WaveForm::WaveForm(int w, int h, SlicerT* instrument, QWidget* parent) void WaveForm::drawSeekerWaveform() { - m_seekerWaveform.fill(m_waveformBgColor); + m_seekerWaveform.fill(s_waveformBgColor); if (m_currentSample.frames() < 2048) { return; } QPainter brush(&m_seekerWaveform); - brush.setPen(m_waveformColor); + brush.setPen(s_waveformColor); m_currentSample.visualize( brush, QRect(0, 0, m_seekerWaveform.width(), m_seekerWaveform.height()), 0, m_currentSample.frames()); @@ -83,12 +83,12 @@ void WaveForm::drawSeekerWaveform() void WaveForm::drawSeeker() { - m_seeker.fill(m_waveformBgColor); + m_seeker.fill(s_waveformBgColor); if (m_currentSample.frames() < 2048) { return; } QPainter brush(&m_seeker); // draw slice points - brush.setPen(m_sliceColor); + brush.setPen(s_sliceColor); for (int i = 0; i < m_slicePoints.size(); i++) { float xPos = (float)m_slicePoints[i] / m_currentSample.frames() * m_seekerWidth; @@ -106,31 +106,31 @@ void WaveForm::drawSeeker() float noteEndPosX = (m_noteEnd - m_noteStart) * m_seekerWidth; // draw current playBack - brush.setPen(m_playColor); + brush.setPen(s_playColor); brush.drawLine(noteCurrentPosX, 0, noteCurrentPosX, m_seekerHeight); - brush.fillRect(noteStartPosX, 0, noteEndPosX, m_seekerHeight, m_playHighlighColor); + brush.fillRect(noteStartPosX, 0, noteEndPosX, m_seekerHeight, s_playHighlighColor); // highlight on selected area - brush.fillRect(seekerStartPosX, 0, seekerMiddleWidth, m_seekerHeight, m_seekerHighlightColor); + brush.fillRect(seekerStartPosX, 0, seekerMiddleWidth, m_seekerHeight, s_seekerHighlightColor); // shadow on not selected area - brush.fillRect(0, 0, seekerStartPosX, m_seekerHeight, m_seekerShadowColor); - brush.fillRect(seekerEndPosX + 1, 0, m_seekerWidth + 1, m_seekerHeight, m_seekerShadowColor); + brush.fillRect(0, 0, seekerStartPosX, m_seekerHeight, s_seekerShadowColor); + brush.fillRect(seekerEndPosX + 1, 0, m_seekerWidth + 1, m_seekerHeight, s_seekerShadowColor); // draw border around selection - brush.setPen(QPen(m_seekerColor, 1)); + brush.setPen(QPen(s_seekerColor, 1)); brush.drawRoundedRect(seekerStartPosX, 0, seekerMiddleWidth - 1, m_seekerHeight - 1, 4, 4); // -1 needed } void WaveForm::drawEditor() { - m_sliceEditor.fill(m_waveformBgColor); + m_sliceEditor.fill(s_waveformBgColor); QPainter brush(&m_sliceEditor); // draw text if no sample loaded if (m_currentSample.frames() < 2048) { - brush.setPen(m_playHighlighColor); + brush.setPen(s_playHighlighColor); brush.setFont(QFont(brush.font().family(), 9.0f, -1, false)); brush.drawText( m_editorWidth / 2 - 100, m_editorHeight / 2 - 100, 200, 200, Qt::AlignCenter, tr("Drag sample to load")); @@ -143,23 +143,23 @@ void WaveForm::drawEditor() float numFramesToDraw = endFrame - startFrame; // 0 centered line - brush.setPen(m_playHighlighColor); + brush.setPen(s_playHighlighColor); brush.drawLine(0, m_editorHeight / 2, m_editorWidth, m_editorHeight / 2); // draw waveform - brush.setPen(m_waveformColor); + brush.setPen(s_waveformColor); float zoomOffset = ((float)m_editorHeight - m_zoomLevel * m_editorHeight) / 2; m_currentSample.visualize( brush, QRect(0, zoomOffset, m_editorWidth, m_zoomLevel * m_editorHeight), startFrame, endFrame); // draw slicepoints - brush.setPen(QPen(m_sliceColor, 2)); + brush.setPen(QPen(s_sliceColor, 2)); for (int i = 0; i < m_slicePoints.size(); i++) { float xPos = (float)(m_slicePoints[i] - startFrame) / numFramesToDraw * m_editorWidth; - if (i == m_sliceSelected) { brush.setPen(QPen(m_selectedSliceColor, 2)); } - else { brush.setPen(QPen(m_sliceColor, 2)); } + if (i == m_sliceSelected) { brush.setPen(QPen(s_selectedSliceColor, 2)); } + else { brush.setPen(QPen(s_sliceColor, 2)); } brush.drawLine(xPos, 0, xPos, m_editorHeight); brush.drawPixmap(xPos - (float)m_sliceArrow.width() / 2, 0, m_sliceArrow); diff --git a/plugins/SlicerT/WaveForm.h b/plugins/SlicerT/WaveForm.h index 2183ceca021..5301fbed7ca 100644 --- a/plugins/SlicerT/WaveForm.h +++ b/plugins/SlicerT/WaveForm.h @@ -1,7 +1,7 @@ /* * WaveForm.h - declaration of class WaveForm * - * Copyright (c) 2006-2008 Daniel Kauss Serna + * Copyright (c) 2023 Daniel Kauss Serna * * This file is part of LMMS - https://lmms.io * @@ -66,18 +66,18 @@ class WaveForm : public QWidget int m_editorWidth; // colors - QColor m_waveformBgColor = QColor(255, 255, 255, 0); - QColor m_waveformColor = QColor(123, 49, 212); + static constexpr QColor s_waveformBgColor = QColor(255, 255, 255, 0); + static constexpr QColor s_waveformColor = QColor(123, 49, 212); - QColor m_playColor = QColor(255, 255, 255, 200); - QColor m_playHighlighColor = QColor(255, 255, 255, 70); + static constexpr QColor s_playColor = QColor(255, 255, 255, 200); + static constexpr QColor s_playHighlighColor = QColor(255, 255, 255, 70); - QColor m_sliceColor = QColor(218, 193, 255); - QColor m_selectedSliceColor = QColor(178, 153, 215); + static constexpr QColor s_sliceColor = QColor(218, 193, 255); + static constexpr QColor s_selectedSliceColor = QColor(178, 153, 215); - QColor m_seekerColor = QColor(178, 115, 255); - QColor m_seekerHighlightColor = QColor(178, 115, 255, 100); - QColor m_seekerShadowColor = QColor(0, 0, 0, 120); + static constexpr QColor s_seekerColor = QColor(178, 115, 255); + static constexpr QColor s_seekerHighlightColor = QColor(178, 115, 255, 100); + static constexpr QColor s_seekerShadowColor = QColor(0, 0, 0, 120); // interaction vars float m_distanceForClick = 0.03f; From ae50fd9ad4c1129967bbf6a0c69d10a3357a69d4 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Fri, 13 Oct 2023 15:59:10 +0200 Subject: [PATCH 58/99] review: memcpy, no array, LMMS header, name change --- include/Clipboard.h | 2 +- plugins/SlicerT/CMakeLists.txt | 2 +- plugins/SlicerT/SlicerT.cpp | 27 ++++--- plugins/SlicerT/SlicerT.h | 26 +++--- plugins/SlicerT/SlicerTView.cpp | 4 +- plugins/SlicerT/SlicerTView.h | 16 ++-- .../{WaveForm.cpp => SlicerTWaveform.cpp} | 79 +++++++++---------- .../SlicerT/{WaveForm.h => SlicerTWaveform.h} | 51 ++++++------ 8 files changed, 104 insertions(+), 103 deletions(-) rename plugins/SlicerT/{WaveForm.cpp => SlicerTWaveform.cpp} (81%) rename plugins/SlicerT/{WaveForm.h => SlicerTWaveform.h} (81%) diff --git a/include/Clipboard.h b/include/Clipboard.h index 491f8ebf748..cee40b33ae4 100644 --- a/include/Clipboard.h +++ b/include/Clipboard.h @@ -46,7 +46,7 @@ namespace lmms::Clipboard bool hasFormat( MimeType mT ); // Helper methods for String data - void LMMS_EXPORT copyString( const QString & str, MimeType mT ); + void LMMS_EXPORT copyString(const QString& str, MimeType mT); QString getString( MimeType mT ); // Helper methods for String Pair data diff --git a/plugins/SlicerT/CMakeLists.txt b/plugins/SlicerT/CMakeLists.txt index 8bec64edaff..49a80ca03aa 100644 --- a/plugins/SlicerT/CMakeLists.txt +++ b/plugins/SlicerT/CMakeLists.txt @@ -7,4 +7,4 @@ INCLUDE_DIRECTORIES(${SAMPLERATE_INCLUDE_DIRS}) LINK_DIRECTORIES(${SAMPLERATE_LIBRARY_DIRS}) LINK_LIBRARIES(${SAMPLERATE_LIBRARIES}) -BUILD_PLUGIN(slicert SlicerT.cpp SlicerT.h SlicerTView.cpp SlicerTView.h WaveForm.cpp WaveForm.h MOCFILES SlicerT.h SlicerTView.h WaveForm.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png") \ No newline at end of file +BUILD_PLUGIN(slicert SlicerT.cpp SlicerT.h SlicerTView.cpp SlicerTView.h SlicerTWaveform.cpp SlicerTWaveform.h MOCFILES SlicerT.h SlicerTView.h SlicerTWaveform.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png") \ No newline at end of file diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index a2abe65e08c..a9f5b76a9c4 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -53,15 +53,16 @@ Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = { } // end extern PhaseVocoder::PhaseVocoder() - : m_FFTInput(s_windowSize, 0) + : m_FFTSpectrum(s_windowSize) + , m_FFTInput(s_windowSize, 0) , m_IFFTReconstruction(s_windowSize, 0) , m_allMagnitudes(s_windowSize, 0) , m_allFrequencies(s_windowSize, 0) , m_processedFreq(s_windowSize, 0) , m_processedMagn(s_windowSize, 0) { - m_fftPlan = fftwf_plan_dft_r2c_1d(s_windowSize, m_FFTInput.data(), m_FFTSpectrum, FFTW_MEASURE); - m_ifftPlan = fftwf_plan_dft_c2r_1d(s_windowSize, m_FFTSpectrum, m_IFFTReconstruction.data(), FFTW_MEASURE); + m_fftPlan = fftwf_plan_dft_r2c_1d(s_windowSize, m_FFTInput.data(), m_FFTSpectrum.data(), FFTW_MEASURE); + m_ifftPlan = fftwf_plan_dft_c2r_1d(s_windowSize, m_FFTSpectrum.data(), m_IFFTReconstruction.data(), FFTW_MEASURE); } PhaseVocoder::~PhaseVocoder() @@ -112,7 +113,7 @@ void PhaseVocoder::getFrames(std::vector& outData, int start, int frames) m_dataLock.lock(); if (m_scaleRatio == 1) { // directly copy original data - memcpy(outData.data(), m_originalBuffer.data() + start, frames*sizeof(float)); + std::copy_n(m_originalBuffer.data() + start, frames, outData.data()); m_dataLock.unlock(); return; } @@ -189,7 +190,7 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) if (!useCache) { // normal stuff - memcpy(m_FFTInput.data(), m_originalBuffer.data() + windowStart, s_windowSize * sizeof(float)); + std::copy_n(m_originalBuffer.data() + windowStart, s_windowSize, m_FFTInput.data()); // FFT fftwf_execute(m_fftPlan); @@ -227,14 +228,14 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) m_allFrequencies[j] = freq; } // write cache - memcpy(m_freqCache.data() + windowIndex, m_allFrequencies.data(), s_windowSize * sizeof(float)); - memcpy(m_magCache.data() + windowIndex, m_allMagnitudes.data(), s_windowSize * sizeof(float)); + std::copy_n(m_allFrequencies.data(), s_windowSize, m_freqCache.data() + windowIndex); + std::copy_n(m_allMagnitudes.data(), s_windowSize, m_magCache.data() + windowIndex); } else { // read cache - memcpy(m_allFrequencies.data(), m_freqCache.data() + windowIndex, s_windowSize * sizeof(float)); - memcpy(m_allMagnitudes.data(), m_magCache.data() + windowIndex, s_windowSize * sizeof(float)); + std::copy_n(m_freqCache.data() + windowIndex, s_windowSize, m_allFrequencies.data()); + std::copy_n(m_magCache.data() + windowIndex, s_windowSize, m_allMagnitudes.data()); } // synthesis, all the operations are the reverse of the analysis @@ -389,7 +390,7 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) instrumentTrack()->processAudioBuffer(workingBuffer, frames + offset, handle); - // calculate absolute for the waveform + // calculate absolute for the SlicerTWaveform float absoluteCurrentNote = (float)currentNoteFrame / totalFrames; float absoluteStartNote = (float)sliceStart / totalFrames; float abslouteEndNote = (float)sliceEnd / totalFrames; @@ -431,9 +432,9 @@ void SlicerT::findSlices() // buffers std::vector prevMags(windowSize / 2, 0); std::vector fftIn(windowSize, 0); - fftwf_complex fftOut[windowSize]; + std::vector fftOut(windowSize); - fftwf_plan fftPlan = fftwf_plan_dft_r2c_1d(windowSize, fftIn.data(), fftOut, FFTW_MEASURE); + fftwf_plan fftPlan = fftwf_plan_dft_r2c_1d(windowSize, fftIn.data(), fftOut.data(), FFTW_MEASURE); int lastPoint = -minDist - 1; // to always store 0 first float spectralFlux = 0; @@ -443,7 +444,7 @@ void SlicerT::findSlices() for (int i = 0; i < singleChannel.size() - windowSize; i += windowSize) { // fft - memcpy(fftIn.data(), singleChannel.data() + i, windowSize * sizeof(float)); + std::copy_n(singleChannel.data() + i, windowSize, fftIn.data()); fftwf_execute(fftPlan); // calculate spectral flux in regard to last window diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 04babfc5371..5c641bdfa29 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -76,7 +76,7 @@ class PhaseVocoder float m_expectedPhaseOut = 0; // buffers - fftwf_complex m_FFTSpectrum[s_windowSize]; + std::vector m_FFTSpectrum; std::vector m_FFTInput; std::vector m_IFFTReconstruction; std::vector m_allMagnitudes; @@ -153,6 +153,13 @@ class SlicerT : public Instrument { Q_OBJECT +public slots: + void updateFile(QString file); + void updateSlices(); + +signals: + void isPlaying(float current, float start, float end); + public: SlicerT(InstrumentTrack* instrumentTrack); ~SlicerT() override = default; @@ -162,18 +169,14 @@ class SlicerT : public Instrument void saveSettings(QDomDocument& document, QDomElement& element) override; void loadSettings(const QDomElement& element) override; + void findSlices(); + void findBPM(); + QString nodeName() const override; gui::PluginView* instantiateView(QWidget* parent) override; void writeToMidi(std::vector* outClip); -public slots: - void updateFile(QString file); - void updateSlices(); - -signals: - void isPlaying(float current, float start, float end); - private: // models FloatModel m_noteThreshold; @@ -190,11 +193,8 @@ public slots: InstrumentTrack* m_parentTrack; - void findSlices(); - void findBPM(); - friend class gui::SlicerTView; - friend class gui::WaveForm; + friend class gui::SlicerTWaveform; }; } // namespace lmms -#endif // SLICERT_H +#endif // LMMS_SLICERT_H diff --git a/plugins/SlicerT/SlicerTView.cpp b/plugins/SlicerT/SlicerTView.cpp index 8ba28f631da..dde522adc95 100644 --- a/plugins/SlicerT/SlicerTView.cpp +++ b/plugins/SlicerT/SlicerTView.cpp @@ -47,8 +47,8 @@ SlicerTView::SlicerTView(SlicerT* instrument, QWidget* parent) , m_noteThresholdKnob(this) , m_fadeOutKnob(this) , m_bpmBox(3, "21pink", this) - , m_snapSetting(this, "Slice snap") - , m_syncToggle("Sync", this, "SyncToggle", LedCheckBox::LedColor::Green) + , m_snapSetting(this, tr("Slice snap")) + , m_syncToggle("Sync", this, tr("SyncToggle"), LedCheckBox::LedColor::Green) , m_resetButton(this, nullptr) , m_midiExportButton(this, nullptr) , m_wf(248, 128, instrument, this) diff --git a/plugins/SlicerT/SlicerTView.h b/plugins/SlicerT/SlicerTView.h index c624dc68708..71dcad46976 100644 --- a/plugins/SlicerT/SlicerTView.h +++ b/plugins/SlicerT/SlicerTView.h @@ -22,8 +22,8 @@ * */ -#ifndef SLICERT_UI_H -#define SLICERT_UI_H +#ifndef LMMS_SLICERT_VIEW_H +#define LMMS_SLICERT_VIEW_H #include @@ -34,7 +34,7 @@ #include "LcdSpinBox.h" #include "LedCheckBox.h" #include "PixmapButton.h" -#include "WaveForm.h" +#include "SlicerTWaveform.h" namespace lmms { @@ -59,12 +59,12 @@ class SlicerTView : public InstrumentViewFixedSize { Q_OBJECT -public: - SlicerTView(SlicerT* instrument, QWidget* parent); - protected slots: void exportMidi(); +public: + SlicerTView(SlicerT* instrument, QWidget* parent); + protected: virtual void dragEnterEvent(QDragEnterEvent* _dee); virtual void dropEvent(QDropEvent* _de); @@ -85,8 +85,8 @@ protected slots: PixmapButton m_resetButton; PixmapButton m_midiExportButton; - WaveForm m_wf; + SlicerTWaveform m_wf; }; } // namespace gui } // namespace lmms -#endif // SLICERT_UI_H +#endif // LMMS_SLICERT_VIEW_H diff --git a/plugins/SlicerT/WaveForm.cpp b/plugins/SlicerT/SlicerTWaveform.cpp similarity index 81% rename from plugins/SlicerT/WaveForm.cpp rename to plugins/SlicerT/SlicerTWaveform.cpp index 7bcc70f337c..889ad001bb9 100644 --- a/plugins/SlicerT/WaveForm.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -1,5 +1,5 @@ /* - * WaveForm.cpp - slice editor for SlicerT + * SlicerTWaveform.cpp - slice editor for SlicerT * * Copyright (c) 2023 Daniel Kauss Serna * @@ -22,16 +22,15 @@ * */ -#include "WaveForm.h" - #include "SlicerT.h" +#include "SlicerTWaveform.h" #include "embed.h" namespace lmms { namespace gui { -WaveForm::WaveForm(int w, int h, SlicerT* instrument, QWidget* parent) +SlicerTWaveform::SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* parent) : QWidget(parent) , // calculate sizes @@ -45,7 +44,7 @@ WaveForm::WaveForm(int w, int h, SlicerT* instrument, QWidget* parent) // create pixmaps m_sliceArrow(PLUGIN_NAME::getIconPixmap("slide_indicator_arrow")) , m_seeker(QPixmap(m_seekerWidth, m_seekerHeight)) - , m_seekerWaveform(QPixmap(m_seekerWidth, m_seekerHeight)) + , m_seekerSlicerTWaveform(QPixmap(m_seekerWidth, m_seekerHeight)) , m_sliceEditor(QPixmap(w, m_editorHeight)) , @@ -60,8 +59,8 @@ WaveForm::WaveForm(int w, int h, SlicerT* instrument, QWidget* parent) setMouseTracking(true); // draw backgrounds - m_sliceEditor.fill(s_waveformBgColor); - m_seekerWaveform.fill(s_waveformBgColor); + m_sliceEditor.fill(s_SlicerTWaveformBgColor); + m_seekerSlicerTWaveform.fill(s_SlicerTWaveformBgColor); // connect to playback connect(m_slicerTParent, SIGNAL(isPlaying(float, float, float)), this, SLOT(isPlaying(float, float, float))); @@ -70,20 +69,20 @@ WaveForm::WaveForm(int w, int h, SlicerT* instrument, QWidget* parent) updateUI(); } -void WaveForm::drawSeekerWaveform() +void SlicerTWaveform::drawSeekerSlicerTWaveform() { - m_seekerWaveform.fill(s_waveformBgColor); + m_seekerSlicerTWaveform.fill(s_SlicerTWaveformBgColor); if (m_currentSample.frames() < 2048) { return; } - QPainter brush(&m_seekerWaveform); - brush.setPen(s_waveformColor); + QPainter brush(&m_seekerSlicerTWaveform); + brush.setPen(s_SlicerTWaveformColor); - m_currentSample.visualize( - brush, QRect(0, 0, m_seekerWaveform.width(), m_seekerWaveform.height()), 0, m_currentSample.frames()); + m_currentSample.visualize(brush, QRect(0, 0, m_seekerSlicerTWaveform.width(), m_seekerSlicerTWaveform.height()), 0, + m_currentSample.frames()); } -void WaveForm::drawSeeker() +void SlicerTWaveform::drawSeeker() { - m_seeker.fill(s_waveformBgColor); + m_seeker.fill(s_SlicerTWaveformBgColor); if (m_currentSample.frames() < 2048) { return; } QPainter brush(&m_seeker); @@ -122,9 +121,9 @@ void WaveForm::drawSeeker() brush.drawRoundedRect(seekerStartPosX, 0, seekerMiddleWidth - 1, m_seekerHeight - 1, 4, 4); // -1 needed } -void WaveForm::drawEditor() +void SlicerTWaveform::drawEditor() { - m_sliceEditor.fill(s_waveformBgColor); + m_sliceEditor.fill(s_SlicerTWaveformBgColor); QPainter brush(&m_sliceEditor); // draw text if no sample loaded @@ -146,8 +145,8 @@ void WaveForm::drawEditor() brush.setPen(s_playHighlighColor); brush.drawLine(0, m_editorHeight / 2, m_editorWidth, m_editorHeight / 2); - // draw waveform - brush.setPen(s_waveformColor); + // draw SlicerTWaveform + brush.setPen(s_SlicerTWaveformColor); float zoomOffset = ((float)m_editorHeight - m_zoomLevel * m_editorHeight) / 2; m_currentSample.visualize( brush, QRect(0, zoomOffset, m_editorWidth, m_zoomLevel * m_editorHeight), startFrame, endFrame); @@ -166,25 +165,25 @@ void WaveForm::drawEditor() } } -void WaveForm::updateUI() +void SlicerTWaveform::updateUI() { - drawSeekerWaveform(); + drawSeekerSlicerTWaveform(); drawSeeker(); drawEditor(); update(); } -void WaveForm::isPlaying(float current, float start, float end) +void SlicerTWaveform::isPlaying(float current, float start, float end) { m_noteCurrent = current; m_noteStart = start; m_noteEnd = end; - drawSeeker(); // only update seeker, else horrible performance because of waveform redraw + drawSeeker(); // only update seeker, else horrible performance because of SlicerTWaveform redraw update(); } // events -void WaveForm::mousePressEvent(QMouseEvent* me) +void SlicerTWaveform::mousePressEvent(QMouseEvent* me) { float normalizedClickSeeker = (float)(me->x() - m_seekerHorMargin) / m_seekerWidth; float normalizedClickEditor = (float)(me->x()) / m_editorWidth; @@ -201,15 +200,15 @@ void WaveForm::mousePressEvent(QMouseEvent* me) { if (abs(normalizedClickSeeker - m_seekerStart) < m_distanceForClick) // dragging start { - m_currentlyDragging = m_draggingTypes::m_seekerStart; + m_currentlyDragging = DraggingTypes::SeekerStart; } else if (abs(normalizedClickSeeker - m_seekerEnd) < m_distanceForClick) // dragging end { - m_currentlyDragging = m_draggingTypes::m_seekerEnd; + m_currentlyDragging = DraggingTypes::SeekerEnd; } else if (normalizedClickSeeker > m_seekerStart && normalizedClickSeeker < m_seekerEnd) // dragging middle { - m_currentlyDragging = m_draggingTypes::m_seekerMiddle; + m_currentlyDragging = DraggingTypes::SeekerMiddle; m_seekerMiddle = normalizedClickSeeker; } } @@ -226,7 +225,7 @@ void WaveForm::mousePressEvent(QMouseEvent* me) if (abs(xPos - normalizedClickEditor) < m_distanceForClick) { - m_currentlyDragging = m_draggingTypes::m_slicePoint; + m_currentlyDragging = DraggingTypes::SlicePoint; m_sliceSelected = i; } } @@ -243,15 +242,15 @@ void WaveForm::mousePressEvent(QMouseEvent* me) updateUI(); } -void WaveForm::mouseReleaseEvent(QMouseEvent* me) +void SlicerTWaveform::mouseReleaseEvent(QMouseEvent* me) { - m_currentlyDragging = m_draggingTypes::nothing; + m_currentlyDragging = DraggingTypes::Nothing; std::sort(m_slicePoints.begin(), m_slicePoints.end()); updateUI(); } -void WaveForm::mouseMoveEvent(QMouseEvent* me) +void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) { float normalizedClickSeeker = (float)(me->x() - m_seekerHorMargin) / m_seekerWidth; float normalizedClickEditor = (float)(me->x()) / m_editorWidth; @@ -264,15 +263,15 @@ void WaveForm::mouseMoveEvent(QMouseEvent* me) // handle dragging events switch (m_currentlyDragging) { - case m_draggingTypes::m_seekerStart: + case DraggingTypes::SeekerStart: m_seekerStart = std::clamp(normalizedClickSeeker, 0.0f, m_seekerEnd - m_minSeekerDistance); break; - case m_draggingTypes::m_seekerEnd: + case DraggingTypes::SeekerEnd: m_seekerEnd = std::clamp(normalizedClickSeeker, m_seekerStart + m_minSeekerDistance, 1.0f); break; - case m_draggingTypes::m_seekerMiddle: + case DraggingTypes::SeekerMiddle: m_seekerMiddle = normalizedClickSeeker; if (m_seekerMiddle + distStart >= 0 && m_seekerMiddle + distEnd <= 1) @@ -282,17 +281,17 @@ void WaveForm::mouseMoveEvent(QMouseEvent* me) } break; - case m_draggingTypes::m_slicePoint: + case DraggingTypes::SlicePoint: m_slicePoints[m_sliceSelected] = startFrame + normalizedClickEditor * (endFrame - startFrame); m_slicePoints[m_sliceSelected] = std::clamp(m_slicePoints[m_sliceSelected], 0, m_currentSample.frames()); break; - case m_draggingTypes::nothing: + case DraggingTypes::Nothing: break; } updateUI(); } -void WaveForm::mouseDoubleClickEvent(QMouseEvent* me) +void SlicerTWaveform::mouseDoubleClickEvent(QMouseEvent* me) { float normalizedClickEditor = (float)(me->x()) / m_editorWidth; float startFrame = m_seekerStart * m_currentSample.frames(); @@ -312,7 +311,7 @@ void WaveForm::mouseDoubleClickEvent(QMouseEvent* me) std::sort(m_slicePoints.begin(), m_slicePoints.end()); } -void WaveForm::wheelEvent(QWheelEvent* _we) +void SlicerTWaveform::wheelEvent(QWheelEvent* _we) { // m_zoomLevel = _we-> / 360.0f * 2.0f; m_zoomLevel += _we->angleDelta().y() / 360.0f * m_zoomSensitivity; @@ -322,10 +321,10 @@ void WaveForm::wheelEvent(QWheelEvent* _we) update(); } -void WaveForm::paintEvent(QPaintEvent* pe) +void SlicerTWaveform::paintEvent(QPaintEvent* pe) { QPainter p(this); - p.drawPixmap(m_seekerHorMargin, 0, m_seekerWaveform); + p.drawPixmap(m_seekerHorMargin, 0, m_seekerSlicerTWaveform); p.drawPixmap(m_seekerHorMargin, 0, m_seeker); p.drawPixmap(0, m_seekerHeight + m_middleMargin, m_sliceEditor); } diff --git a/plugins/SlicerT/WaveForm.h b/plugins/SlicerT/SlicerTWaveform.h similarity index 81% rename from plugins/SlicerT/WaveForm.h rename to plugins/SlicerT/SlicerTWaveform.h index 5301fbed7ca..266fd7162eb 100644 --- a/plugins/SlicerT/WaveForm.h +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -1,5 +1,5 @@ /* - * WaveForm.h - declaration of class WaveForm + * SlicerTWaveform.h - declaration of class SlicerTWaveform * * Copyright (c) 2023 Daniel Kauss Serna * @@ -22,8 +22,8 @@ * */ -#ifndef WAVEFORM_H -#define WAVEFORM_H +#ifndef LMMS_SlicerT_Waveform_H +#define LMMS_SlicerT_Waveform_H #include #include @@ -39,10 +39,26 @@ class SlicerT; namespace gui { -class WaveForm : public QWidget +class SlicerTWaveform : public QWidget { Q_OBJECT +public slots: + void updateUI(); + void isPlaying(float current, float start, float end); + +public: + SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* parent); + + enum class DraggingTypes + { + Nothing, + SeekerStart, + SeekerEnd, + SeekerMiddle, + SlicePoint, + }; + protected: virtual void mousePressEvent(QMouseEvent* me); virtual void mouseReleaseEvent(QMouseEvent* me); @@ -66,8 +82,8 @@ class WaveForm : public QWidget int m_editorWidth; // colors - static constexpr QColor s_waveformBgColor = QColor(255, 255, 255, 0); - static constexpr QColor s_waveformColor = QColor(123, 49, 212); + static constexpr QColor s_SlicerTWaveformBgColor = QColor(255, 255, 255, 0); + static constexpr QColor s_SlicerTWaveformColor = QColor(123, 49, 212); static constexpr QColor s_playColor = QColor(255, 255, 255, 200); static constexpr QColor s_playHighlighColor = QColor(255, 255, 255, 70); @@ -85,15 +101,7 @@ class WaveForm : public QWidget float m_zoomSensitivity = 0.5f; // dragging vars - enum class m_draggingTypes - { - nothing, - m_seekerStart, - m_seekerEnd, - m_seekerMiddle, - m_slicePoint, - }; - m_draggingTypes m_currentlyDragging; + DraggingTypes m_currentlyDragging; // seeker vars float m_seekerStart = 0; @@ -112,7 +120,7 @@ class WaveForm : public QWidget // pixmaps QPixmap m_sliceArrow; QPixmap m_seeker; - QPixmap m_seekerWaveform; // only stores waveform graphic + QPixmap m_seekerSlicerTWaveform; // only stores SlicerTWaveform graphic QPixmap m_sliceEditor; SampleBuffer& m_currentSample; @@ -121,16 +129,9 @@ class WaveForm : public QWidget std::vector& m_slicePoints; void drawEditor(); - void drawSeekerWaveform(); + void drawSeekerSlicerTWaveform(); void drawSeeker(); - -public slots: - void updateUI(); - void isPlaying(float current, float start, float end); - -public: - WaveForm(int w, int h, SlicerT* instrument, QWidget* parent); }; } // namespace gui } // namespace lmms -#endif // WAVEFORM_H \ No newline at end of file +#endif // LMMS_SlicerT_Waveform_H \ No newline at end of file From 2d55539828a6b5f995d2989cbd3b02813d79464e Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Fri, 13 Oct 2023 16:04:47 +0200 Subject: [PATCH 59/99] static constexpr added --- plugins/SlicerT/SlicerT.h | 4 ++-- plugins/SlicerT/SlicerTWaveform.h | 16 +++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 5c641bdfa29..22f65cb45c0 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -64,8 +64,8 @@ class PhaseVocoder std::vector m_processedWindows; // marks a window processed // timeshift stuff - static const int s_windowSize = 512; - static const int s_overSampling = 32; + static constexpr int s_windowSize = 512; + static constexpr int s_overSampling = 32; // depending on scaleRatio int m_stepSize = 0; diff --git a/plugins/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h index 266fd7162eb..fc10cd1314d 100644 --- a/plugins/SlicerT/SlicerTWaveform.h +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -73,11 +73,13 @@ public slots: int m_width; int m_height; - int m_seekerHorMargin = 5; - int m_seekerHeight = 38; // used to calcualte all vertical sizes - int m_seekerWidth; + // predefined sizes + static constexpr int m_seekerHorMargin = 5; + static constexpr int m_seekerHeight = 38; // used to calcualte all vertical sizes + static constexpr int m_middleMargin = 6; - int m_middleMargin = 6; + // later calculated + int m_seekerWidth; int m_editorHeight; int m_editorWidth; @@ -96,9 +98,9 @@ public slots: static constexpr QColor s_seekerShadowColor = QColor(0, 0, 0, 120); // interaction vars - float m_distanceForClick = 0.03f; - float m_minSeekerDistance = 0.13f; - float m_zoomSensitivity = 0.5f; + static constexpr float m_distanceForClick = 0.03f; + static constexpr float m_minSeekerDistance = 0.13f; + static constexpr float m_zoomSensitivity = 0.5f; // dragging vars DraggingTypes m_currentlyDragging; From a44c42e722c9559c25649cbbbca11f7ccb504434 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Fri, 13 Oct 2023 16:15:46 +0200 Subject: [PATCH 60/99] static vars to public + LMMS guards --- plugins/SlicerT/SlicerT.h | 12 ++++---- plugins/SlicerT/SlicerTWaveform.h | 48 +++++++++++++++---------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 22f65cb45c0..d73f1ca9729 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -22,8 +22,8 @@ * */ -#ifndef SLICERT_H -#define SLICERT_H +#ifndef LMMS_SLICERT_H +#define LMMS_SLICERT_H #include @@ -51,6 +51,10 @@ class PhaseVocoder int frames() { return m_processedBuffer.size(); } float scaleRatio() { return m_scaleRatio; } + // timeshift config + static constexpr int s_windowSize = 512; + static constexpr int s_overSampling = 32; + private: QMutex m_dataLock; // original data @@ -63,10 +67,6 @@ class PhaseVocoder std::vector m_processedBuffer; // final output std::vector m_processedWindows; // marks a window processed - // timeshift stuff - static constexpr int s_windowSize = 512; - static constexpr int s_overSampling = 32; - // depending on scaleRatio int m_stepSize = 0; int m_numWindows = 0; diff --git a/plugins/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h index fc10cd1314d..34accb9bdec 100644 --- a/plugins/SlicerT/SlicerTWaveform.h +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -50,6 +50,30 @@ public slots: public: SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* parent); + // predefined sizes + static constexpr int m_seekerHorMargin = 5; + static constexpr int m_seekerHeight = 38; // used to calcualte all vertical sizes + static constexpr int m_middleMargin = 6; + + // colors + static constexpr QColor s_SlicerTWaveformBgColor = QColor(255, 255, 255, 0); + static constexpr QColor s_SlicerTWaveformColor = QColor(123, 49, 212); + + static constexpr QColor s_playColor = QColor(255, 255, 255, 200); + static constexpr QColor s_playHighlighColor = QColor(255, 255, 255, 70); + + static constexpr QColor s_sliceColor = QColor(218, 193, 255); + static constexpr QColor s_selectedSliceColor = QColor(178, 153, 215); + + static constexpr QColor s_seekerColor = QColor(178, 115, 255); + static constexpr QColor s_seekerHighlightColor = QColor(178, 115, 255, 100); + static constexpr QColor s_seekerShadowColor = QColor(0, 0, 0, 120); + + // interaction vars + static constexpr float m_distanceForClick = 0.03f; + static constexpr float m_minSeekerDistance = 0.13f; + static constexpr float m_zoomSensitivity = 0.5f; + enum class DraggingTypes { Nothing, @@ -73,35 +97,11 @@ public slots: int m_width; int m_height; - // predefined sizes - static constexpr int m_seekerHorMargin = 5; - static constexpr int m_seekerHeight = 38; // used to calcualte all vertical sizes - static constexpr int m_middleMargin = 6; - // later calculated int m_seekerWidth; int m_editorHeight; int m_editorWidth; - // colors - static constexpr QColor s_SlicerTWaveformBgColor = QColor(255, 255, 255, 0); - static constexpr QColor s_SlicerTWaveformColor = QColor(123, 49, 212); - - static constexpr QColor s_playColor = QColor(255, 255, 255, 200); - static constexpr QColor s_playHighlighColor = QColor(255, 255, 255, 70); - - static constexpr QColor s_sliceColor = QColor(218, 193, 255); - static constexpr QColor s_selectedSliceColor = QColor(178, 153, 215); - - static constexpr QColor s_seekerColor = QColor(178, 115, 255); - static constexpr QColor s_seekerHighlightColor = QColor(178, 115, 255, 100); - static constexpr QColor s_seekerShadowColor = QColor(0, 0, 0, 120); - - // interaction vars - static constexpr float m_distanceForClick = 0.03f; - static constexpr float m_minSeekerDistance = 0.13f; - static constexpr float m_zoomSensitivity = 0.5f; - // dragging vars DraggingTypes m_currentlyDragging; From 7e7d42c4476723eed7c74e762d63c3830e618d1a Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Fri, 13 Oct 2023 16:43:30 +0200 Subject: [PATCH 61/99] remove references in classes --- plugins/SlicerT/SlicerTWaveform.cpp | 69 +++++++++++++++-------------- plugins/SlicerT/SlicerTWaveform.h | 5 +-- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index 889ad001bb9..9c8d191b5e5 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -22,8 +22,10 @@ * */ -#include "SlicerT.h" #include "SlicerTWaveform.h" + +#include "SlicerT.h" +#include "SlicerTView.h" #include "embed.h" namespace lmms { @@ -46,13 +48,11 @@ SlicerTWaveform::SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* par , m_seeker(QPixmap(m_seekerWidth, m_seekerHeight)) , m_seekerSlicerTWaveform(QPixmap(m_seekerWidth, m_seekerHeight)) , m_sliceEditor(QPixmap(w, m_editorHeight)) - , // references to instrument vars - m_currentSample(instrument->m_originalSample) , m_slicerTParent(instrument) - , m_slicePoints(instrument->m_slicePoints) - + , m_currentSample(&instrument->m_originalSample) + , m_slicePoints(&instrument->m_slicePoints) { // window config setFixedSize(m_width, m_height); @@ -72,25 +72,25 @@ SlicerTWaveform::SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* par void SlicerTWaveform::drawSeekerSlicerTWaveform() { m_seekerSlicerTWaveform.fill(s_SlicerTWaveformBgColor); - if (m_currentSample.frames() < 2048) { return; } + if (m_currentSample->frames() < 2048) { return; } QPainter brush(&m_seekerSlicerTWaveform); brush.setPen(s_SlicerTWaveformColor); - m_currentSample.visualize(brush, QRect(0, 0, m_seekerSlicerTWaveform.width(), m_seekerSlicerTWaveform.height()), 0, - m_currentSample.frames()); + m_currentSample->visualize(brush, QRect(0, 0, m_seekerSlicerTWaveform.width(), m_seekerSlicerTWaveform.height()), 0, + m_currentSample->frames()); } void SlicerTWaveform::drawSeeker() { m_seeker.fill(s_SlicerTWaveformBgColor); - if (m_currentSample.frames() < 2048) { return; } + if (m_currentSample->frames() < 2048) { return; } QPainter brush(&m_seeker); // draw slice points brush.setPen(s_sliceColor); - for (int i = 0; i < m_slicePoints.size(); i++) + for (int i = 0; i < m_slicePoints->size(); i++) { - float xPos = (float)m_slicePoints[i] / m_currentSample.frames() * m_seekerWidth; + float xPos = (float)m_slicePoints->at(i) / m_currentSample->frames() * m_seekerWidth; brush.drawLine(xPos, 0, xPos, m_seekerHeight); } @@ -127,7 +127,7 @@ void SlicerTWaveform::drawEditor() QPainter brush(&m_sliceEditor); // draw text if no sample loaded - if (m_currentSample.frames() < 2048) + if (m_currentSample->frames() < 2048) { brush.setPen(s_playHighlighColor); brush.setFont(QFont(brush.font().family(), 9.0f, -1, false)); @@ -137,8 +137,8 @@ void SlicerTWaveform::drawEditor() } // editor boundaries - float startFrame = m_seekerStart * m_currentSample.frames(); - float endFrame = m_seekerEnd * m_currentSample.frames(); + float startFrame = m_seekerStart * m_currentSample->frames(); + float endFrame = m_seekerEnd * m_currentSample->frames(); float numFramesToDraw = endFrame - startFrame; // 0 centered line @@ -148,14 +148,14 @@ void SlicerTWaveform::drawEditor() // draw SlicerTWaveform brush.setPen(s_SlicerTWaveformColor); float zoomOffset = ((float)m_editorHeight - m_zoomLevel * m_editorHeight) / 2; - m_currentSample.visualize( + m_currentSample->visualize( brush, QRect(0, zoomOffset, m_editorWidth, m_zoomLevel * m_editorHeight), startFrame, endFrame); // draw slicepoints brush.setPen(QPen(s_sliceColor, 2)); - for (int i = 0; i < m_slicePoints.size(); i++) + for (int i = 0; i < m_slicePoints->size(); i++) { - float xPos = (float)(m_slicePoints[i] - startFrame) / numFramesToDraw * m_editorWidth; + float xPos = (float)(m_slicePoints->at(i) - startFrame) / numFramesToDraw * m_editorWidth; if (i == m_sliceSelected) { brush.setPen(QPen(s_selectedSliceColor, 2)); } else { brush.setPen(QPen(s_sliceColor, 2)); } @@ -215,12 +215,12 @@ void SlicerTWaveform::mousePressEvent(QMouseEvent* me) else // editor click { m_sliceSelected = -1; - float startFrame = m_seekerStart * m_currentSample.frames(); - float endFrame = m_seekerEnd * m_currentSample.frames(); + float startFrame = m_seekerStart * m_currentSample->frames(); + float endFrame = m_seekerEnd * m_currentSample->frames(); // select slice - for (int i = 0; i < m_slicePoints.size(); i++) + for (int i = 0; i < m_slicePoints->size(); i++) { - int sliceIndex = m_slicePoints[i]; + int sliceIndex = m_slicePoints->at(i); float xPos = (float)(sliceIndex - startFrame) / (float)(endFrame - startFrame); if (abs(xPos - normalizedClickEditor) < m_distanceForClick) @@ -233,9 +233,9 @@ void SlicerTWaveform::mousePressEvent(QMouseEvent* me) if (me->button() == Qt::MouseButton::RightButton) // erase selected slice { - if (m_sliceSelected != -1 && m_slicePoints.size() > 2) + if (m_sliceSelected != -1 && m_slicePoints->size() > 2) { - m_slicePoints.erase(m_slicePoints.begin() + m_sliceSelected); + m_slicePoints->erase(m_slicePoints->begin() + m_sliceSelected); m_sliceSelected = -1; } } @@ -245,7 +245,7 @@ void SlicerTWaveform::mousePressEvent(QMouseEvent* me) void SlicerTWaveform::mouseReleaseEvent(QMouseEvent* me) { m_currentlyDragging = DraggingTypes::Nothing; - std::sort(m_slicePoints.begin(), m_slicePoints.end()); + std::sort(m_slicePoints->begin(), m_slicePoints->end()); updateUI(); } @@ -257,8 +257,8 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) float distStart = m_seekerStart - m_seekerMiddle; float distEnd = m_seekerEnd - m_seekerMiddle; - float startFrame = m_seekerStart * m_currentSample.frames(); - float endFrame = m_seekerEnd * m_currentSample.frames(); + float startFrame = m_seekerStart * m_currentSample->frames(); + float endFrame = m_seekerEnd * m_currentSample->frames(); // handle dragging events switch (m_currentlyDragging) @@ -282,8 +282,9 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) break; case DraggingTypes::SlicePoint: - m_slicePoints[m_sliceSelected] = startFrame + normalizedClickEditor * (endFrame - startFrame); - m_slicePoints[m_sliceSelected] = std::clamp(m_slicePoints[m_sliceSelected], 0, m_currentSample.frames()); + m_slicePoints->at(m_sliceSelected) = startFrame + normalizedClickEditor * (endFrame - startFrame); + m_slicePoints->at(m_sliceSelected) + = std::clamp(m_slicePoints->at(m_sliceSelected), 0, m_currentSample->frames()); break; case DraggingTypes::Nothing: break; @@ -294,21 +295,21 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) void SlicerTWaveform::mouseDoubleClickEvent(QMouseEvent* me) { float normalizedClickEditor = (float)(me->x()) / m_editorWidth; - float startFrame = m_seekerStart * m_currentSample.frames(); - float endFrame = m_seekerEnd * m_currentSample.frames(); + float startFrame = m_seekerStart * m_currentSample->frames(); + float endFrame = m_seekerEnd * m_currentSample->frames(); float slicePosition = startFrame + normalizedClickEditor * (endFrame - startFrame); - for (int i = 0; i < m_slicePoints.size(); i++) + for (int i = 0; i < m_slicePoints->size(); i++) { - if (m_slicePoints[i] < slicePosition) + if (m_slicePoints->at(i) < slicePosition) { - m_slicePoints.insert(m_slicePoints.begin() + i, slicePosition); + m_slicePoints->insert(m_slicePoints->begin() + i, slicePosition); break; } } - std::sort(m_slicePoints.begin(), m_slicePoints.end()); + std::sort(m_slicePoints->begin(), m_slicePoints->end()); } void SlicerTWaveform::wheelEvent(QWheelEvent* _we) diff --git a/plugins/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h index 34accb9bdec..f86a4c515d1 100644 --- a/plugins/SlicerT/SlicerTWaveform.h +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -125,10 +125,9 @@ public slots: QPixmap m_seekerSlicerTWaveform; // only stores SlicerTWaveform graphic QPixmap m_sliceEditor; - SampleBuffer& m_currentSample; - SlicerT* m_slicerTParent; - std::vector& m_slicePoints; + SampleBuffer* m_currentSample; + std::vector* m_slicePoints; void drawEditor(); void drawSeekerSlicerTWaveform(); From be19d9b1b77acdcea2541f15753e2c575fc9dc44 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sat, 14 Oct 2023 00:45:41 +0200 Subject: [PATCH 62/99] remove constexpr and parent pointer in waveform --- plugins/SlicerT/SlicerT.cpp | 2 +- plugins/SlicerT/SlicerT.h | 4 ++-- plugins/SlicerT/SlicerTWaveform.cpp | 5 ++--- plugins/SlicerT/SlicerTWaveform.h | 31 ++++++++++++++--------------- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index a9f5b76a9c4..0af69bbb3e6 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -664,7 +664,7 @@ extern "C" { // necessary for getting instance out of shared lib PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* m, void*) { - return (new SlicerT(static_cast(m))); + return new SlicerT(static_cast(m)); } } // extern } // namespace lmms diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index d73f1ca9729..a134e0db248 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -52,8 +52,8 @@ class PhaseVocoder float scaleRatio() { return m_scaleRatio; } // timeshift config - static constexpr int s_windowSize = 512; - static constexpr int s_overSampling = 32; + const int s_windowSize = 512; + const int s_overSampling = 32; private: QMutex m_dataLock; diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index 9c8d191b5e5..79469602d91 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -50,7 +50,6 @@ SlicerTWaveform::SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* par , m_sliceEditor(QPixmap(w, m_editorHeight)) // references to instrument vars - , m_slicerTParent(instrument) , m_currentSample(&instrument->m_originalSample) , m_slicePoints(&instrument->m_slicePoints) { @@ -63,8 +62,8 @@ SlicerTWaveform::SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* par m_seekerSlicerTWaveform.fill(s_SlicerTWaveformBgColor); // connect to playback - connect(m_slicerTParent, SIGNAL(isPlaying(float, float, float)), this, SLOT(isPlaying(float, float, float))); - connect(m_slicerTParent, SIGNAL(dataChanged()), this, SLOT(updateUI())); + connect(instrument, SIGNAL(isPlaying(float, float, float)), this, SLOT(isPlaying(float, float, float))); + connect(instrument, SIGNAL(dataChanged()), this, SLOT(updateUI())); updateUI(); } diff --git a/plugins/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h index f86a4c515d1..fb20a758c8d 100644 --- a/plugins/SlicerT/SlicerTWaveform.h +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -51,28 +51,28 @@ public slots: SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* parent); // predefined sizes - static constexpr int m_seekerHorMargin = 5; - static constexpr int m_seekerHeight = 38; // used to calcualte all vertical sizes - static constexpr int m_middleMargin = 6; + const int m_seekerHorMargin = 5; + const int m_seekerHeight = 38; // used to calcualte all vertical sizes + const int m_middleMargin = 6; // colors - static constexpr QColor s_SlicerTWaveformBgColor = QColor(255, 255, 255, 0); - static constexpr QColor s_SlicerTWaveformColor = QColor(123, 49, 212); + const QColor s_SlicerTWaveformBgColor = QColor(255, 255, 255, 0); + const QColor s_SlicerTWaveformColor = QColor(123, 49, 212); - static constexpr QColor s_playColor = QColor(255, 255, 255, 200); - static constexpr QColor s_playHighlighColor = QColor(255, 255, 255, 70); + const QColor s_playColor = QColor(255, 255, 255, 200); + const QColor s_playHighlighColor = QColor(255, 255, 255, 70); - static constexpr QColor s_sliceColor = QColor(218, 193, 255); - static constexpr QColor s_selectedSliceColor = QColor(178, 153, 215); + const QColor s_sliceColor = QColor(218, 193, 255); + const QColor s_selectedSliceColor = QColor(178, 153, 215); - static constexpr QColor s_seekerColor = QColor(178, 115, 255); - static constexpr QColor s_seekerHighlightColor = QColor(178, 115, 255, 100); - static constexpr QColor s_seekerShadowColor = QColor(0, 0, 0, 120); + const QColor s_seekerColor = QColor(178, 115, 255); + const QColor s_seekerHighlightColor = QColor(178, 115, 255, 100); + const QColor s_seekerShadowColor = QColor(0, 0, 0, 120); // interaction vars - static constexpr float m_distanceForClick = 0.03f; - static constexpr float m_minSeekerDistance = 0.13f; - static constexpr float m_zoomSensitivity = 0.5f; + const float m_distanceForClick = 0.03f; + const float m_minSeekerDistance = 0.13f; + const float m_zoomSensitivity = 0.5f; enum class DraggingTypes { @@ -125,7 +125,6 @@ public slots: QPixmap m_seekerSlicerTWaveform; // only stores SlicerTWaveform graphic QPixmap m_sliceEditor; - SlicerT* m_slicerTParent; SampleBuffer* m_currentSample; std::vector* m_slicePoints; From 0511e747e14efe50ab8b595c923179ece7c954b5 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sat, 14 Oct 2023 01:27:09 +0200 Subject: [PATCH 63/99] std::array for fft --- plugins/SlicerT/SlicerT.cpp | 5 ++--- plugins/SlicerT/SlicerT.h | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 0af69bbb3e6..4d5b4b34b21 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -53,8 +53,7 @@ Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = { } // end extern PhaseVocoder::PhaseVocoder() - : m_FFTSpectrum(s_windowSize) - , m_FFTInput(s_windowSize, 0) + : m_FFTInput(s_windowSize, 0) , m_IFFTReconstruction(s_windowSize, 0) , m_allMagnitudes(s_windowSize, 0) , m_allFrequencies(s_windowSize, 0) @@ -432,7 +431,7 @@ void SlicerT::findSlices() // buffers std::vector prevMags(windowSize / 2, 0); std::vector fftIn(windowSize, 0); - std::vector fftOut(windowSize); + std::array fftOut; fftwf_plan fftPlan = fftwf_plan_dft_r2c_1d(windowSize, fftIn.data(), fftOut.data(), FFTW_MEASURE); diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index a134e0db248..3eff4ead1a2 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -52,8 +52,8 @@ class PhaseVocoder float scaleRatio() { return m_scaleRatio; } // timeshift config - const int s_windowSize = 512; - const int s_overSampling = 32; + static const int s_windowSize = 512; + static const int s_overSampling = 32; private: QMutex m_dataLock; @@ -76,7 +76,7 @@ class PhaseVocoder float m_expectedPhaseOut = 0; // buffers - std::vector m_FFTSpectrum; + std::array m_FFTSpectrum; std::vector m_FFTInput; std::vector m_IFFTReconstruction; std::vector m_allMagnitudes; From 9a9ca506cb4aa424676c115a11b8a6447289ffdd Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sat, 14 Oct 2023 01:29:59 +0200 Subject: [PATCH 64/99] fixed wrong name in style --- data/themes/classic/style.css | 2 +- data/themes/default/style.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index 6e39fbcc310..ad60c227dbb 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -902,7 +902,7 @@ lmms--gui--SidInstrumentView lmms--gui--Knob { qproperty-lineWidth: 2; } -lmms--gui--SlicerTUI lmms--gui--Knob { +lmms--gui--SlicerTView lmms--gui--Knob { color: rgb(162, 128, 226); qproperty-outerColor: rgb( 162, 128, 226 ); qproperty-innerRadius: 1; diff --git a/data/themes/default/style.css b/data/themes/default/style.css index 2b875f47ebe..c4c968eb652 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -946,7 +946,7 @@ lmms--gui--SidInstrumentView lmms--gui--Knob { qproperty-lineWidth: 2; } -lmms--gui--SlicerTUI lmms--gui--Knob { +lmms--gui--SlicerTView lmms--gui--Knob { color: rgb(162, 128, 226); qproperty-outerColor: rgb( 162, 128, 226 ); qproperty-innerRadius: 1; From ef81ac5e5b476a6ca7efd7545b6881269f8fa5d1 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sat, 14 Oct 2023 13:44:24 +0200 Subject: [PATCH 65/99] remove c style casts --- plugins/SlicerT/SlicerT.cpp | 53 +++++++++++++++-------------- plugins/SlicerT/SlicerTWaveform.cpp | 20 +++++------ 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 4d5b4b34b21..e68600bda52 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -118,8 +118,8 @@ void PhaseVocoder::getFrames(std::vector& outData, int start, int frames) } int windowMargin = s_overSampling / 2; // numbers of windows before full quality - int startWindow = (float)start / m_outStepSize - windowMargin; - int endWindow = (float)(start + frames) / m_outStepSize + windowMargin; + int startWindow = static_cast(start) / m_outStepSize - windowMargin; + int endWindow = static_cast((start + frames)) / m_outStepSize + windowMargin; startWindow = std::clamp(startWindow, 0, m_numWindows - 1); endWindow = std::clamp(endWindow, 0, m_numWindows - 1); @@ -157,12 +157,12 @@ void PhaseVocoder::updateParams(float newRatio) m_dataLock.lock(); m_scaleRatio = newRatio; - m_stepSize = (float)s_windowSize / s_overSampling; - m_numWindows = (float)m_originalBuffer.size() / m_stepSize - s_overSampling - 1; - m_outStepSize = m_scaleRatio * (float)m_stepSize; // float, else inaccurate + m_stepSize = static_cast(s_windowSize) / s_overSampling; + m_numWindows = static_cast(m_originalBuffer.size()) / m_stepSize - s_overSampling - 1; + m_outStepSize = m_scaleRatio * m_stepSize; // float, else inaccurate m_freqPerBin = m_originalSampleRate / s_windowSize; - m_expectedPhaseIn = 2. * F_PI * (float)m_stepSize / (float)s_windowSize; - m_expectedPhaseOut = 2. * F_PI * (float)m_outStepSize / (float)s_windowSize; + m_expectedPhaseIn = 2. * F_PI * m_stepSize / s_windowSize; + m_expectedPhaseOut = 2. * F_PI * m_outStepSize / s_windowSize; m_processedBuffer.resize(m_scaleRatio * m_originalBuffer.size(), 0); @@ -184,8 +184,8 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) { // declare vars float real, imag, phase, magnitude, freq, deltaPhase = 0; - int windowStart = (float)windowNum * m_stepSize; - int windowIndex = (float)windowNum * s_windowSize; + int windowStart = static_cast(windowNum) * m_stepSize; + int windowIndex = static_cast(windowNum) * s_windowSize; if (!useCache) { // normal stuff @@ -209,7 +209,7 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) - m_lastPhase[std::max(0, windowIndex + j - s_windowSize)]; // subtract prev pahse to get phase diference m_lastPhase[windowIndex + j] = phase; - freq -= (float)j * m_expectedPhaseIn; // subtract expected phase + freq -= m_expectedPhaseIn * j; // subtract expected phase // at this point, freq is the difference in phase // between the last phase, having removed the expected phase at this point in the sample @@ -218,10 +218,10 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) freq = fmod(freq + F_PI, -2.0f * F_PI) + F_PI; // convert phase difference into bin freq mulitplier - freq = (float)s_overSampling * freq / (2. * F_PI); + freq = freq * s_overSampling / (2. * F_PI); // add to the expected freq the change in freq calculated from the phase diff - freq = (float)j * m_freqPerBin + freq * m_freqPerBin; + freq = m_freqPerBin * j + m_freqPerBin * freq; m_allMagnitudes[j] = magnitude; m_allFrequencies[j] = freq; @@ -244,7 +244,7 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) freq = m_allFrequencies[j]; // difference to bin freq mulitplier - deltaPhase = freq - (float)j * m_freqPerBin; + deltaPhase = freq - m_freqPerBin * j; // convert to phase difference deltaPhase /= m_freqPerBin; @@ -253,7 +253,7 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) deltaPhase = 2. * F_PI * deltaPhase / s_overSampling; // add the expected phase - deltaPhase += (float)j * m_expectedPhaseOut; + deltaPhase += m_expectedPhaseOut * j; // sum this phase to the total, to keep track of the out phase along the sample m_sumPhase[windowIndex + j] += deltaPhase; @@ -325,7 +325,7 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) const float inversePitchRatio = 1.0f / pitchRatio; // update scaling parameters - float speedRatio = (float)m_originalBPM.value() / bpm; + float speedRatio = static_cast(m_originalBPM.value()) / bpm; if (!m_enableSync.value()) { speedRatio = 1; } // disable timeshift m_phaseVocoder.setScaleRatio(speedRatio); speedRatio *= inversePitchRatio; // adjust for pitch bend @@ -351,7 +351,8 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) if (noteFramesLeft > 0) { int framesToCopy = pitchRatio * frames + 1; // just in case - int framesIndex = std::min((int)(pitchRatio * currentNoteFrame), m_phaseVocoder.frames() - framesToCopy); + int framesIndex = pitchRatio * currentNoteFrame; + framesIndex = std::min(framesIndex, m_phaseVocoder.frames() - framesToCopy); // load sample segmengt, with regards to pitch settings std::vector prePitchBuffer(framesToCopy, {0.0f, 0.0f}); @@ -377,7 +378,7 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) { for (int i = 0; i < frames; i++) { - float fadeValue = (float)(noteFramesLeft - i) / m_fadeOutFrames.value(); + float fadeValue = static_cast(noteFramesLeft - i) / m_fadeOutFrames.value(); // if the workingbuffer extends the sample fadeValue = std::clamp(fadeValue, 0.0f, 1.0f); fadeValue = pow(fadeValue, 2); @@ -390,9 +391,9 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) instrumentTrack()->processAudioBuffer(workingBuffer, frames + offset, handle); // calculate absolute for the SlicerTWaveform - float absoluteCurrentNote = (float)currentNoteFrame / totalFrames; - float absoluteStartNote = (float)sliceStart / totalFrames; - float abslouteEndNote = (float)sliceEnd / totalFrames; + float absoluteCurrentNote = static_cast(currentNoteFrame) / totalFrames; + float absoluteStartNote = static_cast(sliceStart) / totalFrames; + float abslouteEndNote = static_cast(sliceEnd) / totalFrames; emit isPlaying(absoluteCurrentNote, absoluteStartNote, abslouteEndNote); } else { emit isPlaying(-1, 0, 0); } @@ -531,7 +532,7 @@ void SlicerT::writeToMidi(std::vector* outClip) if (m_originalSample.frames() < 2048) { return; } // update incase bpm changed - float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo(); + float speedRatio = static_cast(m_originalBPM.value()) / Engine::getSong()->getTempo(); m_phaseVocoder.setScaleRatio(speedRatio); // calculate how many "beats" are in the sample @@ -539,7 +540,7 @@ void SlicerT::writeToMidi(std::vector* outClip) float sampleRate = m_originalSample.sampleRate(); float bpm = Engine::getSong()->getTempo(); float samplesPerBeat = 60.0f / bpm * sampleRate; - float beats = (float)m_phaseVocoder.frames() / samplesPerBeat; + float beats = m_phaseVocoder.frames() / samplesPerBeat; // calculate how many ticks in sample float barsInSample = beats / Engine::getSong()->getTimeSigModel().getDenominator(); @@ -551,7 +552,7 @@ void SlicerT::writeToMidi(std::vector* outClip) for (int i = 0; i < m_slicePoints.size() - 1; i++) { float sliceStart = lastEnd; - float sliceEnd = (float)m_slicePoints[i + 1] / m_originalSample.frames() * totalTicks; + float sliceEnd = totalTicks * m_slicePoints[i + 1] / m_originalSample.frames(); Note sliceNote = Note(); sliceNote.setKey(i + m_parentTrack->baseNote()); @@ -571,7 +572,7 @@ void SlicerT::updateFile(QString file) findBPM(); findSlices(); - float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo(); + float speedRatio = static_cast(m_originalBPM.value()) / Engine::getSong()->getTempo(); m_phaseVocoder.loadSample( m_originalSample.data(), m_originalSample.frames(), m_originalSample.sampleRate(), speedRatio); @@ -594,7 +595,7 @@ void SlicerT::saveSettings(QDomDocument& document, QDomElement& element) } // save slice points - element.setAttribute("totalSlices", (int)m_slicePoints.size()); + element.setAttribute("totalSlices", static_cast(m_slicePoints.size())); for (int i = 0; i < m_slicePoints.size(); i++) { element.setAttribute(tr("slice_%1").arg(i), m_slicePoints[i]); @@ -642,7 +643,7 @@ void SlicerT::loadSettings(const QDomElement& element) m_originalBPM.loadSettings(element, "origBPM"); // create dynamic buffer - float speedRatio = (float)m_originalBPM.value() / Engine::getSong()->getTempo(); + float speedRatio = static_cast(m_originalBPM.value()) / Engine::getSong()->getTempo(); m_phaseVocoder.loadSample( m_originalSample.data(), m_originalSample.frames(), m_originalSample.sampleRate(), speedRatio); diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index 79469602d91..f14cd3a01ad 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -89,7 +89,7 @@ void SlicerTWaveform::drawSeeker() brush.setPen(s_sliceColor); for (int i = 0; i < m_slicePoints->size(); i++) { - float xPos = (float)m_slicePoints->at(i) / m_currentSample->frames() * m_seekerWidth; + float xPos = static_cast(m_slicePoints->at(i)) / m_currentSample->frames() * m_seekerWidth; brush.drawLine(xPos, 0, xPos, m_seekerHeight); } @@ -146,7 +146,7 @@ void SlicerTWaveform::drawEditor() // draw SlicerTWaveform brush.setPen(s_SlicerTWaveformColor); - float zoomOffset = ((float)m_editorHeight - m_zoomLevel * m_editorHeight) / 2; + float zoomOffset = (m_editorHeight - m_zoomLevel * m_editorHeight) / 2; m_currentSample->visualize( brush, QRect(0, zoomOffset, m_editorWidth, m_zoomLevel * m_editorHeight), startFrame, endFrame); @@ -154,13 +154,13 @@ void SlicerTWaveform::drawEditor() brush.setPen(QPen(s_sliceColor, 2)); for (int i = 0; i < m_slicePoints->size(); i++) { - float xPos = (float)(m_slicePoints->at(i) - startFrame) / numFramesToDraw * m_editorWidth; + float xPos = (m_slicePoints->at(i) - startFrame) / numFramesToDraw * m_editorWidth; if (i == m_sliceSelected) { brush.setPen(QPen(s_selectedSliceColor, 2)); } else { brush.setPen(QPen(s_sliceColor, 2)); } brush.drawLine(xPos, 0, xPos, m_editorHeight); - brush.drawPixmap(xPos - (float)m_sliceArrow.width() / 2, 0, m_sliceArrow); + brush.drawPixmap(xPos - m_sliceArrow.width() / 2.0f, 0, m_sliceArrow); } } @@ -184,8 +184,8 @@ void SlicerTWaveform::isPlaying(float current, float start, float end) // events void SlicerTWaveform::mousePressEvent(QMouseEvent* me) { - float normalizedClickSeeker = (float)(me->x() - m_seekerHorMargin) / m_seekerWidth; - float normalizedClickEditor = (float)(me->x()) / m_editorWidth; + float normalizedClickSeeker = static_cast(me->x() - m_seekerHorMargin) / m_seekerWidth; + float normalizedClickEditor = static_cast(me->x()) / m_editorWidth; // reset seeker on middle click if (me->button() == Qt::MouseButton::MiddleButton) { @@ -220,7 +220,7 @@ void SlicerTWaveform::mousePressEvent(QMouseEvent* me) for (int i = 0; i < m_slicePoints->size(); i++) { int sliceIndex = m_slicePoints->at(i); - float xPos = (float)(sliceIndex - startFrame) / (float)(endFrame - startFrame); + float xPos = (sliceIndex - startFrame) / (endFrame - startFrame); if (abs(xPos - normalizedClickEditor) < m_distanceForClick) { @@ -251,8 +251,8 @@ void SlicerTWaveform::mouseReleaseEvent(QMouseEvent* me) void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) { - float normalizedClickSeeker = (float)(me->x() - m_seekerHorMargin) / m_seekerWidth; - float normalizedClickEditor = (float)(me->x()) / m_editorWidth; + float normalizedClickSeeker = static_cast(me->x() - m_seekerHorMargin) / m_seekerWidth; + float normalizedClickEditor = static_cast(me->x()) / m_editorWidth; float distStart = m_seekerStart - m_seekerMiddle; float distEnd = m_seekerEnd - m_seekerMiddle; @@ -293,7 +293,7 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) void SlicerTWaveform::mouseDoubleClickEvent(QMouseEvent* me) { - float normalizedClickEditor = (float)(me->x()) / m_editorWidth; + float normalizedClickEditor = static_cast(me->x()) / m_editorWidth; float startFrame = m_seekerStart * m_currentSample->frames(); float endFrame = m_seekerEnd * m_currentSample->frames(); From 44e712dbdd65cee388e13fe766f5dd29029a8a30 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sat, 14 Oct 2023 15:33:03 +0200 Subject: [PATCH 66/99] use src_process --- plugins/SlicerT/SlicerT.cpp | 16 +++++++++++++--- plugins/SlicerT/SlicerT.h | 4 +++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index e68600bda52..47fa0d1ff2b 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -111,7 +111,8 @@ void PhaseVocoder::getFrames(std::vector& outData, int start, int frames) if (m_originalBuffer.size() < 2048) { return; } m_dataLock.lock(); - if (m_scaleRatio == 1) { // directly copy original data + if (typeInfo::isEqual(m_scaleRatio, 1.0f)) + { // directly copy original data std::copy_n(m_originalBuffer.data() + start, frames, outData.data()); m_dataLock.unlock(); return; @@ -309,6 +310,13 @@ SlicerT::SlicerT(InstrumentTrack* instrumentTrack) m_sliceSnap.addItem("1/16"); m_sliceSnap.addItem("1/32"); m_sliceSnap.setValue(0); // no snap by default + + m_resamplerState = src_new(SRC_SINC_MEDIUM_QUALITY, 2, nullptr); // no error +} + +SlicerT::~SlicerT() +{ + src_delete(m_resamplerState); } void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) @@ -359,7 +367,7 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) m_phaseVocoder.getFrames(prePitchBuffer.data(), framesIndex, framesToCopy); // if pitch is changed, resample, else just copy - if (pitchRatio != 1.0f) + if (!typeInfo::isEqual(pitchRatio, 1.0f)) { SRC_DATA resamplerData; @@ -369,7 +377,7 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) resamplerData.output_frames = frames; resamplerData.src_ratio = inversePitchRatio; - src_simple(&resamplerData, SRC_LINEAR, 2); + src_process(m_resamplerState, &resamplerData); } else { memcpy(workingBuffer + offset, prePitchBuffer.data(), frames * sizeof(sampleFrame)); } @@ -576,6 +584,8 @@ void SlicerT::updateFile(QString file) m_phaseVocoder.loadSample( m_originalSample.data(), m_originalSample.frames(), m_originalSample.sampleRate(), speedRatio); + src_reset(m_resamplerState); + emit dataChanged(); } diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 3eff4ead1a2..19ded228d0c 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -162,7 +162,7 @@ public slots: public: SlicerT(InstrumentTrack* instrumentTrack); - ~SlicerT() override = default; + ~SlicerT(); void playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) override; @@ -189,6 +189,8 @@ public slots: SampleBuffer m_originalSample; DynamicPlaybackBuffer m_phaseVocoder; + SRC_STATE* m_resamplerState; + std::vector m_slicePoints; InstrumentTrack* m_parentTrack; From d74ad4ae91f1003a7a9ab965d971ed1a1ba5001a Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 15 Oct 2023 02:46:47 +0200 Subject: [PATCH 67/99] use note vector for return --- plugins/SlicerT/SlicerT.cpp | 8 +++++--- plugins/SlicerT/SlicerT.h | 2 +- plugins/SlicerT/SlicerTView.cpp | 3 +-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 47fa0d1ff2b..8d9902c1bbf 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -535,9 +535,9 @@ void SlicerT::findBPM() m_originalBPM.setInitValue(bpmEstimate); } -void SlicerT::writeToMidi(std::vector* outClip) +std::vector SlicerT::getMidi() { - if (m_originalSample.frames() < 2048) { return; } + std::vector outputNotes; // update incase bpm changed float speedRatio = static_cast(m_originalBPM.value()) / Engine::getSong()->getTempo(); @@ -566,10 +566,12 @@ void SlicerT::writeToMidi(std::vector* outClip) sliceNote.setKey(i + m_parentTrack->baseNote()); sliceNote.setPos(sliceStart); sliceNote.setLength(sliceEnd - sliceStart + 1); // + 1 needed for whatever reason - outClip->push_back(sliceNote); + outputNotes.push_back(sliceNote); lastEnd = sliceEnd; } + + return outputNotes; } void SlicerT::updateFile(QString file) diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 19ded228d0c..035573c75ee 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -175,7 +175,7 @@ public slots: QString nodeName() const override; gui::PluginView* instantiateView(QWidget* parent) override; - void writeToMidi(std::vector* outClip); + std::vector getMidi(); private: // models diff --git a/plugins/SlicerT/SlicerTView.cpp b/plugins/SlicerT/SlicerTView.cpp index dde522adc95..db5ecca6a33 100644 --- a/plugins/SlicerT/SlicerTView.cpp +++ b/plugins/SlicerT/SlicerTView.cpp @@ -117,8 +117,7 @@ void SlicerTView::exportMidi() QDomElement note_list = dataFile.createElement("note-list"); dataFile.content().appendChild(note_list); - std::vector notes; - m_slicerTParent->writeToMidi(¬es); + std::vector notes = m_slicerTParent->getMidi(); if (notes.size() == 0) { return; } TimePos start_pos(notes.front().pos().getBar(), 0); From f745af6d300cd382f285e9f82352c350c92eb7b0 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 15 Oct 2023 14:26:07 +0200 Subject: [PATCH 68/99] Moved PhaseVocoder into core --- include/PhaseVocoder.h | 108 +++++++++++++++ src/core/PhaseVocoder.cpp | 272 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 380 insertions(+) create mode 100644 include/PhaseVocoder.h create mode 100644 src/core/PhaseVocoder.cpp diff --git a/include/PhaseVocoder.h b/include/PhaseVocoder.h new file mode 100644 index 00000000000..6971d0888c4 --- /dev/null +++ b/include/PhaseVocoder.h @@ -0,0 +1,108 @@ +/* + * PhaseVocoder.h - declaration of the PhaseVocoder class + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * 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_PHASEVOCODER_H +#define LMMS_PHASEVOCODER_H + +#include +#include +#include +#include +#include + +#include "lmms_export.h" + +namespace lmms { + +/** + Dynamically timeshifts one audio channel by a changable ratio + Allows access to the timeshifted data in a threadsafe and realtime maner +*/ +class LMMS_EXPORT PhaseVocoder +{ +public: + PhaseVocoder(); + ~PhaseVocoder(); + + //! Loads a new sample, and precomputes the analysis cache + void loadData(std::vector originalData, int sampleRate, float newRatio); + //! Change the output timeshift ratio + void setScaleRatio(float newRatio) { updateParams(newRatio); } + + //! Copy a number of frames from a startpoint into an out buffer. + //! This is NOT relative to the original sample + void getFrames(std::vector& outData, int start, int frames); + + //! Get total number of frames for the timeshifted sample, NOT the original + int frames() { return m_processedBuffer.size(); } + //! Get the current scaleRatio + float scaleRatio() { return m_scaleRatio; } + + // timeshift config + static const int s_windowSize = 512; + static const int s_overSampling = 32; + +private: + QMutex m_dataLock; + // original data + std::vector m_originalBuffer; + int m_originalSampleRate = 0; + + float m_scaleRatio = -1; // to force on first load + + // output data + std::vector m_processedBuffer; // final output + std::vector m_processedWindows; // marks a window processed + + // depending on scaleRatio + int m_stepSize = 0; + int m_numWindows = 0; + float m_outStepSize = 0; + float m_freqPerBin = 0; + float m_expectedPhaseIn = 0; + float m_expectedPhaseOut = 0; + + // buffers + std::array m_FFTSpectrum; + std::vector m_FFTInput; + std::vector m_IFFTReconstruction; + std::vector m_allMagnitudes; + std::vector m_allFrequencies; + std::vector m_processedFreq; + std::vector m_processedMagn; + std::vector m_lastPhase; + std::vector m_sumPhase; + + // cache + std::vector m_freqCache; + std::vector m_magCache; + + // fftw plans + fftwf_plan m_fftPlan; + fftwf_plan m_ifftPlan; + + void updateParams(float newRatio); + void generateWindow(int windowNum, bool useCache); +}; +} // namespace lmms +#endif // LMMS_PHASEVOCODER_H diff --git a/src/core/PhaseVocoder.cpp b/src/core/PhaseVocoder.cpp new file mode 100644 index 00000000000..33211f82298 --- /dev/null +++ b/src/core/PhaseVocoder.cpp @@ -0,0 +1,272 @@ +/* + * PhaseVocoder.cpp - Implementation of the PhaseVocoder class + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * 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 "PhaseVocoder.h" + +#include +#include +#include +#include + +#include "lmms_basics.h" +#include "lmms_constants.h" + +namespace lmms { + +PhaseVocoder::PhaseVocoder() + : m_FFTInput(s_windowSize, 0) + , m_IFFTReconstruction(s_windowSize, 0) + , m_allMagnitudes(s_windowSize, 0) + , m_allFrequencies(s_windowSize, 0) + , m_processedFreq(s_windowSize, 0) + , m_processedMagn(s_windowSize, 0) +{ + m_fftPlan = fftwf_plan_dft_r2c_1d(s_windowSize, m_FFTInput.data(), m_FFTSpectrum.data(), FFTW_MEASURE); + m_ifftPlan = fftwf_plan_dft_c2r_1d(s_windowSize, m_FFTSpectrum.data(), m_IFFTReconstruction.data(), FFTW_MEASURE); +} + +PhaseVocoder::~PhaseVocoder() +{ + fftwf_destroy_plan(m_fftPlan); + fftwf_destroy_plan(m_ifftPlan); +} + +void PhaseVocoder::loadData(std::vector originalData, int sampleRate, float newRatio) +{ + m_dataLock.lock(); + + m_originalBuffer = originalData; + m_originalSampleRate = sampleRate; + m_scaleRatio = -1; // force update, kinda hacky + + m_dataLock.unlock(); // stupid, but QRecursiveMutex is too expensive to have in updateParas and getFrames + updateParams(newRatio); + m_dataLock.lock(); + + // set buffer sizes + m_processedWindows.resize(m_numWindows, false); + m_lastPhase.resize(m_numWindows * s_windowSize, 0); + m_sumPhase.resize((m_numWindows + 1) * s_windowSize, 0); + m_freqCache.resize(m_numWindows * s_windowSize, 0); + m_magCache.resize(m_numWindows * s_windowSize, 0); + + // clear phase buffers + std::fill(m_lastPhase.begin(), m_lastPhase.end(), 0); + std::fill(m_sumPhase.begin(), m_sumPhase.end(), 0); + + // maybe limit this to a set amount of windows to reduce initial lag spikes + for (int i = 0; i < m_numWindows; i++) + { + if (!m_processedWindows[i]) + { + generateWindow(i, false); // first pass, no cache + m_processedWindows[i] = true; + } + } + + m_dataLock.unlock(); +} + +void PhaseVocoder::getFrames(std::vector& outData, int start, int frames) +{ + if (m_originalBuffer.size() < 2048) { return; } + m_dataLock.lock(); + + if (typeInfo::isEqual(m_scaleRatio, 1.0f)) + { // directly copy original data + std::copy_n(m_originalBuffer.data() + start, frames, outData.data()); + m_dataLock.unlock(); + return; + } + + int windowMargin = s_overSampling / 2; // numbers of windows before full quality + int startWindow = static_cast(start) / m_outStepSize - windowMargin; + int endWindow = static_cast((start + frames)) / m_outStepSize + windowMargin; + + startWindow = std::clamp(startWindow, 0, m_numWindows - 1); + endWindow = std::clamp(endWindow, 0, m_numWindows - 1); + + // discard previous phaseSum if not processed + if (!m_processedWindows[startWindow]) + { + std::fill_n(m_sumPhase.data() + startWindow * s_windowSize, s_windowSize, 0); + } + + // this encompases the minimum windows needed to get full quality, + // which must be computed + for (int i = startWindow; i < endWindow; i++) + { + if (!m_processedWindows[i]) + { + generateWindow(i, true); // theses should use the cache + m_processedWindows[i] = true; + } + } + + for (int i = 0; i < frames; i++) + { + outData[i] = m_processedBuffer[start + i]; + } + + m_dataLock.unlock(); +} + +// adjust pv params buffers to a new scale ratio +void PhaseVocoder::updateParams(float newRatio) +{ + if (m_originalBuffer.size() < 2048) { return; } + if (newRatio == m_scaleRatio) { return; } // nothing changed + m_dataLock.lock(); + + m_scaleRatio = newRatio; + m_stepSize = static_cast(s_windowSize) / s_overSampling; + m_numWindows = static_cast(m_originalBuffer.size()) / m_stepSize - s_overSampling - 1; + m_outStepSize = m_scaleRatio * m_stepSize; // float, else inaccurate + m_freqPerBin = m_originalSampleRate / s_windowSize; + m_expectedPhaseIn = 2. * F_PI * m_stepSize / s_windowSize; + m_expectedPhaseOut = 2. * F_PI * m_outStepSize / s_windowSize; + + m_processedBuffer.resize(m_scaleRatio * m_originalBuffer.size(), 0); + + // very slow :( + std::fill(m_processedWindows.begin(), m_processedWindows.end(), false); + std::fill(m_processedBuffer.begin(), m_processedBuffer.end(), 0); + + m_dataLock.unlock(); +} + +// time shifts one window from originalBuffer and writes to m_processedBuffer +// resources: +// http://blogs.zynaptiq.com/bernsee/pitch-shifting-using-the-ft/ +// https://sethares.engr.wisc.edu/vocoders/phasevocoder.html +// https://dsp.stackexchange.com/questions/40101/audio-time-stretching-without-pitch-shifting/40367#40367 +// https://www.guitarpitchshifter.com/ +// https://en.wikipedia.org/wiki/Window_function +void PhaseVocoder::generateWindow(int windowNum, bool useCache) +{ + // declare vars + float real, imag, phase, magnitude, freq, deltaPhase = 0; + int windowStart = static_cast(windowNum) * m_stepSize; + int windowIndex = static_cast(windowNum) * s_windowSize; + + if (!useCache) + { // normal stuff + std::copy_n(m_originalBuffer.data() + windowStart, s_windowSize, m_FFTInput.data()); + + // FFT + fftwf_execute(m_fftPlan); + + // analysis step + for (int j = 0; j < s_windowSize / 2; j++) // only process nyquistic frequency + { + real = m_FFTSpectrum[j][0]; + imag = m_FFTSpectrum[j][1]; + + magnitude = 2. * sqrt(real * real + imag * imag); + phase = atan2(imag, real); + + // calculate difference in phase with prev window + freq = phase; + freq = phase - m_lastPhase[std::max(0, windowIndex + j - s_windowSize)]; // subtract prev pahse to get phase + // diference + m_lastPhase[windowIndex + j] = phase; + + freq -= m_expectedPhaseIn * j; // subtract expected phase + // at this point, freq is the difference in phase + // between the last phase, having removed the expected phase at this point in the sample + + // this puts freq in 0-2pi. Since the phase difference is proportional to the deviation in bin frequency, + // with this we can better estimate the true frequency + freq = fmod(freq + F_PI, -2.0f * F_PI) + F_PI; + + // convert phase difference into bin freq mulitplier + freq = freq * s_overSampling / (2. * F_PI); + + // add to the expected freq the change in freq calculated from the phase diff + freq = m_freqPerBin * j + m_freqPerBin * freq; + + m_allMagnitudes[j] = magnitude; + m_allFrequencies[j] = freq; + } + // write cache + std::copy_n(m_allFrequencies.data(), s_windowSize, m_freqCache.data() + windowIndex); + std::copy_n(m_allMagnitudes.data(), s_windowSize, m_magCache.data() + windowIndex); + } + else + { + // read cache + std::copy_n(m_freqCache.data() + windowIndex, s_windowSize, m_allFrequencies.data()); + std::copy_n(m_magCache.data() + windowIndex, s_windowSize, m_allMagnitudes.data()); + } + + // synthesis, all the operations are the reverse of the analysis + for (int j = 0; j < s_windowSize / 2; j++) + { + magnitude = m_allMagnitudes[j]; + freq = m_allFrequencies[j]; + + // difference to bin freq mulitplier + deltaPhase = freq - m_freqPerBin * j; + + // convert to phase difference + deltaPhase /= m_freqPerBin; + + // difference in phase + deltaPhase = 2. * F_PI * deltaPhase / s_overSampling; + + // add the expected phase + deltaPhase += m_expectedPhaseOut * j; + + // sum this phase to the total, to keep track of the out phase along the sample + m_sumPhase[windowIndex + j] += deltaPhase; + deltaPhase = m_sumPhase[windowIndex + j]; // final bin phase + + m_sumPhase[windowIndex + j + s_windowSize] = deltaPhase; // copy to the next + + m_FFTSpectrum[j][0] = magnitude * cos(deltaPhase); + m_FFTSpectrum[j][1] = magnitude * sin(deltaPhase); + } + + // inverse fft + fftwf_execute(m_ifftPlan); + + // windowing + for (int j = 0; j < s_windowSize; j++) + { + float outIndex = windowNum * m_outStepSize + j; + + // blackman-harris window + float a0 = 0.35875f; + float a1 = 0.48829f; + float a2 = 0.14128f; + float a3 = 0.01168f; + + float piN2 = 2.0f * F_PI * j; + float window + = a0 - (a1 * cos(piN2 / s_windowSize)) + (a2 * cos(2.0f * piN2 / s_windowSize)) - (a3 * cos(3.0f * piN2)); + + // inverse fft magnitudes are windowsSize times bigger + m_processedBuffer[outIndex] += window * (m_IFFTReconstruction[j] / s_windowSize / s_overSampling); + } +} +} // namespace lmms From efd9c61de59241a35df41896462ad4278b23e2b6 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 15 Oct 2023 14:28:51 +0200 Subject: [PATCH 69/99] removed PV from plugin --- plugins/SlicerT/SlicerT.cpp | 236 ------------------------------------ plugins/SlicerT/SlicerT.h | 65 +--------- src/core/CMakeLists.txt | 1 + 3 files changed, 3 insertions(+), 299 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 8d9902c1bbf..55fd5148e9c 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -52,242 +52,6 @@ Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = { }; } // end extern -PhaseVocoder::PhaseVocoder() - : m_FFTInput(s_windowSize, 0) - , m_IFFTReconstruction(s_windowSize, 0) - , m_allMagnitudes(s_windowSize, 0) - , m_allFrequencies(s_windowSize, 0) - , m_processedFreq(s_windowSize, 0) - , m_processedMagn(s_windowSize, 0) -{ - m_fftPlan = fftwf_plan_dft_r2c_1d(s_windowSize, m_FFTInput.data(), m_FFTSpectrum.data(), FFTW_MEASURE); - m_ifftPlan = fftwf_plan_dft_c2r_1d(s_windowSize, m_FFTSpectrum.data(), m_IFFTReconstruction.data(), FFTW_MEASURE); -} - -PhaseVocoder::~PhaseVocoder() -{ - fftwf_destroy_plan(m_fftPlan); - fftwf_destroy_plan(m_ifftPlan); -} - -void PhaseVocoder::loadData(std::vector originalData, int sampleRate, float newRatio) -{ - m_dataLock.lock(); - - m_originalBuffer = originalData; - m_originalSampleRate = sampleRate; - m_scaleRatio = -1; // force update, kinda hacky - - m_dataLock.unlock(); // stupid, but QRecursiveMutex is too expensive to have in updateParas and getFrames - updateParams(newRatio); - m_dataLock.lock(); - - // set buffer sizes - m_processedWindows.resize(m_numWindows, false); - m_lastPhase.resize(m_numWindows * s_windowSize, 0); - m_sumPhase.resize((m_numWindows + 1) * s_windowSize, 0); - m_freqCache.resize(m_numWindows * s_windowSize, 0); - m_magCache.resize(m_numWindows * s_windowSize, 0); - - // clear phase buffers - std::fill(m_lastPhase.begin(), m_lastPhase.end(), 0); - std::fill(m_sumPhase.begin(), m_sumPhase.end(), 0); - - // maybe limit this to a set amount of windows to reduce initial lag spikes - for (int i = 0; i < m_numWindows; i++) - { - if (!m_processedWindows[i]) - { - generateWindow(i, false); // first pass, no cache - m_processedWindows[i] = true; - } - } - - m_dataLock.unlock(); -} - -void PhaseVocoder::getFrames(std::vector& outData, int start, int frames) -{ - if (m_originalBuffer.size() < 2048) { return; } - m_dataLock.lock(); - - if (typeInfo::isEqual(m_scaleRatio, 1.0f)) - { // directly copy original data - std::copy_n(m_originalBuffer.data() + start, frames, outData.data()); - m_dataLock.unlock(); - return; - } - - int windowMargin = s_overSampling / 2; // numbers of windows before full quality - int startWindow = static_cast(start) / m_outStepSize - windowMargin; - int endWindow = static_cast((start + frames)) / m_outStepSize + windowMargin; - - startWindow = std::clamp(startWindow, 0, m_numWindows - 1); - endWindow = std::clamp(endWindow, 0, m_numWindows - 1); - - // discard previous phaseSum if not processed - if (!m_processedWindows[startWindow]) - { - std::fill_n(m_sumPhase.data() + startWindow * s_windowSize, s_windowSize, 0); - } - - // this encompases the minimum windows needed to get full quality, - // which must be computed - for (int i = startWindow; i < endWindow; i++) - { - if (!m_processedWindows[i]) - { - generateWindow(i, true); // theses should use the cache - m_processedWindows[i] = true; - } - } - - for (int i = 0; i < frames; i++) - { - outData[i] = m_processedBuffer[start + i]; - } - - m_dataLock.unlock(); -} - -// adjust pv params buffers to a new scale ratio -void PhaseVocoder::updateParams(float newRatio) -{ - if (m_originalBuffer.size() < 2048) { return; } - if (newRatio == m_scaleRatio) { return; } // nothing changed - m_dataLock.lock(); - - m_scaleRatio = newRatio; - m_stepSize = static_cast(s_windowSize) / s_overSampling; - m_numWindows = static_cast(m_originalBuffer.size()) / m_stepSize - s_overSampling - 1; - m_outStepSize = m_scaleRatio * m_stepSize; // float, else inaccurate - m_freqPerBin = m_originalSampleRate / s_windowSize; - m_expectedPhaseIn = 2. * F_PI * m_stepSize / s_windowSize; - m_expectedPhaseOut = 2. * F_PI * m_outStepSize / s_windowSize; - - m_processedBuffer.resize(m_scaleRatio * m_originalBuffer.size(), 0); - - // very slow :( - std::fill(m_processedWindows.begin(), m_processedWindows.end(), false); - std::fill(m_processedBuffer.begin(), m_processedBuffer.end(), 0); - - m_dataLock.unlock(); -} - -// time shifts one window from originalBuffer and writes to m_processedBuffer -// resources: -// http://blogs.zynaptiq.com/bernsee/pitch-shifting-using-the-ft/ -// https://sethares.engr.wisc.edu/vocoders/phasevocoder.html -// https://dsp.stackexchange.com/questions/40101/audio-time-stretching-without-pitch-shifting/40367#40367 -// https://www.guitarpitchshifter.com/ -// https://en.wikipedia.org/wiki/Window_function -void PhaseVocoder::generateWindow(int windowNum, bool useCache) -{ - // declare vars - float real, imag, phase, magnitude, freq, deltaPhase = 0; - int windowStart = static_cast(windowNum) * m_stepSize; - int windowIndex = static_cast(windowNum) * s_windowSize; - - if (!useCache) - { // normal stuff - std::copy_n(m_originalBuffer.data() + windowStart, s_windowSize, m_FFTInput.data()); - - // FFT - fftwf_execute(m_fftPlan); - - // analysis step - for (int j = 0; j < s_windowSize / 2; j++) // only process nyquistic frequency - { - real = m_FFTSpectrum[j][0]; - imag = m_FFTSpectrum[j][1]; - - magnitude = 2. * sqrt(real * real + imag * imag); - phase = atan2(imag, real); - - // calculate difference in phase with prev window - freq = phase; - freq = phase - - m_lastPhase[std::max(0, windowIndex + j - s_windowSize)]; // subtract prev pahse to get phase diference - m_lastPhase[windowIndex + j] = phase; - - freq -= m_expectedPhaseIn * j; // subtract expected phase - // at this point, freq is the difference in phase - // between the last phase, having removed the expected phase at this point in the sample - - // this puts freq in 0-2pi. Since the phase difference is proportional to the deviation in bin frequency, - // with this we can better estimate the true frequency - freq = fmod(freq + F_PI, -2.0f * F_PI) + F_PI; - - // convert phase difference into bin freq mulitplier - freq = freq * s_overSampling / (2. * F_PI); - - // add to the expected freq the change in freq calculated from the phase diff - freq = m_freqPerBin * j + m_freqPerBin * freq; - - m_allMagnitudes[j] = magnitude; - m_allFrequencies[j] = freq; - } - // write cache - std::copy_n(m_allFrequencies.data(), s_windowSize, m_freqCache.data() + windowIndex); - std::copy_n(m_allMagnitudes.data(), s_windowSize, m_magCache.data() + windowIndex); - } - else - { - // read cache - std::copy_n(m_freqCache.data() + windowIndex, s_windowSize, m_allFrequencies.data()); - std::copy_n(m_magCache.data() + windowIndex, s_windowSize, m_allMagnitudes.data()); - } - - // synthesis, all the operations are the reverse of the analysis - for (int j = 0; j < s_windowSize / 2; j++) - { - magnitude = m_allMagnitudes[j]; - freq = m_allFrequencies[j]; - - // difference to bin freq mulitplier - deltaPhase = freq - m_freqPerBin * j; - - // convert to phase difference - deltaPhase /= m_freqPerBin; - - // difference in phase - deltaPhase = 2. * F_PI * deltaPhase / s_overSampling; - - // add the expected phase - deltaPhase += m_expectedPhaseOut * j; - - // sum this phase to the total, to keep track of the out phase along the sample - m_sumPhase[windowIndex + j] += deltaPhase; - deltaPhase = m_sumPhase[windowIndex + j]; // final bin phase - - m_sumPhase[windowIndex + j + s_windowSize] = deltaPhase; // copy to the next - - - m_FFTSpectrum[j][0] = magnitude * cos(deltaPhase); - m_FFTSpectrum[j][1] = magnitude * sin(deltaPhase); - } - - // inverse fft - fftwf_execute(m_ifftPlan); - - // windowing - for (int j = 0; j < s_windowSize; j++) - { - float outIndex = windowNum * m_outStepSize + j; - - // blackman-harris window - float a0 = 0.35875f; - float a1 = 0.48829f; - float a2 = 0.14128f; - float a3 = 0.01168f; - - float piN2 = 2.0f * F_PI * j; - float window = a0 - (a1 * cos(piN2 / s_windowSize)) + (a2 * cos(2.0f * piN2 / s_windowSize)) - (a3 * cos(3.0f * piN2)); - - // inverse fft magnitudes are windowsSize times bigger - m_processedBuffer[outIndex] += window * (m_IFFTReconstruction[j] / s_windowSize / s_overSampling); - } -} // ################################# SlicerT #################################### diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 035573c75ee..0811db47f49 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -27,6 +27,7 @@ #include +#include "PhaseVocoder.h" #include "AutomatableModel.h" #include "Instrument.h" #include "InstrumentView.h" @@ -36,68 +37,6 @@ namespace lmms { -// takes one audio-channel and timeshifts it -class PhaseVocoder -{ -public: - PhaseVocoder(); - ~PhaseVocoder(); - - void loadData(std::vector originalData, int sampleRate, float newRatio); - void setScaleRatio(float newRatio) { updateParams(newRatio); } - - void getFrames(std::vector& outData, int start, int frames); - - int frames() { return m_processedBuffer.size(); } - float scaleRatio() { return m_scaleRatio; } - - // timeshift config - static const int s_windowSize = 512; - static const int s_overSampling = 32; - -private: - QMutex m_dataLock; - // original data - std::vector m_originalBuffer; - int m_originalSampleRate = 0; - - float m_scaleRatio = -1; // to force on first load - - // output data - std::vector m_processedBuffer; // final output - std::vector m_processedWindows; // marks a window processed - - // depending on scaleRatio - int m_stepSize = 0; - int m_numWindows = 0; - float m_outStepSize = 0; - float m_freqPerBin = 0; - float m_expectedPhaseIn = 0; - float m_expectedPhaseOut = 0; - - // buffers - std::array m_FFTSpectrum; - std::vector m_FFTInput; - std::vector m_IFFTReconstruction; - std::vector m_allMagnitudes; - std::vector m_allFrequencies; - std::vector m_processedFreq; - std::vector m_processedMagn; - std::vector m_lastPhase; - std::vector m_sumPhase; - - // cache - std::vector m_freqCache; - std::vector m_magCache; - - // fftw plans - fftwf_plan m_fftPlan; - fftwf_plan m_ifftPlan; - - void updateParams(float newRatio); - void generateWindow(int windowNum, bool useCache); -}; - // simple helper class that handles the different audio channels class DynamicPlaybackBuffer { @@ -162,7 +101,7 @@ public slots: public: SlicerT(InstrumentTrack* instrumentTrack); - ~SlicerT(); + ~SlicerT() override; void playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) override; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 319882af2f9..b72ab0565c5 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -53,6 +53,7 @@ set(LMMS_SRCS core/PatternStore.cpp core/PeakController.cpp core/PerfLog.cpp + core/PhaseVocoder.cpp core/Piano.cpp core/PlayHandle.cpp core/Plugin.cpp From 525d29bb6f018a2ed4f110bebbcbe1c43f7dab74 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 15 Oct 2023 17:59:07 +0200 Subject: [PATCH 70/99] remove pointers in waveform --- plugins/SlicerT/SlicerTWaveform.cpp | 67 +++++++++++++++-------------- plugins/SlicerT/SlicerTWaveform.h | 6 +-- 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index f14cd3a01ad..75d8402e57a 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -50,8 +50,7 @@ SlicerTWaveform::SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* par , m_sliceEditor(QPixmap(w, m_editorHeight)) // references to instrument vars - , m_currentSample(&instrument->m_originalSample) - , m_slicePoints(&instrument->m_slicePoints) + , m_slicerTParent(instrument) { // window config setFixedSize(m_width, m_height); @@ -71,25 +70,27 @@ SlicerTWaveform::SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* par void SlicerTWaveform::drawSeekerSlicerTWaveform() { m_seekerSlicerTWaveform.fill(s_SlicerTWaveformBgColor); - if (m_currentSample->frames() < 2048) { return; } + if (m_slicerTParent->m_originalSample.frames() < 2048) { return; } QPainter brush(&m_seekerSlicerTWaveform); brush.setPen(s_SlicerTWaveformColor); - m_currentSample->visualize(brush, QRect(0, 0, m_seekerSlicerTWaveform.width(), m_seekerSlicerTWaveform.height()), 0, - m_currentSample->frames()); + m_slicerTParent->m_originalSample.visualize(brush, + QRect(0, 0, m_seekerSlicerTWaveform.width(), m_seekerSlicerTWaveform.height()), 0, + m_slicerTParent->m_originalSample.frames()); } void SlicerTWaveform::drawSeeker() { m_seeker.fill(s_SlicerTWaveformBgColor); - if (m_currentSample->frames() < 2048) { return; } + if (m_slicerTParent->m_originalSample.frames() < 2048) { return; } QPainter brush(&m_seeker); // draw slice points brush.setPen(s_sliceColor); - for (int i = 0; i < m_slicePoints->size(); i++) + for (int i = 0; i < m_slicerTParent->m_slicePoints.size(); i++) { - float xPos = static_cast(m_slicePoints->at(i)) / m_currentSample->frames() * m_seekerWidth; + float xPos = static_cast(m_slicerTParent->m_slicePoints.at(i)) + / m_slicerTParent->m_originalSample.frames() * m_seekerWidth; brush.drawLine(xPos, 0, xPos, m_seekerHeight); } @@ -126,7 +127,7 @@ void SlicerTWaveform::drawEditor() QPainter brush(&m_sliceEditor); // draw text if no sample loaded - if (m_currentSample->frames() < 2048) + if (m_slicerTParent->m_originalSample.frames() < 2048) { brush.setPen(s_playHighlighColor); brush.setFont(QFont(brush.font().family(), 9.0f, -1, false)); @@ -136,8 +137,8 @@ void SlicerTWaveform::drawEditor() } // editor boundaries - float startFrame = m_seekerStart * m_currentSample->frames(); - float endFrame = m_seekerEnd * m_currentSample->frames(); + float startFrame = m_seekerStart * m_slicerTParent->m_originalSample.frames(); + float endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.frames(); float numFramesToDraw = endFrame - startFrame; // 0 centered line @@ -147,14 +148,14 @@ void SlicerTWaveform::drawEditor() // draw SlicerTWaveform brush.setPen(s_SlicerTWaveformColor); float zoomOffset = (m_editorHeight - m_zoomLevel * m_editorHeight) / 2; - m_currentSample->visualize( + m_slicerTParent->m_originalSample.visualize( brush, QRect(0, zoomOffset, m_editorWidth, m_zoomLevel * m_editorHeight), startFrame, endFrame); // draw slicepoints brush.setPen(QPen(s_sliceColor, 2)); - for (int i = 0; i < m_slicePoints->size(); i++) + for (int i = 0; i < m_slicerTParent->m_slicePoints.size(); i++) { - float xPos = (m_slicePoints->at(i) - startFrame) / numFramesToDraw * m_editorWidth; + float xPos = (m_slicerTParent->m_slicePoints.at(i) - startFrame) / numFramesToDraw * m_editorWidth; if (i == m_sliceSelected) { brush.setPen(QPen(s_selectedSliceColor, 2)); } else { brush.setPen(QPen(s_sliceColor, 2)); } @@ -214,12 +215,12 @@ void SlicerTWaveform::mousePressEvent(QMouseEvent* me) else // editor click { m_sliceSelected = -1; - float startFrame = m_seekerStart * m_currentSample->frames(); - float endFrame = m_seekerEnd * m_currentSample->frames(); + float startFrame = m_seekerStart * m_slicerTParent->m_originalSample.frames(); + float endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.frames(); // select slice - for (int i = 0; i < m_slicePoints->size(); i++) + for (int i = 0; i < m_slicerTParent->m_slicePoints.size(); i++) { - int sliceIndex = m_slicePoints->at(i); + int sliceIndex = m_slicerTParent->m_slicePoints.at(i); float xPos = (sliceIndex - startFrame) / (endFrame - startFrame); if (abs(xPos - normalizedClickEditor) < m_distanceForClick) @@ -232,9 +233,9 @@ void SlicerTWaveform::mousePressEvent(QMouseEvent* me) if (me->button() == Qt::MouseButton::RightButton) // erase selected slice { - if (m_sliceSelected != -1 && m_slicePoints->size() > 2) + if (m_sliceSelected != -1 && m_slicerTParent->m_slicePoints.size() > 2) { - m_slicePoints->erase(m_slicePoints->begin() + m_sliceSelected); + m_slicerTParent->m_slicePoints.erase(m_slicerTParent->m_slicePoints.begin() + m_sliceSelected); m_sliceSelected = -1; } } @@ -244,7 +245,7 @@ void SlicerTWaveform::mousePressEvent(QMouseEvent* me) void SlicerTWaveform::mouseReleaseEvent(QMouseEvent* me) { m_currentlyDragging = DraggingTypes::Nothing; - std::sort(m_slicePoints->begin(), m_slicePoints->end()); + std::sort(m_slicerTParent->m_slicePoints.begin(), m_slicerTParent->m_slicePoints.end()); updateUI(); } @@ -256,8 +257,8 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) float distStart = m_seekerStart - m_seekerMiddle; float distEnd = m_seekerEnd - m_seekerMiddle; - float startFrame = m_seekerStart * m_currentSample->frames(); - float endFrame = m_seekerEnd * m_currentSample->frames(); + float startFrame = m_seekerStart * m_slicerTParent->m_originalSample.frames(); + float endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.frames(); // handle dragging events switch (m_currentlyDragging) @@ -281,9 +282,11 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) break; case DraggingTypes::SlicePoint: - m_slicePoints->at(m_sliceSelected) = startFrame + normalizedClickEditor * (endFrame - startFrame); - m_slicePoints->at(m_sliceSelected) - = std::clamp(m_slicePoints->at(m_sliceSelected), 0, m_currentSample->frames()); + if (m_sliceSelected == -1) {break;} + m_slicerTParent->m_slicePoints.at(m_sliceSelected) + = startFrame + normalizedClickEditor * (endFrame - startFrame); + m_slicerTParent->m_slicePoints.at(m_sliceSelected) = std::clamp( + m_slicerTParent->m_slicePoints.at(m_sliceSelected), 0, m_slicerTParent->m_originalSample.frames()); break; case DraggingTypes::Nothing: break; @@ -294,21 +297,21 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) void SlicerTWaveform::mouseDoubleClickEvent(QMouseEvent* me) { float normalizedClickEditor = static_cast(me->x()) / m_editorWidth; - float startFrame = m_seekerStart * m_currentSample->frames(); - float endFrame = m_seekerEnd * m_currentSample->frames(); + float startFrame = m_seekerStart * m_slicerTParent->m_originalSample.frames(); + float endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.frames(); float slicePosition = startFrame + normalizedClickEditor * (endFrame - startFrame); - for (int i = 0; i < m_slicePoints->size(); i++) + for (int i = 0; i < m_slicerTParent->m_slicePoints.size(); i++) { - if (m_slicePoints->at(i) < slicePosition) + if (m_slicerTParent->m_slicePoints.at(i) < slicePosition) { - m_slicePoints->insert(m_slicePoints->begin() + i, slicePosition); + m_slicerTParent->m_slicePoints.insert(m_slicerTParent->m_slicePoints.begin() + i, slicePosition); break; } } - std::sort(m_slicePoints->begin(), m_slicePoints->end()); + std::sort(m_slicerTParent->m_slicePoints.begin(), m_slicerTParent->m_slicePoints.end()); } void SlicerTWaveform::wheelEvent(QWheelEvent* _we) diff --git a/plugins/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h index fb20a758c8d..0051051adc0 100644 --- a/plugins/SlicerT/SlicerTWaveform.h +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -31,6 +31,7 @@ #include #include +#include "Instrument.h" #include "SampleBuffer.h" namespace lmms { @@ -125,8 +126,7 @@ public slots: QPixmap m_seekerSlicerTWaveform; // only stores SlicerTWaveform graphic QPixmap m_sliceEditor; - SampleBuffer* m_currentSample; - std::vector* m_slicePoints; + SlicerT* m_slicerTParent; void drawEditor(); void drawSeekerSlicerTWaveform(); @@ -134,4 +134,4 @@ public slots: }; } // namespace gui } // namespace lmms -#endif // LMMS_SlicerT_Waveform_H \ No newline at end of file +#endif // LMMS_SlicerT_Waveform_H From 669cdd240e58df2124a21a6546379d5a5671c01f Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 15 Oct 2023 19:18:40 +0200 Subject: [PATCH 71/99] clang-format again --- include/PhaseVocoder.h | 4 ++-- plugins/SlicerT/SlicerT.cpp | 3 +-- plugins/SlicerT/SlicerT.h | 5 +++-- plugins/SlicerT/SlicerTWaveform.cpp | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/include/PhaseVocoder.h b/include/PhaseVocoder.h index 6971d0888c4..f419f69b400 100644 --- a/include/PhaseVocoder.h +++ b/include/PhaseVocoder.h @@ -36,7 +36,7 @@ namespace lmms { /** Dynamically timeshifts one audio channel by a changable ratio - Allows access to the timeshifted data in a threadsafe and realtime maner + Allows access to the timeshifted data in a threadsafe and realtime maner */ class LMMS_EXPORT PhaseVocoder { @@ -49,7 +49,7 @@ class LMMS_EXPORT PhaseVocoder //! Change the output timeshift ratio void setScaleRatio(float newRatio) { updateParams(newRatio); } - //! Copy a number of frames from a startpoint into an out buffer. + //! Copy a number of frames from a startpoint into an out buffer. //! This is NOT relative to the original sample void getFrames(std::vector& outData, int start, int frames); diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 55fd5148e9c..d7d55fb3c52 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -52,7 +52,6 @@ Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = { }; } // end extern - // ################################# SlicerT #################################### SlicerT::SlicerT(InstrumentTrack* instrumentTrack) @@ -264,7 +263,7 @@ void SlicerT::findSlices() // fit to sample size m_slicePoints[0] = 0; - m_slicePoints[m_slicePoints.size()-1] = m_originalSample.frames(); + m_slicePoints[m_slicePoints.size() - 1] = m_originalSample.frames(); // update UI emit dataChanged(); diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 0811db47f49..934b61fb290 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -27,11 +27,11 @@ #include -#include "PhaseVocoder.h" #include "AutomatableModel.h" #include "Instrument.h" #include "InstrumentView.h" #include "Note.h" +#include "PhaseVocoder.h" #include "SampleBuffer.h" #include "SlicerTView.h" @@ -44,7 +44,8 @@ class DynamicPlaybackBuffer DynamicPlaybackBuffer() : m_leftChannel() , m_rightChannel() - {} + { + } void loadSample(const sampleFrame* outData, int frames, int sampleRate, float newRatio) { diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index 75d8402e57a..1024a588a6a 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -282,7 +282,7 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) break; case DraggingTypes::SlicePoint: - if (m_sliceSelected == -1) {break;} + if (m_sliceSelected == -1) { break; } m_slicerTParent->m_slicePoints.at(m_sliceSelected) = startFrame + normalizedClickEditor * (endFrame - startFrame); m_slicerTParent->m_slicePoints.at(m_sliceSelected) = std::clamp( From 95eee6dc29514fb7b62d2ec72c718cd28477e288 Mon Sep 17 00:00:00 2001 From: DanielKauss <47889291+DanielKauss@users.noreply.github.com> Date: Sun, 15 Oct 2023 21:17:43 +0200 Subject: [PATCH 72/99] Use std:: + review suggestions Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> Co-authored-by: saker --- include/PhaseVocoder.h | 2 +- plugins/SlicerT/SlicerT.cpp | 12 ++++++------ plugins/SlicerT/SlicerTWaveform.cpp | 12 +++++------- plugins/SlicerT/SlicerTWaveform.h | 24 ++++++++++++------------ src/core/PhaseVocoder.cpp | 12 ++++++------ 5 files changed, 30 insertions(+), 32 deletions(-) diff --git a/include/PhaseVocoder.h b/include/PhaseVocoder.h index f419f69b400..8d51d2c1e45 100644 --- a/include/PhaseVocoder.h +++ b/include/PhaseVocoder.h @@ -27,7 +27,7 @@ #include #include #include -#include +#include #include #include "lmms_export.h" diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index d7d55fb3c52..7896b0f703f 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -152,7 +152,7 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) float fadeValue = static_cast(noteFramesLeft - i) / m_fadeOutFrames.value(); // if the workingbuffer extends the sample fadeValue = std::clamp(fadeValue, 0.0f, 1.0f); - fadeValue = pow(fadeValue, 2); + fadeValue = std::pow(fadeValue, 2); workingBuffer[i + offset][0] *= fadeValue; workingBuffer[i + offset][1] *= fadeValue; @@ -223,10 +223,10 @@ void SlicerT::findSlices() { real = fftOut[j][0]; imag = fftOut[j][1]; - magnitude = sqrt(real * real + imag * imag); + magnitude = std::sqrt(real * real + imag * imag); // using L2-norm (euclidean distance) - diff = sqrt(pow(magnitude - prevMags[j], 2)); + diff = std::sqrt(std::pow(magnitude - prevMags[j], 2)); spectralFlux += diff; prevMags[j] = magnitude; @@ -249,7 +249,7 @@ void SlicerT::findSlices() int noteSnap = m_sliceSnap.value(); int timeSignature = Engine::getSong()->getTimeSigModel().getNumerator(); int samplesPerBar = 60.0f * timeSignature / m_originalBPM.value() * m_originalSample.sampleRate(); - int sliceLock = samplesPerBar / pow(2, noteSnap + 1); // lock to note: 1 / noteSnap² + int sliceLock = samplesPerBar / std::pow(2, noteSnap + 1); // lock to note: 1 / noteSnap² if (noteSnap == 0) { sliceLock = 1; } // disable noteSnap for (int i = 0; i < m_slicePoints.size(); i++) @@ -427,12 +427,12 @@ void SlicerT::loadSettings(const QDomElement& element) QString SlicerT::nodeName() const { - return (slicert_plugin_descriptor.name); + return slicert_plugin_descriptor.name; } gui::PluginView* SlicerT::instantiateView(QWidget* parent) { - return (new gui::SlicerTView(this, parent)); + return new gui::SlicerTView(this, parent); } extern "C" { diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index 1024a588a6a..4d2382332ae 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -34,17 +34,15 @@ namespace gui { SlicerTWaveform::SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* parent) : QWidget(parent) - , // calculate sizes - m_width(w) + , m_width(w) , m_height(h) , m_seekerWidth(w - m_seekerHorMargin * 2) , m_editorHeight(h - m_seekerHeight - m_middleMargin) , m_editorWidth(w) - , // create pixmaps - m_sliceArrow(PLUGIN_NAME::getIconPixmap("slide_indicator_arrow")) + , m_sliceArrow(PLUGIN_NAME::getIconPixmap("slide_indicator_arrow")) , m_seeker(QPixmap(m_seekerWidth, m_seekerHeight)) , m_seekerSlicerTWaveform(QPixmap(m_seekerWidth, m_seekerHeight)) , m_sliceEditor(QPixmap(w, m_editorHeight)) @@ -198,11 +196,11 @@ void SlicerTWaveform::mousePressEvent(QMouseEvent* me) if (me->y() < m_seekerHeight) // seeker click { - if (abs(normalizedClickSeeker - m_seekerStart) < m_distanceForClick) // dragging start + if (std::abs(normalizedClickSeeker - m_seekerStart) < m_distanceForClick) // dragging start { m_currentlyDragging = DraggingTypes::SeekerStart; } - else if (abs(normalizedClickSeeker - m_seekerEnd) < m_distanceForClick) // dragging end + else if (std::abs(normalizedClickSeeker - m_seekerEnd) < m_distanceForClick) // dragging end { m_currentlyDragging = DraggingTypes::SeekerEnd; } @@ -223,7 +221,7 @@ void SlicerTWaveform::mousePressEvent(QMouseEvent* me) int sliceIndex = m_slicerTParent->m_slicePoints.at(i); float xPos = (sliceIndex - startFrame) / (endFrame - startFrame); - if (abs(xPos - normalizedClickEditor) < m_distanceForClick) + if (std::abs(xPos - normalizedClickEditor) < m_distanceForClick) { m_currentlyDragging = DraggingTypes::SlicePoint; m_sliceSelected = i; diff --git a/plugins/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h index 0051051adc0..4d3272a1f9a 100644 --- a/plugins/SlicerT/SlicerTWaveform.h +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -22,8 +22,8 @@ * */ -#ifndef LMMS_SlicerT_Waveform_H -#define LMMS_SlicerT_Waveform_H +#ifndef LMMS_SLICERT_WAVEFORM_H +#define LMMS_SLICERT_WAVEFORM_H #include #include @@ -71,9 +71,9 @@ public slots: const QColor s_seekerShadowColor = QColor(0, 0, 0, 120); // interaction vars - const float m_distanceForClick = 0.03f; - const float m_minSeekerDistance = 0.13f; - const float m_zoomSensitivity = 0.5f; + static constexpr float s_distanceForClick = 0.03f; + static constexpr float s_minSeekerDistance = 0.13f; + static constexpr float s_zoomSensitivity = 0.5f; enum class DraggingTypes { @@ -85,13 +85,13 @@ public slots: }; protected: - virtual void mousePressEvent(QMouseEvent* me); - virtual void mouseReleaseEvent(QMouseEvent* me); - virtual void mouseMoveEvent(QMouseEvent* me); - virtual void mouseDoubleClickEvent(QMouseEvent* me); - virtual void wheelEvent(QWheelEvent* _we); + void mousePressEvent(QMouseEvent* me) override; + void mouseReleaseEvent(QMouseEvent* me) override; + void mouseMoveEvent(QMouseEvent* me) override; + void mouseDoubleClickEvent(QMouseEvent* me) override; + void wheelEvent(QWheelEvent* we) override; - virtual void paintEvent(QPaintEvent* pe); + void paintEvent(QPaintEvent* pe) override; private: // sizes @@ -134,4 +134,4 @@ public slots: }; } // namespace gui } // namespace lmms -#endif // LMMS_SlicerT_Waveform_H +#endif // LMMS_SLICERT_WAVEFORM_T diff --git a/src/core/PhaseVocoder.cpp b/src/core/PhaseVocoder.cpp index 33211f82298..964ec4610e1 100644 --- a/src/core/PhaseVocoder.cpp +++ b/src/core/PhaseVocoder.cpp @@ -182,8 +182,8 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) real = m_FFTSpectrum[j][0]; imag = m_FFTSpectrum[j][1]; - magnitude = 2. * sqrt(real * real + imag * imag); - phase = atan2(imag, real); + magnitude = 2. * std::sqrt(real * real + imag * imag); + phase = std::atan2(imag, real); // calculate difference in phase with prev window freq = phase; @@ -197,7 +197,7 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) // this puts freq in 0-2pi. Since the phase difference is proportional to the deviation in bin frequency, // with this we can better estimate the true frequency - freq = fmod(freq + F_PI, -2.0f * F_PI) + F_PI; + freq = std::fmod(freq + F_PI, -2.0f * F_PI) + F_PI; // convert phase difference into bin freq mulitplier freq = freq * s_overSampling / (2. * F_PI); @@ -243,8 +243,8 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) m_sumPhase[windowIndex + j + s_windowSize] = deltaPhase; // copy to the next - m_FFTSpectrum[j][0] = magnitude * cos(deltaPhase); - m_FFTSpectrum[j][1] = magnitude * sin(deltaPhase); + m_FFTSpectrum[j][0] = magnitude * std::cos(deltaPhase); + m_FFTSpectrum[j][1] = magnitude * std::sin(deltaPhase); } // inverse fft @@ -263,7 +263,7 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) float piN2 = 2.0f * F_PI * j; float window - = a0 - (a1 * cos(piN2 / s_windowSize)) + (a2 * cos(2.0f * piN2 / s_windowSize)) - (a3 * cos(3.0f * piN2)); + = a0 - (a1 * std::cos(piN2 / s_windowSize)) + (a2 * std::cos(2.0f * piN2 / s_windowSize)) - (a3 * std::cos(3.0f * piN2)); // inverse fft magnitudes are windowsSize times bigger m_processedBuffer[outIndex] += window * (m_IFFTReconstruction[j] / s_windowSize / s_overSampling); From eb5cfdb9e848e08f3b6d5795de7faa88a9229a85 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 15 Oct 2023 21:26:56 +0200 Subject: [PATCH 73/99] More review changes --- plugins/SlicerT/SlicerT.cpp | 2 +- plugins/SlicerT/SlicerT.h | 6 +++--- plugins/SlicerT/SlicerTWaveform.cpp | 12 ++++++------ src/core/PhaseVocoder.cpp | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 7896b0f703f..1a3cb132715 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -142,7 +142,7 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) src_process(m_resamplerState, &resamplerData); } - else { memcpy(workingBuffer + offset, prePitchBuffer.data(), frames * sizeof(sampleFrame)); } + else { std::copy_n(prePitchBuffer.data(), frames, workingBuffer + offset); } // exponential fade out, applyRelease kinda sucks if (noteFramesLeft < m_fadeOutFrames.value()) diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 934b61fb290..66fda24727e 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -47,14 +47,14 @@ class DynamicPlaybackBuffer { } - void loadSample(const sampleFrame* outData, int frames, int sampleRate, float newRatio) + void loadSample(const sampleFrame* inData, int frames, int sampleRate, float newRatio) { std::vector leftData(frames, 0); std::vector rightData(frames, 0); for (int i = 0; i < frames; i++) { - leftData[i] = outData[i][0]; - rightData[i] = outData[i][1]; + leftData[i] = inData[i][0]; + rightData[i] = inData[i][1]; } m_leftChannel.loadData(leftData, sampleRate, newRatio); m_rightChannel.loadData(rightData, sampleRate, newRatio); diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index 4d2382332ae..d941c096bd0 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -196,11 +196,11 @@ void SlicerTWaveform::mousePressEvent(QMouseEvent* me) if (me->y() < m_seekerHeight) // seeker click { - if (std::abs(normalizedClickSeeker - m_seekerStart) < m_distanceForClick) // dragging start + if (std::abs(normalizedClickSeeker - m_seekerStart) < s_distanceForClick) // dragging start { m_currentlyDragging = DraggingTypes::SeekerStart; } - else if (std::abs(normalizedClickSeeker - m_seekerEnd) < m_distanceForClick) // dragging end + else if (std::abs(normalizedClickSeeker - m_seekerEnd) < s_distanceForClick) // dragging end { m_currentlyDragging = DraggingTypes::SeekerEnd; } @@ -221,7 +221,7 @@ void SlicerTWaveform::mousePressEvent(QMouseEvent* me) int sliceIndex = m_slicerTParent->m_slicePoints.at(i); float xPos = (sliceIndex - startFrame) / (endFrame - startFrame); - if (std::abs(xPos - normalizedClickEditor) < m_distanceForClick) + if (std::abs(xPos - normalizedClickEditor) < s_distanceForClick) { m_currentlyDragging = DraggingTypes::SlicePoint; m_sliceSelected = i; @@ -262,11 +262,11 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) switch (m_currentlyDragging) { case DraggingTypes::SeekerStart: - m_seekerStart = std::clamp(normalizedClickSeeker, 0.0f, m_seekerEnd - m_minSeekerDistance); + m_seekerStart = std::clamp(normalizedClickSeeker, 0.0f, m_seekerEnd - s_minSeekerDistance); break; case DraggingTypes::SeekerEnd: - m_seekerEnd = std::clamp(normalizedClickSeeker, m_seekerStart + m_minSeekerDistance, 1.0f); + m_seekerEnd = std::clamp(normalizedClickSeeker, m_seekerStart + s_minSeekerDistance, 1.0f); break; case DraggingTypes::SeekerMiddle: @@ -315,7 +315,7 @@ void SlicerTWaveform::mouseDoubleClickEvent(QMouseEvent* me) void SlicerTWaveform::wheelEvent(QWheelEvent* _we) { // m_zoomLevel = _we-> / 360.0f * 2.0f; - m_zoomLevel += _we->angleDelta().y() / 360.0f * m_zoomSensitivity; + m_zoomLevel += _we->angleDelta().y() / 360.0f * s_zoomSensitivity; m_zoomLevel = std::max(0.0f, m_zoomLevel); drawEditor(); diff --git a/src/core/PhaseVocoder.cpp b/src/core/PhaseVocoder.cpp index 964ec4610e1..492d309b3df 100644 --- a/src/core/PhaseVocoder.cpp +++ b/src/core/PhaseVocoder.cpp @@ -165,7 +165,7 @@ void PhaseVocoder::updateParams(float newRatio) void PhaseVocoder::generateWindow(int windowNum, bool useCache) { // declare vars - float real, imag, phase, magnitude, freq, deltaPhase = 0; + float real, imag, phase, magnitude, freq, deltaPhase; int windowStart = static_cast(windowNum) * m_stepSize; int windowIndex = static_cast(windowNum) * s_windowSize; From ae4df116ad516dada88217af62f67c533e658b4d Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 15 Oct 2023 22:33:26 +0200 Subject: [PATCH 74/99] new signal slot + more review --- include/PhaseVocoder.h | 2 +- plugins/SlicerT/SlicerTView.cpp | 4 ++-- plugins/SlicerT/SlicerTWaveform.cpp | 4 ++-- plugins/SlicerT/SlicerTWaveform.h | 2 +- src/core/PhaseVocoder.cpp | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/include/PhaseVocoder.h b/include/PhaseVocoder.h index 8d51d2c1e45..13a41421b93 100644 --- a/include/PhaseVocoder.h +++ b/include/PhaseVocoder.h @@ -45,7 +45,7 @@ class LMMS_EXPORT PhaseVocoder ~PhaseVocoder(); //! Loads a new sample, and precomputes the analysis cache - void loadData(std::vector originalData, int sampleRate, float newRatio); + void loadData(const std::vector& originalData, int sampleRate, float newRatio); //! Change the output timeshift ratio void setScaleRatio(float newRatio) { updateParams(newRatio); } diff --git a/plugins/SlicerT/SlicerTView.cpp b/plugins/SlicerT/SlicerTView.cpp index db5ecca6a33..797e4c1c725 100644 --- a/plugins/SlicerT/SlicerTView.cpp +++ b/plugins/SlicerT/SlicerTView.cpp @@ -98,14 +98,14 @@ SlicerTView::SlicerTView(SlicerT* instrument, QWidget* parent) m_midiExportButton.setActiveGraphic(PLUGIN_NAME::getIconPixmap("copyMidi")); m_midiExportButton.setInactiveGraphic(PLUGIN_NAME::getIconPixmap("copyMidi")); m_midiExportButton.setToolTip(tr("Copy midi pattern to clipboard")); - connect(&m_midiExportButton, SIGNAL(clicked()), this, SLOT(exportMidi())); + connect(&m_midiExportButton, &PixmapButton::clicked, this, &SlicerTView::exportMidi); // slice reset button m_resetButton.move(19, 150); m_resetButton.setActiveGraphic(PLUGIN_NAME::getIconPixmap("resetSlices")); m_resetButton.setInactiveGraphic(PLUGIN_NAME::getIconPixmap("resetSlices")); m_resetButton.setToolTip(tr("Reset Slices")); - connect(&m_resetButton, SIGNAL(clicked()), m_slicerTParent, SLOT(updateSlices())); + connect(&m_resetButton, &PixmapButton::clicked, m_slicerTParent, &SlicerT::updateSlices); } // copied from piano roll diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index d941c096bd0..ec97bff95bb 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -59,8 +59,8 @@ SlicerTWaveform::SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* par m_seekerSlicerTWaveform.fill(s_SlicerTWaveformBgColor); // connect to playback - connect(instrument, SIGNAL(isPlaying(float, float, float)), this, SLOT(isPlaying(float, float, float))); - connect(instrument, SIGNAL(dataChanged()), this, SLOT(updateUI())); + connect(instrument, &SlicerT::isPlaying, this, &SlicerTWaveform::isPlaying); + connect(instrument, &SlicerT::dataChanged, this, &SlicerTWaveform::updateUI); updateUI(); } diff --git a/plugins/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h index 4d3272a1f9a..41a06e727c4 100644 --- a/plugins/SlicerT/SlicerTWaveform.h +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -134,4 +134,4 @@ public slots: }; } // namespace gui } // namespace lmms -#endif // LMMS_SLICERT_WAVEFORM_T +#endif // LMMS_SLICERT_WAVEFORM_H diff --git a/src/core/PhaseVocoder.cpp b/src/core/PhaseVocoder.cpp index 492d309b3df..e287b87b250 100644 --- a/src/core/PhaseVocoder.cpp +++ b/src/core/PhaseVocoder.cpp @@ -51,7 +51,7 @@ PhaseVocoder::~PhaseVocoder() fftwf_destroy_plan(m_ifftPlan); } -void PhaseVocoder::loadData(std::vector originalData, int sampleRate, float newRatio) +void PhaseVocoder::loadData(const std::vector& originalData, int sampleRate, float newRatio) { m_dataLock.lock(); @@ -165,7 +165,7 @@ void PhaseVocoder::updateParams(float newRatio) void PhaseVocoder::generateWindow(int windowNum, bool useCache) { // declare vars - float real, imag, phase, magnitude, freq, deltaPhase; + float real, imag, phase, magnitude, freq, deltaPhase = 0; int windowStart = static_cast(windowNum) * m_stepSize; int windowIndex = static_cast(windowNum) * s_windowSize; From 34ba2dc98bc2abb834b48071409e5fa2167dbc3a Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Wed, 18 Oct 2023 18:42:25 +0200 Subject: [PATCH 75/99] Fixed pitch shifting --- plugins/SlicerT/SlicerT.cpp | 17 ++++++----------- plugins/SlicerT/SlicerT.h | 1 - 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 1a3cb132715..74c522caeca 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -73,19 +73,16 @@ SlicerT::SlicerT(InstrumentTrack* instrumentTrack) m_sliceSnap.addItem("1/16"); m_sliceSnap.addItem("1/32"); m_sliceSnap.setValue(0); // no snap by default - - m_resamplerState = src_new(SRC_SINC_MEDIUM_QUALITY, 2, nullptr); // no error -} - -SlicerT::~SlicerT() -{ - src_delete(m_resamplerState); } void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) { if (m_originalSample.frames() < 2048) { return; } + if (!handle->m_pluginData) { + handle->m_pluginData = src_new(SRC_SINC_MEDIUM_QUALITY, 2, nullptr); + } + // playback parameters const int noteIndex = handle->key() - m_parentTrack->baseNote(); const int playedFrames = handle->totalFramesPlayed(); @@ -140,12 +137,12 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) resamplerData.output_frames = frames; resamplerData.src_ratio = inversePitchRatio; - src_process(m_resamplerState, &resamplerData); + src_process((SRC_STATE*)(handle->m_pluginData), &resamplerData); } else { std::copy_n(prePitchBuffer.data(), frames, workingBuffer + offset); } // exponential fade out, applyRelease kinda sucks - if (noteFramesLeft < m_fadeOutFrames.value()) + if (noteFramesLeft * pitchRatio < m_fadeOutFrames.value()) { for (int i = 0; i < frames; i++) { @@ -349,8 +346,6 @@ void SlicerT::updateFile(QString file) m_phaseVocoder.loadSample( m_originalSample.data(), m_originalSample.frames(), m_originalSample.sampleRate(), speedRatio); - src_reset(m_resamplerState); - emit dataChanged(); } diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 66fda24727e..4d279fb054c 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -102,7 +102,6 @@ public slots: public: SlicerT(InstrumentTrack* instrumentTrack); - ~SlicerT() override; void playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) override; From eb11366d813e3130261036f422c4c1eebdb5e5b6 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 22 Oct 2023 14:10:32 +0200 Subject: [PATCH 76/99] Fixed buffer overflow in PV --- plugins/SlicerT/SlicerT.h | 13 +++++---- plugins/SlicerT/SlicerTWaveform.cpp | 4 +-- plugins/SlicerT/SlicerTWaveform.h | 6 ++-- src/core/PhaseVocoder.cpp | 45 +++++++++++++++-------------- 4 files changed, 35 insertions(+), 33 deletions(-) diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 4d279fb054c..9ef393986d2 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -25,6 +25,7 @@ #ifndef LMMS_SLICERT_H #define LMMS_SLICERT_H +#include #include #include "AutomatableModel.h" @@ -53,11 +54,11 @@ class DynamicPlaybackBuffer std::vector rightData(frames, 0); for (int i = 0; i < frames; i++) { - leftData[i] = inData[i][0]; - rightData[i] = inData[i][1]; + leftData.at(i) = inData[i][0]; + rightData.at(i) = inData[i][1]; } - m_leftChannel.loadData(leftData, sampleRate, newRatio); - m_rightChannel.loadData(rightData, sampleRate, newRatio); + m_leftChannel.loadData(std::move(leftData), sampleRate, newRatio); + m_rightChannel.loadData(std::move(rightData), sampleRate, newRatio); } void getFrames(sampleFrame* outData, int startFrame, int frames) @@ -70,8 +71,8 @@ class DynamicPlaybackBuffer for (int i = 0; i < frames; i++) { - outData[i][0] = leftOut[i]; - outData[i][1] = rightOut[i]; + outData[i][0] = leftOut.at(i); + outData[i][1] = rightOut.at(i); } } diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index ec97bff95bb..47b3628d73e 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -112,11 +112,11 @@ void SlicerTWaveform::drawSeeker() // shadow on not selected area brush.fillRect(0, 0, seekerStartPosX, m_seekerHeight, s_seekerShadowColor); - brush.fillRect(seekerEndPosX + 1, 0, m_seekerWidth + 1, m_seekerHeight, s_seekerShadowColor); + brush.fillRect(seekerEndPosX, 0, m_seekerWidth, m_seekerHeight, s_seekerShadowColor); // draw border around selection brush.setPen(QPen(s_seekerColor, 1)); - brush.drawRoundedRect(seekerStartPosX, 0, seekerMiddleWidth - 1, m_seekerHeight - 1, 4, 4); // -1 needed + brush.drawRoundedRect(seekerStartPosX, 0, seekerMiddleWidth - 1, m_seekerHeight - 1, 0, 0); // -1 needed } void SlicerTWaveform::drawEditor() diff --git a/plugins/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h index 41a06e727c4..d744a532dc1 100644 --- a/plugins/SlicerT/SlicerTWaveform.h +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -52,9 +52,9 @@ public slots: SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* parent); // predefined sizes - const int m_seekerHorMargin = 5; - const int m_seekerHeight = 38; // used to calcualte all vertical sizes - const int m_middleMargin = 6; + static constexpr int m_seekerHorMargin = 5; + static constexpr int m_seekerHeight = 38; // used to calcualte all vertical sizes + static constexpr int m_middleMargin = 6; // colors const QColor s_SlicerTWaveformBgColor = QColor(255, 255, 255, 0); diff --git a/src/core/PhaseVocoder.cpp b/src/core/PhaseVocoder.cpp index e287b87b250..06e91288381 100644 --- a/src/core/PhaseVocoder.cpp +++ b/src/core/PhaseVocoder.cpp @@ -23,6 +23,7 @@ */ #include "PhaseVocoder.h" +#include #include #include #include @@ -55,7 +56,7 @@ void PhaseVocoder::loadData(const std::vector& originalData, int sampleRa { m_dataLock.lock(); - m_originalBuffer = originalData; + m_originalBuffer = std::move(originalData); m_originalSampleRate = sampleRate; m_scaleRatio = -1; // force update, kinda hacky @@ -77,10 +78,10 @@ void PhaseVocoder::loadData(const std::vector& originalData, int sampleRa // maybe limit this to a set amount of windows to reduce initial lag spikes for (int i = 0; i < m_numWindows; i++) { - if (!m_processedWindows[i]) + if (!m_processedWindows.at(i)) { generateWindow(i, false); // first pass, no cache - m_processedWindows[i] = true; + m_processedWindows.at(i) = true; } } @@ -116,16 +117,16 @@ void PhaseVocoder::getFrames(std::vector& outData, int start, int frames) // which must be computed for (int i = startWindow; i < endWindow; i++) { - if (!m_processedWindows[i]) + if (!m_processedWindows.at(i)) { generateWindow(i, true); // theses should use the cache - m_processedWindows[i] = true; + m_processedWindows.at(i) = true; } } for (int i = 0; i < frames; i++) { - outData[i] = m_processedBuffer[start + i]; + outData.at(i) = m_processedBuffer[start + i]; } m_dataLock.unlock(); @@ -146,7 +147,7 @@ void PhaseVocoder::updateParams(float newRatio) m_expectedPhaseIn = 2. * F_PI * m_stepSize / s_windowSize; m_expectedPhaseOut = 2. * F_PI * m_outStepSize / s_windowSize; - m_processedBuffer.resize(m_scaleRatio * m_originalBuffer.size(), 0); + m_processedBuffer.resize(m_numWindows * m_outStepSize + s_windowSize, 0); // very slow :( std::fill(m_processedWindows.begin(), m_processedWindows.end(), false); @@ -165,7 +166,7 @@ void PhaseVocoder::updateParams(float newRatio) void PhaseVocoder::generateWindow(int windowNum, bool useCache) { // declare vars - float real, imag, phase, magnitude, freq, deltaPhase = 0; + float real, imag, phase, magnitude, freq, deltaPhase; int windowStart = static_cast(windowNum) * m_stepSize; int windowIndex = static_cast(windowNum) * s_windowSize; @@ -179,17 +180,17 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) // analysis step for (int j = 0; j < s_windowSize / 2; j++) // only process nyquistic frequency { - real = m_FFTSpectrum[j][0]; - imag = m_FFTSpectrum[j][1]; + real = m_FFTSpectrum.at(j)[0]; + imag = m_FFTSpectrum.at(j)[1]; magnitude = 2. * std::sqrt(real * real + imag * imag); phase = std::atan2(imag, real); // calculate difference in phase with prev window freq = phase; - freq = phase - m_lastPhase[std::max(0, windowIndex + j - s_windowSize)]; // subtract prev pahse to get phase + freq = phase - m_lastPhase.at(std::max(0, windowIndex + j - s_windowSize)); // subtract prev pahse to get phase // diference - m_lastPhase[windowIndex + j] = phase; + m_lastPhase.at(windowIndex + j) = phase; freq -= m_expectedPhaseIn * j; // subtract expected phase // at this point, freq is the difference in phase @@ -205,8 +206,8 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) // add to the expected freq the change in freq calculated from the phase diff freq = m_freqPerBin * j + m_freqPerBin * freq; - m_allMagnitudes[j] = magnitude; - m_allFrequencies[j] = freq; + m_allMagnitudes.at(j) = magnitude; + m_allFrequencies.at(j) = freq; } // write cache std::copy_n(m_allFrequencies.data(), s_windowSize, m_freqCache.data() + windowIndex); @@ -222,8 +223,8 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) // synthesis, all the operations are the reverse of the analysis for (int j = 0; j < s_windowSize / 2; j++) { - magnitude = m_allMagnitudes[j]; - freq = m_allFrequencies[j]; + magnitude = m_allMagnitudes.at(j); + freq = m_allFrequencies.at(j); // difference to bin freq mulitplier deltaPhase = freq - m_freqPerBin * j; @@ -238,13 +239,13 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) deltaPhase += m_expectedPhaseOut * j; // sum this phase to the total, to keep track of the out phase along the sample - m_sumPhase[windowIndex + j] += deltaPhase; - deltaPhase = m_sumPhase[windowIndex + j]; // final bin phase + m_sumPhase.at(windowIndex + j) += deltaPhase; + deltaPhase = m_sumPhase.at(windowIndex + j); // final bin phase - m_sumPhase[windowIndex + j + s_windowSize] = deltaPhase; // copy to the next + m_sumPhase.at(windowIndex + j + s_windowSize) = deltaPhase; // copy to the next - m_FFTSpectrum[j][0] = magnitude * std::cos(deltaPhase); - m_FFTSpectrum[j][1] = magnitude * std::sin(deltaPhase); + m_FFTSpectrum.at(j)[0] = magnitude * std::cos(deltaPhase); + m_FFTSpectrum.at(j)[1] = magnitude * std::sin(deltaPhase); } // inverse fft @@ -266,7 +267,7 @@ void PhaseVocoder::generateWindow(int windowNum, bool useCache) = a0 - (a1 * std::cos(piN2 / s_windowSize)) + (a2 * std::cos(2.0f * piN2 / s_windowSize)) - (a3 * std::cos(3.0f * piN2)); // inverse fft magnitudes are windowsSize times bigger - m_processedBuffer[outIndex] += window * (m_IFFTReconstruction[j] / s_windowSize / s_overSampling); + m_processedBuffer.at(outIndex) += window * (m_IFFTReconstruction.at(j) / s_windowSize / s_overSampling); } } } // namespace lmms From e4e680a7616c84553ebe2c4e7307ff946d65bbd4 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 22 Oct 2023 17:47:44 +0200 Subject: [PATCH 77/99] Fixed mouse bug + better empty screen --- plugins/SlicerT/SlicerTView.cpp | 1 + plugins/SlicerT/SlicerTWaveform.cpp | 21 +++++++++++++++++++-- plugins/SlicerT/SlicerTWaveform.h | 1 + 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/plugins/SlicerT/SlicerTView.cpp b/plugins/SlicerT/SlicerTView.cpp index 797e4c1c725..452f9aa5526 100644 --- a/plugins/SlicerT/SlicerTView.cpp +++ b/plugins/SlicerT/SlicerTView.cpp @@ -112,6 +112,7 @@ SlicerTView::SlicerTView(SlicerT* instrument, QWidget* parent) void SlicerTView::exportMidi() { using namespace Clipboard; + if (m_slicerTParent->m_originalSample.frames() < 2048) { return; } DataFile dataFile(DataFile::Type::ClipboardData); QDomElement note_list = dataFile.createElement("note-list"); diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index 47b3628d73e..f8b6a00e36c 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -22,8 +22,12 @@ * */ +// TODO: restructure most of the interaction code into mouseMoveEvent + #include "SlicerTWaveform.h" +#include + #include "SlicerT.h" #include "SlicerTView.h" #include "embed.h" @@ -46,6 +50,7 @@ SlicerTWaveform::SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* par , m_seeker(QPixmap(m_seekerWidth, m_seekerHeight)) , m_seekerSlicerTWaveform(QPixmap(m_seekerWidth, m_seekerHeight)) , m_sliceEditor(QPixmap(w, m_editorHeight)) + , m_emptySampleIcon(embed::getIconPixmap("sample_track.png")) // references to instrument vars , m_slicerTParent(instrument) @@ -62,6 +67,9 @@ SlicerTWaveform::SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* par connect(instrument, &SlicerT::isPlaying, this, &SlicerTWaveform::isPlaying); connect(instrument, &SlicerT::dataChanged, this, &SlicerTWaveform::updateUI); + // preprocess icons + m_emptySampleIcon = m_emptySampleIcon.createMaskFromColor(QColor(255, 255, 255), Qt::MaskMode::MaskOutColor); + updateUI(); } @@ -130,7 +138,10 @@ void SlicerTWaveform::drawEditor() brush.setPen(s_playHighlighColor); brush.setFont(QFont(brush.font().family(), 9.0f, -1, false)); brush.drawText( - m_editorWidth / 2 - 100, m_editorHeight / 2 - 100, 200, 200, Qt::AlignCenter, tr("Drag sample to load")); + m_editorWidth / 2 - 100, m_editorHeight / 2 - 110, 200, 200, Qt::AlignCenter, tr("Drag sample to load")); + int iconOffsetX = m_emptySampleIcon.width() / 2.0f; + int iconOffsetY = m_emptySampleIcon.height() / 2.0f - 13; + brush.drawPixmap(m_editorWidth / 2.0f - iconOffsetX, m_editorHeight / 2.0f - iconOffsetY, m_emptySampleIcon); return; } @@ -250,6 +261,13 @@ void SlicerTWaveform::mouseReleaseEvent(QMouseEvent* me) void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) { + // Do nothing if no button pressed + if (me->buttons() == Qt::MouseButton::NoButton) + { + m_currentlyDragging = DraggingTypes::Nothing; + + } + float normalizedClickSeeker = static_cast(me->x() - m_seekerHorMargin) / m_seekerWidth; float normalizedClickEditor = static_cast(me->x()) / m_editorWidth; @@ -314,7 +332,6 @@ void SlicerTWaveform::mouseDoubleClickEvent(QMouseEvent* me) void SlicerTWaveform::wheelEvent(QWheelEvent* _we) { - // m_zoomLevel = _we-> / 360.0f * 2.0f; m_zoomLevel += _we->angleDelta().y() / 360.0f * s_zoomSensitivity; m_zoomLevel = std::max(0.0f, m_zoomLevel); diff --git a/plugins/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h index d744a532dc1..230a2dcd2a7 100644 --- a/plugins/SlicerT/SlicerTWaveform.h +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -125,6 +125,7 @@ public slots: QPixmap m_seeker; QPixmap m_seekerSlicerTWaveform; // only stores SlicerTWaveform graphic QPixmap m_sliceEditor; + QPixmap m_emptySampleIcon; SlicerT* m_slicerTParent; From a3b601cbc62bb0503195b8b698c99311d07e7eb6 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sat, 28 Oct 2023 14:15:02 +0200 Subject: [PATCH 78/99] Small editor refactor + improvments --- plugins/SlicerT/SlicerTWaveform.cpp | 147 +++++++++++++++++----------- plugins/SlicerT/SlicerTWaveform.h | 15 ++- 2 files changed, 98 insertions(+), 64 deletions(-) diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index f8b6a00e36c..3ee09a65af5 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -22,8 +22,6 @@ * */ -// TODO: restructure most of the interaction code into mouseMoveEvent - #include "SlicerTWaveform.h" #include @@ -69,7 +67,7 @@ SlicerTWaveform::SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* par // preprocess icons m_emptySampleIcon = m_emptySampleIcon.createMaskFromColor(QColor(255, 255, 255), Qt::MaskMode::MaskOutColor); - + updateUI(); } @@ -116,15 +114,15 @@ void SlicerTWaveform::drawSeeker() brush.fillRect(noteStartPosX, 0, noteEndPosX, m_seekerHeight, s_playHighlighColor); // highlight on selected area - brush.fillRect(seekerStartPosX, 0, seekerMiddleWidth, m_seekerHeight, s_seekerHighlightColor); + brush.fillRect(seekerStartPosX, 0, seekerMiddleWidth - 1, m_seekerHeight, s_seekerHighlightColor); // shadow on not selected area brush.fillRect(0, 0, seekerStartPosX, m_seekerHeight, s_seekerShadowColor); - brush.fillRect(seekerEndPosX, 0, m_seekerWidth, m_seekerHeight, s_seekerShadowColor); + brush.fillRect(seekerEndPosX - 1, 0, m_seekerWidth, m_seekerHeight, s_seekerShadowColor); // draw border around selection brush.setPen(QPen(s_seekerColor, 1)); - brush.drawRoundedRect(seekerStartPosX, 0, seekerMiddleWidth - 1, m_seekerHeight - 1, 0, 0); // -1 needed + brush.drawRect(seekerStartPosX, 0, seekerMiddleWidth - 1, m_seekerHeight - 1); // -1 needed } void SlicerTWaveform::drawEditor() @@ -166,7 +164,7 @@ void SlicerTWaveform::drawEditor() { float xPos = (m_slicerTParent->m_slicePoints.at(i) - startFrame) / numFramesToDraw * m_editorWidth; - if (i == m_sliceSelected) { brush.setPen(QPen(s_selectedSliceColor, 2)); } + if (i == m_closestSlice) { brush.setPen(QPen(s_selectedSliceColor, 2)); } else { brush.setPen(QPen(s_sliceColor, 2)); } brush.drawLine(xPos, 0, xPos, m_editorHeight); @@ -174,14 +172,6 @@ void SlicerTWaveform::drawEditor() } } -void SlicerTWaveform::updateUI() -{ - drawSeekerSlicerTWaveform(); - drawSeeker(); - drawEditor(); - update(); -} - void SlicerTWaveform::isPlaying(float current, float start, float end) { m_noteCurrent = current; @@ -191,39 +181,41 @@ void SlicerTWaveform::isPlaying(float current, float start, float end) update(); } -// events -void SlicerTWaveform::mousePressEvent(QMouseEvent* me) +void SlicerTWaveform::updateUI() +{ + drawSeekerSlicerTWaveform(); + drawSeeker(); + drawEditor(); + update(); +} + +// updates the closest object and changes the cursor respectivly +void SlicerTWaveform::updateClosest(QMouseEvent* me) { float normalizedClickSeeker = static_cast(me->x() - m_seekerHorMargin) / m_seekerWidth; float normalizedClickEditor = static_cast(me->x()) / m_editorWidth; - // reset seeker on middle click - if (me->button() == Qt::MouseButton::MiddleButton) - { - m_seekerStart = 0; - m_seekerEnd = 1; - m_zoomLevel = 1; - return; - } + + m_closestObject = DraggingTypes::Nothing; + m_closestSlice = -1; if (me->y() < m_seekerHeight) // seeker click { if (std::abs(normalizedClickSeeker - m_seekerStart) < s_distanceForClick) // dragging start { - m_currentlyDragging = DraggingTypes::SeekerStart; + m_closestObject = DraggingTypes::SeekerStart; } else if (std::abs(normalizedClickSeeker - m_seekerEnd) < s_distanceForClick) // dragging end { - m_currentlyDragging = DraggingTypes::SeekerEnd; + m_closestObject = DraggingTypes::SeekerEnd; } else if (normalizedClickSeeker > m_seekerStart && normalizedClickSeeker < m_seekerEnd) // dragging middle { - m_currentlyDragging = DraggingTypes::SeekerMiddle; - m_seekerMiddle = normalizedClickSeeker; + m_closestObject = DraggingTypes::SeekerMiddle; } } else // editor click { - m_sliceSelected = -1; + m_closestSlice = -1; float startFrame = m_seekerStart * m_slicerTParent->m_originalSample.frames(); float endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.frames(); // select slice @@ -234,38 +226,82 @@ void SlicerTWaveform::mousePressEvent(QMouseEvent* me) if (std::abs(xPos - normalizedClickEditor) < s_distanceForClick) { - m_currentlyDragging = DraggingTypes::SlicePoint; - m_sliceSelected = i; + m_closestObject = DraggingTypes::SlicePoint; + m_closestSlice = i; } } } + updateCursor(); +} - if (me->button() == Qt::MouseButton::RightButton) // erase selected slice +void SlicerTWaveform::updateCursor() +{ + if (m_closestObject == DraggingTypes::SlicePoint || m_closestObject == DraggingTypes::SeekerStart + || m_closestObject == DraggingTypes::SeekerEnd) { - if (m_sliceSelected != -1 && m_slicerTParent->m_slicePoints.size() > 2) - { - m_slicerTParent->m_slicePoints.erase(m_slicerTParent->m_slicePoints.begin() + m_sliceSelected); - m_sliceSelected = -1; - } + setCursor(Qt::SizeHorCursor); + } + else if (m_closestObject == DraggingTypes::SeekerMiddle && m_seekerEnd - m_seekerStart != 1.0f) + { + setCursor(Qt::SizeAllCursor); + } + else { setCursor(Qt::ArrowCursor); } +} + +// handles reset, deletion and sets what to drag +void SlicerTWaveform::mousePressEvent(QMouseEvent* me) +{ + + updateClosest(me); + + // reset seeker on middle click + if (me->button() == Qt::MouseButton::MiddleButton) + { + m_seekerStart = 0; + m_seekerEnd = 1; + m_zoomLevel = 1; + return; + } + + if (me->button() == Qt::MouseButton::LeftButton) + { + m_draggedObject = m_closestObject; + m_sliceDragged = m_closestSlice; + // update seeker middle for correct movement + m_seekerMiddle = static_cast(me->x() - m_seekerHorMargin) / m_seekerWidth; + } + + // delete closesd slice to mouse + if (me->button() == Qt::MouseButton::RightButton && m_slicerTParent->m_slicePoints.size() > 2 + && m_closestObject == DraggingTypes::SlicePoint) + { + m_slicerTParent->m_slicePoints.erase(m_slicerTParent->m_slicePoints.begin() + m_closestSlice); + updateClosest(me); } updateUI(); } +// sort slices after moving and remove draggable object void SlicerTWaveform::mouseReleaseEvent(QMouseEvent* me) { - m_currentlyDragging = DraggingTypes::Nothing; + updateClosest(me); + m_draggedObject = DraggingTypes::Nothing; std::sort(m_slicerTParent->m_slicePoints.begin(), m_slicerTParent->m_slicePoints.end()); - updateUI(); } +// this handles dragging and mouse cursor changes +// what is being dragged is determined in mousePressEvent void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) { - // Do nothing if no button pressed - if (me->buttons() == Qt::MouseButton::NoButton) - { - m_currentlyDragging = DraggingTypes::Nothing; + // update for mouse changes + updateClosest(me); + // Do nothing if no button pressed + if (me->buttons() == Qt::MouseButton::NoButton) + { + updateUI(); + return; } float normalizedClickSeeker = static_cast(me->x() - m_seekerHorMargin) / m_seekerWidth; @@ -277,7 +313,7 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) float endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.frames(); // handle dragging events - switch (m_currentlyDragging) + switch (m_draggedObject) { case DraggingTypes::SeekerStart: m_seekerStart = std::clamp(normalizedClickSeeker, 0.0f, m_seekerEnd - s_minSeekerDistance); @@ -298,11 +334,11 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) break; case DraggingTypes::SlicePoint: - if (m_sliceSelected == -1) { break; } - m_slicerTParent->m_slicePoints.at(m_sliceSelected) + if (m_sliceDragged == -1) { break; } + m_slicerTParent->m_slicePoints.at(m_sliceDragged) = startFrame + normalizedClickEditor * (endFrame - startFrame); - m_slicerTParent->m_slicePoints.at(m_sliceSelected) = std::clamp( - m_slicerTParent->m_slicePoints.at(m_sliceSelected), 0, m_slicerTParent->m_originalSample.frames()); + m_slicerTParent->m_slicePoints.at(m_sliceDragged) = std::clamp( + m_slicerTParent->m_slicePoints.at(m_sliceDragged), 0, m_slicerTParent->m_originalSample.frames()); break; case DraggingTypes::Nothing: break; @@ -312,21 +348,14 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) void SlicerTWaveform::mouseDoubleClickEvent(QMouseEvent* me) { + if (me->button() != Qt::MouseButton::LeftButton) { return; } + float normalizedClickEditor = static_cast(me->x()) / m_editorWidth; float startFrame = m_seekerStart * m_slicerTParent->m_originalSample.frames(); float endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.frames(); - float slicePosition = startFrame + normalizedClickEditor * (endFrame - startFrame); - for (int i = 0; i < m_slicerTParent->m_slicePoints.size(); i++) - { - if (m_slicerTParent->m_slicePoints.at(i) < slicePosition) - { - m_slicerTParent->m_slicePoints.insert(m_slicerTParent->m_slicePoints.begin() + i, slicePosition); - break; - } - } - + m_slicerTParent->m_slicePoints.insert(m_slicerTParent->m_slicePoints.begin(), slicePosition); std::sort(m_slicerTParent->m_slicePoints.begin(), m_slicerTParent->m_slicePoints.end()); } diff --git a/plugins/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h index 230a2dcd2a7..ec13385d529 100644 --- a/plugins/SlicerT/SlicerTWaveform.h +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -70,8 +70,8 @@ public slots: const QColor s_seekerHighlightColor = QColor(178, 115, 255, 100); const QColor s_seekerShadowColor = QColor(0, 0, 0, 120); - // interaction vars - static constexpr float s_distanceForClick = 0.03f; + // interaction behavior values + static constexpr float s_distanceForClick = 0.02f; static constexpr float s_minSeekerDistance = 0.13f; static constexpr float s_zoomSensitivity = 0.5f; @@ -103,14 +103,16 @@ public slots: int m_editorHeight; int m_editorWidth; - // dragging vars - DraggingTypes m_currentlyDragging; + // interaction vars + DraggingTypes m_draggedObject; + DraggingTypes m_closestObject; + int m_closestSlice = -1; + int m_sliceDragged = -1; // seeker vars float m_seekerStart = 0; float m_seekerEnd = 1; float m_seekerMiddle = 0.5f; - int m_sliceSelected = 0; // playback highlight vars float m_noteCurrent; @@ -132,6 +134,9 @@ public slots: void drawEditor(); void drawSeekerSlicerTWaveform(); void drawSeeker(); + + void updateClosest(QMouseEvent* me); + void updateCursor(); }; } // namespace gui } // namespace lmms From 965850e3ddf6ad08cc125f5b8f8d9020d989bd38 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sat, 28 Oct 2023 17:06:27 +0200 Subject: [PATCH 79/99] Editor playback visual + small fixes --- plugins/SlicerT/SlicerTWaveform.cpp | 159 +++++++++++++++++----------- plugins/SlicerT/SlicerTWaveform.h | 26 +++-- 2 files changed, 111 insertions(+), 74 deletions(-) diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index 3ee09a65af5..9a4fc61a6e7 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -46,7 +46,8 @@ SlicerTWaveform::SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* par // create pixmaps , m_sliceArrow(PLUGIN_NAME::getIconPixmap("slide_indicator_arrow")) , m_seeker(QPixmap(m_seekerWidth, m_seekerHeight)) - , m_seekerSlicerTWaveform(QPixmap(m_seekerWidth, m_seekerHeight)) + , m_seekerWaveform(QPixmap(m_seekerWidth, m_seekerHeight)) + , m_editorWaveform(QPixmap(m_editorWidth, m_editorHeight)) , m_sliceEditor(QPixmap(w, m_editorHeight)) , m_emptySampleIcon(embed::getIconPixmap("sample_track.png")) @@ -58,8 +59,8 @@ SlicerTWaveform::SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* par setMouseTracking(true); // draw backgrounds - m_sliceEditor.fill(s_SlicerTWaveformBgColor); - m_seekerSlicerTWaveform.fill(s_SlicerTWaveformBgColor); + m_seekerWaveform.fill(s_waveformBgColor); + m_editorWaveform.fill(s_waveformBgColor); // connect to playback connect(instrument, &SlicerT::isPlaying, this, &SlicerTWaveform::isPlaying); @@ -71,21 +72,20 @@ SlicerTWaveform::SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* par updateUI(); } -void SlicerTWaveform::drawSeekerSlicerTWaveform() +void SlicerTWaveform::drawSeekerWaveform() { - m_seekerSlicerTWaveform.fill(s_SlicerTWaveformBgColor); + m_seekerWaveform.fill(s_waveformBgColor); if (m_slicerTParent->m_originalSample.frames() < 2048) { return; } - QPainter brush(&m_seekerSlicerTWaveform); - brush.setPen(s_SlicerTWaveformColor); + QPainter brush(&m_seekerWaveform); + brush.setPen(s_waveformColor); - m_slicerTParent->m_originalSample.visualize(brush, - QRect(0, 0, m_seekerSlicerTWaveform.width(), m_seekerSlicerTWaveform.height()), 0, - m_slicerTParent->m_originalSample.frames()); + m_slicerTParent->m_originalSample.visualize(brush, QRect(0, 0, m_seekerWaveform.width(), m_seekerWaveform.height()), + 0, m_slicerTParent->m_originalSample.frames()); } void SlicerTWaveform::drawSeeker() { - m_seeker.fill(s_SlicerTWaveformBgColor); + m_seeker.fill(s_emptyColor); if (m_slicerTParent->m_originalSample.frames() < 2048) { return; } QPainter brush(&m_seeker); @@ -111,7 +111,7 @@ void SlicerTWaveform::drawSeeker() // draw current playBack brush.setPen(s_playColor); brush.drawLine(noteCurrentPosX, 0, noteCurrentPosX, m_seekerHeight); - brush.fillRect(noteStartPosX, 0, noteEndPosX, m_seekerHeight, s_playHighlighColor); + brush.fillRect(noteStartPosX, 0, noteEndPosX, m_seekerHeight, s_playHighlightColor); // highlight on selected area brush.fillRect(seekerStartPosX, 0, seekerMiddleWidth - 1, m_seekerHeight, s_seekerHighlightColor); @@ -125,15 +125,31 @@ void SlicerTWaveform::drawSeeker() brush.drawRect(seekerStartPosX, 0, seekerMiddleWidth - 1, m_seekerHeight - 1); // -1 needed } +void SlicerTWaveform::drawEditorWaveform() +{ + m_editorWaveform.fill(s_emptyColor); + if (m_slicerTParent->m_originalSample.frames() < 2048) { return; } + + // draw SlicerTWaveform + QPainter brush(&m_editorWaveform); + float startFrame = m_seekerStart * m_slicerTParent->m_originalSample.frames(); + float endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.frames(); + + 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); +} + void SlicerTWaveform::drawEditor() { - m_sliceEditor.fill(s_SlicerTWaveformBgColor); + m_sliceEditor.fill(s_waveformBgColor); QPainter brush(&m_sliceEditor); // draw text if no sample loaded if (m_slicerTParent->m_originalSample.frames() < 2048) { - brush.setPen(s_playHighlighColor); + brush.setPen(s_playHighlightColor); brush.setFont(QFont(brush.font().family(), 9.0f, -1, false)); brush.drawText( m_editorWidth / 2 - 100, m_editorHeight / 2 - 110, 200, 200, Qt::AlignCenter, tr("Drag sample to load")); @@ -144,31 +160,50 @@ void SlicerTWaveform::drawEditor() } // editor boundaries - float startFrame = m_seekerStart * m_slicerTParent->m_originalSample.frames(); - float endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.frames(); + float totalFrames = m_slicerTParent->m_originalSample.frames(); + float startFrame = m_seekerStart * totalFrames; + float endFrame = m_seekerEnd * totalFrames; float numFramesToDraw = endFrame - startFrame; + // playback state + float noteCurrentPos = (m_noteCurrent - m_seekerStart) / (m_seekerEnd - m_seekerStart) * m_editorWidth; + float noteStartPos = (m_noteStart - m_seekerStart) / (m_seekerEnd - m_seekerStart) * m_editorWidth; + float noteLength = (m_noteEnd - m_noteStart) / (m_seekerEnd - m_seekerStart) * m_editorWidth; + // 0 centered line - brush.setPen(s_playHighlighColor); + brush.setPen(s_playHighlightColor); brush.drawLine(0, m_editorHeight / 2, m_editorWidth, m_editorHeight / 2); - // draw SlicerTWaveform - brush.setPen(s_SlicerTWaveformColor); - 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); + // draw waveform from pixmap + brush.drawPixmap(0, 0, m_editorWaveform); + + // draw currently playing + brush.setPen(s_playColor); + brush.drawLine(noteCurrentPos, 0, noteCurrentPos, m_editorHeight); + brush.fillRect(noteStartPos, 0, noteLength, m_editorHeight, s_playHighlightColor); // draw slicepoints brush.setPen(QPen(s_sliceColor, 2)); + for (int i = 0; i < m_slicerTParent->m_slicePoints.size(); i++) { float xPos = (m_slicerTParent->m_slicePoints.at(i) - startFrame) / numFramesToDraw * m_editorWidth; - if (i == m_closestSlice) { brush.setPen(QPen(s_selectedSliceColor, 2)); } - else { brush.setPen(QPen(s_sliceColor, 2)); } - - brush.drawLine(xPos, 0, xPos, m_editorHeight); - brush.drawPixmap(xPos - m_sliceArrow.width() / 2.0f, 0, m_sliceArrow); + if (i == m_closestSlice) + { + brush.setPen(QPen(s_sliceHighlightColor, 2)); + brush.drawLine(xPos, 0, xPos, m_editorHeight); + brush.drawPixmap(xPos - m_sliceArrow.width() / 2.0f, 0, m_sliceArrow); + continue; + } + else + { + brush.setPen(QPen(s_sliceShadowColor, 1)); + brush.drawLine(xPos - 1, 0, xPos - 1, m_editorHeight); + brush.setPen(QPen(s_sliceColor, 1)); + brush.drawLine(xPos, 0, xPos, m_editorHeight); + brush.drawPixmap(xPos - m_sliceArrow.width() / 2.0f, 0, m_sliceArrow); + } } } @@ -177,13 +212,15 @@ void SlicerTWaveform::isPlaying(float current, float start, float end) m_noteCurrent = current; m_noteStart = start; m_noteEnd = end; - drawSeeker(); // only update seeker, else horrible performance because of SlicerTWaveform redraw + drawSeeker(); + drawEditor(); update(); } void SlicerTWaveform::updateUI() { - drawSeekerSlicerTWaveform(); + drawSeekerWaveform(); + drawEditorWaveform(); drawSeeker(); drawEditor(); update(); @@ -195,22 +232,22 @@ void SlicerTWaveform::updateClosest(QMouseEvent* me) float normalizedClickSeeker = static_cast(me->x() - m_seekerHorMargin) / m_seekerWidth; float normalizedClickEditor = static_cast(me->x()) / m_editorWidth; - m_closestObject = DraggingTypes::Nothing; + m_closestObject = UIObjects::Nothing; m_closestSlice = -1; if (me->y() < m_seekerHeight) // seeker click { if (std::abs(normalizedClickSeeker - m_seekerStart) < s_distanceForClick) // dragging start { - m_closestObject = DraggingTypes::SeekerStart; + m_closestObject = UIObjects::SeekerStart; } else if (std::abs(normalizedClickSeeker - m_seekerEnd) < s_distanceForClick) // dragging end { - m_closestObject = DraggingTypes::SeekerEnd; + m_closestObject = UIObjects::SeekerEnd; } else if (normalizedClickSeeker > m_seekerStart && normalizedClickSeeker < m_seekerEnd) // dragging middle { - m_closestObject = DraggingTypes::SeekerMiddle; + m_closestObject = UIObjects::SeekerMiddle; } } else // editor click @@ -226,33 +263,33 @@ void SlicerTWaveform::updateClosest(QMouseEvent* me) if (std::abs(xPos - normalizedClickEditor) < s_distanceForClick) { - m_closestObject = DraggingTypes::SlicePoint; + m_closestObject = UIObjects::SlicePoint; m_closestSlice = i; } } } updateCursor(); + updateUI(); } void SlicerTWaveform::updateCursor() { - if (m_closestObject == DraggingTypes::SlicePoint || m_closestObject == DraggingTypes::SeekerStart - || m_closestObject == DraggingTypes::SeekerEnd) + if (m_closestObject == UIObjects::SlicePoint || m_closestObject == UIObjects::SeekerStart + || m_closestObject == UIObjects::SeekerEnd) { setCursor(Qt::SizeHorCursor); } - else if (m_closestObject == DraggingTypes::SeekerMiddle && m_seekerEnd - m_seekerStart != 1.0f) + else if (m_closestObject == UIObjects::SeekerMiddle && m_seekerEnd - m_seekerStart != 1.0f) { setCursor(Qt::SizeAllCursor); } else { setCursor(Qt::ArrowCursor); } } -// handles reset, deletion and sets what to drag +// handles deletion, reset and middles seeker void SlicerTWaveform::mousePressEvent(QMouseEvent* me) { - - updateClosest(me); + /* updateClosest(me); */ // reset seeker on middle click if (me->button() == Qt::MouseButton::MiddleButton) @@ -265,42 +302,34 @@ void SlicerTWaveform::mousePressEvent(QMouseEvent* me) if (me->button() == Qt::MouseButton::LeftButton) { - m_draggedObject = m_closestObject; - m_sliceDragged = m_closestSlice; // update seeker middle for correct movement m_seekerMiddle = static_cast(me->x() - m_seekerHorMargin) / m_seekerWidth; } // delete closesd slice to mouse if (me->button() == Qt::MouseButton::RightButton && m_slicerTParent->m_slicePoints.size() > 2 - && m_closestObject == DraggingTypes::SlicePoint) + && m_closestObject == UIObjects::SlicePoint) { m_slicerTParent->m_slicePoints.erase(m_slicerTParent->m_slicePoints.begin() + m_closestSlice); - updateClosest(me); } - updateUI(); + updateClosest(me); } // sort slices after moving and remove draggable object void SlicerTWaveform::mouseReleaseEvent(QMouseEvent* me) { - updateClosest(me); - m_draggedObject = DraggingTypes::Nothing; std::sort(m_slicerTParent->m_slicePoints.begin(), m_slicerTParent->m_slicePoints.end()); - updateUI(); + updateClosest(me); } // this handles dragging and mouse cursor changes // what is being dragged is determined in mousePressEvent void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) { - // update for mouse changes - updateClosest(me); - - // Do nothing if no button pressed + // if no button pressed, update closest and cursor if (me->buttons() == Qt::MouseButton::NoButton) { - updateUI(); + updateClosest(me); return; } @@ -313,17 +342,17 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) float endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.frames(); // handle dragging events - switch (m_draggedObject) + switch (m_closestObject) { - case DraggingTypes::SeekerStart: + case UIObjects::SeekerStart: m_seekerStart = std::clamp(normalizedClickSeeker, 0.0f, m_seekerEnd - s_minSeekerDistance); break; - case DraggingTypes::SeekerEnd: + case UIObjects::SeekerEnd: m_seekerEnd = std::clamp(normalizedClickSeeker, m_seekerStart + s_minSeekerDistance, 1.0f); break; - case DraggingTypes::SeekerMiddle: + case UIObjects::SeekerMiddle: m_seekerMiddle = normalizedClickSeeker; if (m_seekerMiddle + distStart >= 0 && m_seekerMiddle + distEnd <= 1) @@ -333,16 +362,18 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) } break; - case DraggingTypes::SlicePoint: - if (m_sliceDragged == -1) { break; } - m_slicerTParent->m_slicePoints.at(m_sliceDragged) + case UIObjects::SlicePoint: + if (m_closestSlice == -1) { break; } + m_slicerTParent->m_slicePoints.at(m_closestSlice) = startFrame + normalizedClickEditor * (endFrame - startFrame); - m_slicerTParent->m_slicePoints.at(m_sliceDragged) = std::clamp( - m_slicerTParent->m_slicePoints.at(m_sliceDragged), 0, m_slicerTParent->m_originalSample.frames()); + m_slicerTParent->m_slicePoints.at(m_closestSlice) = std::clamp( + m_slicerTParent->m_slicePoints.at(m_closestSlice), 0, m_slicerTParent->m_originalSample.frames()); break; - case DraggingTypes::Nothing: + case UIObjects::Nothing: break; } + // dont update closest, and update seeker waveform + drawEditorWaveform(); updateUI(); } @@ -371,7 +402,7 @@ void SlicerTWaveform::wheelEvent(QWheelEvent* _we) void SlicerTWaveform::paintEvent(QPaintEvent* pe) { QPainter p(this); - p.drawPixmap(m_seekerHorMargin, 0, m_seekerSlicerTWaveform); + p.drawPixmap(m_seekerHorMargin, 0, m_seekerWaveform); p.drawPixmap(m_seekerHorMargin, 0, m_seeker); p.drawPixmap(0, m_seekerHeight + m_middleMargin, m_sliceEditor); } diff --git a/plugins/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h index ec13385d529..4bbfe08b83b 100644 --- a/plugins/SlicerT/SlicerTWaveform.h +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -57,14 +57,17 @@ public slots: static constexpr int m_middleMargin = 6; // colors - const QColor s_SlicerTWaveformBgColor = QColor(255, 255, 255, 0); - const QColor s_SlicerTWaveformColor = QColor(123, 49, 212); + const QColor s_emptyColor = QColor(0, 0, 0, 0); + + const QColor s_waveformColor = QColor(123, 49, 212); + const QColor s_waveformBgColor = QColor(255, 255, 255, 0); const QColor s_playColor = QColor(255, 255, 255, 200); - const QColor s_playHighlighColor = QColor(255, 255, 255, 70); + const QColor s_playHighlightColor = QColor(255, 255, 255, 70); const QColor s_sliceColor = QColor(218, 193, 255); - const QColor s_selectedSliceColor = QColor(178, 153, 215); + const QColor s_sliceShadowColor = QColor(136, 120, 158); + const QColor s_sliceHighlightColor = QColor(255, 255, 255); const QColor s_seekerColor = QColor(178, 115, 255); const QColor s_seekerHighlightColor = QColor(178, 115, 255, 100); @@ -75,7 +78,7 @@ public slots: static constexpr float s_minSeekerDistance = 0.13f; static constexpr float s_zoomSensitivity = 0.5f; - enum class DraggingTypes + enum class UIObjects { Nothing, SeekerStart, @@ -104,10 +107,11 @@ public slots: int m_editorWidth; // interaction vars - DraggingTypes m_draggedObject; - DraggingTypes m_closestObject; + UIObjects m_draggedObject; + UIObjects m_closestObject; int m_closestSlice = -1; int m_sliceDragged = -1; + bool m_currentlyDragging = false; // seeker vars float m_seekerStart = 0; @@ -125,15 +129,17 @@ public slots: // pixmaps QPixmap m_sliceArrow; QPixmap m_seeker; - QPixmap m_seekerSlicerTWaveform; // only stores SlicerTWaveform graphic + QPixmap m_seekerWaveform; // only stores SlicerTWaveform graphic + QPixmap m_editorWaveform; QPixmap m_sliceEditor; QPixmap m_emptySampleIcon; SlicerT* m_slicerTParent; - void drawEditor(); - void drawSeekerSlicerTWaveform(); + void drawSeekerWaveform(); void drawSeeker(); + void drawEditorWaveform(); + void drawEditor(); void updateClosest(QMouseEvent* me); void updateCursor(); From 57e90fdd883efa5ceea4347b355954ca285c7ac7 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 29 Oct 2023 12:43:02 +0100 Subject: [PATCH 80/99] Roxas UI improvments --- data/themes/default/lcd_19purple.png | Bin 0 -> 9764 bytes data/themes/default/lcd_19purple_dot.png | Bin 0 -> 5403 bytes plugins/SlicerT/SlicerTView.cpp | 65 ++++++++++++++--------- plugins/SlicerT/SlicerTView.h | 28 ++++------ plugins/SlicerT/SlicerTWaveform.cpp | 10 ++++ plugins/SlicerT/SlicerTWaveform.h | 4 +- plugins/SlicerT/artwork.png | Bin 14209 -> 13791 bytes 7 files changed, 64 insertions(+), 43 deletions(-) create mode 100644 data/themes/default/lcd_19purple.png create mode 100644 data/themes/default/lcd_19purple_dot.png diff --git a/data/themes/default/lcd_19purple.png b/data/themes/default/lcd_19purple.png new file mode 100644 index 0000000000000000000000000000000000000000..35ecc4ace0e094e7702bb744acc17df00cf726e5 GIT binary patch literal 9764 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 literal 0 HcmV?d00001 diff --git a/plugins/SlicerT/SlicerTView.cpp b/plugins/SlicerT/SlicerTView.cpp index 452f9aa5526..b61092bead8 100644 --- a/plugins/SlicerT/SlicerTView.cpp +++ b/plugins/SlicerT/SlicerTView.cpp @@ -44,13 +44,11 @@ namespace gui { SlicerTView::SlicerTView(SlicerT* instrument, QWidget* parent) : InstrumentViewFixedSize(instrument, parent) , m_slicerTParent(instrument) - , m_noteThresholdKnob(this) - , m_fadeOutKnob(this) - , m_bpmBox(3, "21pink", this) + , m_bpmBox(3, "19purple", this) , m_snapSetting(this, tr("Slice snap")) , m_syncToggle("Sync", this, tr("SyncToggle"), LedCheckBox::LedColor::Green) - , m_resetButton(this, nullptr) - , m_midiExportButton(this, nullptr) + , m_resetButton(this) + , m_midiExportButton(this) , m_wf(248, 128, instrument, this) { // window settings @@ -76,38 +74,48 @@ SlicerTView::SlicerTView(SlicerT* instrument, QWidget* parent) m_syncToggle.setModel(&m_slicerTParent->m_enableSync); // bpm spin box - m_bpmBox.move(130, 203); + m_bpmBox.move(130, 201); m_bpmBox.setToolTip(tr("Original sample BPM")); - m_bpmBox.setLabel(tr("BPM")); + /* m_bpmBox.setLabel(tr("BPM")); */ m_bpmBox.setModel(&m_slicerTParent->m_originalBPM); // threshold knob - m_noteThresholdKnob.move(9, 195); - m_noteThresholdKnob.setToolTip(tr("Threshold used for slicing")); - m_noteThresholdKnob.setLabel(tr("Threshold")); - m_noteThresholdKnob.setModel(&m_slicerTParent->m_noteThreshold); + m_noteThresholdKnob = createStyledKnob(); + m_noteThresholdKnob->move(10, 197); + m_noteThresholdKnob->setToolTip(tr("Threshold used for slicing")); + /* m_noteThresholdKnob->setLabel(tr("Threshold")); */ + m_noteThresholdKnob->setModel(&m_slicerTParent->m_noteThreshold); // fadeout knob - m_fadeOutKnob.move(75, 195); - m_fadeOutKnob.setToolTip(tr("Fade Out for notes")); - m_fadeOutKnob.setLabel(tr("Fade Out")); - m_fadeOutKnob.setModel(&m_slicerTParent->m_fadeOutFrames); + m_fadeOutKnob = createStyledKnob(); + m_fadeOutKnob->move(64, 197); + m_fadeOutKnob->setToolTip(tr("Fade Out for notes")); + /* m_fadeOutKnob->setLabel(tr("Fade Out")); */ + m_fadeOutKnob->setModel(&m_slicerTParent->m_fadeOutFrames); // midi copy button - m_midiExportButton.move(215, 150); - m_midiExportButton.setActiveGraphic(PLUGIN_NAME::getIconPixmap("copyMidi")); - m_midiExportButton.setInactiveGraphic(PLUGIN_NAME::getIconPixmap("copyMidi")); + m_midiExportButton.move(199, 150); + m_midiExportButton.setIcon(PLUGIN_NAME::getIconPixmap("copyMidi")); m_midiExportButton.setToolTip(tr("Copy midi pattern to clipboard")); connect(&m_midiExportButton, &PixmapButton::clicked, this, &SlicerTView::exportMidi); // slice reset button - m_resetButton.move(19, 150); - m_resetButton.setActiveGraphic(PLUGIN_NAME::getIconPixmap("resetSlices")); - m_resetButton.setInactiveGraphic(PLUGIN_NAME::getIconPixmap("resetSlices")); + m_resetButton.move(18, 150); + m_resetButton.setIcon(PLUGIN_NAME::getIconPixmap("resetSlices")); m_resetButton.setToolTip(tr("Reset Slices")); connect(&m_resetButton, &PixmapButton::clicked, m_slicerTParent, &SlicerT::updateSlices); } +// style knob, defined in data/themes/default/style.css#L949 +Knob* SlicerTView::createStyledKnob() +{ + Knob* newKnob = new Knob(KnobType::Styled, this); + newKnob->setFixedSize(50, 40); + newKnob->setCenterPointX(24.0); + newKnob->setCenterPointY(15.0); + return newKnob; +} + // copied from piano roll void SlicerTView::exportMidi() { @@ -177,10 +185,17 @@ void SlicerTView::paintEvent(QPaintEvent* pe) { QPainter brush(this); brush.setPen(QColor(255, 255, 255)); - brush.setFont(QFont(brush.font().family(), 7.5f, -1, false)); - brush.drawText(212, 165, 25, 20, Qt::AlignCenter, tr("Midi")); - brush.drawText(14, 165, 30, 20, Qt::AlignCenter, tr("Reset")); - brush.drawText(185, 217, 55, 20, Qt::AlignCenter, tr("Snap")); + brush.setFont(QFont(brush.font().family(), 7, -1, false)); + + // top text + brush.drawText(8, topTextY, textBoxWidth, textBoxHeight, Qt::AlignCenter, tr("Reset")); + brush.drawText(188, topTextY, textBoxWidth, textBoxHeight, Qt::AlignCenter, tr("Midi")); + + // bottom text + brush.drawText(8, bottomTextY, textBoxWidth, textBoxHeight, Qt::AlignCenter, tr("Threshold")); + brush.drawText(63, bottomTextY, textBoxWidth, textBoxHeight, Qt::AlignCenter, tr("Fade Out")); + brush.drawText(127, bottomTextY, textBoxWidth, textBoxHeight, Qt::AlignCenter, tr("BPM")); + brush.drawText(188, bottomTextY, textBoxWidth, textBoxHeight, Qt::AlignCenter, tr("Snap")); } } // namespace gui diff --git a/plugins/SlicerT/SlicerTView.h b/plugins/SlicerT/SlicerTView.h index 71dcad46976..64be819c845 100644 --- a/plugins/SlicerT/SlicerTView.h +++ b/plugins/SlicerT/SlicerTView.h @@ -42,19 +42,6 @@ class SlicerT; namespace gui { -// style knob, defined in data/themes/default/style.css#L949 -class SlicerTKnob : public Knob -{ -public: - SlicerTKnob(QWidget* _parent) - : Knob(KnobType::Styled, _parent) - { - setFixedSize(50, 40); - setCenterPointX(24.0); - setCenterPointY(15.0); - } -}; - class SlicerTView : public InstrumentViewFixedSize { Q_OBJECT @@ -65,6 +52,11 @@ protected slots: public: SlicerTView(SlicerT* instrument, QWidget* parent); + static constexpr int textBoxHeight = 20; + static constexpr int textBoxWidth = 50; + static constexpr int topTextY = 170; + static constexpr int bottomTextY = 220; + protected: virtual void dragEnterEvent(QDragEnterEvent* _dee); virtual void dropEvent(QDropEvent* _de); @@ -75,17 +67,19 @@ protected slots: SlicerT* m_slicerTParent; // lmms UI - SlicerTKnob m_noteThresholdKnob; - SlicerTKnob m_fadeOutKnob; + Knob* m_noteThresholdKnob; + Knob* m_fadeOutKnob; LcdSpinBox m_bpmBox; ComboBox m_snapSetting; LedCheckBox m_syncToggle; // buttons - PixmapButton m_resetButton; - PixmapButton m_midiExportButton; + QPushButton m_resetButton; + QPushButton m_midiExportButton; SlicerTWaveform m_wf; + + Knob* createStyledKnob(); }; } // namespace gui } // namespace lmms diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index 9a4fc61a6e7..506a5e494bc 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -81,6 +81,11 @@ void SlicerTWaveform::drawSeekerWaveform() m_slicerTParent->m_originalSample.visualize(brush, QRect(0, 0, m_seekerWaveform.width(), m_seekerWaveform.height()), 0, m_slicerTParent->m_originalSample.frames()); + + // increase brightness in inner color + QBitmap innerMask = m_seekerWaveform.createMaskFromColor(s_waveformMaskColor, Qt::MaskMode::MaskOutColor); + brush.setPen(s_waveformInnerColor); + brush.drawPixmap(0, 0, innerMask); } void SlicerTWaveform::drawSeeker() @@ -139,6 +144,11 @@ void SlicerTWaveform::drawEditorWaveform() 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); + + // increase brightness in inner color + QBitmap innerMask = m_editorWaveform.createMaskFromColor(s_waveformMaskColor, Qt::MaskMode::MaskOutColor); + brush.setPen(s_waveformInnerColor); + brush.drawPixmap(0, 0, innerMask); } void SlicerTWaveform::drawEditor() diff --git a/plugins/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h index 4bbfe08b83b..0403cc46f0e 100644 --- a/plugins/SlicerT/SlicerTWaveform.h +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -58,9 +58,11 @@ public slots: // colors const QColor s_emptyColor = QColor(0, 0, 0, 0); - + const QColor s_waveformColor = QColor(123, 49, 212); const QColor s_waveformBgColor = QColor(255, 255, 255, 0); + const QColor s_waveformMaskColor = QColor(151, 65, 255); // update this if s_waveformColor changes + const QColor s_waveformInnerColor = QColor(183, 124, 255); const QColor s_playColor = QColor(255, 255, 255, 200); const QColor s_playHighlightColor = QColor(255, 255, 255, 70); diff --git a/plugins/SlicerT/artwork.png b/plugins/SlicerT/artwork.png index 76dfbc5c5cd0bdb45e5ee85051f7b2898bb3a1cc..e166273c705362e4e07b907a853492b138c566d8 100644 GIT binary patch delta 13396 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%0DyB>zbkJ3^n!0x^W9FPP5l{B;LY8dLQJIgeI$@8D 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 Date: Sat, 4 Nov 2023 00:50:43 +0100 Subject: [PATCH 81/99] initial timeshift removing --- plugins/SlicerT/SlicerT.cpp | 65 +++++++++++++------------------------ plugins/SlicerT/SlicerT.h | 55 ++----------------------------- 2 files changed, 25 insertions(+), 95 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 74c522caeca..0c8d6033627 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -60,9 +60,8 @@ SlicerT::SlicerT(InstrumentTrack* instrumentTrack) , m_fadeOutFrames(400.0f, 0.0f, 8192.0f, 1.0f, this, tr("FadeOut")) , m_originalBPM(1, 1, 999, this, tr("Original bpm")) , m_sliceSnap(this, tr("Slice snap")) - , m_enableSync(true, this, tr("BPM sync")) + , m_enableSync(false, this, tr("BPM sync")) , m_originalSample() - , m_phaseVocoder() , m_parentTrack(instrumentTrack) { m_sliceSnap.addItem("Off"); @@ -79,26 +78,32 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) { if (m_originalSample.frames() < 2048) { return; } - if (!handle->m_pluginData) { - handle->m_pluginData = src_new(SRC_SINC_MEDIUM_QUALITY, 2, nullptr); - } - // playback parameters const int noteIndex = handle->key() - m_parentTrack->baseNote(); const int playedFrames = handle->totalFramesPlayed(); const fpp_t frames = handle->framesLeftForCurrentPeriod(); const f_cnt_t offset = handle->noteOffset(); const int bpm = Engine::getSong()->getTempo(); - const float pitchRatio = pow(2, m_parentTrack->pitchModel()->value() / 1200); - const float inversePitchRatio = 1.0f / pitchRatio; + const float pitchRatio = 1 / pow(2, m_parentTrack->pitchModel()->value() / 1200); // update scaling parameters float speedRatio = static_cast(m_originalBPM.value()) / bpm; if (!m_enableSync.value()) { speedRatio = 1; } // disable timeshift - m_phaseVocoder.setScaleRatio(speedRatio); - speedRatio *= inversePitchRatio; // adjust for pitch bend + speedRatio *= pitchRatio; // adjust for pitch bend + + int totalFrames = m_originalSample.frames() * speedRatio; + if (m_playBackBuffer.size() != totalFrames) { + m_playBackBuffer.resize(totalFrames); + SRC_DATA resampleRate; + resampleRate.data_in = (float*)m_originalSample.data(); + resampleRate.data_out = (float*)m_playBackBuffer.data(); + resampleRate.input_frames = m_originalSample.frames(); + resampleRate.output_frames = totalFrames; + resampleRate.src_ratio = speedRatio; + + src_simple(&resampleRate, SRC_LINEAR, 2); + } - int totalFrames = inversePitchRatio * m_phaseVocoder.frames(); // adjust frames played with regards to pitch int sliceStart, sliceEnd; if (noteIndex > m_slicePoints.size() - 2 || noteIndex < 0) // full sample if ouside range { @@ -118,31 +123,10 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) if (noteFramesLeft > 0) { - int framesToCopy = pitchRatio * frames + 1; // just in case - int framesIndex = pitchRatio * currentNoteFrame; - framesIndex = std::min(framesIndex, m_phaseVocoder.frames() - framesToCopy); - - // load sample segmengt, with regards to pitch settings - std::vector prePitchBuffer(framesToCopy, {0.0f, 0.0f}); - m_phaseVocoder.getFrames(prePitchBuffer.data(), framesIndex, framesToCopy); - - // if pitch is changed, resample, else just copy - if (!typeInfo::isEqual(pitchRatio, 1.0f)) - { - SRC_DATA resamplerData; - - resamplerData.data_in = (float*)prePitchBuffer.data(); // wtf - resamplerData.data_out = (float*)(workingBuffer + offset); // wtf is this - resamplerData.input_frames = prePitchBuffer.size(); - resamplerData.output_frames = frames; - resamplerData.src_ratio = inversePitchRatio; - - src_process((SRC_STATE*)(handle->m_pluginData), &resamplerData); - } - else { std::copy_n(prePitchBuffer.data(), frames, workingBuffer + offset); } + std::copy_n(m_playBackBuffer.data() + currentNoteFrame, frames, workingBuffer + offset); // exponential fade out, applyRelease kinda sucks - if (noteFramesLeft * pitchRatio < m_fadeOutFrames.value()) + if (noteFramesLeft < m_fadeOutFrames.value()) { for (int i = 0; i < frames; i++) { @@ -301,14 +285,13 @@ std::vector SlicerT::getMidi() // update incase bpm changed float speedRatio = static_cast(m_originalBPM.value()) / Engine::getSong()->getTempo(); - m_phaseVocoder.setScaleRatio(speedRatio); // calculate how many "beats" are in the sample float ticksPerBar = DefaultTicksPerBar; float sampleRate = m_originalSample.sampleRate(); float bpm = Engine::getSong()->getTempo(); float samplesPerBeat = 60.0f / bpm * sampleRate; - float beats = m_phaseVocoder.frames() / samplesPerBeat; + float beats = m_originalSample.frames() * speedRatio / samplesPerBeat; // calculate how many ticks in sample float barsInSample = beats / Engine::getSong()->getTimeSigModel().getDenominator(); @@ -342,9 +325,8 @@ void SlicerT::updateFile(QString file) findBPM(); findSlices(); - float speedRatio = static_cast(m_originalBPM.value()) / Engine::getSong()->getTempo(); - m_phaseVocoder.loadSample( - m_originalSample.data(), m_originalSample.frames(), m_originalSample.sampleRate(), speedRatio); + m_playBackBuffer.resize(m_originalSample.frames()); + std::copy_n(m_originalSample.data(), m_originalSample.frames(), m_playBackBuffer.data()); emit dataChanged(); } @@ -413,9 +395,8 @@ void SlicerT::loadSettings(const QDomElement& element) m_originalBPM.loadSettings(element, "origBPM"); // create dynamic buffer - float speedRatio = static_cast(m_originalBPM.value()) / Engine::getSong()->getTempo(); - m_phaseVocoder.loadSample( - m_originalSample.data(), m_originalSample.frames(), m_originalSample.sampleRate(), speedRatio); + m_playBackBuffer.resize(m_originalSample.frames()); + std::copy_n(m_originalSample.data(), m_originalSample.frames(), m_playBackBuffer.data()); emit dataChanged(); } diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 9ef393986d2..327808e6293 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -35,61 +35,10 @@ #include "PhaseVocoder.h" #include "SampleBuffer.h" #include "SlicerTView.h" +#include "lmms_basics.h" namespace lmms { -// simple helper class that handles the different audio channels -class DynamicPlaybackBuffer -{ -public: - DynamicPlaybackBuffer() - : m_leftChannel() - , m_rightChannel() - { - } - - void loadSample(const sampleFrame* inData, int frames, int sampleRate, float newRatio) - { - std::vector leftData(frames, 0); - std::vector rightData(frames, 0); - for (int i = 0; i < frames; i++) - { - leftData.at(i) = inData[i][0]; - rightData.at(i) = inData[i][1]; - } - m_leftChannel.loadData(std::move(leftData), sampleRate, newRatio); - m_rightChannel.loadData(std::move(rightData), sampleRate, newRatio); - } - - void getFrames(sampleFrame* outData, int startFrame, int frames) - { - std::vector leftOut(frames, 0); // not a huge performance issue - std::vector rightOut(frames, 0); - - m_leftChannel.getFrames(leftOut, startFrame, frames); - m_rightChannel.getFrames(rightOut, startFrame, frames); - - for (int i = 0; i < frames; i++) - { - outData[i][0] = leftOut.at(i); - outData[i][1] = rightOut.at(i); - } - } - - int frames() { return m_leftChannel.frames(); } - float scaleRatio() { return m_leftChannel.scaleRatio(); } - - void setScaleRatio(float newRatio) - { - m_leftChannel.setScaleRatio(newRatio); - m_rightChannel.setScaleRatio(newRatio); - } - -private: - PhaseVocoder m_leftChannel; - PhaseVocoder m_rightChannel; -}; - class SlicerT : public Instrument { Q_OBJECT @@ -127,7 +76,7 @@ public slots: // sample buffers SampleBuffer m_originalSample; - DynamicPlaybackBuffer m_phaseVocoder; + std::vector m_playBackBuffer; SRC_STATE* m_resamplerState; From b23f2dc68ab5d8a8554148234785ec9ce70a34e1 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sat, 4 Nov 2023 14:50:02 +0100 Subject: [PATCH 82/99] Remove timeshift + slice refactor --- plugins/SlicerT/SlicerT.cpp | 106 +++++++++++++--------------- plugins/SlicerT/SlicerT.h | 23 ++++-- plugins/SlicerT/SlicerTWaveform.cpp | 23 +++--- 3 files changed, 79 insertions(+), 73 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 0c8d6033627..95cd8eccce5 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -80,52 +80,56 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) // playback parameters const int noteIndex = handle->key() - m_parentTrack->baseNote(); - const int playedFrames = handle->totalFramesPlayed(); const fpp_t frames = handle->framesLeftForCurrentPeriod(); const f_cnt_t offset = handle->noteOffset(); const int bpm = Engine::getSong()->getTempo(); const float pitchRatio = 1 / pow(2, m_parentTrack->pitchModel()->value() / 1200); - // update scaling parameters + // update sync parameter float speedRatio = static_cast(m_originalBPM.value()) / bpm; if (!m_enableSync.value()) { speedRatio = 1; } // disable timeshift - speedRatio *= pitchRatio; // adjust for pitch bend - - int totalFrames = m_originalSample.frames() * speedRatio; - if (m_playBackBuffer.size() != totalFrames) { - m_playBackBuffer.resize(totalFrames); - SRC_DATA resampleRate; - resampleRate.data_in = (float*)m_originalSample.data(); - resampleRate.data_out = (float*)m_playBackBuffer.data(); - resampleRate.input_frames = m_originalSample.frames(); - resampleRate.output_frames = totalFrames; - resampleRate.src_ratio = speedRatio; - - src_simple(&resampleRate, SRC_LINEAR, 2); - } + speedRatio *= pitchRatio; // adjust for pitch bend - int sliceStart, sliceEnd; + // set start and end points + float sliceStart, sliceEnd; if (noteIndex > m_slicePoints.size() - 2 || noteIndex < 0) // full sample if ouside range { sliceStart = 0; - sliceEnd = totalFrames; + sliceEnd = 1; } else { - sliceStart = m_slicePoints[noteIndex] * speedRatio; - sliceEnd = m_slicePoints[noteIndex + 1] * speedRatio; + sliceStart = m_slicePoints[noteIndex]; + sliceEnd = m_slicePoints[noteIndex + 1]; } + // initliazize handle + if (!handle->m_pluginData) { handle->m_pluginData = new PlayBackState(sliceStart); } + // slice vars - int sliceFrames = sliceEnd - sliceStart; - int currentNoteFrame = sliceStart + playedFrames; - int noteFramesLeft = sliceFrames - playedFrames; + float noteDone = ((PlayBackState*)handle->m_pluginData)->getNoteDone(); + float noteLeft = sliceEnd - noteDone; - if (noteFramesLeft > 0) + if (noteLeft > 0) { - std::copy_n(m_playBackBuffer.data() + currentNoteFrame, frames, workingBuffer + offset); + // resample in chunks + int noteFrame = noteDone * m_originalSample.frames(); + + SRC_STATE* resampleState = ((PlayBackState*)handle->m_pluginData)->getResampleState(); + SRC_DATA resampleData; + resampleData.data_in = (float*)(m_originalSample.data() + noteFrame); + resampleData.data_out = (float*)(workingBuffer + offset); + resampleData.input_frames = noteLeft * m_originalSample.frames(); + resampleData.output_frames = frames; + resampleData.src_ratio = speedRatio; + + src_process(resampleState, &resampleData); + + float nextNoteDone = noteDone + frames * (1.0f / speedRatio) / m_originalSample.frames(); + ((PlayBackState*)handle->m_pluginData)->setNoteDone(nextNoteDone); // exponential fade out, applyRelease kinda sucks + int noteFramesLeft = noteLeft * m_originalSample.frames(); if (noteFramesLeft < m_fadeOutFrames.value()) { for (int i = 0; i < frames; i++) @@ -142,11 +146,7 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) instrumentTrack()->processAudioBuffer(workingBuffer, frames + offset, handle); - // calculate absolute for the SlicerTWaveform - float absoluteCurrentNote = static_cast(currentNoteFrame) / totalFrames; - float absoluteStartNote = static_cast(sliceStart) / totalFrames; - float abslouteEndNote = static_cast(sliceEnd) / totalFrames; - emit isPlaying(absoluteCurrentNote, absoluteStartNote, abslouteEndNote); + emit isPlaying(noteDone, sliceStart, sliceEnd); } else { emit isPlaying(-1, 0, 0); } } @@ -227,24 +227,30 @@ void SlicerT::findSlices() m_slicePoints.push_back(m_originalSample.frames()); // snap slices to notes + float beatsPerMin = m_originalBPM.value() / 60.0f; + float samplesPerBeat = m_originalSample.sampleRate() / beatsPerMin * 4.0f; int noteSnap = m_sliceSnap.value(); - int timeSignature = Engine::getSong()->getTimeSigModel().getNumerator(); - int samplesPerBar = 60.0f * timeSignature / m_originalBPM.value() * m_originalSample.sampleRate(); - int sliceLock = samplesPerBar / std::pow(2, noteSnap + 1); // lock to note: 1 / noteSnap² - if (noteSnap == 0) { sliceLock = 1; } // disable noteSnap + int sliceLock = samplesPerBeat / std::pow(2, noteSnap + 1); // lock to note: 1 / noteSnap² + if (noteSnap == 0) { sliceLock = 1; } // disable noteSnap for (int i = 0; i < m_slicePoints.size(); i++) { m_slicePoints[i] += sliceLock / 2; - m_slicePoints[i] -= m_slicePoints[i] % sliceLock; + m_slicePoints[i] -= static_cast(m_slicePoints[i]) % sliceLock; } // remove duplicates m_slicePoints.erase(std::unique(m_slicePoints.begin(), m_slicePoints.end()), m_slicePoints.end()); + // scale between 0 and 1 + for (float& sliceIndex : m_slicePoints) + { + sliceIndex /= m_originalSample.frames(); + } + // fit to sample size m_slicePoints[0] = 0; - m_slicePoints[m_slicePoints.size() - 1] = m_originalSample.frames(); + m_slicePoints[m_slicePoints.size() - 1] = 1; // update UI emit dataChanged(); @@ -283,27 +289,18 @@ std::vector SlicerT::getMidi() { std::vector outputNotes; - // update incase bpm changed float speedRatio = static_cast(m_originalBPM.value()) / Engine::getSong()->getTempo(); + float outFrames = m_originalSample.frames() * speedRatio; - // calculate how many "beats" are in the sample - float ticksPerBar = DefaultTicksPerBar; - float sampleRate = m_originalSample.sampleRate(); - float bpm = Engine::getSong()->getTempo(); - float samplesPerBeat = 60.0f / bpm * sampleRate; - float beats = m_originalSample.frames() * speedRatio / samplesPerBeat; - - // calculate how many ticks in sample - float barsInSample = beats / Engine::getSong()->getTimeSigModel().getDenominator(); - float totalTicks = ticksPerBar * barsInSample; - + float framesPerTick = Engine::framesPerTick(); + float totalTicks = outFrames / framesPerTick; float lastEnd = 0; // write to midi for (int i = 0; i < m_slicePoints.size() - 1; i++) { float sliceStart = lastEnd; - float sliceEnd = totalTicks * m_slicePoints[i + 1] / m_originalSample.frames(); + float sliceEnd = totalTicks * m_slicePoints[i + 1]; Note sliceNote = Note(); sliceNote.setKey(i + m_parentTrack->baseNote()); @@ -325,9 +322,6 @@ void SlicerT::updateFile(QString file) findBPM(); findSlices(); - m_playBackBuffer.resize(m_originalSample.frames()); - std::copy_n(m_originalSample.data(), m_originalSample.frames(), m_playBackBuffer.data()); - emit dataChanged(); } @@ -357,6 +351,7 @@ void SlicerT::saveSettings(QDomDocument& document, QDomElement& element) m_fadeOutFrames.saveSettings(document, element, "fadeOut"); m_noteThreshold.saveSettings(document, element, "threshold"); m_originalBPM.saveSettings(document, element, "origBPM"); + m_enableSync.saveSettings(document, element, "syncEnable"); } void SlicerT::loadSettings(const QDomElement& element) @@ -385,7 +380,7 @@ void SlicerT::loadSettings(const QDomElement& element) m_slicePoints = {}; for (int i = 0; i < totalSlices; i++) { - m_slicePoints.push_back(element.attribute(tr("slice_%1").arg(i)).toInt()); + m_slicePoints.push_back(element.attribute(tr("slice_%1").arg(i)).toFloat()); } } @@ -393,10 +388,7 @@ void SlicerT::loadSettings(const QDomElement& element) m_fadeOutFrames.loadSettings(element, "fadeOut"); m_noteThreshold.loadSettings(element, "threshold"); m_originalBPM.loadSettings(element, "origBPM"); - - // create dynamic buffer - m_playBackBuffer.resize(m_originalSample.frames()); - std::copy_n(m_originalSample.data(), m_originalSample.frames(), m_playBackBuffer.data()); + m_enableSync.loadSettings(element, "syncEnable"); emit dataChanged(); } diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 327808e6293..5ed3367f4f6 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -39,6 +39,24 @@ namespace lmms { +class PlayBackState +{ +public: + PlayBackState(float startFrame) + : currentNote(startFrame) + { + resamplingState = src_new(SRC_LINEAR, 2, nullptr); + } + ~PlayBackState() { src_delete(resamplingState); } + float getNoteDone() { return currentNote; } + void setNoteDone(float newDone) { currentNote = newDone; } + SRC_STATE* getResampleState() { return resamplingState; } + +private: + float currentNote; // these are all absoute floats + SRC_STATE* resamplingState; +}; + class SlicerT : public Instrument { Q_OBJECT @@ -76,11 +94,8 @@ public slots: // sample buffers SampleBuffer m_originalSample; - std::vector m_playBackBuffer; - - SRC_STATE* m_resamplerState; - std::vector m_slicePoints; + std::vector m_slicePoints; InstrumentTrack* m_parentTrack; diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index 506a5e494bc..edd92948864 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -99,7 +99,7 @@ void SlicerTWaveform::drawSeeker() for (int i = 0; i < m_slicerTParent->m_slicePoints.size(); i++) { float xPos = static_cast(m_slicerTParent->m_slicePoints.at(i)) - / m_slicerTParent->m_originalSample.frames() * m_seekerWidth; + * m_seekerWidth; brush.drawLine(xPos, 0, xPos, m_seekerHeight); } @@ -170,9 +170,8 @@ void SlicerTWaveform::drawEditor() } // editor boundaries - float totalFrames = m_slicerTParent->m_originalSample.frames(); - float startFrame = m_seekerStart * totalFrames; - float endFrame = m_seekerEnd * totalFrames; + float startFrame = m_seekerStart; + float endFrame = m_seekerEnd; float numFramesToDraw = endFrame - startFrame; // playback state @@ -263,12 +262,12 @@ void SlicerTWaveform::updateClosest(QMouseEvent* me) else // editor click { m_closestSlice = -1; - float startFrame = m_seekerStart * m_slicerTParent->m_originalSample.frames(); - float endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.frames(); + float startFrame = m_seekerStart; + float endFrame = m_seekerEnd; // select slice for (int i = 0; i < m_slicerTParent->m_slicePoints.size(); i++) { - int sliceIndex = m_slicerTParent->m_slicePoints.at(i); + float sliceIndex = m_slicerTParent->m_slicePoints.at(i); float xPos = (sliceIndex - startFrame) / (endFrame - startFrame); if (std::abs(xPos - normalizedClickEditor) < s_distanceForClick) @@ -348,8 +347,8 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) float distStart = m_seekerStart - m_seekerMiddle; float distEnd = m_seekerEnd - m_seekerMiddle; - float startFrame = m_seekerStart * m_slicerTParent->m_originalSample.frames(); - float endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.frames(); + float startFrame = m_seekerStart; + float endFrame = m_seekerEnd; // handle dragging events switch (m_closestObject) @@ -377,7 +376,7 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) m_slicerTParent->m_slicePoints.at(m_closestSlice) = startFrame + normalizedClickEditor * (endFrame - startFrame); m_slicerTParent->m_slicePoints.at(m_closestSlice) = std::clamp( - m_slicerTParent->m_slicePoints.at(m_closestSlice), 0, m_slicerTParent->m_originalSample.frames()); + m_slicerTParent->m_slicePoints.at(m_closestSlice), 0.0f, 1.0f); break; case UIObjects::Nothing: break; @@ -392,8 +391,8 @@ void SlicerTWaveform::mouseDoubleClickEvent(QMouseEvent* me) if (me->button() != Qt::MouseButton::LeftButton) { return; } float normalizedClickEditor = static_cast(me->x()) / m_editorWidth; - float startFrame = m_seekerStart * m_slicerTParent->m_originalSample.frames(); - float endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.frames(); + float startFrame = m_seekerStart; + float endFrame = m_seekerEnd; float slicePosition = startFrame + normalizedClickEditor * (endFrame - startFrame); m_slicerTParent->m_slicePoints.insert(m_slicerTParent->m_slicePoints.begin(), slicePosition); From 33c08f320f87ba56a6ea688500a5e0655e49fd2b Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sat, 4 Nov 2023 15:23:54 +0100 Subject: [PATCH 83/99] Removed unused files --- include/PhaseVocoder.h | 108 ------- plugins/SlicerT/SlicerT.h | 1 - plugins/SlicerT/SlicerTWaveform.cpp | 2 +- plugins/SlicerT/artwork.xcf | Bin 92565 -> 0 bytes plugins/SlicerT/logo.png | Bin 11243 -> 0 bytes ...or_arrow.png => slice_indicator_arrow.png} | Bin src/core/CMakeLists.txt | 1 - src/core/PhaseVocoder.cpp | 273 ------------------ 8 files changed, 1 insertion(+), 384 deletions(-) delete mode 100644 include/PhaseVocoder.h delete mode 100644 plugins/SlicerT/artwork.xcf delete mode 100644 plugins/SlicerT/logo.png rename plugins/SlicerT/{slide_indicator_arrow.png => slice_indicator_arrow.png} (100%) delete mode 100644 src/core/PhaseVocoder.cpp diff --git a/include/PhaseVocoder.h b/include/PhaseVocoder.h deleted file mode 100644 index 13a41421b93..00000000000 --- a/include/PhaseVocoder.h +++ /dev/null @@ -1,108 +0,0 @@ -/* - * PhaseVocoder.h - declaration of the PhaseVocoder class - * - * Copyright (c) 2023 Daniel Kauss Serna - * - * 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_PHASEVOCODER_H -#define LMMS_PHASEVOCODER_H - -#include -#include -#include -#include -#include - -#include "lmms_export.h" - -namespace lmms { - -/** - Dynamically timeshifts one audio channel by a changable ratio - Allows access to the timeshifted data in a threadsafe and realtime maner -*/ -class LMMS_EXPORT PhaseVocoder -{ -public: - PhaseVocoder(); - ~PhaseVocoder(); - - //! Loads a new sample, and precomputes the analysis cache - void loadData(const std::vector& originalData, int sampleRate, float newRatio); - //! Change the output timeshift ratio - void setScaleRatio(float newRatio) { updateParams(newRatio); } - - //! Copy a number of frames from a startpoint into an out buffer. - //! This is NOT relative to the original sample - void getFrames(std::vector& outData, int start, int frames); - - //! Get total number of frames for the timeshifted sample, NOT the original - int frames() { return m_processedBuffer.size(); } - //! Get the current scaleRatio - float scaleRatio() { return m_scaleRatio; } - - // timeshift config - static const int s_windowSize = 512; - static const int s_overSampling = 32; - -private: - QMutex m_dataLock; - // original data - std::vector m_originalBuffer; - int m_originalSampleRate = 0; - - float m_scaleRatio = -1; // to force on first load - - // output data - std::vector m_processedBuffer; // final output - std::vector m_processedWindows; // marks a window processed - - // depending on scaleRatio - int m_stepSize = 0; - int m_numWindows = 0; - float m_outStepSize = 0; - float m_freqPerBin = 0; - float m_expectedPhaseIn = 0; - float m_expectedPhaseOut = 0; - - // buffers - std::array m_FFTSpectrum; - std::vector m_FFTInput; - std::vector m_IFFTReconstruction; - std::vector m_allMagnitudes; - std::vector m_allFrequencies; - std::vector m_processedFreq; - std::vector m_processedMagn; - std::vector m_lastPhase; - std::vector m_sumPhase; - - // cache - std::vector m_freqCache; - std::vector m_magCache; - - // fftw plans - fftwf_plan m_fftPlan; - fftwf_plan m_ifftPlan; - - void updateParams(float newRatio); - void generateWindow(int windowNum, bool useCache); -}; -} // namespace lmms -#endif // LMMS_PHASEVOCODER_H diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 5ed3367f4f6..4acdfee1b35 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -32,7 +32,6 @@ #include "Instrument.h" #include "InstrumentView.h" #include "Note.h" -#include "PhaseVocoder.h" #include "SampleBuffer.h" #include "SlicerTView.h" #include "lmms_basics.h" diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index edd92948864..e01e3902575 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -44,7 +44,7 @@ SlicerTWaveform::SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* par , m_editorWidth(w) // create pixmaps - , m_sliceArrow(PLUGIN_NAME::getIconPixmap("slide_indicator_arrow")) + , m_sliceArrow(PLUGIN_NAME::getIconPixmap("slice_indicator_arrow")) , m_seeker(QPixmap(m_seekerWidth, m_seekerHeight)) , m_seekerWaveform(QPixmap(m_seekerWidth, m_seekerHeight)) , m_editorWaveform(QPixmap(m_editorWidth, m_editorHeight)) diff --git a/plugins/SlicerT/artwork.xcf b/plugins/SlicerT/artwork.xcf deleted file mode 100644 index 63cd2e12f50071c764f3f15005df8b9d7ff18497..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 92565 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 diff --git a/plugins/SlicerT/slide_indicator_arrow.png b/plugins/SlicerT/slice_indicator_arrow.png similarity index 100% rename from plugins/SlicerT/slide_indicator_arrow.png rename to plugins/SlicerT/slice_indicator_arrow.png diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b72ab0565c5..319882af2f9 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -53,7 +53,6 @@ set(LMMS_SRCS core/PatternStore.cpp core/PeakController.cpp core/PerfLog.cpp - core/PhaseVocoder.cpp core/Piano.cpp core/PlayHandle.cpp core/Plugin.cpp diff --git a/src/core/PhaseVocoder.cpp b/src/core/PhaseVocoder.cpp deleted file mode 100644 index 06e91288381..00000000000 --- a/src/core/PhaseVocoder.cpp +++ /dev/null @@ -1,273 +0,0 @@ -/* - * PhaseVocoder.cpp - Implementation of the PhaseVocoder class - * - * Copyright (c) 2023 Daniel Kauss Serna - * - * 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 "PhaseVocoder.h" - -#include -#include -#include -#include -#include - -#include "lmms_basics.h" -#include "lmms_constants.h" - -namespace lmms { - -PhaseVocoder::PhaseVocoder() - : m_FFTInput(s_windowSize, 0) - , m_IFFTReconstruction(s_windowSize, 0) - , m_allMagnitudes(s_windowSize, 0) - , m_allFrequencies(s_windowSize, 0) - , m_processedFreq(s_windowSize, 0) - , m_processedMagn(s_windowSize, 0) -{ - m_fftPlan = fftwf_plan_dft_r2c_1d(s_windowSize, m_FFTInput.data(), m_FFTSpectrum.data(), FFTW_MEASURE); - m_ifftPlan = fftwf_plan_dft_c2r_1d(s_windowSize, m_FFTSpectrum.data(), m_IFFTReconstruction.data(), FFTW_MEASURE); -} - -PhaseVocoder::~PhaseVocoder() -{ - fftwf_destroy_plan(m_fftPlan); - fftwf_destroy_plan(m_ifftPlan); -} - -void PhaseVocoder::loadData(const std::vector& originalData, int sampleRate, float newRatio) -{ - m_dataLock.lock(); - - m_originalBuffer = std::move(originalData); - m_originalSampleRate = sampleRate; - m_scaleRatio = -1; // force update, kinda hacky - - m_dataLock.unlock(); // stupid, but QRecursiveMutex is too expensive to have in updateParas and getFrames - updateParams(newRatio); - m_dataLock.lock(); - - // set buffer sizes - m_processedWindows.resize(m_numWindows, false); - m_lastPhase.resize(m_numWindows * s_windowSize, 0); - m_sumPhase.resize((m_numWindows + 1) * s_windowSize, 0); - m_freqCache.resize(m_numWindows * s_windowSize, 0); - m_magCache.resize(m_numWindows * s_windowSize, 0); - - // clear phase buffers - std::fill(m_lastPhase.begin(), m_lastPhase.end(), 0); - std::fill(m_sumPhase.begin(), m_sumPhase.end(), 0); - - // maybe limit this to a set amount of windows to reduce initial lag spikes - for (int i = 0; i < m_numWindows; i++) - { - if (!m_processedWindows.at(i)) - { - generateWindow(i, false); // first pass, no cache - m_processedWindows.at(i) = true; - } - } - - m_dataLock.unlock(); -} - -void PhaseVocoder::getFrames(std::vector& outData, int start, int frames) -{ - if (m_originalBuffer.size() < 2048) { return; } - m_dataLock.lock(); - - if (typeInfo::isEqual(m_scaleRatio, 1.0f)) - { // directly copy original data - std::copy_n(m_originalBuffer.data() + start, frames, outData.data()); - m_dataLock.unlock(); - return; - } - - int windowMargin = s_overSampling / 2; // numbers of windows before full quality - int startWindow = static_cast(start) / m_outStepSize - windowMargin; - int endWindow = static_cast((start + frames)) / m_outStepSize + windowMargin; - - startWindow = std::clamp(startWindow, 0, m_numWindows - 1); - endWindow = std::clamp(endWindow, 0, m_numWindows - 1); - - // discard previous phaseSum if not processed - if (!m_processedWindows[startWindow]) - { - std::fill_n(m_sumPhase.data() + startWindow * s_windowSize, s_windowSize, 0); - } - - // this encompases the minimum windows needed to get full quality, - // which must be computed - for (int i = startWindow; i < endWindow; i++) - { - if (!m_processedWindows.at(i)) - { - generateWindow(i, true); // theses should use the cache - m_processedWindows.at(i) = true; - } - } - - for (int i = 0; i < frames; i++) - { - outData.at(i) = m_processedBuffer[start + i]; - } - - m_dataLock.unlock(); -} - -// adjust pv params buffers to a new scale ratio -void PhaseVocoder::updateParams(float newRatio) -{ - if (m_originalBuffer.size() < 2048) { return; } - if (newRatio == m_scaleRatio) { return; } // nothing changed - m_dataLock.lock(); - - m_scaleRatio = newRatio; - m_stepSize = static_cast(s_windowSize) / s_overSampling; - m_numWindows = static_cast(m_originalBuffer.size()) / m_stepSize - s_overSampling - 1; - m_outStepSize = m_scaleRatio * m_stepSize; // float, else inaccurate - m_freqPerBin = m_originalSampleRate / s_windowSize; - m_expectedPhaseIn = 2. * F_PI * m_stepSize / s_windowSize; - m_expectedPhaseOut = 2. * F_PI * m_outStepSize / s_windowSize; - - m_processedBuffer.resize(m_numWindows * m_outStepSize + s_windowSize, 0); - - // very slow :( - std::fill(m_processedWindows.begin(), m_processedWindows.end(), false); - std::fill(m_processedBuffer.begin(), m_processedBuffer.end(), 0); - - m_dataLock.unlock(); -} - -// time shifts one window from originalBuffer and writes to m_processedBuffer -// resources: -// http://blogs.zynaptiq.com/bernsee/pitch-shifting-using-the-ft/ -// https://sethares.engr.wisc.edu/vocoders/phasevocoder.html -// https://dsp.stackexchange.com/questions/40101/audio-time-stretching-without-pitch-shifting/40367#40367 -// https://www.guitarpitchshifter.com/ -// https://en.wikipedia.org/wiki/Window_function -void PhaseVocoder::generateWindow(int windowNum, bool useCache) -{ - // declare vars - float real, imag, phase, magnitude, freq, deltaPhase; - int windowStart = static_cast(windowNum) * m_stepSize; - int windowIndex = static_cast(windowNum) * s_windowSize; - - if (!useCache) - { // normal stuff - std::copy_n(m_originalBuffer.data() + windowStart, s_windowSize, m_FFTInput.data()); - - // FFT - fftwf_execute(m_fftPlan); - - // analysis step - for (int j = 0; j < s_windowSize / 2; j++) // only process nyquistic frequency - { - real = m_FFTSpectrum.at(j)[0]; - imag = m_FFTSpectrum.at(j)[1]; - - magnitude = 2. * std::sqrt(real * real + imag * imag); - phase = std::atan2(imag, real); - - // calculate difference in phase with prev window - freq = phase; - freq = phase - m_lastPhase.at(std::max(0, windowIndex + j - s_windowSize)); // subtract prev pahse to get phase - // diference - m_lastPhase.at(windowIndex + j) = phase; - - freq -= m_expectedPhaseIn * j; // subtract expected phase - // at this point, freq is the difference in phase - // between the last phase, having removed the expected phase at this point in the sample - - // this puts freq in 0-2pi. Since the phase difference is proportional to the deviation in bin frequency, - // with this we can better estimate the true frequency - freq = std::fmod(freq + F_PI, -2.0f * F_PI) + F_PI; - - // convert phase difference into bin freq mulitplier - freq = freq * s_overSampling / (2. * F_PI); - - // add to the expected freq the change in freq calculated from the phase diff - freq = m_freqPerBin * j + m_freqPerBin * freq; - - m_allMagnitudes.at(j) = magnitude; - m_allFrequencies.at(j) = freq; - } - // write cache - std::copy_n(m_allFrequencies.data(), s_windowSize, m_freqCache.data() + windowIndex); - std::copy_n(m_allMagnitudes.data(), s_windowSize, m_magCache.data() + windowIndex); - } - else - { - // read cache - std::copy_n(m_freqCache.data() + windowIndex, s_windowSize, m_allFrequencies.data()); - std::copy_n(m_magCache.data() + windowIndex, s_windowSize, m_allMagnitudes.data()); - } - - // synthesis, all the operations are the reverse of the analysis - for (int j = 0; j < s_windowSize / 2; j++) - { - magnitude = m_allMagnitudes.at(j); - freq = m_allFrequencies.at(j); - - // difference to bin freq mulitplier - deltaPhase = freq - m_freqPerBin * j; - - // convert to phase difference - deltaPhase /= m_freqPerBin; - - // difference in phase - deltaPhase = 2. * F_PI * deltaPhase / s_overSampling; - - // add the expected phase - deltaPhase += m_expectedPhaseOut * j; - - // sum this phase to the total, to keep track of the out phase along the sample - m_sumPhase.at(windowIndex + j) += deltaPhase; - deltaPhase = m_sumPhase.at(windowIndex + j); // final bin phase - - m_sumPhase.at(windowIndex + j + s_windowSize) = deltaPhase; // copy to the next - - m_FFTSpectrum.at(j)[0] = magnitude * std::cos(deltaPhase); - m_FFTSpectrum.at(j)[1] = magnitude * std::sin(deltaPhase); - } - - // inverse fft - fftwf_execute(m_ifftPlan); - - // windowing - for (int j = 0; j < s_windowSize; j++) - { - float outIndex = windowNum * m_outStepSize + j; - - // blackman-harris window - float a0 = 0.35875f; - float a1 = 0.48829f; - float a2 = 0.14128f; - float a3 = 0.01168f; - - float piN2 = 2.0f * F_PI * j; - float window - = a0 - (a1 * std::cos(piN2 / s_windowSize)) + (a2 * std::cos(2.0f * piN2 / s_windowSize)) - (a3 * std::cos(3.0f * piN2)); - - // inverse fft magnitudes are windowsSize times bigger - m_processedBuffer.at(outIndex) += window * (m_IFFTReconstruction.at(j) / s_windowSize / s_overSampling); - } -} -} // namespace lmms From efde89d90fe09bc5a0dd891fc2027749ee49b61e Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sat, 4 Nov 2023 15:36:11 +0100 Subject: [PATCH 84/99] Fix export bug --- plugins/SlicerT/SlicerT.cpp | 1 + plugins/SlicerT/SlicerT.h | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 95cd8eccce5..adac39c7295 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -89,6 +89,7 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) float speedRatio = static_cast(m_originalBPM.value()) / bpm; if (!m_enableSync.value()) { speedRatio = 1; } // disable timeshift speedRatio *= pitchRatio; // adjust for pitch bend + speedRatio *= Engine::audioEngine()->processingSampleRate() / static_cast(m_originalSample.sampleRate()) ; // set start and end points float sliceStart, sliceEnd; diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 4acdfee1b35..902f7cda34c 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -44,7 +44,7 @@ class PlayBackState PlayBackState(float startFrame) : currentNote(startFrame) { - resamplingState = src_new(SRC_LINEAR, 2, nullptr); + resamplingState = src_new(SRC_SINC_MEDIUM_QUALITY, 2, nullptr); } ~PlayBackState() { src_delete(resamplingState); } float getNoteDone() { return currentNote; } From adb81481dc6d2ace3faee72bc69f9c3ac9834b32 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sat, 4 Nov 2023 15:53:20 +0100 Subject: [PATCH 85/99] Fix zoom bug --- plugins/SlicerT/SlicerTWaveform.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index e01e3902575..15f42497a5e 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -404,8 +404,7 @@ void SlicerTWaveform::wheelEvent(QWheelEvent* _we) m_zoomLevel += _we->angleDelta().y() / 360.0f * s_zoomSensitivity; m_zoomLevel = std::max(0.0f, m_zoomLevel); - drawEditor(); - update(); + updateUI(); } void SlicerTWaveform::paintEvent(QPaintEvent* pe) From a4ab2b478e0ea36e505c6f8ad637f389724d9ce1 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 5 Nov 2023 03:02:37 +0100 Subject: [PATCH 86/99] Review changes SakerTooth#2 --- plugins/SlicerT/SlicerT.cpp | 21 +++++------ plugins/SlicerT/SlicerT.h | 2 +- plugins/SlicerT/SlicerTView.cpp | 57 ++++++++++++++-------------- plugins/SlicerT/SlicerTView.h | 12 +++--- plugins/SlicerT/SlicerTWaveform.cpp | 58 ++++++++++++++--------------- plugins/SlicerT/SlicerTWaveform.h | 2 +- 6 files changed, 74 insertions(+), 78 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index adac39c7295..ad44cf7a0f7 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -26,7 +26,7 @@ #include #include -#include +#include #include "Engine.h" #include "InstrumentTrack.h" @@ -76,7 +76,7 @@ SlicerT::SlicerT(InstrumentTrack* instrumentTrack) void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) { - if (m_originalSample.frames() < 2048) { return; } + if (m_originalSample.frames() <= 1) { return; } // playback parameters const int noteIndex = handle->key() - m_parentTrack->baseNote(); @@ -89,7 +89,7 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) float speedRatio = static_cast(m_originalBPM.value()) / bpm; if (!m_enableSync.value()) { speedRatio = 1; } // disable timeshift speedRatio *= pitchRatio; // adjust for pitch bend - speedRatio *= Engine::audioEngine()->processingSampleRate() / static_cast(m_originalSample.sampleRate()) ; + speedRatio *= Engine::audioEngine()->processingSampleRate() / static_cast(m_originalSample.sampleRate()); // set start and end points float sliceStart, sliceEnd; @@ -157,7 +157,7 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) // http://www.iro.umontreal.ca/~pift6080/H09/documents/papers/bello_onset_tutorial.pdf void SlicerT::findSlices() { - if (m_originalSample.frames() < 2048) { return; } + if (m_originalSample.frames() <= 1) { return; } m_slicePoints = {}; // computacion params @@ -177,9 +177,9 @@ void SlicerT::findSlices() } // normalize - for (int i = 0; i < singleChannel.size(); i++) + for (float& channelValue : singleChannel) { - singleChannel[i] /= maxMag; + channelValue /= maxMag; } // buffers @@ -234,10 +234,10 @@ void SlicerT::findSlices() int sliceLock = samplesPerBeat / std::pow(2, noteSnap + 1); // lock to note: 1 / noteSnap² if (noteSnap == 0) { sliceLock = 1; } // disable noteSnap - for (int i = 0; i < m_slicePoints.size(); i++) + for (float& sliceValue : m_slicePoints) { - m_slicePoints[i] += sliceLock / 2; - m_slicePoints[i] -= static_cast(m_slicePoints[i]) % sliceLock; + sliceValue += sliceLock / 2; + sliceValue -= static_cast(sliceValue) % sliceLock; } // remove duplicates @@ -261,7 +261,7 @@ void SlicerT::findSlices() // and lies in the 100 - 200 bpm range void SlicerT::findBPM() { - if (m_originalSample.frames() < 2048) { return; } + if (m_originalSample.frames() <= 1) { return; } // caclulate length of sample float sampleRate = m_originalSample.sampleRate(); @@ -318,7 +318,6 @@ std::vector SlicerT::getMidi() void SlicerT::updateFile(QString file) { m_originalSample.setAudioFile(file); - if (m_originalSample.frames() < 2048) { return; } findBPM(); findSlices(); diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 902f7cda34c..c2ca265555a 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -43,8 +43,8 @@ class PlayBackState public: PlayBackState(float startFrame) : currentNote(startFrame) + , resamplingState(src_new(SRC_LINEAR, 2, nullptr)) { - resamplingState = src_new(SRC_SINC_MEDIUM_QUALITY, 2, nullptr); } ~PlayBackState() { src_delete(resamplingState); } float getNoteDone() { return currentNote; } diff --git a/plugins/SlicerT/SlicerTView.cpp b/plugins/SlicerT/SlicerTView.cpp index b61092bead8..350f0f5ff70 100644 --- a/plugins/SlicerT/SlicerTView.cpp +++ b/plugins/SlicerT/SlicerTView.cpp @@ -44,12 +44,6 @@ namespace gui { SlicerTView::SlicerTView(SlicerT* instrument, QWidget* parent) : InstrumentViewFixedSize(instrument, parent) , m_slicerTParent(instrument) - , m_bpmBox(3, "19purple", this) - , m_snapSetting(this, tr("Slice snap")) - , m_syncToggle("Sync", this, tr("SyncToggle"), LedCheckBox::LedColor::Green) - , m_resetButton(this) - , m_midiExportButton(this) - , m_wf(248, 128, instrument, this) { // window settings setAcceptDrops(true); @@ -61,23 +55,26 @@ SlicerTView::SlicerTView(SlicerT* instrument, QWidget* parent) setPalette(pal); // move editor and seeker - m_wf.move(2, 6); + m_wf = new SlicerTWaveform(248, 128, instrument, this); + m_wf->move(2, 6); // snap combo box - m_snapSetting.setGeometry(185, 200, 55, ComboBox::DEFAULT_HEIGHT); - m_snapSetting.setToolTip(tr("Set slice snapping for detection")); - m_snapSetting.setModel(&m_slicerTParent->m_sliceSnap); + m_snapSetting = new ComboBox(this, tr("Slice snap")); + m_snapSetting->setGeometry(185, 200, 55, ComboBox::DEFAULT_HEIGHT); + m_snapSetting->setToolTip(tr("Set slice snapping for detection")); + m_snapSetting->setModel(&m_slicerTParent->m_sliceSnap); // sync toggle - m_syncToggle.move(135, 187); - m_syncToggle.setToolTip(tr("Enable BPM sync")); - m_syncToggle.setModel(&m_slicerTParent->m_enableSync); + m_syncToggle = new LedCheckBox("Sync", this, tr("SyncToggle"), LedCheckBox::LedColor::Green); + m_syncToggle->move(135, 187); + m_syncToggle->setToolTip(tr("Enable BPM sync")); + m_syncToggle->setModel(&m_slicerTParent->m_enableSync); // bpm spin box - m_bpmBox.move(130, 201); - m_bpmBox.setToolTip(tr("Original sample BPM")); - /* m_bpmBox.setLabel(tr("BPM")); */ - m_bpmBox.setModel(&m_slicerTParent->m_originalBPM); + m_bpmBox = new LcdSpinBox(3, "19purple", this); + m_bpmBox->move(130, 201); + m_bpmBox->setToolTip(tr("Original sample BPM")); + m_bpmBox->setModel(&m_slicerTParent->m_originalBPM); // threshold knob m_noteThresholdKnob = createStyledKnob(); @@ -94,19 +91,20 @@ SlicerTView::SlicerTView(SlicerT* instrument, QWidget* parent) m_fadeOutKnob->setModel(&m_slicerTParent->m_fadeOutFrames); // midi copy button - m_midiExportButton.move(199, 150); - m_midiExportButton.setIcon(PLUGIN_NAME::getIconPixmap("copyMidi")); - m_midiExportButton.setToolTip(tr("Copy midi pattern to clipboard")); - connect(&m_midiExportButton, &PixmapButton::clicked, this, &SlicerTView::exportMidi); + m_midiExportButton = new QPushButton(this); + m_midiExportButton->move(199, 150); + m_midiExportButton->setIcon(PLUGIN_NAME::getIconPixmap("copyMidi")); + m_midiExportButton->setToolTip(tr("Copy midi pattern to clipboard")); + connect(m_midiExportButton, &PixmapButton::clicked, this, &SlicerTView::exportMidi); // slice reset button - m_resetButton.move(18, 150); - m_resetButton.setIcon(PLUGIN_NAME::getIconPixmap("resetSlices")); - m_resetButton.setToolTip(tr("Reset Slices")); - connect(&m_resetButton, &PixmapButton::clicked, m_slicerTParent, &SlicerT::updateSlices); + m_resetButton = new QPushButton(this); + m_resetButton->move(18, 150); + m_resetButton->setIcon(PLUGIN_NAME::getIconPixmap("resetSlices")); + m_resetButton->setToolTip(tr("Reset Slices")); + connect(m_resetButton, &PixmapButton::clicked, m_slicerTParent, &SlicerT::updateSlices); } -// style knob, defined in data/themes/default/style.css#L949 Knob* SlicerTView::createStyledKnob() { Knob* newKnob = new Knob(KnobType::Styled, this); @@ -120,7 +118,7 @@ Knob* SlicerTView::createStyledKnob() void SlicerTView::exportMidi() { using namespace Clipboard; - if (m_slicerTParent->m_originalSample.frames() < 2048) { return; } + if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } DataFile dataFile(DataFile::Type::ClipboardData); QDomElement note_list = dataFile.createElement("note-list"); @@ -132,9 +130,8 @@ void SlicerTView::exportMidi() TimePos start_pos(notes.front().pos().getBar(), 0); for (Note note : notes) { - Note clip_note(note); - clip_note.setPos(clip_note.pos(start_pos)); - clip_note.saveState(dataFile, note_list); + note.setPos(note.pos(start_pos)); + note.saveState(dataFile, note_list); } copyString(dataFile.toString(), MimeType::Default); diff --git a/plugins/SlicerT/SlicerTView.h b/plugins/SlicerT/SlicerTView.h index 64be819c845..b1b1d4158d2 100644 --- a/plugins/SlicerT/SlicerTView.h +++ b/plugins/SlicerT/SlicerTView.h @@ -69,15 +69,15 @@ protected slots: // lmms UI Knob* m_noteThresholdKnob; Knob* m_fadeOutKnob; - LcdSpinBox m_bpmBox; - ComboBox m_snapSetting; - LedCheckBox m_syncToggle; + LcdSpinBox* m_bpmBox; + ComboBox* m_snapSetting; + LedCheckBox* m_syncToggle; // buttons - QPushButton m_resetButton; - QPushButton m_midiExportButton; + QPushButton* m_resetButton; + QPushButton* m_midiExportButton; - SlicerTWaveform m_wf; + SlicerTWaveform* m_wf; Knob* createStyledKnob(); }; diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index 15f42497a5e..2a8823e3048 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -34,22 +34,22 @@ namespace lmms { namespace gui { -SlicerTWaveform::SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* parent) +SlicerTWaveform::SlicerTWaveform(int totalWidth, int totalHeight, SlicerT* instrument, QWidget* parent) : QWidget(parent) // calculate sizes - , m_width(w) - , m_height(h) - , m_seekerWidth(w - m_seekerHorMargin * 2) - , m_editorHeight(h - m_seekerHeight - m_middleMargin) - , m_editorWidth(w) + , m_width(totalWidth) + , m_height(totalHeight) + , m_seekerWidth(totalWidth - m_seekerHorMargin * 2) + , m_editorHeight(totalHeight - m_seekerHeight - m_middleMargin) + , m_editorWidth(totalWidth) // create pixmaps , m_sliceArrow(PLUGIN_NAME::getIconPixmap("slice_indicator_arrow")) , m_seeker(QPixmap(m_seekerWidth, m_seekerHeight)) , m_seekerWaveform(QPixmap(m_seekerWidth, m_seekerHeight)) , m_editorWaveform(QPixmap(m_editorWidth, m_editorHeight)) - , m_sliceEditor(QPixmap(w, m_editorHeight)) - , m_emptySampleIcon(embed::getIconPixmap("sample_track.png")) + , m_sliceEditor(QPixmap(totalWidth, m_editorHeight)) + , m_emptySampleIcon(embed::getIconPixmap("sample_track")) // references to instrument vars , m_slicerTParent(instrument) @@ -75,7 +75,7 @@ SlicerTWaveform::SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* par void SlicerTWaveform::drawSeekerWaveform() { m_seekerWaveform.fill(s_waveformBgColor); - if (m_slicerTParent->m_originalSample.frames() < 2048) { return; } + if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } QPainter brush(&m_seekerWaveform); brush.setPen(s_waveformColor); @@ -91,15 +91,14 @@ void SlicerTWaveform::drawSeekerWaveform() void SlicerTWaveform::drawSeeker() { m_seeker.fill(s_emptyColor); - if (m_slicerTParent->m_originalSample.frames() < 2048) { return; } + if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } QPainter brush(&m_seeker); // draw slice points brush.setPen(s_sliceColor); - for (int i = 0; i < m_slicerTParent->m_slicePoints.size(); i++) + for (float sliceValue: m_slicerTParent->m_slicePoints) { - float xPos = static_cast(m_slicerTParent->m_slicePoints.at(i)) - * m_seekerWidth; + float xPos = sliceValue * m_seekerWidth; brush.drawLine(xPos, 0, xPos, m_seekerHeight); } @@ -133,7 +132,7 @@ void SlicerTWaveform::drawSeeker() void SlicerTWaveform::drawEditorWaveform() { m_editorWaveform.fill(s_emptyColor); - if (m_slicerTParent->m_originalSample.frames() < 2048) { return; } + if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } // draw SlicerTWaveform QPainter brush(&m_editorWaveform); @@ -157,7 +156,7 @@ void SlicerTWaveform::drawEditor() QPainter brush(&m_sliceEditor); // draw text if no sample loaded - if (m_slicerTParent->m_originalSample.frames() < 2048) + if (m_slicerTParent->m_originalSample.frames() <= 1) { brush.setPen(s_playHighlightColor); brush.setFont(QFont(brush.font().family(), 9.0f, -1, false)); @@ -301,25 +300,26 @@ void SlicerTWaveform::mousePressEvent(QMouseEvent* me) /* updateClosest(me); */ // reset seeker on middle click - if (me->button() == Qt::MouseButton::MiddleButton) + switch (me->button()) { + case Qt::MouseButton::MiddleButton: m_seekerStart = 0; m_seekerEnd = 1; m_zoomLevel = 1; - return; - } - - if (me->button() == Qt::MouseButton::LeftButton) - { + break; + case Qt::MouseButton::LeftButton: // update seeker middle for correct movement m_seekerMiddle = static_cast(me->x() - m_seekerHorMargin) / m_seekerWidth; - } - + break; // delete closesd slice to mouse - if (me->button() == Qt::MouseButton::RightButton && m_slicerTParent->m_slicePoints.size() > 2 - && m_closestObject == UIObjects::SlicePoint) - { - m_slicerTParent->m_slicePoints.erase(m_slicerTParent->m_slicePoints.begin() + m_closestSlice); + case Qt::MouseButton::RightButton: + if (m_slicerTParent->m_slicePoints.size() > 2 && m_closestObject == UIObjects::SlicePoint) + { + m_slicerTParent->m_slicePoints.erase(m_slicerTParent->m_slicePoints.begin() + m_closestSlice); + } + break; + default: + ; } updateClosest(me); } @@ -375,8 +375,8 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) if (m_closestSlice == -1) { break; } m_slicerTParent->m_slicePoints.at(m_closestSlice) = startFrame + normalizedClickEditor * (endFrame - startFrame); - m_slicerTParent->m_slicePoints.at(m_closestSlice) = std::clamp( - m_slicerTParent->m_slicePoints.at(m_closestSlice), 0.0f, 1.0f); + m_slicerTParent->m_slicePoints.at(m_closestSlice) + = std::clamp(m_slicerTParent->m_slicePoints.at(m_closestSlice), 0.0f, 1.0f); break; case UIObjects::Nothing: break; diff --git a/plugins/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h index 0403cc46f0e..7643dee1b85 100644 --- a/plugins/SlicerT/SlicerTWaveform.h +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -49,7 +49,7 @@ public slots: void isPlaying(float current, float start, float end); public: - SlicerTWaveform(int w, int h, SlicerT* instrument, QWidget* parent); + SlicerTWaveform(int totalWidth, int totalHeight, SlicerT* instrument, QWidget* parent); // predefined sizes static constexpr int m_seekerHorMargin = 5; From 4a2236be5b98f076f768595246b1697fc5b3c602 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 5 Nov 2023 13:02:53 +0100 Subject: [PATCH 87/99] Remove most comments --- plugins/SlicerT/SlicerT.cpp | 45 ++++----------------- plugins/SlicerT/SlicerT.h | 4 +- plugins/SlicerT/SlicerTView.cpp | 13 ------ plugins/SlicerT/SlicerTView.h | 4 +- plugins/SlicerT/SlicerTWaveform.cpp | 62 ++++++++++++----------------- plugins/SlicerT/SlicerTWaveform.h | 30 +------------- 6 files changed, 36 insertions(+), 122 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index ad44cf7a0f7..b2819e58dff 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -25,8 +25,8 @@ #include "SlicerT.h" #include -#include #include +#include #include "Engine.h" #include "InstrumentTrack.h" @@ -71,27 +71,24 @@ SlicerT::SlicerT(InstrumentTrack* instrumentTrack) m_sliceSnap.addItem("1/8"); m_sliceSnap.addItem("1/16"); m_sliceSnap.addItem("1/32"); - m_sliceSnap.setValue(0); // no snap by default + m_sliceSnap.setValue(0); } void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) { if (m_originalSample.frames() <= 1) { return; } - // playback parameters const int noteIndex = handle->key() - m_parentTrack->baseNote(); const fpp_t frames = handle->framesLeftForCurrentPeriod(); const f_cnt_t offset = handle->noteOffset(); const int bpm = Engine::getSong()->getTempo(); const float pitchRatio = 1 / pow(2, m_parentTrack->pitchModel()->value() / 1200); - // update sync parameter float speedRatio = static_cast(m_originalBPM.value()) / bpm; - if (!m_enableSync.value()) { speedRatio = 1; } // disable timeshift - speedRatio *= pitchRatio; // adjust for pitch bend + if (!m_enableSync.value()) { speedRatio = 1; } + speedRatio *= pitchRatio; speedRatio *= Engine::audioEngine()->processingSampleRate() / static_cast(m_originalSample.sampleRate()); - // set start and end points float sliceStart, sliceEnd; if (noteIndex > m_slicePoints.size() - 2 || noteIndex < 0) // full sample if ouside range { @@ -104,16 +101,13 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) sliceEnd = m_slicePoints[noteIndex + 1]; } - // initliazize handle if (!handle->m_pluginData) { handle->m_pluginData = new PlayBackState(sliceStart); } - // slice vars float noteDone = ((PlayBackState*)handle->m_pluginData)->getNoteDone(); float noteLeft = sliceEnd - noteDone; if (noteLeft > 0) { - // resample in chunks int noteFrame = noteDone * m_originalSample.frames(); SRC_STATE* resampleState = ((PlayBackState*)handle->m_pluginData)->getResampleState(); @@ -129,14 +123,13 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) float nextNoteDone = noteDone + frames * (1.0f / speedRatio) / m_originalSample.frames(); ((PlayBackState*)handle->m_pluginData)->setNoteDone(nextNoteDone); - // exponential fade out, applyRelease kinda sucks + // exponential fade out, applyRelease() not used since it extends the note length int noteFramesLeft = noteLeft * m_originalSample.frames(); if (noteFramesLeft < m_fadeOutFrames.value()) { for (int i = 0; i < frames; i++) { float fadeValue = static_cast(noteFramesLeft - i) / m_fadeOutFrames.value(); - // if the workingbuffer extends the sample fadeValue = std::clamp(fadeValue, 0.0f, 1.0f); fadeValue = std::pow(fadeValue, 2); @@ -160,14 +153,12 @@ void SlicerT::findSlices() if (m_originalSample.frames() <= 1) { return; } m_slicePoints = {}; - // computacion params const int windowSize = 512; const float minBeatLength = 0.05f; // in seconds, ~ 1/4 length at 220 bpm int sampleRate = m_originalSample.sampleRate(); int minDist = sampleRate * minBeatLength; - // copy vector into one vector, averaging channels float maxMag = -1; std::vector singleChannel(m_originalSample.frames(), 0); for (int i = 0; i < m_originalSample.frames(); i++) @@ -176,13 +167,11 @@ void SlicerT::findSlices() maxMag = std::max(maxMag, singleChannel[i]); } - // normalize for (float& channelValue : singleChannel) { channelValue /= maxMag; } - // buffers std::vector prevMags(windowSize / 2, 0); std::vector fftIn(windowSize, 0); std::array fftOut; @@ -214,7 +203,6 @@ void SlicerT::findSlices() prevMags[j] = magnitude; } - // detect increases in flux if (spectralFlux / prevFlux > 1.0f + m_noteThreshold.value() && i - lastPoint > minDist) { m_slicePoints.push_back(i); @@ -227,33 +215,27 @@ void SlicerT::findSlices() m_slicePoints.push_back(m_originalSample.frames()); - // snap slices to notes float beatsPerMin = m_originalBPM.value() / 60.0f; float samplesPerBeat = m_originalSample.sampleRate() / beatsPerMin * 4.0f; int noteSnap = m_sliceSnap.value(); - int sliceLock = samplesPerBeat / std::pow(2, noteSnap + 1); // lock to note: 1 / noteSnap² - if (noteSnap == 0) { sliceLock = 1; } // disable noteSnap - + int sliceLock = samplesPerBeat / std::pow(2, noteSnap + 1); + if (noteSnap == 0) { sliceLock = 1; } for (float& sliceValue : m_slicePoints) { sliceValue += sliceLock / 2; sliceValue -= static_cast(sliceValue) % sliceLock; } - // remove duplicates m_slicePoints.erase(std::unique(m_slicePoints.begin(), m_slicePoints.end()), m_slicePoints.end()); - // scale between 0 and 1 for (float& sliceIndex : m_slicePoints) { sliceIndex /= m_originalSample.frames(); } - // fit to sample size m_slicePoints[0] = 0; m_slicePoints[m_slicePoints.size() - 1] = 1; - // update UI emit dataChanged(); } @@ -263,15 +245,12 @@ void SlicerT::findBPM() { if (m_originalSample.frames() <= 1) { return; } - // caclulate length of sample float sampleRate = m_originalSample.sampleRate(); float totalFrames = m_originalSample.frames(); float sampleLength = totalFrames / sampleRate; - // this assumes the sample has a time signature of x/4 float bpmEstimate = 240.0f / sampleLength; - // get into 100 - 200 range while (bpmEstimate < 100) { bpmEstimate *= 2; @@ -297,7 +276,6 @@ std::vector SlicerT::getMidi() float totalTicks = outFrames / framesPerTick; float lastEnd = 0; - // write to midi for (int i = 0; i < m_slicePoints.size() - 1; i++) { float sliceStart = lastEnd; @@ -306,7 +284,7 @@ std::vector SlicerT::getMidi() Note sliceNote = Note(); sliceNote.setKey(i + m_parentTrack->baseNote()); sliceNote.setPos(sliceStart); - sliceNote.setLength(sliceEnd - sliceStart + 1); // + 1 needed for whatever reason + sliceNote.setLength(sliceEnd - sliceStart + 1); // + 1 so that the notes allign outputNotes.push_back(sliceNote); lastEnd = sliceEnd; @@ -332,7 +310,6 @@ void SlicerT::updateSlices() void SlicerT::saveSettings(QDomDocument& document, QDomElement& element) { - // save sample element.setAttribute("src", m_originalSample.audioFile()); if (m_originalSample.audioFile().isEmpty()) { @@ -340,14 +317,12 @@ void SlicerT::saveSettings(QDomDocument& document, QDomElement& element) element.setAttribute("sampledata", m_originalSample.toBase64(s)); } - // save slice points element.setAttribute("totalSlices", static_cast(m_slicePoints.size())); for (int i = 0; i < m_slicePoints.size(); i++) { element.setAttribute(tr("slice_%1").arg(i), m_slicePoints[i]); } - // save knobs m_fadeOutFrames.saveSettings(document, element, "fadeOut"); m_noteThreshold.saveSettings(document, element, "threshold"); m_originalBPM.saveSettings(document, element, "origBPM"); @@ -356,7 +331,6 @@ void SlicerT::saveSettings(QDomDocument& document, QDomElement& element) void SlicerT::loadSettings(const QDomElement& element) { - // load sample if (!element.attribute("src").isEmpty()) { m_originalSample.setAudioFile(element.attribute("src")); @@ -373,7 +347,6 @@ void SlicerT::loadSettings(const QDomElement& element) m_originalSample.loadFromBase64(element.attribute("srcdata")); } - // load slices if (!element.attribute("totalSlices").isEmpty()) { int totalSlices = element.attribute("totalSlices").toInt(); @@ -384,7 +357,6 @@ void SlicerT::loadSettings(const QDomElement& element) } } - // load knobs m_fadeOutFrames.loadSettings(element, "fadeOut"); m_noteThreshold.loadSettings(element, "threshold"); m_originalBPM.loadSettings(element, "origBPM"); @@ -404,7 +376,6 @@ gui::PluginView* SlicerT::instantiateView(QWidget* parent) } extern "C" { -// necessary for getting instance out of shared lib PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* m, void*) { return new SlicerT(static_cast(m)); diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index c2ca265555a..771aa1da99e 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -52,7 +52,7 @@ class PlayBackState SRC_STATE* getResampleState() { return resamplingState; } private: - float currentNote; // these are all absoute floats + float currentNote; SRC_STATE* resamplingState; }; @@ -84,14 +84,12 @@ public slots: std::vector getMidi(); private: - // models FloatModel m_noteThreshold; FloatModel m_fadeOutFrames; IntModel m_originalBPM; ComboBoxModel m_sliceSnap; BoolModel m_enableSync; - // sample buffers SampleBuffer m_originalSample; std::vector m_slicePoints; diff --git a/plugins/SlicerT/SlicerTView.cpp b/plugins/SlicerT/SlicerTView.cpp index 350f0f5ff70..d18e32327cb 100644 --- a/plugins/SlicerT/SlicerTView.cpp +++ b/plugins/SlicerT/SlicerTView.cpp @@ -54,50 +54,40 @@ SlicerTView::SlicerTView(SlicerT* instrument, QWidget* parent) pal.setBrush(backgroundRole(), PLUGIN_NAME::getIconPixmap("artwork")); setPalette(pal); - // move editor and seeker m_wf = new SlicerTWaveform(248, 128, instrument, this); m_wf->move(2, 6); - // snap combo box m_snapSetting = new ComboBox(this, tr("Slice snap")); m_snapSetting->setGeometry(185, 200, 55, ComboBox::DEFAULT_HEIGHT); m_snapSetting->setToolTip(tr("Set slice snapping for detection")); m_snapSetting->setModel(&m_slicerTParent->m_sliceSnap); - // sync toggle m_syncToggle = new LedCheckBox("Sync", this, tr("SyncToggle"), LedCheckBox::LedColor::Green); m_syncToggle->move(135, 187); m_syncToggle->setToolTip(tr("Enable BPM sync")); m_syncToggle->setModel(&m_slicerTParent->m_enableSync); - // bpm spin box m_bpmBox = new LcdSpinBox(3, "19purple", this); m_bpmBox->move(130, 201); m_bpmBox->setToolTip(tr("Original sample BPM")); m_bpmBox->setModel(&m_slicerTParent->m_originalBPM); - // threshold knob m_noteThresholdKnob = createStyledKnob(); m_noteThresholdKnob->move(10, 197); m_noteThresholdKnob->setToolTip(tr("Threshold used for slicing")); - /* m_noteThresholdKnob->setLabel(tr("Threshold")); */ m_noteThresholdKnob->setModel(&m_slicerTParent->m_noteThreshold); - // fadeout knob m_fadeOutKnob = createStyledKnob(); m_fadeOutKnob->move(64, 197); m_fadeOutKnob->setToolTip(tr("Fade Out for notes")); - /* m_fadeOutKnob->setLabel(tr("Fade Out")); */ m_fadeOutKnob->setModel(&m_slicerTParent->m_fadeOutFrames); - // midi copy button m_midiExportButton = new QPushButton(this); m_midiExportButton->move(199, 150); m_midiExportButton->setIcon(PLUGIN_NAME::getIconPixmap("copyMidi")); m_midiExportButton->setToolTip(tr("Copy midi pattern to clipboard")); connect(m_midiExportButton, &PixmapButton::clicked, this, &SlicerTView::exportMidi); - // slice reset button m_resetButton = new QPushButton(this); m_resetButton->move(18, 150); m_resetButton->setIcon(PLUGIN_NAME::getIconPixmap("resetSlices")); @@ -177,18 +167,15 @@ void SlicerTView::dropEvent(QDropEvent* de) de->ignore(); } -// display button text void SlicerTView::paintEvent(QPaintEvent* pe) { QPainter brush(this); brush.setPen(QColor(255, 255, 255)); brush.setFont(QFont(brush.font().family(), 7, -1, false)); - // top text brush.drawText(8, topTextY, textBoxWidth, textBoxHeight, Qt::AlignCenter, tr("Reset")); brush.drawText(188, topTextY, textBoxWidth, textBoxHeight, Qt::AlignCenter, tr("Midi")); - // bottom text brush.drawText(8, bottomTextY, textBoxWidth, textBoxHeight, Qt::AlignCenter, tr("Threshold")); brush.drawText(63, bottomTextY, textBoxWidth, textBoxHeight, Qt::AlignCenter, tr("Fade Out")); brush.drawText(127, bottomTextY, textBoxWidth, textBoxHeight, Qt::AlignCenter, tr("BPM")); diff --git a/plugins/SlicerT/SlicerTView.h b/plugins/SlicerT/SlicerTView.h index b1b1d4158d2..690fc9a0e36 100644 --- a/plugins/SlicerT/SlicerTView.h +++ b/plugins/SlicerT/SlicerTView.h @@ -46,7 +46,7 @@ class SlicerTView : public InstrumentViewFixedSize { Q_OBJECT -protected slots: +public slots: void exportMidi(); public: @@ -66,14 +66,12 @@ protected slots: private: SlicerT* m_slicerTParent; - // lmms UI Knob* m_noteThresholdKnob; Knob* m_fadeOutKnob; LcdSpinBox* m_bpmBox; ComboBox* m_snapSetting; LedCheckBox* m_syncToggle; - // buttons QPushButton* m_resetButton; QPushButton* m_midiExportButton; diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index 2a8823e3048..a43aafc5917 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -34,39 +34,47 @@ namespace lmms { namespace gui { +static QColor s_emptyColor = QColor(0, 0, 0, 0); +static QColor s_waveformColor = QColor(123, 49, 212); +static QColor s_waveformBgColor = QColor(255, 255, 255, 0); +static QColor s_waveformMaskColor = QColor(151, 65, 255); // update this if s_waveformColor changes +static QColor s_waveformInnerColor = QColor(183, 124, 255); + +static QColor s_playColor = QColor(255, 255, 255, 200); +static QColor s_playHighlightColor = QColor(255, 255, 255, 70); + +static QColor s_sliceColor = QColor(218, 193, 255); +static QColor s_sliceShadowColor = QColor(136, 120, 158); +static QColor s_sliceHighlightColor = QColor(255, 255, 255); + +static QColor s_seekerColor = QColor(178, 115, 255); +static QColor s_seekerHighlightColor = QColor(178, 115, 255, 100); +static QColor s_seekerShadowColor = QColor(0, 0, 0, 120); + SlicerTWaveform::SlicerTWaveform(int totalWidth, int totalHeight, SlicerT* instrument, QWidget* parent) : QWidget(parent) - // calculate sizes , m_width(totalWidth) , m_height(totalHeight) , m_seekerWidth(totalWidth - m_seekerHorMargin * 2) , m_editorHeight(totalHeight - m_seekerHeight - m_middleMargin) , m_editorWidth(totalWidth) - - // create pixmaps , m_sliceArrow(PLUGIN_NAME::getIconPixmap("slice_indicator_arrow")) , m_seeker(QPixmap(m_seekerWidth, m_seekerHeight)) , m_seekerWaveform(QPixmap(m_seekerWidth, m_seekerHeight)) , m_editorWaveform(QPixmap(m_editorWidth, m_editorHeight)) , m_sliceEditor(QPixmap(totalWidth, m_editorHeight)) , m_emptySampleIcon(embed::getIconPixmap("sample_track")) - - // references to instrument vars , m_slicerTParent(instrument) { - // window config setFixedSize(m_width, m_height); setMouseTracking(true); - // draw backgrounds m_seekerWaveform.fill(s_waveformBgColor); m_editorWaveform.fill(s_waveformBgColor); - // connect to playback connect(instrument, &SlicerT::isPlaying, this, &SlicerTWaveform::isPlaying); connect(instrument, &SlicerT::dataChanged, this, &SlicerTWaveform::updateUI); - // preprocess icons m_emptySampleIcon = m_emptySampleIcon.createMaskFromColor(QColor(255, 255, 255), Qt::MaskMode::MaskOutColor); updateUI(); @@ -94,37 +102,30 @@ void SlicerTWaveform::drawSeeker() if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } QPainter brush(&m_seeker); - // draw slice points brush.setPen(s_sliceColor); - for (float sliceValue: m_slicerTParent->m_slicePoints) + for (float sliceValue : m_slicerTParent->m_slicePoints) { float xPos = sliceValue * m_seekerWidth; brush.drawLine(xPos, 0, xPos, m_seekerHeight); } - // seeker vars float seekerStartPosX = m_seekerStart * m_seekerWidth; float seekerEndPosX = m_seekerEnd * m_seekerWidth; float seekerMiddleWidth = (m_seekerEnd - m_seekerStart) * m_seekerWidth; - // note playback vars float noteCurrentPosX = m_noteCurrent * m_seekerWidth; float noteStartPosX = m_noteStart * m_seekerWidth; float noteEndPosX = (m_noteEnd - m_noteStart) * m_seekerWidth; - // draw current playBack brush.setPen(s_playColor); brush.drawLine(noteCurrentPosX, 0, noteCurrentPosX, m_seekerHeight); brush.fillRect(noteStartPosX, 0, noteEndPosX, m_seekerHeight, s_playHighlightColor); - // highlight on selected area brush.fillRect(seekerStartPosX, 0, seekerMiddleWidth - 1, m_seekerHeight, s_seekerHighlightColor); - // shadow on not selected area brush.fillRect(0, 0, seekerStartPosX, m_seekerHeight, s_seekerShadowColor); brush.fillRect(seekerEndPosX - 1, 0, m_seekerWidth, m_seekerHeight, s_seekerShadowColor); - // draw border around selection brush.setPen(QPen(s_seekerColor, 1)); brush.drawRect(seekerStartPosX, 0, seekerMiddleWidth - 1, m_seekerHeight - 1); // -1 needed } @@ -134,7 +135,6 @@ void SlicerTWaveform::drawEditorWaveform() m_editorWaveform.fill(s_emptyColor); if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } - // draw SlicerTWaveform QPainter brush(&m_editorWaveform); float startFrame = m_seekerStart * m_slicerTParent->m_originalSample.frames(); float endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.frames(); @@ -155,7 +155,7 @@ void SlicerTWaveform::drawEditor() m_sliceEditor.fill(s_waveformBgColor); QPainter brush(&m_sliceEditor); - // draw text if no sample loaded + // No sample loaded if (m_slicerTParent->m_originalSample.frames() <= 1) { brush.setPen(s_playHighlightColor); @@ -168,7 +168,6 @@ void SlicerTWaveform::drawEditor() return; } - // editor boundaries float startFrame = m_seekerStart; float endFrame = m_seekerEnd; float numFramesToDraw = endFrame - startFrame; @@ -178,19 +177,15 @@ void SlicerTWaveform::drawEditor() float noteStartPos = (m_noteStart - m_seekerStart) / (m_seekerEnd - m_seekerStart) * m_editorWidth; float noteLength = (m_noteEnd - m_noteStart) / (m_seekerEnd - m_seekerStart) * m_editorWidth; - // 0 centered line brush.setPen(s_playHighlightColor); brush.drawLine(0, m_editorHeight / 2, m_editorWidth, m_editorHeight / 2); - // draw waveform from pixmap brush.drawPixmap(0, 0, m_editorWaveform); - // draw currently playing brush.setPen(s_playColor); brush.drawLine(noteCurrentPos, 0, noteCurrentPos, m_editorHeight); brush.fillRect(noteStartPos, 0, noteLength, m_editorHeight, s_playHighlightColor); - // draw slicepoints brush.setPen(QPen(s_sliceColor, 2)); for (int i = 0; i < m_slicerTParent->m_slicePoints.size(); i++) @@ -243,27 +238,26 @@ void SlicerTWaveform::updateClosest(QMouseEvent* me) m_closestObject = UIObjects::Nothing; m_closestSlice = -1; - if (me->y() < m_seekerHeight) // seeker click + if (me->y() < m_seekerHeight) { - if (std::abs(normalizedClickSeeker - m_seekerStart) < s_distanceForClick) // dragging start + if (std::abs(normalizedClickSeeker - m_seekerStart) < s_distanceForClick) { m_closestObject = UIObjects::SeekerStart; } - else if (std::abs(normalizedClickSeeker - m_seekerEnd) < s_distanceForClick) // dragging end + else if (std::abs(normalizedClickSeeker - m_seekerEnd) < s_distanceForClick) { m_closestObject = UIObjects::SeekerEnd; } - else if (normalizedClickSeeker > m_seekerStart && normalizedClickSeeker < m_seekerEnd) // dragging middle + else if (normalizedClickSeeker > m_seekerStart && normalizedClickSeeker < m_seekerEnd) { m_closestObject = UIObjects::SeekerMiddle; } } - else // editor click + else { m_closestSlice = -1; float startFrame = m_seekerStart; float endFrame = m_seekerEnd; - // select slice for (int i = 0; i < m_slicerTParent->m_slicePoints.size(); i++) { float sliceIndex = m_slicerTParent->m_slicePoints.at(i); @@ -297,9 +291,6 @@ void SlicerTWaveform::updateCursor() // handles deletion, reset and middles seeker void SlicerTWaveform::mousePressEvent(QMouseEvent* me) { - /* updateClosest(me); */ - - // reset seeker on middle click switch (me->button()) { case Qt::MouseButton::MiddleButton: @@ -311,15 +302,13 @@ void SlicerTWaveform::mousePressEvent(QMouseEvent* me) // update seeker middle for correct movement m_seekerMiddle = static_cast(me->x() - m_seekerHorMargin) / m_seekerWidth; break; - // delete closesd slice to mouse case Qt::MouseButton::RightButton: if (m_slicerTParent->m_slicePoints.size() > 2 && m_closestObject == UIObjects::SlicePoint) { m_slicerTParent->m_slicePoints.erase(m_slicerTParent->m_slicePoints.begin() + m_closestSlice); } break; - default: - ; + default:; } updateClosest(me); } @@ -350,7 +339,6 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) float startFrame = m_seekerStart; float endFrame = m_seekerEnd; - // handle dragging events switch (m_closestObject) { case UIObjects::SeekerStart: diff --git a/plugins/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h index 7643dee1b85..01947289bd7 100644 --- a/plugins/SlicerT/SlicerTWaveform.h +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -56,24 +56,6 @@ public slots: static constexpr int m_seekerHeight = 38; // used to calcualte all vertical sizes static constexpr int m_middleMargin = 6; - // colors - const QColor s_emptyColor = QColor(0, 0, 0, 0); - - const QColor s_waveformColor = QColor(123, 49, 212); - const QColor s_waveformBgColor = QColor(255, 255, 255, 0); - const QColor s_waveformMaskColor = QColor(151, 65, 255); // update this if s_waveformColor changes - const QColor s_waveformInnerColor = QColor(183, 124, 255); - - const QColor s_playColor = QColor(255, 255, 255, 200); - const QColor s_playHighlightColor = QColor(255, 255, 255, 70); - - const QColor s_sliceColor = QColor(218, 193, 255); - const QColor s_sliceShadowColor = QColor(136, 120, 158); - const QColor s_sliceHighlightColor = QColor(255, 255, 255); - - const QColor s_seekerColor = QColor(178, 115, 255); - const QColor s_seekerHighlightColor = QColor(178, 115, 255, 100); - const QColor s_seekerShadowColor = QColor(0, 0, 0, 120); // interaction behavior values static constexpr float s_distanceForClick = 0.02f; @@ -99,39 +81,29 @@ public slots: void paintEvent(QPaintEvent* pe) override; private: - // sizes int m_width; int m_height; - // later calculated int m_seekerWidth; int m_editorHeight; int m_editorWidth; - // interaction vars - UIObjects m_draggedObject; UIObjects m_closestObject; int m_closestSlice = -1; - int m_sliceDragged = -1; - bool m_currentlyDragging = false; - // seeker vars float m_seekerStart = 0; float m_seekerEnd = 1; float m_seekerMiddle = 0.5f; - // playback highlight vars float m_noteCurrent; float m_noteStart; float m_noteEnd; - // editor vars float m_zoomLevel = 1.0f; - // pixmaps QPixmap m_sliceArrow; QPixmap m_seeker; - QPixmap m_seekerWaveform; // only stores SlicerTWaveform graphic + QPixmap m_seekerWaveform; QPixmap m_editorWaveform; QPixmap m_sliceEditor; QPixmap m_emptySampleIcon; From f44fa4d1f7e597baadf8ff3eee074e5c90da38ee Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 5 Nov 2023 14:35:48 +0100 Subject: [PATCH 88/99] Performance + click to load --- plugins/SlicerT/SlicerT.cpp | 1 + plugins/SlicerT/SlicerTView.cpp | 6 ++++++ plugins/SlicerT/SlicerTView.h | 1 + plugins/SlicerT/SlicerTWaveform.cpp | 17 ++++++++++++++--- plugins/SlicerT/SlicerTWaveform.h | 3 +++ 5 files changed, 25 insertions(+), 3 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index b2819e58dff..adbf5172b7b 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -207,6 +207,7 @@ void SlicerT::findSlices() { m_slicePoints.push_back(i); lastPoint = i; + if (m_slicePoints.size() > 128) { break; } // no more keys on the keyboard } prevFlux = spectralFlux; diff --git a/plugins/SlicerT/SlicerTView.cpp b/plugins/SlicerT/SlicerTView.cpp index d18e32327cb..75463d3a681 100644 --- a/plugins/SlicerT/SlicerTView.cpp +++ b/plugins/SlicerT/SlicerTView.cpp @@ -127,6 +127,12 @@ void SlicerTView::exportMidi() copyString(dataFile.toString(), MimeType::Default); } +void SlicerTView::openFiles() { + QString audioFile = m_slicerTParent->m_originalSample.openAudioFile(); + if (audioFile.isEmpty()) { return; } + m_slicerTParent->updateFile(audioFile); +} + // all the drag stuff is copied from AudioFileProcessor void SlicerTView::dragEnterEvent(QDragEnterEvent* dee) { diff --git a/plugins/SlicerT/SlicerTView.h b/plugins/SlicerT/SlicerTView.h index 690fc9a0e36..d9cbdfae4d1 100644 --- a/plugins/SlicerT/SlicerTView.h +++ b/plugins/SlicerT/SlicerTView.h @@ -48,6 +48,7 @@ class SlicerTView : public InstrumentViewFixedSize public slots: void exportMidi(); + void openFiles(); public: SlicerTView(SlicerT* instrument, QWidget* parent); diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index a43aafc5917..976610a6811 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -77,6 +77,7 @@ SlicerTWaveform::SlicerTWaveform(int totalWidth, int totalHeight, SlicerT* instr m_emptySampleIcon = m_emptySampleIcon.createMaskFromColor(QColor(255, 255, 255), Qt::MaskMode::MaskOutColor); + m_updateTimer.start(); updateUI(); } @@ -161,7 +162,7 @@ void SlicerTWaveform::drawEditor() brush.setPen(s_playHighlightColor); brush.setFont(QFont(brush.font().family(), 9.0f, -1, false)); brush.drawText( - m_editorWidth / 2 - 100, m_editorHeight / 2 - 110, 200, 200, Qt::AlignCenter, tr("Drag sample to load")); + m_editorWidth / 2 - 100, m_editorHeight / 2 - 110, 200, 200, Qt::AlignCenter, tr("Click to load sample")); int iconOffsetX = m_emptySampleIcon.width() / 2.0f; int iconOffsetY = m_emptySampleIcon.height() / 2.0f - 13; brush.drawPixmap(m_editorWidth / 2.0f - iconOffsetX, m_editorHeight / 2.0f - iconOffsetY, m_emptySampleIcon); @@ -212,14 +213,17 @@ void SlicerTWaveform::drawEditor() void SlicerTWaveform::isPlaying(float current, float start, float end) { + if (!m_updateTimer.hasExpired(s_minMilisPassed)) {return;} m_noteCurrent = current; m_noteStart = start; m_noteEnd = end; drawSeeker(); drawEditor(); update(); + m_updateTimer.restart(); } +// this should only be called if one of the waveforms has to update void SlicerTWaveform::updateUI() { drawSeekerWaveform(); @@ -271,7 +275,9 @@ void SlicerTWaveform::updateClosest(QMouseEvent* me) } } updateCursor(); - updateUI(); + drawSeeker(); + drawEditor(); + update(); } void SlicerTWaveform::updateCursor() @@ -299,6 +305,9 @@ void SlicerTWaveform::mousePressEvent(QMouseEvent* me) m_zoomLevel = 1; break; case Qt::MouseButton::LeftButton: + if (m_slicerTParent->m_originalSample.frames() <= 1) { + static_cast(parent())->openFiles(); + } // update seeker middle for correct movement m_seekerMiddle = static_cast(me->x() - m_seekerHorMargin) / m_seekerWidth; break; @@ -371,7 +380,9 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) } // dont update closest, and update seeker waveform drawEditorWaveform(); - updateUI(); + drawSeeker(); + drawEditor(); + update(); } void SlicerTWaveform::mouseDoubleClickEvent(QMouseEvent* me) diff --git a/plugins/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h index 01947289bd7..59e4a1aafcc 100644 --- a/plugins/SlicerT/SlicerTWaveform.h +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -30,6 +30,7 @@ #include #include #include +#include #include "Instrument.h" #include "SampleBuffer.h" @@ -61,6 +62,7 @@ public slots: static constexpr float s_distanceForClick = 0.02f; static constexpr float s_minSeekerDistance = 0.13f; static constexpr float s_zoomSensitivity = 0.5f; + static constexpr int s_minMilisPassed = 10; enum class UIObjects { @@ -110,6 +112,7 @@ public slots: SlicerT* m_slicerTParent; + QElapsedTimer m_updateTimer; void drawSeekerWaveform(); void drawSeeker(); void drawEditorWaveform(); From cdf45fc56f10e441eab97711d46b3394a718683b Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sun, 5 Nov 2023 17:33:00 +0100 Subject: [PATCH 89/99] update PlaybackState + zerocross snapping --- plugins/SlicerT/SlicerT.cpp | 29 +++++++++++++++++++++++------ plugins/SlicerT/SlicerT.h | 23 +++++++++++++---------- plugins/SlicerT/SlicerTWaveform.cpp | 5 ++++- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index adbf5172b7b..264dbaf777a 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -101,16 +101,16 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) sliceEnd = m_slicePoints[noteIndex + 1]; } - if (!handle->m_pluginData) { handle->m_pluginData = new PlayBackState(sliceStart); } + if (!handle->m_pluginData) { handle->m_pluginData = new PlaybackState(sliceStart); } - float noteDone = ((PlayBackState*)handle->m_pluginData)->getNoteDone(); + float noteDone = ((PlaybackState*)handle->m_pluginData)->noteDone(); float noteLeft = sliceEnd - noteDone; if (noteLeft > 0) { int noteFrame = noteDone * m_originalSample.frames(); - SRC_STATE* resampleState = ((PlayBackState*)handle->m_pluginData)->getResampleState(); + SRC_STATE* resampleState = ((PlaybackState*)handle->m_pluginData)->resamplingState(); SRC_DATA resampleData; resampleData.data_in = (float*)(m_originalSample.data() + noteFrame); resampleData.data_out = (float*)(workingBuffer + offset); @@ -121,7 +121,7 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) src_process(resampleState, &resampleData); float nextNoteDone = noteDone + frames * (1.0f / speedRatio) / m_originalSample.frames(); - ((PlayBackState*)handle->m_pluginData)->setNoteDone(nextNoteDone); + ((PlaybackState*)handle->m_pluginData)->setNoteDone(nextNoteDone); // exponential fade out, applyRelease() not used since it extends the note length int noteFramesLeft = noteLeft * m_originalSample.frames(); @@ -167,9 +167,17 @@ void SlicerT::findSlices() maxMag = std::max(maxMag, singleChannel[i]); } - for (float& channelValue : singleChannel) + // normalize and find 0 crossings + std::vector zeroCrossings; + float lastValue = 1; + for (int i = 0; i < singleChannel.size(); i++) { - channelValue /= maxMag; + singleChannel[i] /= maxMag; + if (sign(lastValue) != sign(singleChannel[i])) + { + zeroCrossings.push_back(i); + lastValue = singleChannel[i]; + } } std::vector prevMags(windowSize / 2, 0); @@ -216,6 +224,15 @@ void SlicerT::findSlices() m_slicePoints.push_back(m_originalSample.frames()); + for (float& sliceValue : m_slicePoints) + { + int closestZeroCrossing = *std::lower_bound(zeroCrossings.begin(), zeroCrossings.end(), sliceValue); + if (abs(sliceValue - closestZeroCrossing) < windowSize) + { + sliceValue = *std::lower_bound(zeroCrossings.begin(), zeroCrossings.end(), sliceValue); + } + } + float beatsPerMin = m_originalBPM.value() / 60.0f; float samplesPerBeat = m_originalSample.sampleRate() / beatsPerMin * 4.0f; int noteSnap = m_sliceSnap.value(); diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 771aa1da99e..aafe834656b 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -38,22 +38,25 @@ namespace lmms { -class PlayBackState +class PlaybackState { public: - PlayBackState(float startFrame) - : currentNote(startFrame) - , resamplingState(src_new(SRC_LINEAR, 2, nullptr)) + explicit PlaybackState(float startFrame) + : m_currentNoteDone(startFrame) + , m_resamplingState(src_new(SRC_LINEAR, DEFAULT_CHANNELS, nullptr)) { + if (!m_resamplingState) { throw std::runtime_error{"Failed to create sample rate converter object"}; } } - ~PlayBackState() { src_delete(resamplingState); } - float getNoteDone() { return currentNote; } - void setNoteDone(float newDone) { currentNote = newDone; } - SRC_STATE* getResampleState() { return resamplingState; } + ~PlaybackState() noexcept { src_delete(m_resamplingState); } + + float noteDone() const { return m_currentNoteDone; } + void setNoteDone(float newNoteDone) { m_currentNoteDone = newNoteDone; } + + SRC_STATE* resamplingState() const { return m_resamplingState; } private: - float currentNote; - SRC_STATE* resamplingState; + float m_currentNoteDone; + SRC_STATE* m_resamplingState; }; class SlicerT : public Instrument diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index 976610a6811..89d224a55a4 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -303,6 +303,7 @@ void SlicerTWaveform::mousePressEvent(QMouseEvent* me) m_seekerStart = 0; m_seekerEnd = 1; m_zoomLevel = 1; + drawEditorWaveform(); break; case Qt::MouseButton::LeftButton: if (m_slicerTParent->m_originalSample.frames() <= 1) { @@ -352,10 +353,12 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) { case UIObjects::SeekerStart: m_seekerStart = std::clamp(normalizedClickSeeker, 0.0f, m_seekerEnd - s_minSeekerDistance); + drawEditorWaveform(); break; case UIObjects::SeekerEnd: m_seekerEnd = std::clamp(normalizedClickSeeker, m_seekerStart + s_minSeekerDistance, 1.0f); + drawEditorWaveform(); break; case UIObjects::SeekerMiddle: @@ -366,6 +369,7 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) m_seekerStart = m_seekerMiddle + distStart; m_seekerEnd = m_seekerMiddle + distEnd; } + drawEditorWaveform(); break; case UIObjects::SlicePoint: @@ -379,7 +383,6 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) break; } // dont update closest, and update seeker waveform - drawEditorWaveform(); drawSeeker(); drawEditor(); update(); From faeeaef1e93bbb6d7b71d43ff818c112b2b0231e Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Mon, 6 Nov 2023 00:36:08 +0100 Subject: [PATCH 90/99] Fix windows build issue --- plugins/SlicerT/SlicerT.h | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index aafe834656b..73c4e9b5c6c 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -27,6 +27,7 @@ #include #include +#include #include "AutomatableModel.h" #include "Instrument.h" From 24738bb54f3fedae685727196731e1551041cbb9 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Wed, 8 Nov 2023 20:31:05 +0100 Subject: [PATCH 91/99] Review + version --- plugins/SlicerT/SlicerT.cpp | 11 ++++++----- plugins/SlicerT/SlicerTView.cpp | 5 +++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 264dbaf777a..4601dd6225f 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -110,10 +110,10 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) { int noteFrame = noteDone * m_originalSample.frames(); - SRC_STATE* resampleState = ((PlaybackState*)handle->m_pluginData)->resamplingState(); + SRC_STATE* resampleState = static_cast(handle->m_pluginData)->resamplingState(); SRC_DATA resampleData; - resampleData.data_in = (float*)(m_originalSample.data() + noteFrame); - resampleData.data_out = (float*)(workingBuffer + offset); + resampleData.data_in = &(m_originalSample.data() + noteFrame)[0][0]; + resampleData.data_out = &(workingBuffer + offset)[0][0]; resampleData.input_frames = noteLeft * m_originalSample.frames(); resampleData.output_frames = frames; resampleData.src_ratio = speedRatio; @@ -227,9 +227,9 @@ void SlicerT::findSlices() for (float& sliceValue : m_slicePoints) { int closestZeroCrossing = *std::lower_bound(zeroCrossings.begin(), zeroCrossings.end(), sliceValue); - if (abs(sliceValue - closestZeroCrossing) < windowSize) + if (std::abs(sliceValue - closestZeroCrossing) < windowSize) { - sliceValue = *std::lower_bound(zeroCrossings.begin(), zeroCrossings.end(), sliceValue); + sliceValue = closestZeroCrossing; } } @@ -328,6 +328,7 @@ 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()) { diff --git a/plugins/SlicerT/SlicerTView.cpp b/plugins/SlicerT/SlicerTView.cpp index 75463d3a681..200b2c9099a 100644 --- a/plugins/SlicerT/SlicerTView.cpp +++ b/plugins/SlicerT/SlicerTView.cpp @@ -118,7 +118,7 @@ void SlicerTView::exportMidi() if (notes.size() == 0) { return; } TimePos start_pos(notes.front().pos().getBar(), 0); - for (Note note : notes) + for (Note& note : notes) { note.setPos(note.pos(start_pos)); note.saveState(dataFile, note_list); @@ -127,7 +127,8 @@ void SlicerTView::exportMidi() copyString(dataFile.toString(), MimeType::Default); } -void SlicerTView::openFiles() { +void SlicerTView::openFiles() +{ QString audioFile = m_slicerTParent->m_originalSample.openAudioFile(); if (audioFile.isEmpty()) { return; } m_slicerTParent->updateFile(audioFile); From df8b90feaff1e729f21e2c214f5f079dc5a8b6f7 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Wed, 8 Nov 2023 20:57:13 +0100 Subject: [PATCH 92/99] Fixed fade out bug --- plugins/SlicerT/SlicerT.cpp | 33 ++++++++++++++------------------- plugins/SlicerT/SlicerTView.cpp | 2 +- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 4601dd6225f..b296e9b25f5 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -57,7 +57,7 @@ Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = { SlicerT::SlicerT(InstrumentTrack* instrumentTrack) : Instrument(instrumentTrack, &slicert_plugin_descriptor) , m_noteThreshold(0.6f, 0.0f, 2.0f, 0.01f, this, tr("Note threshold")) - , m_fadeOutFrames(400.0f, 0.0f, 8192.0f, 1.0f, this, tr("FadeOut")) + , m_fadeOutFrames(10.0f, 0.0f, 100.0f, 0.1f, this, tr("FadeOut")) , m_originalBPM(1, 1, 999, this, tr("Original bpm")) , m_sliceSnap(this, tr("Slice snap")) , m_enableSync(false, this, tr("BPM sync")) @@ -112,8 +112,8 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) SRC_STATE* resampleState = static_cast(handle->m_pluginData)->resamplingState(); SRC_DATA resampleData; - resampleData.data_in = &(m_originalSample.data() + noteFrame)[0][0]; - resampleData.data_out = &(workingBuffer + offset)[0][0]; + resampleData.data_in = (m_originalSample.data() + noteFrame)->data(); + resampleData.data_out = (workingBuffer + offset)->data(); resampleData.input_frames = noteLeft * m_originalSample.frames(); resampleData.output_frames = frames; resampleData.src_ratio = speedRatio; @@ -121,21 +121,19 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) src_process(resampleState, &resampleData); float nextNoteDone = noteDone + frames * (1.0f / speedRatio) / m_originalSample.frames(); - ((PlaybackState*)handle->m_pluginData)->setNoteDone(nextNoteDone); + static_cast(handle->m_pluginData)->setNoteDone(nextNoteDone); // exponential fade out, applyRelease() not used since it extends the note length - int noteFramesLeft = noteLeft * m_originalSample.frames(); - if (noteFramesLeft < m_fadeOutFrames.value()) + int fadeOutFrames = m_fadeOutFrames.value() / 1000.0f * Engine::audioEngine()->processingSampleRate(); + int noteFramesLeft = noteLeft * m_originalSample.frames() * speedRatio; + for (int i = 0; i < frames; i++) { - for (int i = 0; i < frames; i++) - { - float fadeValue = static_cast(noteFramesLeft - i) / m_fadeOutFrames.value(); - fadeValue = std::clamp(fadeValue, 0.0f, 1.0f); - fadeValue = std::pow(fadeValue, 2); - - workingBuffer[i + offset][0] *= fadeValue; - workingBuffer[i + offset][1] *= fadeValue; - } + float fadeValue = static_cast(noteFramesLeft - i) / fadeOutFrames; + fadeValue = std::clamp(fadeValue, 0.0f, 1.0f); + fadeValue = std::pow(fadeValue, 2); + + workingBuffer[i + offset][0] *= fadeValue; + workingBuffer[i + offset][1] *= fadeValue; } instrumentTrack()->processAudioBuffer(workingBuffer, frames + offset, handle); @@ -227,10 +225,7 @@ void SlicerT::findSlices() for (float& sliceValue : m_slicePoints) { int closestZeroCrossing = *std::lower_bound(zeroCrossings.begin(), zeroCrossings.end(), sliceValue); - if (std::abs(sliceValue - closestZeroCrossing) < windowSize) - { - sliceValue = closestZeroCrossing; - } + if (std::abs(sliceValue - closestZeroCrossing) < windowSize) { sliceValue = closestZeroCrossing; } } float beatsPerMin = m_originalBPM.value() / 60.0f; diff --git a/plugins/SlicerT/SlicerTView.cpp b/plugins/SlicerT/SlicerTView.cpp index 200b2c9099a..33b83fc69bf 100644 --- a/plugins/SlicerT/SlicerTView.cpp +++ b/plugins/SlicerT/SlicerTView.cpp @@ -79,7 +79,7 @@ SlicerTView::SlicerTView(SlicerT* instrument, QWidget* parent) m_fadeOutKnob = createStyledKnob(); m_fadeOutKnob->move(64, 197); - m_fadeOutKnob->setToolTip(tr("Fade Out for notes")); + m_fadeOutKnob->setToolTip(tr("Fade Out per note in milliseconds")); m_fadeOutKnob->setModel(&m_slicerTParent->m_fadeOutFrames); m_midiExportButton = new QPushButton(this); From b2e7f11a32a4dc62d05d63e0130ff3a7ea73c3a4 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Wed, 8 Nov 2023 21:26:07 +0100 Subject: [PATCH 93/99] Use cosine interpolation --- plugins/SlicerT/SlicerT.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index b296e9b25f5..5097723e4e6 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -130,7 +130,7 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) { float fadeValue = static_cast(noteFramesLeft - i) / fadeOutFrames; fadeValue = std::clamp(fadeValue, 0.0f, 1.0f); - fadeValue = std::pow(fadeValue, 2); + fadeValue = cosinusInterpolate(0, 1, fadeValue); workingBuffer[i + offset][0] *= fadeValue; workingBuffer[i + offset][1] *= fadeValue; From 1f5c4946427835465a36cc5ee32139f85cd545c3 Mon Sep 17 00:00:00 2001 From: DanielKauss Date: Wed, 8 Nov 2023 23:58:53 +0100 Subject: [PATCH 94/99] Apply suggestions from code review Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> --- plugins/SlicerT/SlicerT.cpp | 6 +++--- plugins/SlicerT/SlicerTView.cpp | 14 +++++++------- plugins/SlicerT/SlicerTView.h | 10 +++++----- plugins/SlicerT/SlicerTWaveform.cpp | 6 +++--- plugins/SlicerT/SlicerTWaveform.h | 6 +++--- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 5097723e4e6..0fa888ec81f 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -82,7 +82,7 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) const fpp_t frames = handle->framesLeftForCurrentPeriod(); const f_cnt_t offset = handle->noteOffset(); const int bpm = Engine::getSong()->getTempo(); - const float pitchRatio = 1 / pow(2, m_parentTrack->pitchModel()->value() / 1200); + const float pitchRatio = 1 / std::exp2(m_parentTrack->pitchModel()->value() / 1200); float speedRatio = static_cast(m_originalBPM.value()) / bpm; if (!m_enableSync.value()) { speedRatio = 1; } @@ -171,7 +171,7 @@ void SlicerT::findSlices() for (int i = 0; i < singleChannel.size(); i++) { singleChannel[i] /= maxMag; - if (sign(lastValue) != sign(singleChannel[i])) + if (std::sign(lastValue) != std::sign(singleChannel[i])) { zeroCrossings.push_back(i); lastValue = singleChannel[i]; @@ -231,7 +231,7 @@ void SlicerT::findSlices() float beatsPerMin = m_originalBPM.value() / 60.0f; float samplesPerBeat = m_originalSample.sampleRate() / beatsPerMin * 4.0f; int noteSnap = m_sliceSnap.value(); - int sliceLock = samplesPerBeat / std::pow(2, noteSnap + 1); + int sliceLock = samplesPerBeat / std::exp2(noteSnap + 1); if (noteSnap == 0) { sliceLock = 1; } for (float& sliceValue : m_slicePoints) { diff --git a/plugins/SlicerT/SlicerTView.cpp b/plugins/SlicerT/SlicerTView.cpp index 33b83fc69bf..95977a1a3c9 100644 --- a/plugins/SlicerT/SlicerTView.cpp +++ b/plugins/SlicerT/SlicerTView.cpp @@ -111,17 +111,17 @@ void SlicerTView::exportMidi() if (m_slicerTParent->m_originalSample.frames() <= 1) { return; } DataFile dataFile(DataFile::Type::ClipboardData); - QDomElement note_list = dataFile.createElement("note-list"); - dataFile.content().appendChild(note_list); + QDomElement noteList = dataFile.createElement("note-list"); + dataFile.content().appendChild(noteList); - std::vector notes = m_slicerTParent->getMidi(); - if (notes.size() == 0) { return; } + auto notes = m_slicerTParent->getMidi(); + if (notes.empty()) { return; } - TimePos start_pos(notes.front().pos().getBar(), 0); + TimePos startPos(notes.front().pos().getBar(), 0); for (Note& note : notes) { - note.setPos(note.pos(start_pos)); - note.saveState(dataFile, note_list); + note.setPos(note.pos(startPos)); + note.saveState(dataFile, noteList); } copyString(dataFile.toString(), MimeType::Default); diff --git a/plugins/SlicerT/SlicerTView.h b/plugins/SlicerT/SlicerTView.h index d9cbdfae4d1..fbed046c613 100644 --- a/plugins/SlicerT/SlicerTView.h +++ b/plugins/SlicerT/SlicerTView.h @@ -22,8 +22,8 @@ * */ -#ifndef LMMS_SLICERT_VIEW_H -#define LMMS_SLICERT_VIEW_H +#ifndef LMMS_GUI_SLICERT_VIEW_H +#define LMMS_GUI_SLICERT_VIEW_H #include @@ -59,8 +59,8 @@ public slots: static constexpr int bottomTextY = 220; protected: - virtual void dragEnterEvent(QDragEnterEvent* _dee); - virtual void dropEvent(QDropEvent* _de); + virtual void dragEnterEvent(QDragEnterEvent* dee); + virtual void dropEvent(QDropEvent* de); virtual void paintEvent(QPaintEvent* pe); @@ -82,4 +82,4 @@ public slots: }; } // namespace gui } // namespace lmms -#endif // LMMS_SLICERT_VIEW_H +#endif // LMMS_GUI_SLICERT_VIEW_H diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index 89d224a55a4..2e8a88ed4ec 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -213,7 +213,7 @@ void SlicerTWaveform::drawEditor() void SlicerTWaveform::isPlaying(float current, float start, float end) { - if (!m_updateTimer.hasExpired(s_minMilisPassed)) {return;} + if (!m_updateTimer.hasExpired(s_minMilisPassed)) { return; } m_noteCurrent = current; m_noteStart = start; m_noteEnd = end; @@ -401,9 +401,9 @@ void SlicerTWaveform::mouseDoubleClickEvent(QMouseEvent* me) std::sort(m_slicerTParent->m_slicePoints.begin(), m_slicerTParent->m_slicePoints.end()); } -void SlicerTWaveform::wheelEvent(QWheelEvent* _we) +void SlicerTWaveform::wheelEvent(QWheelEvent* we) { - m_zoomLevel += _we->angleDelta().y() / 360.0f * s_zoomSensitivity; + m_zoomLevel += we->angleDelta().y() / 360.0f * s_zoomSensitivity; m_zoomLevel = std::max(0.0f, m_zoomLevel); updateUI(); diff --git a/plugins/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h index 59e4a1aafcc..b714ad93261 100644 --- a/plugins/SlicerT/SlicerTWaveform.h +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -22,8 +22,8 @@ * */ -#ifndef LMMS_SLICERT_WAVEFORM_H -#define LMMS_SLICERT_WAVEFORM_H +#ifndef LMMS_GUI_SLICERT_WAVEFORM_H +#define LMMS_GUI_SLICERT_WAVEFORM_H #include #include @@ -123,4 +123,4 @@ public slots: }; } // namespace gui } // namespace lmms -#endif // LMMS_SLICERT_WAVEFORM_H +#endif // LMMS_GUI_SLICERT_WAVEFORM_H From d2734c9862e6fcf222459fe54710b3661c328e83 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Thu, 9 Nov 2023 00:08:03 +0100 Subject: [PATCH 95/99] More review changes --- plugins/SlicerT/SlicerT.cpp | 2 +- plugins/SlicerT/SlicerTView.cpp | 12 +++++----- plugins/SlicerT/SlicerTView.h | 8 +++---- plugins/SlicerT/SlicerTWaveform.cpp | 36 ++++++++++++++--------------- plugins/SlicerT/SlicerTWaveform.h | 6 ++--- 5 files changed, 32 insertions(+), 32 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 0fa888ec81f..7e03f2e0629 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -171,7 +171,7 @@ void SlicerT::findSlices() for (int i = 0; i < singleChannel.size(); i++) { singleChannel[i] /= maxMag; - if (std::sign(lastValue) != std::sign(singleChannel[i])) + if (sign(lastValue) != sign(singleChannel[i])) { zeroCrossings.push_back(i); lastValue = singleChannel[i]; diff --git a/plugins/SlicerT/SlicerTView.cpp b/plugins/SlicerT/SlicerTView.cpp index 95977a1a3c9..ae1b7c96dc3 100644 --- a/plugins/SlicerT/SlicerTView.cpp +++ b/plugins/SlicerT/SlicerTView.cpp @@ -180,13 +180,13 @@ void SlicerTView::paintEvent(QPaintEvent* pe) brush.setPen(QColor(255, 255, 255)); brush.setFont(QFont(brush.font().family(), 7, -1, false)); - brush.drawText(8, topTextY, textBoxWidth, textBoxHeight, Qt::AlignCenter, tr("Reset")); - brush.drawText(188, topTextY, textBoxWidth, textBoxHeight, Qt::AlignCenter, tr("Midi")); + brush.drawText(8, s_topTextY, s_textBoxWidth, s_textBoxHeight, Qt::AlignCenter, tr("Reset")); + brush.drawText(188, s_topTextY, s_textBoxWidth, s_textBoxHeight, Qt::AlignCenter, tr("Midi")); - brush.drawText(8, bottomTextY, textBoxWidth, textBoxHeight, Qt::AlignCenter, tr("Threshold")); - brush.drawText(63, bottomTextY, textBoxWidth, textBoxHeight, Qt::AlignCenter, tr("Fade Out")); - brush.drawText(127, bottomTextY, textBoxWidth, textBoxHeight, Qt::AlignCenter, tr("BPM")); - brush.drawText(188, bottomTextY, textBoxWidth, textBoxHeight, Qt::AlignCenter, tr("Snap")); + brush.drawText(8, s_bottomTextY, s_textBoxWidth, s_textBoxHeight, Qt::AlignCenter, tr("Threshold")); + brush.drawText(63, s_bottomTextY, s_textBoxWidth, s_textBoxHeight, Qt::AlignCenter, tr("Fade Out")); + brush.drawText(127, s_bottomTextY, s_textBoxWidth, s_textBoxHeight, Qt::AlignCenter, tr("BPM")); + brush.drawText(188, s_bottomTextY, s_textBoxWidth, s_textBoxHeight, Qt::AlignCenter, tr("Snap")); } } // namespace gui diff --git a/plugins/SlicerT/SlicerTView.h b/plugins/SlicerT/SlicerTView.h index fbed046c613..ea2b979fc42 100644 --- a/plugins/SlicerT/SlicerTView.h +++ b/plugins/SlicerT/SlicerTView.h @@ -53,10 +53,10 @@ public slots: public: SlicerTView(SlicerT* instrument, QWidget* parent); - static constexpr int textBoxHeight = 20; - static constexpr int textBoxWidth = 50; - static constexpr int topTextY = 170; - static constexpr int bottomTextY = 220; + static constexpr int s_textBoxHeight = 20; + static constexpr int s_textBoxWidth = 50; + static constexpr int s_topTextY = 170; + static constexpr int s_bottomTextY = 220; protected: virtual void dragEnterEvent(QDragEnterEvent* dee); diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index 2e8a88ed4ec..1c2d85079d7 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -55,12 +55,12 @@ SlicerTWaveform::SlicerTWaveform(int totalWidth, int totalHeight, SlicerT* instr : QWidget(parent) , m_width(totalWidth) , m_height(totalHeight) - , m_seekerWidth(totalWidth - m_seekerHorMargin * 2) - , m_editorHeight(totalHeight - m_seekerHeight - m_middleMargin) + , m_seekerWidth(totalWidth - s_seekerHorMargin * 2) + , m_editorHeight(totalHeight - s_seekerHeight - s_middleMargin) , m_editorWidth(totalWidth) , m_sliceArrow(PLUGIN_NAME::getIconPixmap("slice_indicator_arrow")) - , m_seeker(QPixmap(m_seekerWidth, m_seekerHeight)) - , m_seekerWaveform(QPixmap(m_seekerWidth, m_seekerHeight)) + , m_seeker(QPixmap(m_seekerWidth, s_seekerHeight)) + , m_seekerWaveform(QPixmap(m_seekerWidth, s_seekerHeight)) , m_editorWaveform(QPixmap(m_editorWidth, m_editorHeight)) , m_sliceEditor(QPixmap(totalWidth, m_editorHeight)) , m_emptySampleIcon(embed::getIconPixmap("sample_track")) @@ -107,7 +107,7 @@ void SlicerTWaveform::drawSeeker() for (float sliceValue : m_slicerTParent->m_slicePoints) { float xPos = sliceValue * m_seekerWidth; - brush.drawLine(xPos, 0, xPos, m_seekerHeight); + brush.drawLine(xPos, 0, xPos, s_seekerHeight); } float seekerStartPosX = m_seekerStart * m_seekerWidth; @@ -119,16 +119,16 @@ void SlicerTWaveform::drawSeeker() float noteEndPosX = (m_noteEnd - m_noteStart) * m_seekerWidth; brush.setPen(s_playColor); - brush.drawLine(noteCurrentPosX, 0, noteCurrentPosX, m_seekerHeight); - brush.fillRect(noteStartPosX, 0, noteEndPosX, m_seekerHeight, s_playHighlightColor); + brush.drawLine(noteCurrentPosX, 0, noteCurrentPosX, s_seekerHeight); + brush.fillRect(noteStartPosX, 0, noteEndPosX, s_seekerHeight, s_playHighlightColor); - brush.fillRect(seekerStartPosX, 0, seekerMiddleWidth - 1, m_seekerHeight, s_seekerHighlightColor); + brush.fillRect(seekerStartPosX, 0, seekerMiddleWidth - 1, s_seekerHeight, s_seekerHighlightColor); - brush.fillRect(0, 0, seekerStartPosX, m_seekerHeight, s_seekerShadowColor); - brush.fillRect(seekerEndPosX - 1, 0, m_seekerWidth, m_seekerHeight, s_seekerShadowColor); + brush.fillRect(0, 0, seekerStartPosX, s_seekerHeight, s_seekerShadowColor); + brush.fillRect(seekerEndPosX - 1, 0, m_seekerWidth, s_seekerHeight, s_seekerShadowColor); brush.setPen(QPen(s_seekerColor, 1)); - brush.drawRect(seekerStartPosX, 0, seekerMiddleWidth - 1, m_seekerHeight - 1); // -1 needed + brush.drawRect(seekerStartPosX, 0, seekerMiddleWidth - 1, s_seekerHeight - 1); // -1 needed } void SlicerTWaveform::drawEditorWaveform() @@ -236,13 +236,13 @@ void SlicerTWaveform::updateUI() // updates the closest object and changes the cursor respectivly void SlicerTWaveform::updateClosest(QMouseEvent* me) { - float normalizedClickSeeker = static_cast(me->x() - m_seekerHorMargin) / m_seekerWidth; + float normalizedClickSeeker = static_cast(me->x() - s_seekerHorMargin) / m_seekerWidth; float normalizedClickEditor = static_cast(me->x()) / m_editorWidth; m_closestObject = UIObjects::Nothing; m_closestSlice = -1; - if (me->y() < m_seekerHeight) + if (me->y() < s_seekerHeight) { if (std::abs(normalizedClickSeeker - m_seekerStart) < s_distanceForClick) { @@ -310,7 +310,7 @@ void SlicerTWaveform::mousePressEvent(QMouseEvent* me) static_cast(parent())->openFiles(); } // update seeker middle for correct movement - m_seekerMiddle = static_cast(me->x() - m_seekerHorMargin) / m_seekerWidth; + m_seekerMiddle = static_cast(me->x() - s_seekerHorMargin) / m_seekerWidth; break; case Qt::MouseButton::RightButton: if (m_slicerTParent->m_slicePoints.size() > 2 && m_closestObject == UIObjects::SlicePoint) @@ -341,7 +341,7 @@ void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me) return; } - float normalizedClickSeeker = static_cast(me->x() - m_seekerHorMargin) / m_seekerWidth; + float normalizedClickSeeker = static_cast(me->x() - s_seekerHorMargin) / m_seekerWidth; float normalizedClickEditor = static_cast(me->x()) / m_editorWidth; float distStart = m_seekerStart - m_seekerMiddle; @@ -412,9 +412,9 @@ void SlicerTWaveform::wheelEvent(QWheelEvent* we) void SlicerTWaveform::paintEvent(QPaintEvent* pe) { QPainter p(this); - p.drawPixmap(m_seekerHorMargin, 0, m_seekerWaveform); - p.drawPixmap(m_seekerHorMargin, 0, m_seeker); - p.drawPixmap(0, m_seekerHeight + m_middleMargin, m_sliceEditor); + p.drawPixmap(s_seekerHorMargin, 0, m_seekerWaveform); + p.drawPixmap(s_seekerHorMargin, 0, m_seeker); + p.drawPixmap(0, s_seekerHeight + s_middleMargin, m_sliceEditor); } } // namespace gui } // namespace lmms diff --git a/plugins/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h index b714ad93261..44c7528ef72 100644 --- a/plugins/SlicerT/SlicerTWaveform.h +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -53,9 +53,9 @@ public slots: SlicerTWaveform(int totalWidth, int totalHeight, SlicerT* instrument, QWidget* parent); // predefined sizes - static constexpr int m_seekerHorMargin = 5; - static constexpr int m_seekerHeight = 38; // used to calcualte all vertical sizes - static constexpr int m_middleMargin = 6; + static constexpr int s_seekerHorMargin = 5; + static constexpr int s_seekerHeight = 38; // used to calcualte all vertical sizes + static constexpr int s_middleMargin = 6; // interaction behavior values From b4b64b1eebc20af64dd47354012a3d9da1c1d6d4 Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Fri, 10 Nov 2023 17:22:42 +0100 Subject: [PATCH 96/99] Renamed files --- plugins/SlicerT/SlicerT.cpp | 2 +- plugins/SlicerT/SlicerTView.cpp | 4 ++-- plugins/SlicerT/{copyMidi.png => copy_midi.png} | Bin plugins/SlicerT/{icon.png => logo.png} | Bin .../SlicerT/{resetSlices.png => reset_slices.png} | Bin 5 files changed, 3 insertions(+), 3 deletions(-) rename plugins/SlicerT/{copyMidi.png => copy_midi.png} (100%) rename plugins/SlicerT/{icon.png => logo.png} (100%) rename plugins/SlicerT/{resetSlices.png => reset_slices.png} (100%) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 7e03f2e0629..04f2b05593e 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -46,7 +46,7 @@ Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = { "Daniel Kauss Serna ", 0x0100, Plugin::Type::Instrument, - new PluginPixmapLoader("icon"), + new PluginPixmapLoader("logo"), nullptr, nullptr, }; diff --git a/plugins/SlicerT/SlicerTView.cpp b/plugins/SlicerT/SlicerTView.cpp index ae1b7c96dc3..833d4b434af 100644 --- a/plugins/SlicerT/SlicerTView.cpp +++ b/plugins/SlicerT/SlicerTView.cpp @@ -84,13 +84,13 @@ SlicerTView::SlicerTView(SlicerT* instrument, QWidget* parent) m_midiExportButton = new QPushButton(this); m_midiExportButton->move(199, 150); - m_midiExportButton->setIcon(PLUGIN_NAME::getIconPixmap("copyMidi")); + m_midiExportButton->setIcon(PLUGIN_NAME::getIconPixmap("copy_midi")); m_midiExportButton->setToolTip(tr("Copy midi pattern to clipboard")); connect(m_midiExportButton, &PixmapButton::clicked, this, &SlicerTView::exportMidi); m_resetButton = new QPushButton(this); m_resetButton->move(18, 150); - m_resetButton->setIcon(PLUGIN_NAME::getIconPixmap("resetSlices")); + m_resetButton->setIcon(PLUGIN_NAME::getIconPixmap("reset_slices")); m_resetButton->setToolTip(tr("Reset Slices")); connect(m_resetButton, &PixmapButton::clicked, m_slicerTParent, &SlicerT::updateSlices); } diff --git a/plugins/SlicerT/copyMidi.png b/plugins/SlicerT/copy_midi.png similarity index 100% rename from plugins/SlicerT/copyMidi.png rename to plugins/SlicerT/copy_midi.png diff --git a/plugins/SlicerT/icon.png b/plugins/SlicerT/logo.png similarity index 100% rename from plugins/SlicerT/icon.png rename to plugins/SlicerT/logo.png diff --git a/plugins/SlicerT/resetSlices.png b/plugins/SlicerT/reset_slices.png similarity index 100% rename from plugins/SlicerT/resetSlices.png rename to plugins/SlicerT/reset_slices.png From 1fb46183b932892eb1c7ef3aed7362850ea80a6c Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sat, 11 Nov 2023 16:33:59 +0100 Subject: [PATCH 97/99] Full sample only at base note --- plugins/SlicerT/SlicerT.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 04f2b05593e..569ecd961df 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -78,7 +78,7 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) { if (m_originalSample.frames() <= 1) { return; } - const int noteIndex = handle->key() - m_parentTrack->baseNote(); + int noteIndex = handle->key() - m_parentTrack->baseNote(); const fpp_t frames = handle->framesLeftForCurrentPeriod(); const f_cnt_t offset = handle->noteOffset(); const int bpm = Engine::getSong()->getTempo(); @@ -90,16 +90,22 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) speedRatio *= Engine::audioEngine()->processingSampleRate() / static_cast(m_originalSample.sampleRate()); float sliceStart, sliceEnd; - if (noteIndex > m_slicePoints.size() - 2 || noteIndex < 0) // full sample if ouside range + if (noteIndex == 0) // full sample at base note { sliceStart = 0; sliceEnd = 1; } - else + else if (noteIndex > 0 && noteIndex < m_slicePoints.size()) { + noteIndex -= 1; sliceStart = m_slicePoints[noteIndex]; sliceEnd = m_slicePoints[noteIndex + 1]; } + else + { + emit isPlaying(-1, 0, 0); + return; + } if (!handle->m_pluginData) { handle->m_pluginData = new PlaybackState(sliceStart); } @@ -295,7 +301,7 @@ std::vector SlicerT::getMidi() float sliceEnd = totalTicks * m_slicePoints[i + 1]; Note sliceNote = Note(); - sliceNote.setKey(i + m_parentTrack->baseNote()); + sliceNote.setKey(i + m_parentTrack->baseNote() + 1); sliceNote.setPos(sliceStart); sliceNote.setLength(sliceEnd - sliceStart + 1); // + 1 so that the notes allign outputNotes.push_back(sliceNote); From bf7bdc418623b7310359d4c671d40fb1449ccb4c Mon Sep 17 00:00:00 2001 From: DanielKauss Date: Sat, 11 Nov 2023 23:40:51 +0100 Subject: [PATCH 98/99] Fix memory leak Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> --- plugins/SlicerT/SlicerT.cpp | 12 +++++++++--- plugins/SlicerT/SlicerT.h | 1 + 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 569ecd961df..2918265cead 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -108,15 +108,16 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) } if (!handle->m_pluginData) { handle->m_pluginData = new PlaybackState(sliceStart); } + auto playbackState = static_cast(handle->m_pluginData); - float noteDone = ((PlaybackState*)handle->m_pluginData)->noteDone(); + float noteDone = playbackState->noteDone(); float noteLeft = sliceEnd - noteDone; if (noteLeft > 0) { int noteFrame = noteDone * m_originalSample.frames(); - SRC_STATE* resampleState = static_cast(handle->m_pluginData)->resamplingState(); + SRC_STATE* resampleState = playbackState->resamplingState(); SRC_DATA resampleData; resampleData.data_in = (m_originalSample.data() + noteFrame)->data(); resampleData.data_out = (workingBuffer + offset)->data(); @@ -127,7 +128,7 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) src_process(resampleState, &resampleData); float nextNoteDone = noteDone + frames * (1.0f / speedRatio) / m_originalSample.frames(); - static_cast(handle->m_pluginData)->setNoteDone(nextNoteDone); + 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(); @@ -149,6 +150,11 @@ void SlicerT::playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) else { emit isPlaying(-1, 0, 0); } } +void SlicerT::deleteNotePluginData(NotePlayHandle* handle) +{ + delete static_cast(handle->m_pluginData); +} + // uses the spectral flux to determine the change in magnitude // resources: // http://www.iro.umontreal.ca/~pift6080/H09/documents/papers/bello_onset_tutorial.pdf diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 73c4e9b5c6c..6ca10349c72 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -75,6 +75,7 @@ public slots: SlicerT(InstrumentTrack* instrumentTrack); void playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) override; +void deleteNotePluginData(NotePlayHandle* handle) override; void saveSettings(QDomDocument& document, QDomElement& element) override; void loadSettings(const QDomElement& element) override; From 4894e9541de440610d5ce975d800671861cac40b Mon Sep 17 00:00:00 2001 From: Daniel Kauss Date: Sat, 11 Nov 2023 23:46:08 +0100 Subject: [PATCH 99/99] Style fixes --- plugins/SlicerT/SlicerT.h | 2 +- plugins/SlicerT/SlicerTWaveform.cpp | 4 +--- plugins/SlicerT/SlicerTWaveform.h | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h index 6ca10349c72..8671eecd1f8 100644 --- a/plugins/SlicerT/SlicerT.h +++ b/plugins/SlicerT/SlicerT.h @@ -75,7 +75,7 @@ public slots: SlicerT(InstrumentTrack* instrumentTrack); void playNote(NotePlayHandle* handle, sampleFrame* workingBuffer) override; -void deleteNotePluginData(NotePlayHandle* handle) override; + void deleteNotePluginData(NotePlayHandle* handle) override; void saveSettings(QDomDocument& document, QDomElement& element) override; void loadSettings(const QDomElement& element) override; diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index 1c2d85079d7..6685f4f8cec 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -306,9 +306,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.frames() <= 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/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h index 44c7528ef72..6478e7f8684 100644 --- a/plugins/SlicerT/SlicerTWaveform.h +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -26,11 +26,11 @@ #define LMMS_GUI_SLICERT_WAVEFORM_H #include +#include #include #include #include #include -#include #include "Instrument.h" #include "SampleBuffer.h" @@ -57,7 +57,6 @@ public slots: static constexpr int s_seekerHeight = 38; // used to calcualte all vertical sizes static constexpr int s_middleMargin = 6; - // interaction behavior values static constexpr float s_distanceForClick = 0.02f; static constexpr float s_minSeekerDistance = 0.13f;