From c7795217304e2a9b1950d8c050bcda7f62e491e6 Mon Sep 17 00:00:00 2001 From: DanielKauss Date: Sun, 12 Nov 2023 00:09:38 +0100 Subject: [PATCH] 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 * 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 Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> Co-authored-by: saker --- cmake/modules/PluginList.cmake | 1 + data/themes/classic/style.css | 8 + data/themes/default/lcd_19purple.png | Bin 0 -> 9764 bytes data/themes/default/lcd_19purple_dot.png | Bin 0 -> 5403 bytes data/themes/default/style.css | 8 + include/Clipboard.h | 6 +- plugins/SlicerT/CMakeLists.txt | 10 + plugins/SlicerT/SlicerT.cpp | 410 +++++++++++++++++++++ plugins/SlicerT/SlicerT.h | 108 ++++++ plugins/SlicerT/SlicerTView.cpp | 193 ++++++++++ plugins/SlicerT/SlicerTView.h | 85 +++++ plugins/SlicerT/SlicerTWaveform.cpp | 418 ++++++++++++++++++++++ plugins/SlicerT/SlicerTWaveform.h | 125 +++++++ plugins/SlicerT/artwork.png | Bin 0 -> 13791 bytes plugins/SlicerT/copy_midi.png | Bin 0 -> 4249 bytes plugins/SlicerT/logo.png | Bin 0 -> 759 bytes plugins/SlicerT/reset_slices.png | Bin 0 -> 493 bytes plugins/SlicerT/slice_indicator_arrow.png | Bin 0 -> 234 bytes 18 files changed, 1370 insertions(+), 2 deletions(-) create mode 100644 data/themes/default/lcd_19purple.png create mode 100644 data/themes/default/lcd_19purple_dot.png create mode 100644 plugins/SlicerT/CMakeLists.txt create mode 100644 plugins/SlicerT/SlicerT.cpp create mode 100644 plugins/SlicerT/SlicerT.h create mode 100644 plugins/SlicerT/SlicerTView.cpp create mode 100644 plugins/SlicerT/SlicerTView.h create mode 100644 plugins/SlicerT/SlicerTWaveform.cpp create mode 100644 plugins/SlicerT/SlicerTWaveform.h create mode 100644 plugins/SlicerT/artwork.png create mode 100644 plugins/SlicerT/copy_midi.png create mode 100644 plugins/SlicerT/logo.png create mode 100644 plugins/SlicerT/reset_slices.png create mode 100644 plugins/SlicerT/slice_indicator_arrow.png diff --git a/cmake/modules/PluginList.cmake b/cmake/modules/PluginList.cmake index afeca3548..8c444aca2 100644 --- a/cmake/modules/PluginList.cmake +++ b/cmake/modules/PluginList.cmake @@ -60,6 +60,7 @@ SET(LMMS_PLUGIN_LIST Sf2Player Sfxr Sid + SlicerT SpectrumAnalyzer StereoEnhancer StereoMatrix diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index c4fe0e153..b378c4b8e 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -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; diff --git a/data/themes/default/lcd_19purple.png b/data/themes/default/lcd_19purple.png new file mode 100644 index 0000000000000000000000000000000000000000..35ecc4ace0e094e7702bb744acc17df00cf726e5 GIT binary patch literal 9764 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 literal 0 HcmV?d00001 diff --git a/data/themes/default/style.css b/data/themes/default/style.css index 80d56a4bb..323f6d03d 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -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; diff --git a/include/Clipboard.h b/include/Clipboard.h index c6ae66ee8..cee40b33a 100644 --- a/include/Clipboard.h +++ b/include/Clipboard.h @@ -25,8 +25,10 @@ #ifndef LMMS_CLIPBOARD_H #define LMMS_CLIPBOARD_H -#include #include +#include + +#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 diff --git a/plugins/SlicerT/CMakeLists.txt b/plugins/SlicerT/CMakeLists.txt new file mode 100644 index 000000000..49a80ca03 --- /dev/null +++ b/plugins/SlicerT/CMakeLists.txt @@ -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") \ No newline at end of file diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp new file mode 100644 index 000000000..2918265ce --- /dev/null +++ b/plugins/SlicerT/SlicerT.cpp @@ -0,0 +1,410 @@ +/* + * SlicerT.cpp - simple slicer plugin + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "SlicerT.h" + +#include +#include +#include + +#include "Engine.h" +#include "InstrumentTrack.h" +#include "PathUtil.h" +#include "Song.h" +#include "embed.h" +#include "lmms_constants.h" +#include "plugin_export.h" + +namespace lmms { + +extern "C" { +Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = { + LMMS_STRINGIFY(PLUGIN_NAME), + "SlicerT", + QT_TRANSLATE_NOOP("PluginBrowser", "Basic Slicer"), + "Daniel Kauss Serna ", + 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(m_originalBPM.value()) / bpm; + if (!m_enableSync.value()) { speedRatio = 1; } + speedRatio *= pitchRatio; + speedRatio *= Engine::audioEngine()->processingSampleRate() / static_cast(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(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(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(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 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 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 prevMags(windowSize / 2, 0); + std::vector fftIn(windowSize, 0); + std::array 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(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 SlicerT::getMidi() +{ + std::vector outputNotes; + + float speedRatio = static_cast(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(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(m)); +} +} // extern +} // namespace lmms diff --git a/plugins/SlicerT/SlicerT.h b/plugins/SlicerT/SlicerT.h new file mode 100644 index 000000000..8671eecd1 --- /dev/null +++ b/plugins/SlicerT/SlicerT.h @@ -0,0 +1,108 @@ +/* + * SlicerT.h - declaration of class SlicerT + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_SLICERT_H +#define LMMS_SLICERT_H + +#include +#include +#include + +#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 getMidi(); + +private: + FloatModel m_noteThreshold; + FloatModel m_fadeOutFrames; + IntModel m_originalBPM; + ComboBoxModel m_sliceSnap; + BoolModel m_enableSync; + + SampleBuffer m_originalSample; + + std::vector m_slicePoints; + + InstrumentTrack* m_parentTrack; + + friend class gui::SlicerTView; + friend class gui::SlicerTWaveform; +}; +} // namespace lmms +#endif // LMMS_SLICERT_H diff --git a/plugins/SlicerT/SlicerTView.cpp b/plugins/SlicerT/SlicerTView.cpp new file mode 100644 index 000000000..833d4b434 --- /dev/null +++ b/plugins/SlicerT/SlicerTView.cpp @@ -0,0 +1,193 @@ +/* + * SlicerTView.cpp - controls the UI for slicerT + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "SlicerTView.h" + +#include +#include + +#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(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(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 diff --git a/plugins/SlicerT/SlicerTView.h b/plugins/SlicerT/SlicerTView.h new file mode 100644 index 000000000..ea2b979fc --- /dev/null +++ b/plugins/SlicerT/SlicerTView.h @@ -0,0 +1,85 @@ +/* + * SlicerTView.h - declaration of class SlicerTView + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_GUI_SLICERT_VIEW_H +#define LMMS_GUI_SLICERT_VIEW_H + +#include + +#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 diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp new file mode 100644 index 000000000..6685f4f8c --- /dev/null +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -0,0 +1,418 @@ +/* + * SlicerTWaveform.cpp - slice editor for SlicerT + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "SlicerTWaveform.h" + +#include + +#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(me->x() - s_seekerHorMargin) / m_seekerWidth; + float normalizedClickEditor = static_cast(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(parent())->openFiles(); } + // update seeker middle for correct movement + m_seekerMiddle = static_cast(me->x() - s_seekerHorMargin) / m_seekerWidth; + break; + case Qt::MouseButton::RightButton: + if (m_slicerTParent->m_slicePoints.size() > 2 && m_closestObject == UIObjects::SlicePoint) + { + 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(me->x() - s_seekerHorMargin) / m_seekerWidth; + float normalizedClickEditor = static_cast(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(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 diff --git a/plugins/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h new file mode 100644 index 000000000..6478e7f86 --- /dev/null +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -0,0 +1,125 @@ +/* + * SlicerTWaveform.h - declaration of class SlicerTWaveform + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_GUI_SLICERT_WAVEFORM_H +#define LMMS_GUI_SLICERT_WAVEFORM_H + +#include +#include +#include +#include +#include +#include + +#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 diff --git a/plugins/SlicerT/artwork.png b/plugins/SlicerT/artwork.png new file mode 100644 index 0000000000000000000000000000000000000000..e166273c705362e4e07b907a853492b138c566d8 GIT binary patch literal 13791 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 literal 0 HcmV?d00001 diff --git a/plugins/SlicerT/logo.png b/plugins/SlicerT/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f2c4fabf14facdba53038995a70700bd9a14ad06 GIT binary patch literal 759 zcmVe zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00Li0L_t(&-tC%AXcJKsM$g1( zO9)OEDF{*!@h3D0y3#C^P!MVnbfa{kM07J11ee)}h%SO|G*S&!Qlp?kMT=Teiedz@ zRTo975~bCGDYOPFQwfc2(sA<|CJka2NjtC1x4iS1`3`sPeRqbZghMe_f}8-M@bQwM zs;awyAP@k&7V(+_l0aP7^#SvRa43dj;c8V?uLJ#nW+4cK7Y*na9!um{0Fe-jtu9S? zEe8>l%@UPGvpF*kQ%z_R*0?6T7c&x+xM2m&AOVR%0%j1e#30_Xt|hZ<5KE3I%eoo( zPU7Es00pHaUYN^suDhkOAgwJLr-ElH3h?dw7uv72vvc$QG6-p2t)IVs5b3x`lkW(t z98NiFWIC7SO6LW3Z#+uPYL}eFUMS@0?253(wHMXpk+;|%Kf6!giv;ePH~(F07;}{e z>3j8v(Xru0-LP@f4o)0CT@uUvnPzzO6CdBE$z(^!WJi{#{Z39zFz|k2Q8!%YSzi|W z{B?-Cce>cqc!;13D;vZHu|aGQ8^i{&L2M8k#G0zwf33lE4}-vmXaVUnmzyy0|Ltik~Q2Lw1U7OI!%|$-36MPWU}Jl{r2D8nGNcVCS^vY zR2opt6B1`vfjwYd6X02M5Ju+Jn;VCbi2bm0Rr3rBXxy3?Eqci3OE8x(UE%!`vCUC$ULOu@9aXIWx@Pt z11njA|H`{S4d@ra5@%!JEKm23+|qGW;0%4uI8XN-`lYdSlwR0A&@YWYMdIvc;;baj zK4t-*bnN`Y(1DJ_EZ`k51-gl|?LWBN0Cs@yOrZAW2@n9cMbO0A7I4q3N>0Pbyv?=1 z|GqL>ug{|OzJN7#b*N^BVPv-b&4ToQsL&p>uBqjioC4>-Wf+;yLZAmue_s{gMe{n0 j%zM_lT;Q@%dSU+nYpk5`^k80=00000NkvXXu0mjf#CFZh literal 0 HcmV?d00001 diff --git a/plugins/SlicerT/slice_indicator_arrow.png b/plugins/SlicerT/slice_indicator_arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..1f60fc3bfcc85a43cc42cb0137d9f5da04b3e999 GIT binary patch literal 234 zcmeAS@N?(olHy`uVBq!ia0vp^96-#CCT&G`<3$!H1Ou^vFmQ^Y-C8V`j#&Dcq033 zLlwSz_YV|2yD#LX#>U3hHaC8^bP0l+XkKOgTya literal 0 HcmV?d00001