Mixxx

/home/maxime/Projets/Mixxx/1.10/mixxx/src/widget/wspinny.cpp

Go to the documentation of this file.
00001 #include <math.h>
00002 #include "mathstuff.h"
00003 #include "wpixmapstore.h"
00004 #include "controlobject.h"
00005 #include "controlobjectthreadmain.h"
00006 #include "sharedglcontext.h"
00007 #include "wspinny.h"
00008 
00009 WSpinny::WSpinny(QWidget* parent, VinylControlManager* pVCMan) : QGLWidget(SharedGLContext::getContext(), parent),
00010     m_pBG(NULL),
00011     m_pFG(NULL),
00012     m_pGhost(NULL),
00013     m_pPlay(NULL),
00014     m_pPlayPos(NULL),
00015     m_pVisualPlayPos(NULL),
00016     m_pDuration(NULL),
00017     m_pTrackSamples(NULL),
00018     m_pBPM(NULL),
00019     m_pScratch(NULL),
00020     m_pScratchToggle(NULL),
00021     m_pScratchPos(NULL),
00022     m_pVinylControlSpeedType(NULL),
00023     m_pVinylControlEnabled(NULL),
00024     m_bVinylActive(false),
00025     m_bSignalActive(true),
00026     m_iSize(0),
00027     m_iTimerId(0),
00028     m_iSignalUpdateTick(0),
00029     m_fAngle(0.0f),
00030     m_fGhostAngle(0.0f),
00031     m_dPausedPosition(0.0f),
00032     m_bGhostPlayback(false),
00033     m_iStartMouseX(-1),
00034     m_iStartMouseY(-1),
00035     m_iFullRotations(0),
00036     m_dPrevTheta(0.)
00037 {
00038 #ifdef __VINYLCONTROL__
00039     m_pVCManager = pVCMan;
00040     m_pVinylControl = NULL;
00041 #endif
00042     //Drag and drop
00043     setAcceptDrops(true);
00044 }
00045 
00046 WSpinny::~WSpinny()
00047 {
00048     //Don't delete these because the pixmap store takes care of them.
00049     //delete m_pBG;
00050     //delete m_pFG;
00051     //delete m_pGhost;
00052     WPixmapStore::deletePixmap(m_pBG);
00053     WPixmapStore::deletePixmap(m_pFG);
00054     WPixmapStore::deletePixmap(m_pGhost);
00055     delete m_pPlay;
00056     delete m_pPlayPos;
00057     delete m_pVisualPlayPos;
00058     delete m_pDuration;
00059     delete m_pTrackSamples;
00060     delete m_pTrackSampleRate;
00061     delete m_pBPM;
00062     delete m_pScratch;
00063     delete m_pScratchToggle;
00064     delete m_pScratchPos;
00065 #ifdef __VINYLCONTROL__
00066     delete m_pVinylControlSpeedType;
00067     delete m_pVinylControlEnabled;
00068     delete m_pSignalEnabled;
00069     delete m_pRate;
00070 #endif
00071 
00072 }
00073 
00074 void WSpinny::setup(QDomNode node, QString group)
00075 {
00076     m_group = group;
00077 
00078     // Set pixmaps
00079     m_pBG = WPixmapStore::getPixmap(WWidget::getPath(WWidget::selectNodeQString(node,
00080                                                     "PathBackground")));
00081     m_pFG = WPixmapStore::getPixmap(WWidget::getPath(WWidget::selectNodeQString(node,
00082                                                     "PathForeground")));
00083     m_pGhost = WPixmapStore::getPixmap(WWidget::getPath(WWidget::selectNodeQString(node,
00084                                                     "PathGhost")));
00085     if (m_pBG && !m_pBG->isNull()) {
00086         setFixedSize(m_pBG->size());
00087     }
00088 
00089 #ifdef __VINYLCONTROL__
00090     m_iSize = MIXXX_VINYL_SCOPE_SIZE;
00091     m_qImage = QImage(m_iSize, m_iSize, QImage::Format_ARGB32);
00092     //fill with transparent black
00093     m_qImage.fill(qRgba(0,0,0,0));
00094 #endif
00095 
00096     m_pPlay = new ControlObjectThreadMain(ControlObject::getControl(
00097                         ConfigKey(group, "play")));
00098     m_pPlayPos = new ControlObjectThreadMain(ControlObject::getControl(
00099                         ConfigKey(group, "playposition")));
00100     m_pVisualPlayPos = new ControlObjectThreadMain(ControlObject::getControl(
00101                         ConfigKey(group, "visual_playposition")));
00102     m_pDuration = new ControlObjectThreadMain(ControlObject::getControl(
00103                         ConfigKey(group, "duration")));
00104     m_pTrackSamples = new ControlObjectThreadMain(ControlObject::getControl(
00105                         ConfigKey(group, "track_samples")));
00106     m_pTrackSampleRate = new ControlObjectThreadMain(
00107                                     ControlObject::getControl(
00108                                     ConfigKey(group, "track_samplerate")));
00109     m_pBPM = new ControlObjectThreadMain(ControlObject::getControl(
00110                         ConfigKey(group, "bpm")));
00111 
00112     m_pScratch = new ControlObjectThreadMain(ControlObject::getControl(
00113                         ConfigKey(group, "scratch2")));
00114     m_pScratchToggle = new ControlObjectThreadMain(ControlObject::getControl(
00115                         ConfigKey(group, "scratch_position_enable")));
00116     m_pScratchPos = new ControlObjectThreadMain(ControlObject::getControl(
00117                         ConfigKey(group, "scratch_position")));
00118 
00119     Q_ASSERT(m_pPlayPos);
00120     Q_ASSERT(m_pDuration);
00121 
00122     //Repaint when visual_playposition changes.
00123     connect(m_pVisualPlayPos, SIGNAL(valueChanged(double)),
00124             this, SLOT(updateAngle(double)));
00125 
00126 #ifdef __VINYLCONTROL__
00127     m_pVinylControlSpeedType = new ControlObjectThreadMain(ControlObject::getControl(
00128                         ConfigKey(group, "vinylcontrol_speed_type")));
00129     if (m_pVinylControlSpeedType)
00130     {
00131         //Initialize the rotational speed.
00132         this->updateVinylControlSpeed(m_pVinylControlSpeedType->get());
00133     }
00134     m_pVinylControlEnabled = new ControlObjectThreadMain(ControlObject::getControl(
00135                         ConfigKey(group, "vinylcontrol_enabled")));
00136     m_pSignalEnabled = new ControlObjectThreadMain(ControlObject::getControl(
00137                         ConfigKey(group, "vinylcontrol_signal_enabled")));
00138     m_pRate = new ControlObjectThreadMain(ControlObject::getControl(
00139                         ConfigKey(group, "rate")));
00140 
00141     //Match the vinyl control's set RPM so that the spinny widget rotates at the same
00142     //speed as your physical decks, if you're using vinyl control.
00143     connect(m_pVinylControlSpeedType, SIGNAL(valueChanged(double)),
00144             this, SLOT(updateVinylControlSpeed(double)));
00145 
00146     //Make sure vinyl control proxies are up to date
00147     connect(m_pVinylControlEnabled, SIGNAL(valueChanged(double)),
00148             this, SLOT(updateVinylControlEnabled(double)));
00149 
00150     //Check the rate to see if we are stopped
00151     connect(m_pRate, SIGNAL(valueChanged(double)),
00152             this, SLOT(updateRate(double)));
00153 #else
00154     //if no vinyl control, just call it 33
00155     this->updateVinylControlSpeed(33.0);
00156 #endif
00157 }
00158 
00159 void WSpinny::paintEvent(QPaintEvent *e)
00160 {
00161     Q_UNUSED(e); //ditch unused param warning
00162 
00163     QPainter p(this);
00164 
00165     if (m_pBG) {
00166         p.drawPixmap(0, 0, *m_pBG);
00167     }
00168 
00169 #ifdef __VINYLCONTROL__
00170     // Overlay the signal quality drawing if vinyl is active
00171     if (m_bVinylActive && m_bSignalActive)
00172     {
00173         //reduce cpu load by only updating every 3 times
00174         m_iSignalUpdateTick = (m_iSignalUpdateTick + 1) % 3;
00175         if (m_iSignalUpdateTick == 0)
00176         {
00177             unsigned char * buf = m_pVinylControl->getScopeBytemap();
00178             int r,g,b;
00179             QColor qual_color = QColor();
00180             float signalQuality = m_pVinylControl->getTimecodeQuality();
00181 
00182             //color is related to signal quality
00183             //hsv:  s=1, v=1
00184             //h is the only variable.
00185             //h=0 is red, h=120 is green
00186             qual_color.setHsv((int)(120.0 * signalQuality), 255, 255);
00187             qual_color.getRgb(&r, &g, &b);
00188 
00189             if (buf) {
00190                 for (int y=0; y<m_iSize; y++) {
00191                     QRgb *line = (QRgb *)m_qImage.scanLine(y);
00192                     for(int x=0; x<m_iSize; x++) {
00193                         //use xwax's bitmap to set alpha data only
00194                         //adjust alpha by 3/4 so it's not quite so distracting
00195                         //setpixel is slow, use scanlines instead
00196                         //m_qImage.setPixel(x, y, qRgba(r,g,b,(int)buf[x+m_iSize*y] * .75));
00197                         *line = qRgba(r,g,b,(int)(buf[x+m_iSize*y] * .75));
00198                         line++;
00199                     }
00200                 }
00201                 p.drawImage(this->rect(), m_qImage);
00202             }
00203         }
00204         else
00205         {
00206             //draw the last good image
00207             p.drawImage(this->rect(), m_qImage);
00208         }
00209     }
00210 #endif
00211 
00212     //To rotate the foreground pixmap around the center of the image,
00213     //we use the classic trick of translating the coordinate system such that
00214     //the origin is at the center of the image. We then rotate the coordinate system,
00215     //and draw the pixmap at the corner.
00216     p.translate(width() / 2, height() / 2);
00217 
00218     if (m_bGhostPlayback)
00219         p.save();
00220 
00221     if (m_pFG && !m_pFG->isNull()) {
00222         //Now rotate the pixmap and draw it on the screen.
00223         p.rotate(m_fAngle);
00224         p.drawPixmap(-(width() / 2), -(height() / 2), *m_pFG);
00225     }
00226 
00227     if (m_bGhostPlayback && m_pGhost && !m_pGhost->isNull())
00228     {
00229         p.restore();
00230         p.save();
00231         p.rotate(m_fGhostAngle);
00232         p.drawPixmap(-(width() / 2), -(height() / 2), *m_pGhost);
00233 
00234         //Rotate back to the playback position (not the ghost positon),
00235         //and draw the beat marks from there.
00236         p.restore();
00237 
00238         /*
00239         //Draw a line where the next 4 beats are
00240         double bpm = m_pBPM->get();
00241         double duration = m_pDuration->get();
00242         if (bpm <= 0. || duration <= 0.) {
00243             return; //Prevent div by zero
00244         }
00245         double beatLengthInSec = 60. / bpm;
00246         double beatLengthNormalized = beatLengthInSec / duration; //Noramlized to duration
00247         double beatAngle = calculateAngle(beatLengthNormalized);
00248         //qDebug() << "beatAngle:" << beatAngle;
00249         //qDebug() << "beatLenInSec:" << beatLengthInSec << "norm:" << beatLengthNormalized;
00250         p.rotate(m_fAngle);
00251         for (int i = 0; i < 4; i++) {
00252             QLineF beatLine(-(width()*0.6 / 2), -(height()*0.6 / 2),
00253                             -(width()*0.8 / 2), -(height()*0.8 / 2));
00254             //p.drawPoint(-(width()*0.5 / 2), -(height()*0.5 / 2));
00255             p.drawLine(beatLine);
00256             p.rotate(beatAngle);
00257         } */
00258     }
00259 }
00260 
00261 /* Convert between a normalized playback position (0.0 - 1.0) and an angle
00262    in our polar coordinate system.
00263    Returns an angle clamped between -180 and 180 degrees. */
00264 double WSpinny::calculateAngle(double playpos)
00265 {
00266     if (isnan(playpos))
00267         return 0.0f;
00268 
00269     //Convert playpos to seconds.
00270     //double t = playpos * m_pDuration->get();
00271     double t = playpos * (m_pTrackSamples->get()/2 /  // Stereo audio!
00272                           m_pTrackSampleRate->get());
00273 
00274     if (isnan(t)) //Bad samplerate or number of track samples.
00275         return 0.0f;
00276 
00277     //33 RPM is approx. 0.5 rotations per second.
00278     double angle = 360*m_dRotationsPerSecond*t;
00279     //Clamp within -180 and 180 degrees
00280     //qDebug() << "pc:" << angle;
00281     //angle = ((angle + 180) % 360.) - 180;
00282     //modulo for doubles :)
00283     if (angle > 0)
00284     {
00285         int x = (angle+180)/360;
00286         angle = angle - (360*x);
00287     } else
00288     {
00289         int x = (angle-180)/360;
00290         angle = angle - (360*x);
00291     }
00292 
00293     Q_ASSERT(angle <= 180 && angle >= -180);
00294     return angle;
00295 }
00296 
00299 int WSpinny::calculateFullRotations(double playpos)
00300 {
00301     if (isnan(playpos))
00302         return 0.0f;
00303     //Convert playpos to seconds.
00304     //double t = playpos * m_pDuration->get();
00305     double t = playpos * (m_pTrackSamples->get()/2 /  // Stereo audio!
00306                           m_pTrackSampleRate->get());
00307 
00308     //33 RPM is approx. 0.5 rotations per second.
00309     //qDebug() << t;
00310     double angle = 360*m_dRotationsPerSecond*t;
00311 
00312     return (((int)angle+180) / 360);
00313 }
00314 
00315 //Inverse of calculateAngle()
00316 double WSpinny::calculatePositionFromAngle(double angle)
00317 {
00318     if (isnan(angle))
00319         return 0.0f;
00320 
00321     //33 RPM is approx. 0.5 rotations per second.
00322     double t = angle/(360*m_dRotationsPerSecond); //time in seconds
00323 
00324     //Convert t from seconds into a normalized playposition value.
00325     //double playpos = t / m_pDuration->get();
00326     double playpos = t / (m_pTrackSamples->get()/2 /  // Stereo audio!
00327                           m_pTrackSampleRate->get());
00328     return playpos;
00329 }
00330 
00334 void WSpinny::updateAngle(double playpos)
00335 {
00336     m_fAngle = calculateAngle(playpos);
00337 
00338     // if we had the timer going, kill it
00339     if (m_iTimerId != 0) {
00340         killTimer(m_iTimerId);
00341         m_iTimerId = 0;
00342     }
00343     update();
00344 }
00345 
00346 void WSpinny::updateRate(double rate)
00347 {
00348     //if rate is zero, updateAngle won't get called,
00349     if (rate == 0.0 && m_bVinylActive)
00350     {
00351         if (m_iTimerId == 0)
00352         {
00353             m_iTimerId = startTimer(10);
00354         }
00355     }
00356 }
00357 
00358 void WSpinny::timerEvent(QTimerEvent *event)
00359 {
00360     update();
00361 }
00362 
00363 //Update the angle using the ghost playback position.
00364 void WSpinny::updateAngleForGhost()
00365 {
00366     qint64 elapsed = m_time.elapsed();
00367     double duration = m_pDuration->get();
00368     double newPlayPos = m_dPausedPosition +
00369                          (((double)elapsed)/1000.)/duration;
00370     m_fGhostAngle = calculateAngle(newPlayPos);
00371     update();
00372 }
00373 
00374 void WSpinny::updateVinylControlSpeed(double rpm)
00375 {
00376     m_dRotationsPerSecond = rpm/60.;
00377 }
00378 
00379 void WSpinny::updateVinylControlEnabled(double enabled)
00380 {
00381 #ifdef __VINYLCONTROL__
00382     if (enabled)
00383     {
00384         if (m_pVinylControl == NULL)
00385         {
00386             m_pVinylControl = m_pVCManager->getVinylControlProxyForChannel(m_group);
00387             if (m_pVinylControl != NULL)
00388             {
00389                 m_bVinylActive = true;
00390                 m_bSignalActive = m_pSignalEnabled->get();
00391                 connect(m_pVinylControl, SIGNAL(destroyed()),
00392                     this, SLOT(invalidateVinylControl()));
00393             }
00394         }
00395         else
00396         {
00397             m_bVinylActive = true;
00398         }
00399     }
00400     else
00401     {
00402         m_bVinylActive = false;
00403         //don't need the timer anymore
00404         if (m_iTimerId != 0)
00405         {
00406             killTimer(m_iTimerId);
00407         }
00408         // draw once more to erase signal
00409         update();
00410     }
00411 #endif
00412 }
00413 
00414 void WSpinny::invalidateVinylControl()
00415 {
00416 #ifdef __VINYLCONTROL__
00417     m_bVinylActive = false;
00418     m_pVinylControl = NULL;
00419     update();
00420 #endif
00421 }
00422 
00423 
00424 void WSpinny::mouseMoveEvent(QMouseEvent * e)
00425 {
00426     int y = e->y();
00427     int x = e->x();
00428 
00429     //Keeping these around in case we want to switch to control relative
00430     //to the original mouse position.
00431     //int dX = x-m_iStartMouseX;
00432     //int dY = y-m_iStartMouseY;
00433 
00434     //Coordinates from center of widget
00435     double c_x = x - width()/2;
00436     double c_y = y - height()/2;
00437     double theta = (180.0f/M_PI)*atan2(c_x, -c_y);
00438 
00439     //qDebug() << "c_x:" << c_x << "c_y:" << c_y <<
00440     //            "dX:" << dX << "dY:" << dY;
00441 
00442     //When we finish one full rotation (clockwise or anticlockwise),
00443     //we'll need to manually add/sub 360 degrees because atan2()'s range is
00444     //only within -180 to 180 degrees. We need a wider range so your position
00445     //in the song can be tracked.
00446     if (m_dPrevTheta > 100 && theta < 0) {
00447         m_iFullRotations++;
00448     }
00449     else if (m_dPrevTheta < -100 && theta > 0) {
00450         m_iFullRotations--;
00451     }
00452 
00453     m_dPrevTheta = theta;
00454     theta += m_iFullRotations*360;
00455 
00456     //qDebug() << "c t:" << theta << "pt:" << m_dPrevTheta <<
00457     //            "icr" << m_iFullRotations;
00458 
00459     if (e->buttons() & Qt::LeftButton && !m_bVinylActive)
00460     {
00461         //Convert deltaTheta into a percentage of song length.
00462         double absPos = calculatePositionFromAngle(theta);
00463 
00464         double absPosInSamples = absPos * m_pTrackSamples->get();
00465         m_pScratchPos->slotSet(absPosInSamples - m_dInitialPos);
00466     }
00467     else if (e->buttons() & Qt::MidButton)
00468     {
00469     }
00470     else if (e->buttons() & Qt::NoButton)
00471     {
00472         setCursor(QCursor(Qt::OpenHandCursor));
00473     }
00474 }
00475 
00476 void WSpinny::mousePressEvent(QMouseEvent * e)
00477 {
00478     int y = e->y();
00479     int x = e->x();
00480 
00481     m_iStartMouseX = x;
00482     m_iStartMouseY = y;
00483 
00484     //don't do anything if vinyl control is active
00485     if (m_bVinylActive)
00486         return;
00487 
00488     if (e->button() == Qt::LeftButton)
00489     {
00490         QApplication::setOverrideCursor(QCursor(Qt::ClosedHandCursor));
00491 
00492         // Coordinates from center of widget
00493         double c_x = x - width()/2;
00494         double c_y = y - height()/2;
00495         double theta = (180.0f/M_PI)*atan2(c_x, -c_y);
00496         m_dPrevTheta = theta;
00497         m_iFullRotations = calculateFullRotations(m_pPlayPos->get());
00498         theta += m_iFullRotations * 360.0;
00499         m_dInitialPos = calculatePositionFromAngle(theta) * m_pTrackSamples->get();
00500 
00501         m_pScratchPos->slotSet(0);
00502         m_pScratchToggle->slotSet(1.0f);
00503 
00504         //Trigger a mouse move to immediately line up the vinyl with the cursor
00505         mouseMoveEvent(e);
00506     }
00507     else if (e->button() == Qt::MidButton)
00508     {
00509     }
00510     else if (e->button() == Qt::RightButton)
00511     {
00512         //Stop playback and start the timer for ghost playback
00513         m_time.start();
00514         m_dPausedPosition = m_pPlayPos->get();
00515         updateAngleForGhost(); //Need to recalc the ghost angle right away
00516         m_bGhostPlayback = true;
00517         m_ghostPaintTimer.start(30);
00518         connect(&m_ghostPaintTimer, SIGNAL(timeout()),
00519                 this, SLOT(updateAngleForGhost()));
00520 
00521         //TODO: Ramp down (brake) over a period of 1 beat
00522         //      instead? Would be sweet.
00523         m_pPlay->slotSet(0.0f);
00524     }
00525 }
00526 
00527 void WSpinny::mouseReleaseEvent(QMouseEvent * e)
00528 {
00529     if (e->button() == Qt::LeftButton)
00530     {
00531         QApplication::restoreOverrideCursor();
00532         m_pScratchToggle->slotSet(0.0f);
00533         m_iFullRotations = 0;
00534     }
00535     else if (e->button() == Qt::RightButton)
00536     {
00537         //Start playback by jumping forwards in the song as if playback
00538         //was never paused. (useful for bleeping or adding silence breaks)
00539         qint64 elapsed = m_time.elapsed();
00540         //qDebug() << "elapsed:" << elapsed;
00541         m_ghostPaintTimer.stop();
00542         m_bGhostPlayback = false;
00543 
00544         //Convert elapsed to seconds, then normalize it to the duration so we can
00545         //move the playback position ahead by the elapsed amount.
00546         double duration = m_pDuration->get();
00547         double newPlayPos = m_dPausedPosition + (((double)elapsed)/1000.)/duration;
00548         //qDebug() << m_dPausedPosition << newPlayPos;
00549         m_pPlay->slotSet(1.0f);
00550         m_pPlayPos->slotSet(newPlayPos);
00551         //m_bRightButtonPressed = true;
00552     }
00553 }
00554 
00555 void WSpinny::wheelEvent(QWheelEvent *e)
00556 {
00557     Q_UNUSED(e); //ditch unused param warning
00558 
00559     /*
00560     double wheelDirection = ((QWheelEvent *)e)->delta() / 120.;
00561     double newValue = getValue() + (wheelDirection);
00562     this->updateValue(newValue);
00563 
00564     e->accept();
00565     */
00566 }
00567 
00569 void WSpinny::dragEnterEvent(QDragEnterEvent * event)
00570 {
00571     // Accept the enter event if the thing is a filepath and nothing's playing
00572     // in this deck.
00573     if (event->mimeData()->hasUrls()) {
00574         if (m_pPlay && m_pPlay->get()) {
00575             event->ignore();
00576         } else {
00577             event->acceptProposedAction();
00578         }
00579     }
00580 }
00581 
00582 void WSpinny::dropEvent(QDropEvent * event)
00583 {
00584     if (event->mimeData()->hasUrls()) {
00585         QList<QUrl> urls(event->mimeData()->urls());
00586         QUrl url = urls.first();
00587         QString name = url.toLocalFile();
00588         //If the file is on a network share, try just converting the URL to a string...
00589         if (name == "")
00590             name = url.toString();
00591 
00592         event->accept();
00593         emit(trackDropped(name, m_group));
00594     } else {
00595         event->ignore();
00596     }
00597 }
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Defines