Add slicer plugin (#6857)

* extremly basic slicer, note playback and gui works

* very simple peak detection working

* basic phase vocoder implementation, no effects yet

* phase vocoder slight rewrite

* pitch shifting works more or less

* basic timeshift working

* PV timeshift working (no pitch shift)

* basic functions work (UI, editing, playback)

* slice editor Ui working

* fundamental  functionality done

* Everything basic works fully

* cleanup and code guidelines

* more file cleanup

* Tried fixing multi slice playback (still broken)

* remove includes, add license

* code factoring issues

* more code factoring

* fixed multinote playback and bpm check

* UI performance improvments + code style

* initial UI changes + more code style

* threadsafe(maybe) + UI finished

* preparing for dinamic timeshifting

* dynamic timeshifting start

* realtime time scaling (no stereo)

* stereo added, very slow

* playback performance improvments

* Roxas new UI start

* fixed cmake

* Waveform UI finished

* Roxas UI knobs + layout

* Spectral flux onset detection

* build + PV fixes

* clang-format formatting

* slice snap + better defaults

* windows build fixes

* windows build fixes part 2

* Fixed slice bug + Waveform code cleanup

* UI button text + reorder + file cleanup

* Added knob colors

* comments + code cleanup

* var names fit convention

* PV better windowing

* waveform zoom

* Minor style fixes.

* Initial artistic rebalancing of the plugin artwork.

* PV phase ghosting fix

* Use base note as keyboard slice start

* Good draft of Artwork, renamed bg to artwork

* Removed soft glow.

* Fixed load crashes + findSlices cleanup

* Added sync button

* added pitch shifting, sometimes crashes

* pitch fixes

* MacOs build fixes

* use linear interpolation

* copyright + div by 0 fixes

* Fixed rare crash + name changes + license

* review: memcpy, no array, LMMS header, name change

* static constexpr added

* static vars to public + LMMS guards

* remove references in classes

* remove constexpr and parent pointer in waveform

* std::array for fft

* fixed wrong name in style

* remove c style casts

* use src_process

* use note vector for return

* Moved PhaseVocoder into core

* removed PV from plugin

* remove pointers in waveform

* clang-format again

* Use std:: + review suggestions

Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com>
Co-authored-by: saker <sakertooth@gmail.com>

* More review changes

* new signal slot + more review

* Fixed pitch shifting

* Fixed buffer overflow in PV

* Fixed mouse bug + better empty screen

* Small editor refactor + improvments

* Editor playback visual + small fixes

* Roxas UI improvments

* initial timeshift removing

* Remove timeshift + slice refactor

* Removed unused files

* Fix export bug

* Fix zoom bug

* Review changes SakerTooth#2

* Remove most comments

* Performance + click to load

* update PlaybackState + zerocross snapping

* Fix windows build issue

* Review + version

* Fixed fade out bug

* Use cosine interpolation

* Apply suggestions from code review

Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com>

* More review changes

* Renamed files

* Full sample only at base note

* Fix memory leak

Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com>

* Style fixes

---------

Co-authored-by: Katherine Pratt <consolegrl@gmail.com>
Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com>
Co-authored-by: saker <sakertooth@gmail.com>
This commit is contained in:
DanielKauss
2023-11-12 00:09:38 +01:00
committed by GitHub
parent 8b2769bb69
commit c779521730
18 changed files with 1370 additions and 2 deletions

View File

@@ -60,6 +60,7 @@ SET(LMMS_PLUGIN_LIST
Sf2Player
Sfxr
Sid
SlicerT
SpectrumAnalyzer
StereoEnhancer
StereoMatrix

View File

@@ -899,6 +899,14 @@ lmms--gui--SidInstrumentView lmms--gui--Knob {
qproperty-lineWidth: 2;
}
lmms--gui--SlicerTView 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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -943,6 +943,14 @@ lmms--gui--SidInstrumentView lmms--gui--Knob {
qproperty-lineWidth: 2;
}
lmms--gui--SlicerTView 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;

View File

@@ -25,8 +25,10 @@
#ifndef LMMS_CLIPBOARD_H
#define LMMS_CLIPBOARD_H
#include <QMap>
#include <QDomElement>
#include <QMap>
#include "lmms_export.h"
class QMimeData;
@@ -44,7 +46,7 @@ namespace lmms::Clipboard
bool hasFormat( MimeType mT );
// Helper methods for String data
void 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

View File

@@ -0,0 +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 SlicerTView.cpp SlicerTView.h SlicerTWaveform.cpp SlicerTWaveform.h MOCFILES SlicerT.h SlicerTView.h SlicerTWaveform.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png")

410
plugins/SlicerT/SlicerT.cpp Normal file
View File

@@ -0,0 +1,410 @@
/*
* SlicerT.cpp - simple slicer plugin
*
* Copyright (c) 2023 Daniel Kauss Serna <daniel.kauss.serna@gmail.com>
*
* This file is part of LMMS - https://lmms.io
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program (see COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
*/
#include "SlicerT.h"
#include <QDomElement>
#include <cmath>
#include <fftw3.h>
#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 {
extern "C" {
Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = {
LMMS_STRINGIFY(PLUGIN_NAME),
"SlicerT",
QT_TRANSLATE_NOOP("PluginBrowser", "Basic Slicer"),
"Daniel Kauss Serna <daniel.kauss.serna@gmail.com>",
0x0100,
Plugin::Type::Instrument,
new PluginPixmapLoader("logo"),
nullptr,
nullptr,
};
} // end extern
// ################################# 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(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"))
, m_originalSample()
, m_parentTrack(instrumentTrack)
{
m_sliceSnap.addItem("Off");
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)
{
if (m_originalSample.frames() <= 1) { return; }
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 / std::exp2(m_parentTrack->pitchModel()->value() / 1200);
float speedRatio = static_cast<float>(m_originalBPM.value()) / bpm;
if (!m_enableSync.value()) { speedRatio = 1; }
speedRatio *= pitchRatio;
speedRatio *= Engine::audioEngine()->processingSampleRate() / static_cast<float>(m_originalSample.sampleRate());
float sliceStart, sliceEnd;
if (noteIndex == 0) // full sample at base note
{
sliceStart = 0;
sliceEnd = 1;
}
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); }
auto playbackState = static_cast<PlaybackState*>(handle->m_pluginData);
float noteDone = playbackState->noteDone();
float noteLeft = sliceEnd - noteDone;
if (noteLeft > 0)
{
int noteFrame = noteDone * m_originalSample.frames();
SRC_STATE* resampleState = playbackState->resamplingState();
SRC_DATA resampleData;
resampleData.data_in = (m_originalSample.data() + noteFrame)->data();
resampleData.data_out = (workingBuffer + offset)->data();
resampleData.input_frames = noteLeft * m_originalSample.frames();
resampleData.output_frames = frames;
resampleData.src_ratio = speedRatio;
src_process(resampleState, &resampleData);
float nextNoteDone = noteDone + frames * (1.0f / speedRatio) / m_originalSample.frames();
playbackState->setNoteDone(nextNoteDone);
// exponential fade out, applyRelease() not used since it extends the note length
int fadeOutFrames = m_fadeOutFrames.value() / 1000.0f * Engine::audioEngine()->processingSampleRate();
int noteFramesLeft = noteLeft * m_originalSample.frames() * speedRatio;
for (int i = 0; i < frames; i++)
{
float fadeValue = static_cast<float>(noteFramesLeft - i) / fadeOutFrames;
fadeValue = std::clamp(fadeValue, 0.0f, 1.0f);
fadeValue = cosinusInterpolate(0, 1, fadeValue);
workingBuffer[i + offset][0] *= fadeValue;
workingBuffer[i + offset][1] *= fadeValue;
}
instrumentTrack()->processAudioBuffer(workingBuffer, frames + offset, handle);
emit isPlaying(noteDone, sliceStart, sliceEnd);
}
else { emit isPlaying(-1, 0, 0); }
}
void SlicerT::deleteNotePluginData(NotePlayHandle* handle)
{
delete static_cast<PlaybackState*>(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
void SlicerT::findSlices()
{
if (m_originalSample.frames() <= 1) { return; }
m_slicePoints = {};
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;
float maxMag = -1;
std::vector<float> 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 and find 0 crossings
std::vector<int> zeroCrossings;
float lastValue = 1;
for (int i = 0; i < singleChannel.size(); i++)
{
singleChannel[i] /= maxMag;
if (sign(lastValue) != sign(singleChannel[i]))
{
zeroCrossings.push_back(i);
lastValue = singleChannel[i];
}
}
std::vector<float> prevMags(windowSize / 2, 0);
std::vector<float> fftIn(windowSize, 0);
std::array<fftwf_complex, windowSize> fftOut;
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;
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)
{
// fft
std::copy_n(singleChannel.data() + i, windowSize, fftIn.data());
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 = std::sqrt(real * real + imag * imag);
// using L2-norm (euclidean distance)
diff = std::sqrt(std::pow(magnitude - prevMags[j], 2));
spectralFlux += diff;
prevMags[j] = magnitude;
}
if (spectralFlux / prevFlux > 1.0f + m_noteThreshold.value() && i - lastPoint > minDist)
{
m_slicePoints.push_back(i);
lastPoint = i;
if (m_slicePoints.size() > 128) { break; } // no more keys on the keyboard
}
prevFlux = spectralFlux;
spectralFlux = 1E-10; // again for no divison by zero
}
m_slicePoints.push_back(m_originalSample.frames());
for (float& sliceValue : m_slicePoints)
{
int closestZeroCrossing = *std::lower_bound(zeroCrossings.begin(), zeroCrossings.end(), sliceValue);
if (std::abs(sliceValue - closestZeroCrossing) < windowSize) { sliceValue = closestZeroCrossing; }
}
float beatsPerMin = m_originalBPM.value() / 60.0f;
float samplesPerBeat = m_originalSample.sampleRate() / beatsPerMin * 4.0f;
int noteSnap = m_sliceSnap.value();
int sliceLock = samplesPerBeat / std::exp2(noteSnap + 1);
if (noteSnap == 0) { sliceLock = 1; }
for (float& sliceValue : m_slicePoints)
{
sliceValue += sliceLock / 2;
sliceValue -= static_cast<int>(sliceValue) % sliceLock;
}
m_slicePoints.erase(std::unique(m_slicePoints.begin(), m_slicePoints.end()), m_slicePoints.end());
for (float& sliceIndex : m_slicePoints)
{
sliceIndex /= m_originalSample.frames();
}
m_slicePoints[0] = 0;
m_slicePoints[m_slicePoints.size() - 1] = 1;
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() <= 1) { return; }
float sampleRate = m_originalSample.sampleRate();
float totalFrames = m_originalSample.frames();
float sampleLength = totalFrames / sampleRate;
float bpmEstimate = 240.0f / sampleLength;
while (bpmEstimate < 100)
{
bpmEstimate *= 2;
}
while (bpmEstimate > 200)
{
bpmEstimate /= 2;
}
m_originalBPM.setValue(bpmEstimate);
m_originalBPM.setInitValue(bpmEstimate);
}
std::vector<Note> SlicerT::getMidi()
{
std::vector<Note> outputNotes;
float speedRatio = static_cast<float>(m_originalBPM.value()) / Engine::getSong()->getTempo();
float outFrames = m_originalSample.frames() * speedRatio;
float framesPerTick = Engine::framesPerTick();
float totalTicks = outFrames / framesPerTick;
float lastEnd = 0;
for (int i = 0; i < m_slicePoints.size() - 1; i++)
{
float sliceStart = lastEnd;
float sliceEnd = totalTicks * m_slicePoints[i + 1];
Note sliceNote = Note();
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);
lastEnd = sliceEnd;
}
return outputNotes;
}
void SlicerT::updateFile(QString file)
{
m_originalSample.setAudioFile(file);
findBPM();
findSlices();
emit dataChanged();
}
void SlicerT::updateSlices()
{
findSlices();
}
void SlicerT::saveSettings(QDomDocument& document, QDomElement& element)
{
element.setAttribute("version", "1");
element.setAttribute("src", m_originalSample.audioFile());
if (m_originalSample.audioFile().isEmpty())
{
QString s;
element.setAttribute("sampledata", m_originalSample.toBase64(s));
}
element.setAttribute("totalSlices", static_cast<int>(m_slicePoints.size()));
for (int i = 0; i < m_slicePoints.size(); i++)
{
element.setAttribute(tr("slice_%1").arg(i), m_slicePoints[i]);
}
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)
{
if (!element.attribute("src").isEmpty())
{
m_originalSample.setAudioFile(element.attribute("src"));
QString absolutePath = PathUtil::toAbsolute(m_originalSample.audioFile());
if (!QFileInfo(absolutePath).exists())
{
QString message = tr("Sample not found: %1").arg(m_originalSample.audioFile());
Engine::getSong()->collectError(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 < totalSlices; i++)
{
m_slicePoints.push_back(element.attribute(tr("slice_%1").arg(i)).toFloat());
}
}
m_fadeOutFrames.loadSettings(element, "fadeOut");
m_noteThreshold.loadSettings(element, "threshold");
m_originalBPM.loadSettings(element, "origBPM");
m_enableSync.loadSettings(element, "syncEnable");
emit dataChanged();
}
QString SlicerT::nodeName() const
{
return slicert_plugin_descriptor.name;
}
gui::PluginView* SlicerT::instantiateView(QWidget* parent)
{
return new gui::SlicerTView(this, parent);
}
extern "C" {
PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* m, void*)
{
return new SlicerT(static_cast<InstrumentTrack*>(m));
}
} // extern
} // namespace lmms

108
plugins/SlicerT/SlicerT.h Normal file
View File

@@ -0,0 +1,108 @@
/*
* SlicerT.h - declaration of class SlicerT
*
* Copyright (c) 2023 Daniel Kauss Serna <daniel.kauss.serna@gmail.com>
*
* This file is part of LMMS - https://lmms.io
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program (see COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
*/
#ifndef LMMS_SLICERT_H
#define LMMS_SLICERT_H
#include <algorithm>
#include <fftw3.h>
#include <stdexcept>
#include "AutomatableModel.h"
#include "Instrument.h"
#include "InstrumentView.h"
#include "Note.h"
#include "SampleBuffer.h"
#include "SlicerTView.h"
#include "lmms_basics.h"
namespace lmms {
class PlaybackState
{
public:
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() 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 m_currentNoteDone;
SRC_STATE* m_resamplingState;
};
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);
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;
void findSlices();
void findBPM();
QString nodeName() const override;
gui::PluginView* instantiateView(QWidget* parent) override;
std::vector<Note> getMidi();
private:
FloatModel m_noteThreshold;
FloatModel m_fadeOutFrames;
IntModel m_originalBPM;
ComboBoxModel m_sliceSnap;
BoolModel m_enableSync;
SampleBuffer m_originalSample;
std::vector<float> m_slicePoints;
InstrumentTrack* m_parentTrack;
friend class gui::SlicerTView;
friend class gui::SlicerTWaveform;
};
} // namespace lmms
#endif // LMMS_SLICERT_H

View File

@@ -0,0 +1,193 @@
/*
* SlicerTView.cpp - controls the UI for slicerT
*
* Copyright (c) 2023 Daniel Kauss Serna <daniel.kauss.serna@gmail.com>
*
* This file is part of LMMS - https://lmms.io
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program (see COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
*/
#include "SlicerTView.h"
#include <QDropEvent>
#include <QFileInfo>
#include "Clipboard.h"
#include "DataFile.h"
#include "Engine.h"
#include "InstrumentTrack.h"
#include "SlicerT.h"
#include "Song.h"
#include "StringPairDrag.h"
#include "Track.h"
#include "embed.h"
namespace lmms {
namespace gui {
SlicerTView::SlicerTView(SlicerT* instrument, QWidget* parent)
: InstrumentViewFixedSize(instrument, parent)
, m_slicerTParent(instrument)
{
// window settings
setAcceptDrops(true);
setAutoFillBackground(true);
// render background
QPalette pal;
pal.setBrush(backgroundRole(), PLUGIN_NAME::getIconPixmap("artwork"));
setPalette(pal);
m_wf = new SlicerTWaveform(248, 128, instrument, this);
m_wf->move(2, 6);
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);
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);
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);
m_noteThresholdKnob = createStyledKnob();
m_noteThresholdKnob->move(10, 197);
m_noteThresholdKnob->setToolTip(tr("Threshold used for slicing"));
m_noteThresholdKnob->setModel(&m_slicerTParent->m_noteThreshold);
m_fadeOutKnob = createStyledKnob();
m_fadeOutKnob->move(64, 197);
m_fadeOutKnob->setToolTip(tr("Fade Out per note in milliseconds"));
m_fadeOutKnob->setModel(&m_slicerTParent->m_fadeOutFrames);
m_midiExportButton = new QPushButton(this);
m_midiExportButton->move(199, 150);
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("reset_slices"));
m_resetButton->setToolTip(tr("Reset Slices"));
connect(m_resetButton, &PixmapButton::clicked, m_slicerTParent, &SlicerT::updateSlices);
}
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()
{
using namespace Clipboard;
if (m_slicerTParent->m_originalSample.frames() <= 1) { return; }
DataFile dataFile(DataFile::Type::ClipboardData);
QDomElement noteList = dataFile.createElement("note-list");
dataFile.content().appendChild(noteList);
auto notes = m_slicerTParent->getMidi();
if (notes.empty()) { return; }
TimePos startPos(notes.front().pos().getBar(), 0);
for (Note& note : notes)
{
note.setPos(note.pos(startPos));
note.saveState(dataFile, noteList);
}
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)
{
// 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<int>(Track::Type::Sample)))
{
dee->acceptProposedAction();
}
else if (txt.section(':', 0, 0) == "samplefile") { dee->acceptProposedAction(); }
else { dee->ignore(); }
}
else { dee->ignore(); }
}
void SlicerTView::dropEvent(QDropEvent* de)
{
QString type = StringPairDrag::decodeKey(de);
QString value = StringPairDrag::decodeValue(de);
if (type == "samplefile")
{
// set m_wf wave file
m_slicerTParent->updateFile(value);
return;
}
else if (type == QString("clip_%1").arg(static_cast<int>(Track::Type::Sample)))
{
DataFile dataFile(value.toUtf8());
m_slicerTParent->updateFile(dataFile.content().firstChild().toElement().attribute("src"));
de->accept();
return;
}
de->ignore();
}
void SlicerTView::paintEvent(QPaintEvent* pe)
{
QPainter brush(this);
brush.setPen(QColor(255, 255, 255));
brush.setFont(QFont(brush.font().family(), 7, -1, false));
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, 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
} // namespace lmms

View File

@@ -0,0 +1,85 @@
/*
* SlicerTView.h - declaration of class SlicerTView
*
* Copyright (c) 2023 Daniel Kauss Serna <daniel.kauss.serna@gmail.com>
*
* This file is part of LMMS - https://lmms.io
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program (see COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
*/
#ifndef LMMS_GUI_SLICERT_VIEW_H
#define LMMS_GUI_SLICERT_VIEW_H
#include <QPushButton>
#include "ComboBox.h"
#include "Instrument.h"
#include "InstrumentView.h"
#include "Knob.h"
#include "LcdSpinBox.h"
#include "LedCheckBox.h"
#include "PixmapButton.h"
#include "SlicerTWaveform.h"
namespace lmms {
class SlicerT;
namespace gui {
class SlicerTView : public InstrumentViewFixedSize
{
Q_OBJECT
public slots:
void exportMidi();
void openFiles();
public:
SlicerTView(SlicerT* instrument, QWidget* parent);
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);
virtual void dropEvent(QDropEvent* de);
virtual void paintEvent(QPaintEvent* pe);
private:
SlicerT* m_slicerTParent;
Knob* m_noteThresholdKnob;
Knob* m_fadeOutKnob;
LcdSpinBox* m_bpmBox;
ComboBox* m_snapSetting;
LedCheckBox* m_syncToggle;
QPushButton* m_resetButton;
QPushButton* m_midiExportButton;
SlicerTWaveform* m_wf;
Knob* createStyledKnob();
};
} // namespace gui
} // namespace lmms
#endif // LMMS_GUI_SLICERT_VIEW_H

View File

@@ -0,0 +1,418 @@
/*
* SlicerTWaveform.cpp - slice editor for SlicerT
*
* Copyright (c) 2023 Daniel Kauss Serna <daniel.kauss.serna@gmail.com>
*
* This file is part of LMMS - https://lmms.io
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program (see COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
*/
#include "SlicerTWaveform.h"
#include <QBitmap>
#include "SlicerT.h"
#include "SlicerTView.h"
#include "embed.h"
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)
, m_width(totalWidth)
, m_height(totalHeight)
, 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, 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"))
, m_slicerTParent(instrument)
{
setFixedSize(m_width, m_height);
setMouseTracking(true);
m_seekerWaveform.fill(s_waveformBgColor);
m_editorWaveform.fill(s_waveformBgColor);
connect(instrument, &SlicerT::isPlaying, this, &SlicerTWaveform::isPlaying);
connect(instrument, &SlicerT::dataChanged, this, &SlicerTWaveform::updateUI);
m_emptySampleIcon = m_emptySampleIcon.createMaskFromColor(QColor(255, 255, 255), Qt::MaskMode::MaskOutColor);
m_updateTimer.start();
updateUI();
}
void SlicerTWaveform::drawSeekerWaveform()
{
m_seekerWaveform.fill(s_waveformBgColor);
if (m_slicerTParent->m_originalSample.frames() <= 1) { return; }
QPainter brush(&m_seekerWaveform);
brush.setPen(s_waveformColor);
m_slicerTParent->m_originalSample.visualize(brush, QRect(0, 0, m_seekerWaveform.width(), m_seekerWaveform.height()),
0, m_slicerTParent->m_originalSample.frames());
// 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()
{
m_seeker.fill(s_emptyColor);
if (m_slicerTParent->m_originalSample.frames() <= 1) { return; }
QPainter brush(&m_seeker);
brush.setPen(s_sliceColor);
for (float sliceValue : m_slicerTParent->m_slicePoints)
{
float xPos = sliceValue * m_seekerWidth;
brush.drawLine(xPos, 0, xPos, s_seekerHeight);
}
float seekerStartPosX = m_seekerStart * m_seekerWidth;
float seekerEndPosX = m_seekerEnd * m_seekerWidth;
float seekerMiddleWidth = (m_seekerEnd - m_seekerStart) * m_seekerWidth;
float noteCurrentPosX = m_noteCurrent * m_seekerWidth;
float noteStartPosX = m_noteStart * m_seekerWidth;
float noteEndPosX = (m_noteEnd - m_noteStart) * m_seekerWidth;
brush.setPen(s_playColor);
brush.drawLine(noteCurrentPosX, 0, noteCurrentPosX, s_seekerHeight);
brush.fillRect(noteStartPosX, 0, noteEndPosX, s_seekerHeight, s_playHighlightColor);
brush.fillRect(seekerStartPosX, 0, seekerMiddleWidth - 1, s_seekerHeight, s_seekerHighlightColor);
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, s_seekerHeight - 1); // -1 needed
}
void SlicerTWaveform::drawEditorWaveform()
{
m_editorWaveform.fill(s_emptyColor);
if (m_slicerTParent->m_originalSample.frames() <= 1) { return; }
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);
// 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()
{
m_sliceEditor.fill(s_waveformBgColor);
QPainter brush(&m_sliceEditor);
// No sample loaded
if (m_slicerTParent->m_originalSample.frames() <= 1)
{
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("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);
return;
}
float startFrame = m_seekerStart;
float endFrame = m_seekerEnd;
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;
brush.setPen(s_playHighlightColor);
brush.drawLine(0, m_editorHeight / 2, m_editorWidth, m_editorHeight / 2);
brush.drawPixmap(0, 0, m_editorWaveform);
brush.setPen(s_playColor);
brush.drawLine(noteCurrentPos, 0, noteCurrentPos, m_editorHeight);
brush.fillRect(noteStartPos, 0, noteLength, m_editorHeight, s_playHighlightColor);
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_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);
}
}
}
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();
drawEditorWaveform();
drawSeeker();
drawEditor();
update();
}
// updates the closest object and changes the cursor respectivly
void SlicerTWaveform::updateClosest(QMouseEvent* me)
{
float normalizedClickSeeker = static_cast<float>(me->x() - s_seekerHorMargin) / m_seekerWidth;
float normalizedClickEditor = static_cast<float>(me->x()) / m_editorWidth;
m_closestObject = UIObjects::Nothing;
m_closestSlice = -1;
if (me->y() < s_seekerHeight)
{
if (std::abs(normalizedClickSeeker - m_seekerStart) < s_distanceForClick)
{
m_closestObject = UIObjects::SeekerStart;
}
else if (std::abs(normalizedClickSeeker - m_seekerEnd) < s_distanceForClick)
{
m_closestObject = UIObjects::SeekerEnd;
}
else if (normalizedClickSeeker > m_seekerStart && normalizedClickSeeker < m_seekerEnd)
{
m_closestObject = UIObjects::SeekerMiddle;
}
}
else
{
m_closestSlice = -1;
float startFrame = m_seekerStart;
float endFrame = m_seekerEnd;
for (int i = 0; i < m_slicerTParent->m_slicePoints.size(); i++)
{
float sliceIndex = m_slicerTParent->m_slicePoints.at(i);
float xPos = (sliceIndex - startFrame) / (endFrame - startFrame);
if (std::abs(xPos - normalizedClickEditor) < s_distanceForClick)
{
m_closestObject = UIObjects::SlicePoint;
m_closestSlice = i;
}
}
}
updateCursor();
drawSeeker();
drawEditor();
update();
}
void SlicerTWaveform::updateCursor()
{
if (m_closestObject == UIObjects::SlicePoint || m_closestObject == UIObjects::SeekerStart
|| m_closestObject == UIObjects::SeekerEnd)
{
setCursor(Qt::SizeHorCursor);
}
else if (m_closestObject == UIObjects::SeekerMiddle && m_seekerEnd - m_seekerStart != 1.0f)
{
setCursor(Qt::SizeAllCursor);
}
else { setCursor(Qt::ArrowCursor); }
}
// handles deletion, reset and middles seeker
void SlicerTWaveform::mousePressEvent(QMouseEvent* me)
{
switch (me->button())
{
case Qt::MouseButton::MiddleButton:
m_seekerStart = 0;
m_seekerEnd = 1;
m_zoomLevel = 1;
drawEditorWaveform();
break;
case Qt::MouseButton::LeftButton:
if (m_slicerTParent->m_originalSample.frames() <= 1) { static_cast<SlicerTView*>(parent())->openFiles(); }
// update seeker middle for correct movement
m_seekerMiddle = static_cast<float>(me->x() - s_seekerHorMargin) / m_seekerWidth;
break;
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);
}
// sort slices after moving and remove draggable object
void SlicerTWaveform::mouseReleaseEvent(QMouseEvent* me)
{
std::sort(m_slicerTParent->m_slicePoints.begin(), m_slicerTParent->m_slicePoints.end());
updateClosest(me);
}
// this handles dragging and mouse cursor changes
// what is being dragged is determined in mousePressEvent
void SlicerTWaveform::mouseMoveEvent(QMouseEvent* me)
{
// if no button pressed, update closest and cursor
if (me->buttons() == Qt::MouseButton::NoButton)
{
updateClosest(me);
return;
}
float normalizedClickSeeker = static_cast<float>(me->x() - s_seekerHorMargin) / m_seekerWidth;
float normalizedClickEditor = static_cast<float>(me->x()) / m_editorWidth;
float distStart = m_seekerStart - m_seekerMiddle;
float distEnd = m_seekerEnd - m_seekerMiddle;
float startFrame = m_seekerStart;
float endFrame = m_seekerEnd;
switch (m_closestObject)
{
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:
m_seekerMiddle = normalizedClickSeeker;
if (m_seekerMiddle + distStart >= 0 && m_seekerMiddle + distEnd <= 1)
{
m_seekerStart = m_seekerMiddle + distStart;
m_seekerEnd = m_seekerMiddle + distEnd;
}
drawEditorWaveform();
break;
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_closestSlice)
= std::clamp(m_slicerTParent->m_slicePoints.at(m_closestSlice), 0.0f, 1.0f);
break;
case UIObjects::Nothing:
break;
}
// dont update closest, and update seeker waveform
drawSeeker();
drawEditor();
update();
}
void SlicerTWaveform::mouseDoubleClickEvent(QMouseEvent* me)
{
if (me->button() != Qt::MouseButton::LeftButton) { return; }
float normalizedClickEditor = static_cast<float>(me->x()) / m_editorWidth;
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);
std::sort(m_slicerTParent->m_slicePoints.begin(), m_slicerTParent->m_slicePoints.end());
}
void SlicerTWaveform::wheelEvent(QWheelEvent* we)
{
m_zoomLevel += we->angleDelta().y() / 360.0f * s_zoomSensitivity;
m_zoomLevel = std::max(0.0f, m_zoomLevel);
updateUI();
}
void SlicerTWaveform::paintEvent(QPaintEvent* pe)
{
QPainter p(this);
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

View File

@@ -0,0 +1,125 @@
/*
* SlicerTWaveform.h - declaration of class SlicerTWaveform
*
* Copyright (c) 2023 Daniel Kauss Serna <daniel.kauss.serna@gmail.com>
*
* This file is part of LMMS - https://lmms.io
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program (see COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
*/
#ifndef LMMS_GUI_SLICERT_WAVEFORM_H
#define LMMS_GUI_SLICERT_WAVEFORM_H
#include <QApplication>
#include <QElapsedTimer>
#include <QFontMetrics>
#include <QInputDialog>
#include <QMouseEvent>
#include <QPainter>
#include "Instrument.h"
#include "SampleBuffer.h"
namespace lmms {
class SlicerT;
namespace gui {
class SlicerTWaveform : public QWidget
{
Q_OBJECT
public slots:
void updateUI();
void isPlaying(float current, float start, float end);
public:
SlicerTWaveform(int totalWidth, int totalHeight, SlicerT* instrument, QWidget* parent);
// predefined sizes
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
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
{
Nothing,
SeekerStart,
SeekerEnd,
SeekerMiddle,
SlicePoint,
};
protected:
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;
void paintEvent(QPaintEvent* pe) override;
private:
int m_width;
int m_height;
int m_seekerWidth;
int m_editorHeight;
int m_editorWidth;
UIObjects m_closestObject;
int m_closestSlice = -1;
float m_seekerStart = 0;
float m_seekerEnd = 1;
float m_seekerMiddle = 0.5f;
float m_noteCurrent;
float m_noteStart;
float m_noteEnd;
float m_zoomLevel = 1.0f;
QPixmap m_sliceArrow;
QPixmap m_seeker;
QPixmap m_seekerWaveform;
QPixmap m_editorWaveform;
QPixmap m_sliceEditor;
QPixmap m_emptySampleIcon;
SlicerT* m_slicerTParent;
QElapsedTimer m_updateTimer;
void drawSeekerWaveform();
void drawSeeker();
void drawEditorWaveform();
void drawEditor();
void updateClosest(QMouseEvent* me);
void updateCursor();
};
} // namespace gui
} // namespace lmms
#endif // LMMS_GUI_SLICERT_WAVEFORM_H

BIN
plugins/SlicerT/artwork.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
plugins/SlicerT/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B