summaryrefslogtreecommitdiffstats
path: root/src/widget/wspinny.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/widget/wspinny.cpp')
-rw-r--r--src/widget/wspinny.cpp516
1 files changed, 516 insertions, 0 deletions
diff --git a/src/widget/wspinny.cpp b/src/widget/wspinny.cpp
new file mode 100644
index 0000000000..a386edf2aa
--- /dev/null
+++ b/src/widget/wspinny.cpp
@@ -0,0 +1,516 @@
+#include <math.h>
+
+#include <QtDebug>
+
+#include "mathstuff.h"
+#include "wimagestore.h"
+#include "controlobject.h"
+#include "controlobjectthreadmain.h"
+#include "sharedglcontext.h"
+#include "widget/wspinny.h"
+#include "vinylcontrol/vinylcontrolmanager.h"
+#include "vinylcontrol/vinylcontrol.h"
+
+WSpinny::WSpinny(QWidget* parent, VinylControlManager* pVCMan)
+ : QGLWidget(parent, SharedGLContext::getWidget()),
+ m_pBgImage(NULL),
+ m_pFgImage(NULL),
+ m_pGhostImage(NULL),
+ m_pPlay(NULL),
+ m_pPlayPos(NULL),
+ m_pVisualPlayPos(NULL),
+ m_pDuration(NULL),
+ m_pTrackSamples(NULL),
+ m_pScratch(NULL),
+ m_pScratchToggle(NULL),
+ m_pScratchPos(NULL),
+ m_pVinylControlSpeedType(NULL),
+ m_pVinylControlEnabled(NULL),
+ m_iVinylInput(-1),
+ m_bVinylActive(false),
+ m_bSignalActive(true),
+ m_iVinylScopeSize(0),
+ m_fAngle(0.0f),
+ m_dAngleLastPlaypos(-1),
+ m_fGhostAngle(0.0f),
+ m_dGhostAngleLastPlaypos(-1),
+ m_iStartMouseX(-1),
+ m_iStartMouseY(-1),
+ m_iFullRotations(0),
+ m_dPrevTheta(0.),
+ m_bClampFailedWarning(false) {
+#ifdef __VINYLCONTROL__
+ m_pVCManager = pVCMan;
+#endif
+ //Drag and drop
+ setAcceptDrops(true);
+ qDebug() << "WSpinny(): Created QGLWidget, Context"
+ << "Valid:" << context()->isValid()
+ << "Sharing:" << context()->isSharing();
+}
+
+WSpinny::~WSpinny() {
+ // No need to delete anything if m_group is empty because setup() was not called.
+ if (!m_group.isEmpty()) {
+ WImageStore::deleteImage(m_pBgImage);
+ WImageStore::deleteImage(m_pFgImage);
+ WImageStore::deleteImage(m_pGhostImage);
+ delete m_pPlay;
+ delete m_pPlayPos;
+ delete m_pVisualPlayPos;
+ delete m_pDuration;
+ delete m_pTrackSamples;
+ delete m_pTrackSampleRate;
+ delete m_pScratch;
+ delete m_pScratchToggle;
+ delete m_pScratchPos;
+ #ifdef __VINYLCONTROL__
+ delete m_pVinylControlSpeedType;
+ delete m_pVinylControlEnabled;
+ delete m_pSignalEnabled;
+ #endif
+ }
+}
+
+void WSpinny::onVinylSignalQualityUpdate(const VinylSignalQualityReport& report) {
+#ifdef __VINYLCONTROL__
+ // Skip reports for vinyl inputs we don't care about.
+ if (report.processor != m_iVinylInput) {
+ return;
+ }
+ int r,g,b;
+ QColor qual_color = QColor();
+ float signalQuality = report.timecode_quality;
+
+ // color is related to signal quality
+ // hsv: s=1, v=1
+ // h is the only variable.
+ // h=0 is red, h=120 is green
+ qual_color.setHsv((int)(120.0 * signalQuality), 255, 255);
+ qual_color.getRgb(&r, &g, &b);
+
+ for (int y = 0; y < m_iVinylScopeSize; ++y) {
+ QRgb *line = (QRgb *)m_qImage.scanLine(y);
+ for (int x = 0; x < m_iVinylScopeSize; ++x) {
+ // use xwax's bitmap to set alpha data only
+ // adjust alpha by 3/4 so it's not quite so distracting
+ // setpixel is slow, use scanlines instead
+ //m_qImage.setPixel(x, y, qRgba(r,g,b,(int)buf[x+m_iVinylScopeSize*y] * .75));
+ *line = qRgba(r,g,b,(int)(report.scope[x+m_iVinylScopeSize*y] * .75));
+ line++;
+ }
+ }
+#endif
+}
+
+void WSpinny::setup(QDomNode node, QString group) {
+ m_group = group;
+
+ // Set images
+ m_pBgImage = WImageStore::getImage(WWidget::getPath(WWidget::selectNodeQString(node,
+ "PathBackground")));
+ m_pFgImage = WImageStore::getImage(WWidget::getPath(WWidget::selectNodeQString(node,
+ "PathForeground")));
+ m_pGhostImage = WImageStore::getImage(WWidget::getPath(WWidget::selectNodeQString(node,
+ "PathGhost")));
+ if (m_pBgImage && !m_pBgImage->isNull()) {
+ setFixedSize(m_pBgImage->size());
+ }
+
+#ifdef __VINYLCONTROL__
+ // Find the vinyl input we should listen to reports about.
+ if (m_pVCManager) {
+ m_iVinylInput = m_pVCManager->vinylInputFromGroup(m_group);
+ }
+ m_iVinylScopeSize = MIXXX_VINYL_SCOPE_SIZE;
+ m_qImage = QImage(m_iVinylScopeSize, m_iVinylScopeSize, QImage::Format_ARGB32);
+ // fill with transparent black
+ m_qImage.fill(qRgba(0,0,0,0));
+#endif
+
+ m_pPlay = new ControlObjectThreadMain(ControlObject::getControl(
+ ConfigKey(group, "play")));
+ m_pPlayPos = new ControlObjectThreadMain(ControlObject::getControl(
+ ConfigKey(group, "playposition")));
+ m_pVisualPlayPos = new ControlObjectThreadMain(ControlObject::getControl(
+ ConfigKey(group, "visual_playposition")));
+ m_pDuration = new ControlObjectThreadMain(ControlObject::getControl(
+ ConfigKey(group, "duration")));
+ m_pTrackSamples = new ControlObjectThreadMain(ControlObject::getControl(
+ ConfigKey(group, "track_samples")));
+ m_pTrackSampleRate = new ControlObjectThreadMain(
+ ControlObject::getControl(
+ ConfigKey(group, "track_samplerate")));
+
+ m_pScratch = new ControlObjectThreadMain(ControlObject::getControl(
+ ConfigKey(group, "scratch2")));
+ m_pScratchToggle = new ControlObjectThreadMain(ControlObject::getControl(
+ ConfigKey(group, "scratch_position_enable")));
+ m_pScratchPos = new ControlObjectThreadMain(ControlObject::getControl(
+ ConfigKey(group, "scratch_position")));
+
+ m_pSlipEnabled = new ControlObjectThreadMain(ControlObject::getControl(
+ ConfigKey(group, "slip_enabled")));
+ m_pSlipPosition = new ControlObjectThreadMain(ControlObject::getControl(
+ ConfigKey(group, "slip_playposition")));
+
+#ifdef __VINYLCONTROL__
+ m_pVinylControlSpeedType = new ControlObjectThreadMain(ControlObject::getControl(
+ ConfigKey(group, "vinylcontrol_speed_type")));
+ if (m_pVinylControlSpeedType)
+ {
+ //Initialize the rotational speed.
+ this->updateVinylControlSpeed(m_pVinylControlSpeedType->get());
+ }
+
+ m_pVinylControlEnabled = new ControlObjectThreadMain(ControlObject::getControl(
+ ConfigKey(group, "vinylcontrol_enabled")));
+ connect(m_pVinylControlEnabled, SIGNAL(valueChanged(double)),
+ this, SLOT(updateVinylControlEnabled(double)));
+
+ m_pSignalEnabled = new ControlObjectThreadMain(ControlObject::getControl(
+ ConfigKey(group, "vinylcontrol_signal_enabled")));
+ connect(m_pSignalEnabled, SIGNAL(valueChanged(double)),
+ this, SLOT(updateVinylControlSignalEnabled(double)));
+
+ //Match the vinyl control's set RPM so that the spinny widget rotates at the same
+ //speed as your physical decks, if you're using vinyl control.
+ connect(m_pVinylControlSpeedType, SIGNAL(valueChanged(double)),
+ this, SLOT(updateVinylControlSpeed(double)));
+
+
+
+#else
+ //if no vinyl control, just call it 33
+ this->updateVinylControlSpeed(33.0);
+#endif
+}
+
+void WSpinny::paintEvent(QPaintEvent *e) {
+ Q_UNUSED(e); //ditch unused param warning
+
+ QPainter p(this);
+ p.setRenderHint(QPainter::SmoothPixmapTransform);
+
+ if (m_pBgImage) {
+ p.drawImage(0, 0, *m_pBgImage);
+ }
+
+#ifdef __VINYLCONTROL__
+ // Overlay the signal quality drawing if vinyl is active
+ if (m_bVinylActive && m_bSignalActive) {
+ // draw the last good image
+ p.drawImage(this->rect(), m_qImage);
+ }
+#endif
+
+ // To rotate the foreground image around the center of the image,
+ // we use the classic trick of translating the coordinate system such that
+ // the origin is at the center of the image. We then rotate the coordinate system,
+ // and draw the image at the corner.
+ p.translate(width() / 2, height() / 2);
+
+ bool bGhostPlayback = m_pSlipEnabled->get();
+
+ if (bGhostPlayback) {
+ p.save();
+ }
+
+ double playPosition = m_pVisualPlayPos->get();
+ double slipPosition = m_pSlipPosition->get();
+
+ if (playPosition != m_dAngleLastPlaypos) {
+ m_fAngle = calculateAngle(playPosition);
+ m_dAngleLastPlaypos = playPosition;
+ }
+
+ if (slipPosition != m_dGhostAngleLastPlaypos) {
+ m_fGhostAngle = calculateAngle(slipPosition);
+ m_dGhostAngleLastPlaypos = slipPosition;
+ }
+
+ if (m_pFgImage && !m_pFgImage->isNull()) {
+ // Now rotate the image and draw it on the screen.
+ p.rotate(m_fAngle);
+ p.drawImage(-(m_pFgImage->width() / 2),
+ -(m_pFgImage->height() / 2), *m_pFgImage);
+ }
+
+ if (bGhostPlayback && m_pGhostImage && !m_pGhostImage->isNull()) {
+ p.restore();
+ p.save();
+ p.rotate(m_fGhostAngle);
+ p.drawImage(-(m_pGhostImage->width() / 2),
+ -(m_pGhostImage->height() / 2), *m_pGhostImage);
+
+ //Rotate back to the playback position (not the ghost positon),
+ //and draw the beat marks from there.
+ p.restore();
+ }
+}
+
+/* Convert between a normalized playback position (0.0 - 1.0) and an angle
+ in our polar coordinate system.
+ Returns an angle clamped between -180 and 180 degrees. */
+double WSpinny::calculateAngle(double playpos) {
+ double trackFrames = m_pTrackSamples->get() / 2;
+ double trackSampleRate = m_pTrackSampleRate->get();
+ if (isnan(playpos) || isnan(trackFrames) || isnan(trackSampleRate) ||
+ trackFrames <= 0 || trackSampleRate <= 0) {
+ return 0.0f;
+ }
+
+ // Convert playpos to seconds.
+ double t = playpos * trackFrames / trackSampleRate;
+
+ // Bad samplerate or number of track samples.
+ if (isnan(t)) {
+ return 0.0f;
+ }
+
+ //33 RPM is approx. 0.5 rotations per second.
+ double angle = 360.0 * m_dRotationsPerSecond * t;
+ //Clamp within -180 and 180 degrees
+ //qDebug() << "pc:" << angle;
+ //angle = ((angle + 180) % 360.) - 180;
+ //modulo for doubles :)
+ const double originalAngle = angle;
+ if (angle > 0)
+ {
+ int x = (angle+180)/360;
+ angle = angle - (360*x);
+ } else
+ {
+ int x = (angle-180)/360;
+ angle = angle - (360*x);
+ }
+
+ if (angle <= -180 || angle > 180) {
+ // Only warn once per session. This can tank performance since it prints
+ // like crazy.
+ if (!m_bClampFailedWarning) {
+ qDebug() << "Angle clamping failed!" << t << originalAngle << "->" << angle
+ << "Please file a bug or email mixxx-devel@lists.sourceforge.net";
+ m_bClampFailedWarning = true;
+ }
+ return 0.0;
+ }
+ return angle;
+}
+
+/** Given a normalized playpos, calculate the integer number of rotations
+ that it would take to wind the vinyl to that position. */
+int WSpinny::calculateFullRotations(double playpos)
+{
+ if (isnan(playpos))
+ return 0.0f;
+ //Convert playpos to seconds.
+ //double t = playpos * m_pDuration->get();
+ double t = playpos * (m_pTrackSamples->get()/2 / // Stereo audio!
+ m_pTrackSampleRate->get());
+
+ //33 RPM is approx. 0.5 rotations per second.
+ //qDebug() << t;
+ double angle = 360*m_dRotationsPerSecond*t;
+
+ return (((int)angle+180) / 360);
+}
+
+//Inverse of calculateAngle()
+double WSpinny::calculatePositionFromAngle(double angle)
+{
+ if (isnan(angle)) {
+ return 0.0f;
+ }
+
+ //33 RPM is approx. 0.5 rotations per second.
+ double t = angle/(360.0 * m_dRotationsPerSecond); //time in seconds
+
+ double trackFrames = m_pTrackSamples->get() / 2;
+ double trackSampleRate = m_pTrackSampleRate->get();
+ if (isnan(trackFrames) || isnan(trackSampleRate) ||
+ trackFrames <= 0 || trackSampleRate <= 0) {
+ return 0.0f;
+ }
+
+ //Convert t from seconds into a normalized playposition value.
+ //double playpos = t / m_pDuration->get();
+ double playpos = t * trackSampleRate / trackFrames;
+ if (isnan(playpos)) {
+ return 0.0;
+ }
+ return playpos;
+}
+
+void WSpinny::updateVinylControlSpeed(double rpm) {
+ m_dRotationsPerSecond = rpm/60.;
+}
+
+void WSpinny::updateVinylControlSignalEnabled(double enabled) {
+ m_bSignalActive = enabled;
+
+ if (enabled && m_iVinylInput != -1) {
+ m_pVCManager->addSignalQualityListener(this);
+ } else {
+ m_pVCManager->removeSignalQualityListener(this);
+ // fill with transparent black
+ m_qImage.fill(qRgba(0,0,0,0));
+ }
+}
+
+void WSpinny::updateVinylControlEnabled(double enabled) {
+ m_bVinylActive = enabled;
+}
+
+void WSpinny::mouseMoveEvent(QMouseEvent * e) {
+ int y = e->y();
+ int x = e->x();
+
+ //Keeping these around in case we want to switch to control relative
+ //to the original mouse position.
+ //int dX = x-m_iStartMouseX;
+ //int dY = y-m_iStartMouseY;
+
+ //Coordinates from center of widget
+ double c_x = x - width()/2;
+ double c_y = y - height()/2;
+ double theta = (180.0f/M_PI)*atan2(c_x, -c_y);
+
+ //qDebug() << "c_x:" << c_x << "c_y:" << c_y <<
+ // "dX:" << dX << "dY:" << dY;
+
+ //When we finish one full rotation (clockwise or anticlockwise),
+ //we'll need to manually add/sub 360 degrees because atan2()'s range is
+ //only within -180 to 180 degrees. We need a wider range so your position
+ //in the song can be tracked.
+ if (m_dPrevTheta > 100 && theta < 0) {
+ m_iFullRotations++;
+ } else if (m_dPrevTheta < -100 && theta > 0) {
+ m_iFullRotations--;
+ }
+
+ m_dPrevTheta = theta;
+ theta += m_iFullRotations*360;
+
+ //qDebug() << "c t:" << theta << "pt:" << m_dPrevTheta <<
+ // "icr" << m_iFullRotations;
+
+ if ((e->buttons() & Qt::LeftButton || e->buttons() & Qt::RightButton) && !m_bVinylActive) {
+ //Convert deltaTheta into a percentage of song length.
+ double absPos = calculatePositionFromAngle(theta);
+ double absPosInSamples = absPos * m_pTrackSamples->get();
+ m_pScratchPos->slotSet(absPosInSamples - m_dInitialPos);
+ } else if (e->buttons() & Qt::MidButton) {
+ } else if (e->buttons() & Qt::NoButton) {
+ setCursor(QCursor(Qt::OpenHandCursor));
+ }
+}
+
+void WSpinny::mousePressEvent(QMouseEvent * e)
+{
+ int y = e->y();
+ int x = e->x();
+
+ m_iStartMouseX = x;
+ m_iStartMouseY = y;
+
+ //don't do anything if vinyl control is active
+ if (m_bVinylActive)
+ return;
+
+ if (e->button() == Qt::LeftButton || e->button() == Qt::RightButton) {
+ QApplication::setOverrideCursor(QCursor(Qt::ClosedHandCursor));
+
+ // Coordinates from center of widget
+ double c_x = x - width()/2;
+ double c_y = y - height()/2;
+ double theta = (180.0f/M_PI)*atan2(c_x, -c_y);
+ m_dPrevTheta = theta;
+ m_iFullRotations = calculateFullRotations(m_pPlayPos->get());
+ theta += m_iFullRotations * 360.0;
+ m_dInitialPos = calculatePositionFromAngle(theta) * m_pTrackSamples->get();
+
+ m_pScratchPos->slotSet(0);
+ m_pScratchToggle->slotSet(1.0f);
+
+ if (e->button() == Qt::RightButton) {
+ m_pSlipEnabled->slotSet(1.0);
+ }
+
+ // Trigger a mouse move to immediately line up the vinyl with the cursor
+ mouseMoveEvent(e);
+ }
+}
+
+void WSpinny::mouseReleaseEvent(QMouseEvent * e)
+{
+ if (e->button() == Qt::LeftButton || e->button() == Qt::RightButton) {
+ QApplication::restoreOverrideCursor();
+ m_pScratchToggle->slotSet(0.0f);
+ m_iFullRotations = 0;
+ if (e->button() == Qt::RightButton) {
+ m_pSlipEnabled->slotSet(0.0);
+ }
+ }
+}
+
+void WSpinny::wheelEvent(QWheelEvent *e)
+{
+ Q_UNUSED(e); //ditch unused param warning
+
+ /*
+ double wheelDirection = ((QWheelEvent *)e)->delta() / 120.;
+ double newValue = getValue() + (wheelDirection);
+ this->updateValue(newValue);
+
+ e->accept();
+ */
+}
+
+void WSpinny::showEvent(QShowEvent* event) {
+ // If we want to draw the VC signal on this widget then register for
+ // updates.
+ if (m_bSignalActive && m_iVinylInput != -1 && m_pVCManager) {
+ m_pVCManager->addSignalQualityListener(this);
+ }
+}
+
+void WSpinny::hideEvent(QHideEvent* event) {
+ // When we are hidden we do not want signal quality updates.
+ if (m_pVCManager) {
+ m_pVCManager->removeSignalQualityListener(this);
+ }
+ // fill with transparent black
+ m_qImage.fill(qRgba(0,0,0,0));
+}
+
+/** DRAG AND DROP **/
+void WSpinny::dragEnterEvent(QDragEnterEvent * event)
+{
+ // Accept the enter event if the thing is a filepath and nothing's playing
+ // in this deck.
+ if (event->mimeData()->hasUrls()) {
+ if (m_pPlay && m_pPlay->get()) {
+ event->ignore();
+ } else {
+ event->acceptProposedAction();
+ }
+ }
+}
+
+void WSpinny::dropEvent(QDropEvent * event)
+{
+ if (event->mimeData()->hasUrls()) {
+ QList<QUrl> urls(event->mimeData()->urls());
+ QUrl url = urls.first();
+ QString name = url.toLocalFile();
+ //If the file is on a network share, try just converting the URL to a string...
+ if (name == "")
+ name = url.toString();
+
+ event->accept();
+ emit(trackDropped(name, m_group));
+ } else {
+ event->ignore();
+ }
+}