Mixxx
|
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 }