diff --git a/include/SampleBuffer.h b/include/SampleBuffer.h index 032604c8cf3..f1078f7b3bb 100644 --- a/include/SampleBuffer.h +++ b/include/SampleBuffer.h @@ -48,6 +48,85 @@ class QRect; // may need to be higher - conversely, to optimize, some may work with lower values const f_cnt_t MARGIN[] = { 64, 64, 64, 4, 4 }; + +class ControlPoint +{ +public: + ControlPoint(float x, float y) + { + this->setX(x); + this->setY(y); + } + + void setX(float x) + { + m_x = adjustPoint(x); + } + + void setY(float y) + { + m_y = adjustPoint(y); + } + + const float x() const + { + return m_x; + } + + const float y() const + { + return m_y; + } + +private: + float m_x; + float m_y; + inline float adjustPoint(float val) + { + /* + Constrain the control points to lie in the interval [0.0, 1.0]. + This ensures that the fade function has a unique value at each sample + and that the fade function can not go above 1.0 and below 0.0. + */ + return std::min(std::max(val, 0.0f), 1.0f); + } +}; + + +class CubicBezier +{ +/* +Very comprehensive overview of Bezier curves: +https://pomax.github.io/bezierinfo/ +*/ +public: + CubicBezier(float p1x, float p1y, float p2x, float p2y) + { + m_p1 = ControlPoint(p1x, p1y); + m_p2 = ControlPoint(p2x, p2y); + }; + + float evaluate(float); + ControlPoint m_p1 = ControlPoint(0.0f, 0.0f); + ControlPoint m_p2 = ControlPoint(0.0f, 0.0f); + +private: + float cubicBezier(float t, float p1, float p2); + float cubicBezierDerivative(float t, float p1, float p2); + float cubicBezierLastSegmentP1(float t, float p1, float p2); + float cubicBezierLastSegmentP2(float t, float p1, float p2); + // Solve with Newton's method: Very fast, but t might lie outside [0.0, 1.0] + float solveXforTNewton(float x, bool useCache); + // Solve with Bisection method: Slow, but t is guaranteed to lie inside [0.0, 1.0] + float solveXforTBisection(float x); + float solveXforT(float x) + { + return solveXforTBisection(x); + } + float m_tCache = -1.0f; +}; + + class LMMS_EXPORT SampleBuffer : public QObject, public sharedObject { Q_OBJECT @@ -273,6 +352,75 @@ class LMMS_EXPORT SampleBuffer : public QObject, public sharedObject } + // Fading related members + + float fadingVal(f_cnt_t pos, bool isLeft); + + void setTcoStartTime(float startTime) + { + m_TcoStartTime = startTime; + } + + void setTcoFrameLength(float len) + { + m_TcoFrameLength = len; + } + + float leftFaderPos() + { + return m_leftFader; + } + + float rightFaderPos() + { + return m_rightFader; + } + + ControlPoint getLeftP1() + { + return m_leftBezierFade.m_p1; + } + + ControlPoint getLeftP2() + { + return m_leftBezierFade.m_p2; + } + + ControlPoint getRightP1() + { + return m_rightBezierFade.m_p1; + } + + ControlPoint getRightP2() + { + return m_rightBezierFade.m_p2; + } + + void setLeftP1(float x, float y) + { + m_leftBezierFade.m_p1 = ControlPoint(x, y); + } + + void setLeftP2(float x, float y) + { + m_leftBezierFade.m_p2 = ControlPoint(x, y); + } + + void setRightP1(float x, float y) + { + m_rightBezierFade.m_p1 = ControlPoint(1.0f - x, y); + } + + void setRightP2(float x, float y) + { + m_rightBezierFade.m_p2 = ControlPoint(1.0f - x, y); + } + + void setLeftFader(float lfader); + void setRightFader(float rfader); + void checkFadingActive(); + + public slots: void setAudioFile(const QString & audioFile); void loadFromBase64(const QString & data); @@ -340,11 +488,30 @@ public slots: f_cnt_t getLoopedIndex(f_cnt_t index, f_cnt_t startf, f_cnt_t endf) const; f_cnt_t getPingPongIndex(f_cnt_t index, f_cnt_t startf, f_cnt_t endf) const; + // Fading related members + bool m_fading = false; + float m_leftFader = 0.0f; + float m_rightFader = 1.0f; + + f_cnt_t m_cachePos = -mixerSampleRate(); + float m_cacheAmp = 1.0f; + + + CubicBezier m_leftBezierFade = CubicBezier(0.5f, 0.0f, 0.5f, 1.0f); + CubicBezier m_rightBezierFade = CubicBezier(0.5f, 0.0f, 0.5f, 1.0f); + + float bezierFade(float, bool); + typedef float(SampleBuffer::*fadingFunc)(float, bool); + fadingFunc fadefuncPtr = &SampleBuffer::bezierFade; + float fadefunc(f_cnt_t); + + float m_TcoStartTime = 0.0f; + float m_TcoFrameLength = 0.0f; + signals: void sampleUpdated(); } ; - #endif diff --git a/include/SampleTrack.h b/include/SampleTrack.h index 0cdc213181a..7f10486836f 100644 --- a/include/SampleTrack.h +++ b/include/SampleTrack.h @@ -27,6 +27,7 @@ #include #include +#include #include "AudioPort.h" #include "FadeButton.h" @@ -39,6 +40,7 @@ class EffectRackView; class Knob; class SampleBuffer; +class ControlPoint; class SampleTrackWindow; class TrackLabelButton; class QLineEdit; @@ -79,6 +81,8 @@ class SampleTCO : public TrackContentObject bool isPlaying() const; void setIsPlaying(bool isPlaying); + void setStartTimeOffset(const TimePos &startTimeOffset) override; + public slots: void setSampleBuffer( SampleBuffer* sb ); void setSampleFile( const QString & _sf ); @@ -92,7 +96,12 @@ public slots: SampleBuffer* m_sampleBuffer; BoolModel m_recordModel; bool m_isPlaying; + bool m_changed = false; + TimePos m_lenOld; + TimePos m_startTimeOffsetOld; + void saveFaderSettings(QDomElement&); + void loadFaderSettings(const QDomElement&); friend class SampleTCOView; @@ -116,22 +125,55 @@ class SampleTCOView : public TrackContentObjectView public slots: void updateSample(); void reverseSample(); - - + void setSFade(); + void setCosFade(); + void setLinearFade(); + void setManualFade(); protected: void contextMenuEvent( QContextMenuEvent * _cme ) override; void mousePressEvent( QMouseEvent * _me ) override; void mouseReleaseEvent( QMouseEvent * _me ) override; + void mouseMoveEvent(QMouseEvent* e) override; void dragEnterEvent( QDragEnterEvent * _dee ) override; void dropEvent( QDropEvent * _de ) override; void mouseDoubleClickEvent( QMouseEvent * ) override; void paintEvent( QPaintEvent * ) override; + void enterEvent(QEvent *e) override; + void leaveEvent(QEvent *e) override; private: SampleTCO * m_tco; QPixmap m_paintPixmap; + + // Members related to fading + bool m_moveLeftCorner = false; + bool m_moveRightCorner = false; + bool m_leftFadeClicked = false; + bool m_rightFadeClicked = false; + bool m_leftFadeManual = false; + bool m_rightFadeManual = false; + std::unique_ptr m_leftHandle; + std::unique_ptr m_rightHandle; + std::unique_ptr m_leftP1; + std::unique_ptr m_leftP2; + std::unique_ptr m_rightP1; + std::unique_ptr m_rightP2; + bool m_moveLeftP1 = false; + bool m_moveLeftP2 = false; + bool m_moveRightP1 = false; + bool m_moveRightP2 = false; + QVector m_leftPath; + QVector m_rightPath; + bool m_inside = false; + void drawFadePoints(bool drawLeft, QVector& points); + void drawFades(); + void checkCornersClicked(const QMouseEvent *e); + void checkFadesClicked(const QMouseEvent *e); + void mouseMoveFadingPos(const QMouseEvent *e); + void mouseMoveLeftControlPoints(const QMouseEvent *e); + void mouseMoveRightControlPoints(const QMouseEvent *e); } ; diff --git a/include/TrackContentObject.h b/include/TrackContentObject.h index dddd2b75c39..c9a4e5eae55 100644 --- a/include/TrackContentObject.h +++ b/include/TrackContentObject.h @@ -133,7 +133,7 @@ class LMMS_EXPORT TrackContentObject : public Model, public JournallingObject static bool comparePosition(const TrackContentObject* a, const TrackContentObject* b); TimePos startTimeOffset() const; - void setStartTimeOffset( const TimePos &startTimeOffset ); + virtual void setStartTimeOffset(const TimePos &startTimeOffset); void updateColor(); diff --git a/include/TrackContentObjectView.h b/include/TrackContentObjectView.h index e9461c5dd50..b8970e57f90 100644 --- a/include/TrackContentObjectView.h +++ b/include/TrackContentObjectView.h @@ -156,6 +156,7 @@ public slots: } float pixelsPerBar(); + int m_textLabelHeight; DataFile createTCODataFiles(const QVector & tcos) const; diff --git a/src/core/SampleBuffer.cpp b/src/core/SampleBuffer.cpp index 370e89bc732..c8e8c30a1b6 100644 --- a/src/core/SampleBuffer.cpp +++ b/src/core/SampleBuffer.cpp @@ -852,6 +852,11 @@ bool SampleBuffer::play( { ab[i][0] *= m_amplification; ab[i][1] *= m_amplification; + if (m_fading) + { + ab[i][0] *= fadefunc(playFrame+i); + ab[i][1] *= fadefunc(playFrame+i); + } } return true; @@ -1027,9 +1032,14 @@ void SampleBuffer::visualize( for (int frame = first; frame <= last; frame += fpp) { + float fade = 1.0f; + if (m_fading) + { + fade = fadefunc(frame); + } auto x = xb + ((frame - first) * double(w) / nbFrames); // Partial Y calculation - auto py = ySpace * m_amplification; + auto py = ySpace * m_amplification * fade; l[n] = QPointF(x, (yb - (m_data[frame][0] * py))); r[n] = QPointF(x, (yb - (m_data[frame][1] * py))); ++n; @@ -1514,3 +1524,155 @@ SampleBuffer::handleState::~handleState() { src_delete(m_resamplingData); } + + + +// Fading related functions + +float SampleBuffer::fadingVal(f_cnt_t pos, bool isLeft) +{ + float relativePos = static_cast(pos) / static_cast(m_frames); + return (this->*fadefuncPtr)(relativePos, isLeft); +} + + +void SampleBuffer::setLeftFader(float lfader) +{ + m_leftFader = lfader; + checkFadingActive(); +} + + +void SampleBuffer::setRightFader(float rfader) +{ + m_rightFader = rfader; + checkFadingActive(); +} + + +void SampleBuffer::checkFadingActive() +{ + if ((m_leftFader > 0.0) || (m_rightFader < 1.0)) + { + m_fading = true; + } + else + { + m_fading = false; + } +} + + +float SampleBuffer::bezierFade(float relativePos, bool isLeft) +{ + float amp = 1.0f; + if (isLeft) + { + amp *= m_leftBezierFade.evaluate(relativePos / m_leftFader); + } + else + { + amp *= m_rightBezierFade.evaluate(1 - (relativePos - m_rightFader) / (1 - m_rightFader)); + } + return amp; +} + + +float CubicBezier::cubicBezierLastSegmentP1(float t, float p1, float p2) +{ + return ( + 2 * p1 * t * (1 - t) + + p2 * pow(t, 2) + ); +} + + +float CubicBezier::cubicBezierLastSegmentP2(float t, float p1, float p2) +{ + return ( + p1 * pow(1 - t, 2) + + 2 * p2 * t * (1 - t) + + pow(t, 2) + ); +} + + +float CubicBezier::cubicBezier(float t, float p1, float p2) +{ + return ( + cubicBezierLastSegmentP1(t, p1, p2) * (1 - t) + + cubicBezierLastSegmentP2(t, p1, p2) * t + ); +} + + +float CubicBezier::cubicBezierDerivative(float t, float p1, float p2) +{ + return ( + 3 * (3 * pow(t, 2) - 4 * t + 1) * p1 + + 3 * t * (2 - 3 * t) * p2 + + 3 * pow(t, 2) + ); +} + + +float CubicBezier::solveXforTNewton(float x, bool useCache=true) +{ + int maxIter = 100; + float tUp, f, fPrime; + float t = useCache ? m_tCache : 0.5f; + for (int i = 0; i < maxIter; ++i) + { + f = cubicBezier(t, m_p1.x(), m_p2.x()) - x; + fPrime = cubicBezierDerivative(t, m_p1.x(), m_p2.x()); + tUp = t - f / fPrime; + + if (abs(tUp - t) < 10e-3) { break; } + t = tUp; + } + if (useCache) { m_tCache = tUp; } + return tUp; +} + + +float CubicBezier::solveXforTBisection(float x) +{ + float a = 0.0f; + float b = 1.0f; + float t; + + int maxIter = 10; + for (int i = 0; i < maxIter; ++i) + { + t = (a + b) / 2; + if (cubicBezier(t, m_p1.x(), m_p2.x()) < x) { a = t; } + else { b = t; } + } + return t; +} + + +float CubicBezier::evaluate(float x) +{ + float t = solveXforT(x); + return cubicBezier(t, m_p1.y(), m_p2.y()); +} + + +float SampleBuffer::fadefunc(f_cnt_t pos) +{ + // TODO: This function should be cached in the future + float amp = 1.0f; + float relativePos = qMin((pos + m_TcoStartTime) / m_TcoFrameLength, 1.0f); + if (relativePos < m_leftFader) + { + amp *= (this->*fadefuncPtr)(relativePos, true); + } + if (relativePos > m_rightFader) + { + amp *= (this->*fadefuncPtr)(relativePos, false); + } + m_cachePos = pos; + m_cacheAmp = amp; + return m_cacheAmp; +} diff --git a/src/gui/TrackContentObjectView.cpp b/src/gui/TrackContentObjectView.cpp index 6aa78a1dd6e..b635bcbb03c 100644 --- a/src/gui/TrackContentObjectView.cpp +++ b/src/gui/TrackContentObjectView.cpp @@ -532,7 +532,8 @@ void TrackContentObjectView::paintTextLabel(QString const & text, QPainter & pai elidedPatternName = text.trimmed(); } - painter.fillRect(QRect(0, 0, width(), fontMetrics.height() + 2 * textTop), textBackgroundColor()); + m_textLabelHeight = fontMetrics.height() + 2 * textTop; + painter.fillRect(QRect(0, 0, width(), m_textLabelHeight), textBackgroundColor()); int const finalTextTop = textTop + fontMetrics.ascent(); painter.setPen(textShadowColor()); diff --git a/src/tracks/SampleTrack.cpp b/src/tracks/SampleTrack.cpp index fd48217f633..946a6ac8afb 100644 --- a/src/tracks/SampleTrack.cpp +++ b/src/tracks/SampleTrack.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include "BBTrack.h" @@ -49,6 +50,7 @@ #include "SampleRecordHandle.h" #include "Song.h" #include "SongEditor.h" +#include "stdshims.h" #include "StringPairDrag.h" #include "TabWidget.h" #include "TimeLineWidget.h" @@ -135,10 +137,29 @@ SampleTCO::~SampleTCO() } +void SampleTCO::setStartTimeOffset(const TimePos &startTimeOffset) +{ + if (startTimeOffset != m_startTimeOffsetOld) + { + m_sampleBuffer->setTcoStartTime( + static_cast(startTimeOffset) * Engine::framesPerTick() + ); + m_startTimeOffsetOld = startTimeOffset; + } + TrackContentObject::setStartTimeOffset(startTimeOffset); +} + void SampleTCO::changeLength( const TimePos & _length ) { + if (_length != m_lenOld) + { + m_sampleBuffer->setTcoFrameLength( + static_cast(_length) * Engine::framesPerTick() + ); + m_lenOld = _length; + } TrackContentObject::changeLength( qMax( static_cast( _length ), 1 ) ); } @@ -267,6 +288,22 @@ void SampleTCO::setSamplePlayLength(f_cnt_t length) } +void SampleTCO::saveFaderSettings(QDomElement& dom) +{ + dom.setAttribute("leftFaderPos", m_sampleBuffer->leftFaderPos()); + dom.setAttribute("rightFaderPos", m_sampleBuffer->rightFaderPos()); + + auto saveControlPoint = [&dom](QString s, ControlPoint p) + { + dom.setAttribute(s + "x", p.x()); + dom.setAttribute(s + "y", p.y()); + }; + + saveControlPoint("leftP1", m_sampleBuffer->getLeftP1()); + saveControlPoint("leftP2", m_sampleBuffer->getLeftP2()); + saveControlPoint("rightP1", m_sampleBuffer->getRightP1()); + saveControlPoint("rightP2", m_sampleBuffer->getRightP2()); +} void SampleTCO::saveSettings( QDomDocument & _doc, QDomElement & _this ) @@ -283,6 +320,7 @@ void SampleTCO::saveSettings( QDomDocument & _doc, QDomElement & _this ) _this.setAttribute( "muted", isMuted() ); _this.setAttribute( "src", sampleFile() ); _this.setAttribute( "off", startTimeOffset() ); + if( sampleFile() == "" ) { QString s; @@ -299,9 +337,59 @@ void SampleTCO::saveSettings( QDomDocument & _doc, QDomElement & _this ) _this.setAttribute("reversed", "true"); } // TODO: start- and end-frame + + saveFaderSettings(_this); } +void SampleTCO::loadFaderSettings(const QDomElement& dom) +{ + auto limitToZeroOne = [](float val) + { + return qBound(0.0f, val, 1.0f); + }; + + if (dom.hasAttribute("leftP1x") && dom.hasAttribute("leftP1y") + && dom.hasAttribute("leftP2x") && dom.hasAttribute("leftP2y")) + { + m_sampleBuffer->setLeftP1( + limitToZeroOne(dom.attribute("leftP1x").toFloat()), + limitToZeroOne(dom.attribute("leftP1y").toFloat()) + ); + m_sampleBuffer->setLeftP2( + limitToZeroOne(dom.attribute("leftP2x").toFloat()), + limitToZeroOne(dom.attribute("leftP2y").toFloat()) + ); + } + + if (dom.hasAttribute("rightP1x") && dom.hasAttribute("rightP1y") + && dom.hasAttribute("rightP2x") && dom.hasAttribute("rightP2y")) + { + m_sampleBuffer->setRightP1( + limitToZeroOne(1.0f-dom.attribute("rightP1x").toFloat()), + limitToZeroOne(dom.attribute("rightP1y").toFloat()) + ); + m_sampleBuffer->setRightP2( + limitToZeroOne(1.0f-dom.attribute("rightP2x").toFloat()), + limitToZeroOne(dom.attribute("rightP2y").toFloat()) + ); + } + + if (dom.hasAttribute("leftFaderPos")) + { + m_sampleBuffer->setLeftFader( + limitToZeroOne(dom.attribute("leftFaderPos").toFloat()) + ); + m_sampleBuffer->checkFadingActive(); + } + if (dom.hasAttribute("rightFaderPos")) + { + m_sampleBuffer->setRightFader( + limitToZeroOne(dom.attribute("rightFaderPos").toFloat()) + ); + m_sampleBuffer->checkFadingActive(); + } +} void SampleTCO::loadSettings( const QDomElement & _this ) @@ -334,8 +422,9 @@ void SampleTCO::loadSettings( const QDomElement & _this ) m_sampleBuffer->setReversed(true); emit wasReversed(); // tell SampleTCOView to update the view } -} + loadFaderSettings(_this); +} @@ -361,6 +450,7 @@ SampleTCOView::SampleTCOView( SampleTCO * _tco, TrackView * _tv ) : connect(m_tco, SIGNAL(wasReversed()), this, SLOT(update())); setStyle( QApplication::style() ); + setAttribute(Qt::WA_Hover, true); } void SampleTCOView::updateSample() @@ -421,7 +511,6 @@ void SampleTCOView::contextMenuEvent( QContextMenuEvent * _cme ) [this](){ contextMenuAction( Paste ); } ); contextMenu.addSeparator(); - contextMenu.addAction( embed::getIconPixmap( "muted" ), (individualTCO @@ -429,6 +518,17 @@ void SampleTCOView::contextMenuEvent( QContextMenuEvent * _cme ) : tr("Mute/unmute selection (<%1> + middle click)")).arg(UI_CTRL_KEY), [this](){ contextMenuAction( Mute ); } ); + contextMenu.addSeparator(); + if (m_leftFadeClicked || m_rightFadeClicked) + { + QMenu* fadingMenu = contextMenu.addMenu("Fading Shape"); + fadingMenu->addAction(tr("S-shaped"), this, SLOT(setSFade())); + fadingMenu->addAction(tr("Sine"), this, SLOT(setCosFade())); + fadingMenu->addAction(tr("Linear"), this, SLOT(setLinearFade())); + fadingMenu->addAction(tr("Manual Mode"), this, SLOT(setManualFade())); + contextMenu.addMenu(fadingMenu); + } + /*contextMenu.addAction( embed::getIconPixmap( "record" ), tr( "Set/clear record" ), m_tco, SLOT( toggleRecord() ) );*/ @@ -492,10 +592,81 @@ void SampleTCOView::dropEvent( QDropEvent * _de ) } +void SampleTCOView::checkCornersClicked(const QMouseEvent *e) +{ + QPointF currentPos = e->localPos(); + + if (m_leftHandle->contains(currentPos)) + { + m_moveLeftCorner = true; + } + else if (m_rightHandle->contains(currentPos)) + { + m_moveRightCorner = true; + } + else if ((m_leftP1 != nullptr) && (m_leftP1->contains(currentPos))) + { + m_moveLeftP1 = true; + } + else if ((m_leftP2 != nullptr) && (m_leftP2->contains(currentPos))) + { + m_moveLeftP2 = true; + } + else if ((m_rightP1 != nullptr) && (m_rightP1->contains(currentPos))) + { + m_moveRightP1 = true; + } + else if ((m_rightP2 != nullptr) && (m_rightP2->contains(currentPos))) + { + m_moveRightP2 = true; + } +} + + +void SampleTCOView::checkFadesClicked(const QMouseEvent *e) +{ + qreal curX = e->localPos().x(); + qreal curY = e->localPos().y(); + QPointF curPos = QPointF(curX, curY); + QPointF pointMin; + qreal qDistMin = pow(static_cast(this->width()), 4); + qreal clickTol = 10e-4; + m_leftFadeClicked = false; + m_rightFadeClicked = false; + for (auto &point: m_leftPath) + { + qreal dist = (pow(curPos.x() - point.x(), 2) + pow(curPos.y() - point.y(), 2)); + if (dist < qDistMin) + { + qDistMin = dist; + if ((qDistMin / (this->width() * this->height())) < clickTol) + { + m_leftFadeClicked = true; + break; + } + } + } + + for (auto &point: m_rightPath) + { + qreal dist = (pow(curPos.x() - point.x(), 2) + pow(curPos.y() - point.y(), 2)); + if (dist < qDistMin) + { + qDistMin = dist; + if ((qDistMin / (this->width() * this->height())) < clickTol) + { + m_rightFadeClicked = true; + break; + } + } + } +} void SampleTCOView::mousePressEvent( QMouseEvent * _me ) { + checkCornersClicked(_me); + if( _me->button() == Qt::LeftButton && _me->modifiers() & Qt::ControlModifier && _me->modifiers() & Qt::ShiftModifier ) @@ -512,6 +683,10 @@ void SampleTCOView::mousePressEvent( QMouseEvent * _me ) sTco->updateTrackTcos(); } } + if (_me->button() == Qt::RightButton) + { + checkFadesClicked(_me); + } TrackContentObjectView::mousePressEvent( _me ); } } @@ -529,6 +704,22 @@ void SampleTCOView::mouseReleaseEvent(QMouseEvent *_me) sTco->playbackPositionChanged(); } } + if (m_moveLeftCorner || m_moveRightCorner) + { + m_moveLeftCorner = false; + m_moveRightCorner = false; + } + if (m_moveLeftP1 || m_moveLeftP2) + { + m_moveLeftP1 = false; + m_moveLeftP2 = false; + } + if (m_moveRightP1 || m_moveRightP2) + { + m_moveRightP1 = false; + m_moveRightP2 = false; + } + TrackContentObjectView::mouseReleaseEvent( _me ); } @@ -553,6 +744,262 @@ void SampleTCOView::mouseDoubleClickEvent( QMouseEvent * ) } +void SampleTCOView::drawFadePoints(bool drawLeft, QVector& pointsLine) +{ + float widgetWidth = static_cast(this->width()); + float widgetHeight = static_cast(this->height()); + float faderPos = 0.0; + if (drawLeft) + { + faderPos = m_tco->m_sampleBuffer->leftFaderPos() * widgetWidth; + } + else + { + faderPos = (1.0 - m_tco->m_sampleBuffer->rightFaderPos()) * widgetWidth; + } + + f_cnt_t samples = m_tco->m_sampleBuffer->frames(); + + QVector points(1000); + for (int i = 0; i < points.size(); ++i) + { + float xpos = ( + (points.size() - 1 - i) + / static_cast(points.size() - 1) + * (faderPos / widgetWidth) + ); + if (!drawLeft) + { + xpos = xpos - (faderPos / widgetWidth) + 1.0f; + } + float ypos = m_tco->m_sampleBuffer->fadingVal( + static_cast(roundf(xpos * samples)), + drawLeft + ); + + points[i] = QPointF( + xpos * (widgetWidth - 2 * TCO_BORDER_WIDTH) + TCO_BORDER_WIDTH, + (1.0f - ypos) * (widgetHeight - m_textLabelHeight - TCO_BORDER_WIDTH) + m_textLabelHeight + ); + } + if (drawLeft) + { + points[0].setY(m_textLabelHeight); + } + else + { + points[points.size() - 1].setY(m_textLabelHeight); + } + + QPointF endPoint(TCO_BORDER_WIDTH, m_textLabelHeight); + if (!drawLeft) + { + endPoint = QPointF(widgetWidth - TCO_BORDER_WIDTH, m_textLabelHeight); + } + points.push_back(endPoint); + + QPainterPath shadedPath; + QPainterPath solidPath; + shadedPath.moveTo(points.last()); + solidPath.moveTo(points[1]); + pointsLine.clear(); + for (int i = 0; i < points.size(); ++i) + { + if ((i > 0) && (i < (points.size() - 3))) + { + solidPath.lineTo(points[i + 1]); + pointsLine.push_back(points[i + 1]); + } + shadedPath.lineTo(points[i]); + } + QPainter pShader(this); + pShader.setPen(QColor(0, 0, 0, 0)); + pShader.setBrush(QColor(0, 0, 0, 100)); + pShader.setRenderHint(QPainter::Antialiasing, false); + pShader.drawPath(shadedPath); + QPainter pLine(this); + pLine.setRenderHint(QPainter::Antialiasing, true); + pLine.setPen(QColor(0, 0, 0, 255)); + pLine.drawPath(solidPath); +} + + +void SampleTCOView::drawFades() +{ + drawFadePoints(true, m_leftPath); // Draw left points + drawFadePoints(false, m_rightPath); // Draw right points + + if (m_inside) + { + float widgetWidth = static_cast(this->width()); + float widgetHeight = static_cast(this->height()); + + float leftFaderAbsolutePos = m_tco->m_sampleBuffer->leftFaderPos() * widgetWidth; + float rightFaderAbsolutePos = m_tco->m_sampleBuffer->rightFaderPos() * widgetWidth; + float squareSideLen = qMin(widgetHeight / 6.0f, 7.0f); + + QPainter p(this); + QPen pen = QPen(Qt::black); + pen.setWidth(1.75); + p.setPen(pen); + if (leftFaderAbsolutePos + squareSideLen > widgetWidth) + { + m_leftHandle = make_unique(QRectF( + leftFaderAbsolutePos - squareSideLen - 1, + m_textLabelHeight, + squareSideLen, + squareSideLen + )); + } + else + { + m_leftHandle = make_unique(QRectF( + leftFaderAbsolutePos, + m_textLabelHeight, + squareSideLen, + squareSideLen + )); + } + if ((rightFaderAbsolutePos-squareSideLen-1) < 0) + { + m_rightHandle = make_unique(QRectF( + rightFaderAbsolutePos, + m_textLabelHeight, + squareSideLen, + squareSideLen + )); + } + else + { + m_rightHandle = make_unique(QRectF( + rightFaderAbsolutePos - squareSideLen - 1, + m_textLabelHeight, + squareSideLen, + squareSideLen + )); + } + + p.drawRect(*m_leftHandle); + p.drawRect(*m_rightHandle); + + auto xPosWidgetLeft = [&](float x) + { + return x * leftFaderAbsolutePos; + }; + + auto yPosWidget = [&](float y) + { + float val = ( + (1 - y) * (this->height() - m_textLabelHeight - TCO_BORDER_WIDTH) + + m_textLabelHeight + ); + return qMin(val, this->height() - squareSideLen); + }; + + if (m_leftFadeManual) + { + const auto p1L = m_tco->m_sampleBuffer->getLeftP1(); + const auto p2L = m_tco->m_sampleBuffer->getLeftP2(); + m_leftP1 = make_unique(QRectF( + xPosWidgetLeft(p1L.x()), + yPosWidget(p1L.y()), + squareSideLen, + squareSideLen + )); + m_leftP2 = make_unique(QRectF( + xPosWidgetLeft(p2L.x()), + yPosWidget(p2L.y()), + squareSideLen, + squareSideLen + )); + p.setRenderHint(QPainter::Antialiasing, true); + p.setOpacity(0.35); + p.drawLine( + QPointF( + m_leftP1->x() + m_leftP1->width() / 2, + m_leftP1->y() + m_leftP1->height() / 2 + ), + QPointF(TCO_BORDER_WIDTH, this->height()) + ); + p.drawLine( + QPointF( + m_leftP2->x() + m_leftP2->width() / 2, + m_leftP2->y() + m_leftP2->height() / 2 + ), + QPointF(m_leftHandle->x(), m_leftHandle->y()) + ); + p.setOpacity(1.0); + p.setRenderHint(QPainter::Antialiasing, false); + } + else + { + m_leftP1 = nullptr; + m_leftP2 = nullptr; + } + + auto xPosWidgetRight = [&](float x) + { + return ( + (1 - x) * (this->width() - rightFaderAbsolutePos) + - squareSideLen + rightFaderAbsolutePos + ); + }; + + if (m_rightFadeManual) + { + const auto p1R = m_tco->m_sampleBuffer->getRightP1(); + const auto p2R = m_tco->m_sampleBuffer->getRightP2(); + m_rightP1 = make_unique(QRectF( + xPosWidgetRight(p1R.x()), + yPosWidget(p1R.y()), + squareSideLen, + squareSideLen + )); + m_rightP2 = make_unique(QRectF( + xPosWidgetRight(p2R.x()), + yPosWidget(p2R.y()), + squareSideLen, + squareSideLen + )); + p.setOpacity(0.35); + p.setRenderHint(QPainter::Antialiasing, true); + p.drawLine( + QPointF( + m_rightP1->x() + m_rightP1->width() / 2, + m_rightP1->y() + m_rightP1->height() / 2 + ), + QPointF(this->width(), this->height()) + ); + p.drawLine( + QPointF( + m_rightP2->x() + m_rightP2->width() / 2, + m_rightP2->y() + m_rightP2->height() / 2 + ), + QPointF(m_rightHandle->x(), m_rightHandle->y()) + ); + } + else + { + m_rightP1 = nullptr; + m_rightP2 = nullptr; + } + + p.setRenderHint(QPainter::Antialiasing, false); + QBrush b(Qt::white); + p.setBrush(b); + p.setOpacity(1.0); + if ((m_leftP1 != nullptr) && (m_leftP2 != nullptr)) + { + p.drawEllipse(*m_leftP1); + p.drawEllipse(*m_leftP2); + } + if ((m_rightP1 != nullptr) && (m_rightP2 != nullptr)) + { + p.drawEllipse(*m_rightP1); + p.drawEllipse(*m_rightP2); + } + } +} void SampleTCOView::paintEvent( QPaintEvent * pe ) @@ -562,6 +1009,7 @@ void SampleTCOView::paintEvent( QPaintEvent * pe ) if( !needsUpdate() ) { painter.drawPixmap( 0, 0, m_paintPixmap ); + drawFades(); return; } @@ -605,7 +1053,7 @@ void SampleTCOView::paintEvent( QPaintEvent * pe ) float den = Engine::getSong()->getTimeSigModel().getDenominator(); float ticksPerBar = DefaultTicksPerBar * nom / den; - float offset = m_tco->startTimeOffset() / ticksPerBar * pixelsPerBar(); + float offset = m_tco->startTimeOffset() / ticksPerBar * pixelsPerBar(); QRect r = QRect( TCO_BORDER_WIDTH + offset, spacing, qMax( static_cast( m_tco->sampleLength() * ppb / ticksPerBar ), 1 ), rect().bottom() - 2 * spacing ); m_tco->m_sampleBuffer->visualize( p, r, pe->rect() ); @@ -652,9 +1100,21 @@ void SampleTCOView::paintEvent( QPaintEvent * pe ) p.end(); painter.drawPixmap( 0, 0, m_paintPixmap ); + drawFades(); } +void SampleTCOView::enterEvent(QEvent *e) +{ + if (e != nullptr) { m_inside = true; } +} + + +void SampleTCOView::leaveEvent(QEvent *e) +{ + if (e != nullptr) { m_inside = false; } + TrackContentObjectView::leaveEvent(e); +} void SampleTCOView::reverseSample() @@ -665,8 +1125,168 @@ void SampleTCOView::reverseSample() } +void SampleTCOView::mouseMoveFadingPos(const QMouseEvent* e) +{ + float xpos = static_cast(e->localPos().x()); + float width = static_cast(this->width()); + float faderPos = qMin(qMax(0.0f, xpos / width), 1.0f); + if (m_moveLeftCorner) + { + m_tco->m_sampleBuffer->setLeftFader(faderPos); + } + else if (m_moveRightCorner) + { + m_tco->m_sampleBuffer->setRightFader(faderPos); + } + updateSample(); +} + + +void SampleTCOView::mouseMoveLeftControlPoints(const QMouseEvent* e) +{ + float xpos = e->localPos().x(); + float ypos = qMin( + qMax(e->localPos().y(), static_cast(m_textLabelHeight)), + static_cast(this->height()) + ); + float leftFaderAbsolutePos = m_tco->m_sampleBuffer->leftFaderPos() * this->width(); + float Px = xpos / leftFaderAbsolutePos; + float Py = 1 - (ypos - m_textLabelHeight) / (this->height() - m_textLabelHeight); + if (m_moveLeftP1) + { + m_tco->m_sampleBuffer->setLeftP1(Px, Py); + } + else if (m_moveLeftP2) + { + m_tco->m_sampleBuffer->setLeftP2(Px, Py); + } + updateSample(); +} + + +void SampleTCOView::mouseMoveRightControlPoints(const QMouseEvent* e) +{ + float xpos = e->localPos().x(); + float ypos = e->localPos().y(); + float rightFaderAbsolutePos = m_tco->m_sampleBuffer->rightFaderPos() * this->width(); + float Px = (xpos - rightFaderAbsolutePos) / (this->width() - rightFaderAbsolutePos); + float Py = 1 - (ypos - m_textLabelHeight) / (this->height() - m_textLabelHeight); + if (m_moveRightP1) + { + m_tco->m_sampleBuffer->setRightP1(Px, Py); + } + else if (m_moveRightP2) + { + m_tco->m_sampleBuffer->setRightP2(Px, Py); + } + updateSample(); +} + + +void SampleTCOView::mouseMoveEvent(QMouseEvent* e) +{ + auto leftButtonDown = e->buttons() & Qt::LeftButton; + if (leftButtonDown && (m_moveLeftCorner || m_moveRightCorner)) + { + mouseMoveFadingPos(e); + } + else if (leftButtonDown && (m_moveLeftP1 || m_moveLeftP2)) + { + mouseMoveLeftControlPoints(e); + } + else if (leftButtonDown && (m_moveRightP1 || m_moveRightP2)) + { + mouseMoveRightControlPoints(e); + } + else + { + // This event is only forwarded, when the fades are not currently adjusted, in order + // to prevent the SampleTCO from beeing moved around in the timeline when adjusting + // the fades. + TrackContentObjectView::mouseMoveEvent(e); + } +} + + +/** + * For getting an S-shaped curve the control points are set to + * lie in the middle of the x-interval, i.e., at 0.5. + * By changing this to e.g. [0.2, 0.8] the steepness + * at the inflection point can be controlled. + * By setting the y coordinates to [0, 1] it is ensured + * that the derivative at the start and end points is zero. + */ +void SampleTCOView::setSFade() +{ + if (m_leftFadeClicked) + { + m_leftFadeManual = false; + m_tco->m_sampleBuffer->setLeftP1(0.5f, 0.0f); + m_tco->m_sampleBuffer->setLeftP2(0.5f, 1.0f); + } + else if (m_rightFadeClicked) + { + m_rightFadeManual = false; + m_tco->m_sampleBuffer->setRightP1(0.5f, 0.0f); + m_tco->m_sampleBuffer->setRightP2(0.5f, 1.0f); + } + updateSample(); +} +/** + * The control points for the cosine like fade are set + * by manual tuning to resemble a cosine function. + * It might be somehow possible to derive the control + * points for the cosine function. + */ +void SampleTCOView::setCosFade() +{ + if (m_leftFadeClicked) + { + m_leftFadeManual = false; + m_tco->m_sampleBuffer->setLeftP1(0.2f, 0.8f); + m_tco->m_sampleBuffer->setLeftP2(0.5f, 1.0f); + } + else if (m_rightFadeClicked) + { + m_rightFadeManual = false; + m_tco->m_sampleBuffer->setRightP1(0.8f, 0.8f); + m_tco->m_sampleBuffer->setRightP2(0.5f, 1.0f); + } + updateSample(); +} + + +/** + * For getting a linear fading shape the control points + * have to lie on the diagonal of the rectangle defined + * by the start and end point of the Bezier curve. + */ +void SampleTCOView::setLinearFade() +{ + if (m_leftFadeClicked) + { + m_leftFadeManual = false; + m_tco->m_sampleBuffer->setLeftP1(0.25f, 0.25f); + m_tco->m_sampleBuffer->setLeftP2(0.75f, 0.75f); + } + else if (m_rightFadeClicked) + { + m_rightFadeManual = false; + m_tco->m_sampleBuffer->setRightP1(0.25f, 0.75f); + m_tco->m_sampleBuffer->setRightP2(0.75f, 0.25f); + } + updateSample(); +} + + +void SampleTCOView::setManualFade() +{ + if (m_leftFadeClicked) { m_leftFadeManual = true; } + else if (m_rightFadeClicked) { m_rightFadeManual = true; } + updateSample(); +} SampleTrack::SampleTrack(TrackContainer* tc) : @@ -1046,7 +1666,6 @@ void SampleTrackView::dropEvent(QDropEvent *de) SampleTCO * sTco = static_cast(getTrack()->createTCO(tcoPos)); if (sTco) { sTco->setSampleFile(value); } } - }