mirror of
https://github.com/LMMS/lmms.git
synced 2026-03-12 02:57:31 -04:00
* 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>
419 lines
14 KiB
C++
419 lines
14 KiB
C++
/*
|
|
* 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
|