![]() |
Mixxx
|
00001 // bpmcontrol.cpp 00002 // Created 7/5/2009 by RJ Ryan (rryan@mit.edu) 00003 00004 #include "controlobject.h" 00005 #include "controlpushbutton.h" 00006 00007 #include "engine/enginebuffer.h" 00008 #include "engine/bpmcontrol.h" 00009 00010 const int minBpm = 30; 00011 const int maxInterval = (int)(1000.*(60./(CSAMPLE)minBpm)); 00012 const int filterLength = 5; 00013 00014 BpmControl::BpmControl(const char* _group, 00015 ConfigObject<ConfigValue>* _config) : 00016 EngineControl(_group, _config), 00017 m_tapFilter(this, filterLength, maxInterval) { 00018 m_pPlayButton = ControlObject::getControl(ConfigKey(_group, "play")); 00019 m_pRateSlider = ControlObject::getControl(ConfigKey(_group, "rate")); 00020 connect(m_pRateSlider, SIGNAL(valueChanged(double)), 00021 this, SLOT(slotRateChanged(double)), 00022 Qt::DirectConnection); 00023 connect(m_pRateSlider, SIGNAL(valueChangedFromEngine(double)), 00024 this, SLOT(slotRateChanged(double)), 00025 Qt::DirectConnection); 00026 00027 m_pRateRange = ControlObject::getControl(ConfigKey(_group, "rateRange")); 00028 connect(m_pRateRange, SIGNAL(valueChanged(double)), 00029 this, SLOT(slotRateChanged(double)), 00030 Qt::DirectConnection); 00031 connect(m_pRateRange, SIGNAL(valueChangedFromEngine(double)), 00032 this, SLOT(slotRateChanged(double)), 00033 Qt::DirectConnection); 00034 00035 m_pRateDir = ControlObject::getControl(ConfigKey(_group, "rate_dir")); 00036 connect(m_pRateDir, SIGNAL(valueChanged(double)), 00037 this, SLOT(slotRateChanged(double)), 00038 Qt::DirectConnection); 00039 connect(m_pRateDir, SIGNAL(valueChangedFromEngine(double)), 00040 this, SLOT(slotRateChanged(double)), 00041 Qt::DirectConnection); 00042 00043 m_pFileBpm = new ControlObject(ConfigKey(_group, "file_bpm")); 00044 connect(m_pFileBpm, SIGNAL(valueChanged(double)), 00045 this, SLOT(slotFileBpmChanged(double)), 00046 Qt::DirectConnection); 00047 00048 m_pEngineBpm = new ControlObject(ConfigKey(_group, "bpm")); 00049 connect(m_pEngineBpm, SIGNAL(valueChanged(double)), 00050 this, SLOT(slotSetEngineBpm(double)), 00051 Qt::DirectConnection); 00052 00053 m_pButtonTap = new ControlPushButton(ConfigKey(_group, "bpm_tap")); 00054 connect(m_pButtonTap, SIGNAL(valueChanged(double)), 00055 this, SLOT(slotBpmTap(double)), 00056 Qt::DirectConnection); 00057 00058 // Beat sync (scale buffer tempo relative to tempo of other buffer) 00059 m_pButtonSync = new ControlPushButton(ConfigKey(_group, "beatsync")); 00060 connect(m_pButtonSync, SIGNAL(valueChanged(double)), 00061 this, SLOT(slotControlBeatSync(double)), 00062 Qt::DirectConnection); 00063 00064 m_pButtonSyncPhase = new ControlPushButton(ConfigKey(_group, "beatsync_phase")); 00065 connect(m_pButtonSyncPhase, SIGNAL(valueChanged(double)), 00066 this, SLOT(slotControlBeatSyncPhase(double)), 00067 Qt::DirectConnection); 00068 00069 m_pButtonSyncTempo = new ControlPushButton(ConfigKey(_group, "beatsync_tempo")); 00070 connect(m_pButtonSyncTempo, SIGNAL(valueChanged(double)), 00071 this, SLOT(slotControlBeatSyncTempo(double)), 00072 Qt::DirectConnection); 00073 00074 m_pTranslateBeats = new ControlPushButton( 00075 ConfigKey(_group, "beats_translate_curpos")); 00076 connect(m_pTranslateBeats, SIGNAL(valueChanged(double)), 00077 this, SLOT(slotBeatsTranslate(double)), 00078 Qt::DirectConnection); 00079 00080 connect(&m_tapFilter, SIGNAL(tapped(double,int)), 00081 this, SLOT(slotTapFilter(double,int)), 00082 Qt::DirectConnection); 00083 } 00084 00085 BpmControl::~BpmControl() { 00086 delete m_pEngineBpm; 00087 delete m_pFileBpm; 00088 delete m_pButtonSync; 00089 delete m_pButtonSyncTempo; 00090 delete m_pButtonSyncPhase; 00091 delete m_pButtonTap; 00092 delete m_pTranslateBeats; 00093 } 00094 00095 double BpmControl::getBpm() { 00096 return m_pEngineBpm->get(); 00097 } 00098 00099 void BpmControl::slotFileBpmChanged(double bpm) { 00100 //qDebug() << this << "slotFileBpmChanged" << bpm; 00101 // Adjust the file-bpm with the current setting of the rate to get the 00102 // engine BPM. 00103 double dRate = 1.0 + m_pRateDir->get() * m_pRateRange->get() * m_pRateSlider->get(); 00104 m_pEngineBpm->set(bpm * dRate); 00105 } 00106 00107 void BpmControl::slotSetEngineBpm(double bpm) { 00108 double filebpm = m_pFileBpm->get(); 00109 00110 if (filebpm != 0.0) { 00111 double newRate = bpm / filebpm - 1.0f; 00112 newRate = math_max(-1.0f, math_min(1.0f, newRate)); 00113 m_pRateSlider->set(newRate * m_pRateDir->get()); 00114 } 00115 } 00116 00117 void BpmControl::slotBpmTap(double v) { 00118 if (v > 0) { 00119 m_tapFilter.tap(); 00120 } 00121 } 00122 00123 void BpmControl::slotTapFilter(double averageLength, int numSamples) { 00124 // averageLength is the average interval in milliseconds tapped over 00125 // numSamples samples. Have to convert to BPM now: 00126 00127 if (averageLength <= 0) 00128 return; 00129 00130 if (numSamples < 4) 00131 return; 00132 00133 // (60 seconds per minute) * (1000 milliseconds per second) / (X millis per 00134 // beat) = Y beats/minute 00135 double averageBpm = 60.0 * 1000.0 / averageLength; 00136 m_pFileBpm->set(averageBpm); 00137 slotFileBpmChanged(averageBpm); 00138 } 00139 00140 void BpmControl::slotControlBeatSyncPhase(double v) { 00141 if (!v) 00142 return; 00143 syncPhase(); 00144 } 00145 00146 void BpmControl::slotControlBeatSyncTempo(double v) { 00147 if (!v) 00148 return; 00149 syncTempo(); 00150 } 00151 00152 void BpmControl::slotControlBeatSync(double v) { 00153 if (!v) 00154 return; 00155 00156 // If the player is playing, and adjusting its tempo succeeded, adjust its 00157 // phase so that it plays in sync. 00158 if (syncTempo() && m_pPlayButton->get() > 0) { 00159 syncPhase(); 00160 } 00161 } 00162 00163 bool BpmControl::syncTempo() { 00164 EngineBuffer* pOtherEngineBuffer = getOtherEngineBuffer(); 00165 00166 if(!pOtherEngineBuffer) 00167 return false; 00168 00169 double fThisBpm = m_pEngineBpm->get(); 00170 //double fThisRate = m_pRateDir->get() * m_pRateSlider->get() * m_pRateRange->get(); 00171 double fThisFileBpm = m_pFileBpm->get(); 00172 00173 double fOtherBpm = pOtherEngineBuffer->getBpm(); 00174 double fOtherRate = pOtherEngineBuffer->getRate(); 00175 double fOtherFileBpm = fOtherBpm / (1.0 + fOtherRate); 00176 00177 //qDebug() << "this" << "bpm" << fThisBpm << "filebpm" << fThisFileBpm << "rate" << fThisRate; 00178 //qDebug() << "other" << "bpm" << fOtherBpm << "filebpm" << fOtherFileBpm << "rate" << fOtherRate; 00179 00181 // Rough proof of how syncing works -- rryan 3/2011 00182 // ------------------------------------------------ 00183 // 00184 // Let this and other denote this deck versus the sync-target deck. 00185 // 00186 // The goal is for this deck's effective BPM to equal the other decks. 00187 // 00188 // thisBpm = otherBpm 00189 // 00190 // The overall rate is the product of range, direction, and scale plus 1: 00191 // 00192 // rate = 1.0 + rateDir * rateRange * rateScale 00193 // 00194 // An effective BPM is the file-bpm times the rate: 00195 // 00196 // bpm = fileBpm * rate 00197 // 00198 // So our goal is to tweak thisRate such that this equation is true: 00199 // 00200 // thisFileBpm * (1.0 + thisRate) = otherFileBpm * (1.0 + otherRate) 00201 // 00202 // so rearrange this equation in terms of thisRate: 00203 // 00204 // thisRate = (otherFileBpm * (1.0 + otherRate)) / thisFileBpm - 1.0 00205 // 00206 // So the new rateScale to set is: 00207 // 00208 // thisRateScale = ((otherFileBpm * (1.0 + otherRate)) / thisFileBpm - 1.0) / (thisRateDir * thisRateRange) 00209 00210 if (fOtherBpm > 0.0 && fThisBpm > 0.0) { 00211 // The desired rate is the other decks effective rate divided by this 00212 // deck's file BPM. This gives us the playback rate that will produe an 00213 // effective BPM equivalent to the other decks. 00214 double fDesiredRate = fOtherBpm / fThisFileBpm; 00215 00216 // Test if this buffers bpm is the double of the other one, and adjust 00217 // the rate scale. I believe this is intended to account for our BPM 00218 // algorithm sometimes finding double or half BPMs. This avoid drastic 00219 // scales. 00220 float fFileBpmDelta = fabs(fThisFileBpm-fOtherFileBpm); 00221 if (fabs(fThisFileBpm*2.0 - fOtherFileBpm) < fFileBpmDelta) { 00222 fDesiredRate /= 2.0; 00223 } else if (fabs(fThisFileBpm - 2.0*fOtherFileBpm) < fFileBpmDelta) { 00224 fDesiredRate *= 2.0; 00225 } 00226 00227 // Subtract the base 1.0, now fDesiredRate is the percentage 00228 // increase/decrease in playback rate, not the playback rate. 00229 fDesiredRate -= 1.0; 00230 00231 // Ensure the rate is within resonable boundaries. Remember, this is the 00232 // percent to scale the rate, not the rate itself. If fDesiredRate was -1, 00233 // that would mean the deck would be completely stopped. If fDesiredRate 00234 // is 1, that means it is playing at 2x speed. This limit enforces that 00235 // we are scaled between 0.5x and 2x. 00236 if (fDesiredRate < 1.0 && fDesiredRate > -0.5) 00237 { 00238 // Adjust the rateScale. We have to divide by the range and 00239 // direction to get the correct rateScale. 00240 fDesiredRate = fDesiredRate/(m_pRateRange->get() * m_pRateDir->get()); 00241 00242 // And finally, set the slider 00243 m_pRateSlider->set(fDesiredRate); 00244 return true; 00245 } 00246 } 00247 return false; 00248 } 00249 00250 bool BpmControl::syncPhase() { 00251 EngineBuffer* pOtherEngineBuffer = getOtherEngineBuffer(); 00252 TrackPointer otherTrack = pOtherEngineBuffer->getLoadedTrack(); 00253 BeatsPointer otherBeats = otherTrack ? otherTrack->getBeats() : BeatsPointer(); 00254 00255 // If either track does not have beats, then we can't adjust the phase. 00256 if (!m_pBeats || !otherBeats) { 00257 return false; 00258 } 00259 00260 // Get the file BPM of each song. 00261 //double dThisBpm = m_pBeats->getBpm(); 00262 //double dOtherBpm = ControlObject::getControl( 00263 //ConfigKey(pOtherEngineBuffer->getGroup(), "file_bpm"))->get(); 00264 00265 // Get the current position of both decks 00266 double dThisPosition = getCurrentSample(); 00267 double dOtherLength = ControlObject::getControl( 00268 ConfigKey(pOtherEngineBuffer->getGroup(), "track_samples"))->get(); 00269 double dOtherPosition = dOtherLength * ControlObject::getControl( 00270 ConfigKey(pOtherEngineBuffer->getGroup(), "visual_playposition"))->get(); 00271 00272 double dThisPrevBeat = m_pBeats->findPrevBeat(dThisPosition); 00273 double dThisNextBeat = m_pBeats->findNextBeat(dThisPosition); 00274 00275 if (dThisPrevBeat == -1 || dThisNextBeat == -1) { 00276 return false; 00277 } 00278 00279 // Protect against the case where we are sitting exactly on the beat. 00280 if (dThisPrevBeat == dThisNextBeat) { 00281 dThisNextBeat = m_pBeats->findNthBeat(dThisPosition, 2); 00282 } 00283 00284 double dOtherPrevBeat = otherBeats->findPrevBeat(dOtherPosition); 00285 double dOtherNextBeat = otherBeats->findNextBeat(dOtherPosition); 00286 00287 if (dOtherPrevBeat == -1 || dOtherNextBeat == -1) { 00288 return false; 00289 } 00290 00291 // Protect against the case where we are sitting exactly on the beat. 00292 if (dOtherPrevBeat == dOtherNextBeat) { 00293 dOtherNextBeat = otherBeats->findNthBeat(dOtherPosition, 2); 00294 } 00295 00296 double dThisBeatLength = fabs(dThisNextBeat - dThisPrevBeat); 00297 double dOtherBeatLength = fabs(dOtherNextBeat - dOtherPrevBeat); 00298 double dOtherBeatFraction = (dOtherPosition - dOtherPrevBeat) / dOtherBeatLength; 00299 00300 double dNewPlaypos; 00301 bool this_near_next = dThisNextBeat - dThisPosition <= dThisPosition - dThisPrevBeat; 00302 bool other_near_next = dOtherNextBeat - dOtherPosition <= dOtherPosition - dOtherPrevBeat; 00303 00304 // We want our beat fraction to be identical to theirs. 00305 00306 // If the two tracks have similar alignment, adjust phase is straight- 00307 // forward. Use the same fraction for both beats, starting from the previous 00308 // beat. But if This track is nearer to the next beat and the Other track 00309 // is nearer to the previous beat, use This Next beat as the starting point 00310 // for the phase. (ie, we pushed the sync button late). If This track 00311 // is nearer to the previous beat, but the Other track is nearer to the 00312 // next beat, we pushed the sync button early so use the double-previous 00313 // beat as the basis for the adjustment. 00314 // 00315 // This makes way more sense when you're actually mixing. 00316 // 00317 // TODO(XXX) Revisit this logic once we move away from tempo-locked, 00318 // infinite beatgrids because the assumption that findNthBeat(-2) always 00319 // works will be wrong then. 00320 00321 if (this_near_next == other_near_next) { 00322 dNewPlaypos = dThisPrevBeat + dOtherBeatFraction * dThisBeatLength; 00323 } else if (this_near_next && !other_near_next) { 00324 dNewPlaypos = dThisNextBeat + dOtherBeatFraction * dThisBeatLength; 00325 } else { 00326 dThisPrevBeat = m_pBeats->findNthBeat(dThisPosition, -2); 00327 dNewPlaypos = dThisPrevBeat + dOtherBeatFraction * dThisBeatLength; 00328 } 00329 00330 emit(seekAbs(dNewPlaypos)); 00331 return true; 00332 } 00333 00334 void BpmControl::slotRateChanged(double) { 00335 double dFileBpm = m_pFileBpm->get(); 00336 slotFileBpmChanged(dFileBpm); 00337 } 00338 00339 void BpmControl::trackLoaded(TrackPointer pTrack) { 00340 if (m_pTrack) { 00341 trackUnloaded(m_pTrack); 00342 } 00343 00344 if (pTrack) { 00345 m_pTrack = pTrack; 00346 m_pBeats = m_pTrack->getBeats(); 00347 connect(m_pTrack.data(), SIGNAL(beatsUpdated()), 00348 this, SLOT(slotUpdatedTrackBeats())); 00349 } 00350 } 00351 00352 void BpmControl::trackUnloaded(TrackPointer pTrack) { 00353 if (m_pTrack) { 00354 disconnect(m_pTrack.data(), SIGNAL(beatsUpdated()), 00355 this, SLOT(slotUpdatedTrackBeats())); 00356 } 00357 m_pTrack.clear(); 00358 m_pBeats.clear(); 00359 } 00360 00361 void BpmControl::slotUpdatedTrackBeats() 00362 { 00363 if (m_pTrack) { 00364 m_pBeats = m_pTrack->getBeats(); 00365 } 00366 } 00367 00368 void BpmControl::slotBeatsTranslate(double v) { 00369 if (v > 0 && m_pBeats && (m_pBeats->getCapabilities() & Beats::BEATSCAP_TRANSLATE)) { 00370 double currentSample = getCurrentSample(); 00371 double closestBeat = m_pBeats->findClosestBeat(currentSample); 00372 int delta = currentSample - closestBeat; 00373 if (delta % 2 != 0) { 00374 delta--; 00375 } 00376 m_pBeats->translate(delta); 00377 } 00378 }