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>
@@ -60,6 +60,7 @@ SET(LMMS_PLUGIN_LIST
|
||||
Sf2Player
|
||||
Sfxr
|
||||
Sid
|
||||
SlicerT
|
||||
SpectrumAnalyzer
|
||||
StereoEnhancer
|
||||
StereoMatrix
|
||||
|
||||
@@ -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;
|
||||
|
||||
BIN
data/themes/default/lcd_19purple.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
data/themes/default/lcd_19purple_dot.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
10
plugins/SlicerT/CMakeLists.txt
Normal 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
@@ -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
@@ -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
|
||||
193
plugins/SlicerT/SlicerTView.cpp
Normal 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
|
||||
85
plugins/SlicerT/SlicerTView.h
Normal 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
|
||||
418
plugins/SlicerT/SlicerTWaveform.cpp
Normal 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
|
||||
125
plugins/SlicerT/SlicerTWaveform.h
Normal 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
|
After Width: | Height: | Size: 14 KiB |
BIN
plugins/SlicerT/copy_midi.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
plugins/SlicerT/logo.png
Normal file
|
After Width: | Height: | Size: 759 B |
BIN
plugins/SlicerT/reset_slices.png
Normal file
|
After Width: | Height: | Size: 493 B |
BIN
plugins/SlicerT/slice_indicator_arrow.png
Normal file
|
After Width: | Height: | Size: 234 B |