void DlgTrackInfo::populateFields(TrackPointer pTrack) { setWindowTitle(pTrack->getTitle()); // Editable fields txtTrackName->setText(pTrack->getTitle()); txtArtist->setText(pTrack->getArtist()); txtAlbum->setText(pTrack->getAlbum()); txtAlbumArtist->setText(pTrack->getAlbumArtist()); txtGenre->setText(pTrack->getGenre()); txtComposer->setText(pTrack->getComposer()); txtGrouping->setText(pTrack->getGrouping()); txtYear->setText(pTrack->getYear()); txtTrackNumber->setText(pTrack->getTrackNumber()); txtComment->setText(pTrack->getComment()); spinBpm->setValue(pTrack->getBpm()); // Non-editable fields txtDuration->setText(pTrack->getDurationStr()); txtFilepath->setText(pTrack->getFilename()); txtLocation->setText(pTrack->getLocation()); txtType->setText(pTrack->getType()); txtBitrate->setText(QString(pTrack->getBitrateStr()) + (" ") + tr("kbps")); txtBpm->setText(pTrack->getBpmStr()); txtKey->setText(pTrack->getKeyText()); BeatsPointer pBeats = pTrack->getBeats(); bool beatsSupportsSet = !pBeats || (pBeats->getCapabilities() & Beats::BEATSCAP_SET); bool enableBpmEditing = !pTrack->hasBpmLock() && beatsSupportsSet; spinBpm->setEnabled(enableBpmEditing); bpmTap->setEnabled(enableBpmEditing); bpmDouble->setEnabled(enableBpmEditing); bpmHalve->setEnabled(enableBpmEditing); bpmTwoThirds->setEnabled(enableBpmEditing); bpmThreeFourth->setEnabled(enableBpmEditing); }
void BpmControl::slotAdjustBeatsSlower(double v) { BeatsPointer pBeats = m_pBeats; if (v > 0 && pBeats && (pBeats->getCapabilities() & Beats::BEATSCAP_SETBPM)) { double new_bpm = math_max(10.0, pBeats->getBpm() - .01); pBeats->setBpm(new_bpm); } }
void BpmControl::slotTranslateBeatsEarlier(double v) { BeatsPointer pBeats = m_pBeats; if (v > 0 && pBeats && (pBeats->getCapabilities() & Beats::BEATSCAP_TRANSLATE)) { const int translate_dist = getSampleOfTrack().rate * -.01; pBeats->translate(translate_dist); } }
void BpmControl::slotTranslateBeatsLater(double v) { BeatsPointer pBeats = m_pBeats; if (v > 0 && pBeats && (pBeats->getCapabilities() & Beats::BEATSCAP_TRANSLATE)) { // TODO(rryan): Track::getSampleRate is possibly inaccurate! const int translate_dist = getSampleOfTrack().rate * .01; pBeats->translate(translate_dist); } }
void BpmControl::slotBeatsTranslateMatchAlignment(double v) { BeatsPointer pBeats = m_pBeats; if (v > 0 && pBeats && (pBeats->getCapabilities() & Beats::BEATSCAP_TRANSLATE)) { // Must reset the user offset *before* calling getPhaseOffset(), // otherwise it will always return 0 if master sync is active. m_dUserOffset.setValue(0.0); double offset = getPhaseOffset(getSampleOfTrack().current); pBeats->translate(-offset); } }
void BpmControl::slotBeatsTranslate(double v) { BeatsPointer pBeats = m_pBeats; if (v > 0 && pBeats && (pBeats->getCapabilities() & Beats::BEATSCAP_TRANSLATE)) { double currentSample = getSampleOfTrack().current; double closestBeat = pBeats->findClosestBeat(currentSample); int delta = currentSample - closestBeat; if (delta % 2 != 0) { delta--; } pBeats->translate(delta); } }
// static bool BpmControl::getBeatContext(const BeatsPointer& pBeats, const double dPosition, double* dpPrevBeat, double* dpNextBeat, double* dpBeatLength, double* dpBeatPercentage) { if (!pBeats) { return false; } double dPrevBeat; double dNextBeat; if (!pBeats->findPrevNextBeats(dPosition, &dPrevBeat, &dNextBeat)) { return false; } if (dpPrevBeat != NULL) { *dpPrevBeat = dPrevBeat; } if (dpNextBeat != NULL) { *dpNextBeat = dNextBeat; } return getBeatContextNoLookup(dPosition, dPrevBeat, dNextBeat, dpBeatLength, dpBeatPercentage); }
double BpmControl::updateLocalBpm() { double prev_local_bpm = m_pLocalBpm->get(); double local_bpm = 0; BeatsPointer pBeats = m_pBeats; if (pBeats) { local_bpm = pBeats->getBpmAroundPosition( getSampleOfTrack().current, kLocalBpmSpan); if (local_bpm == -1) { local_bpm = m_pFileBpm->get(); } } else { local_bpm = m_pFileBpm->get(); } if (local_bpm != prev_local_bpm) { m_pLocalBpm->set(local_bpm); slotUpdateEngineBpm(); } return local_bpm; }
void BpmControl::slotTapFilter(double averageLength, int numSamples) { // averageLength is the average interval in milliseconds tapped over // numSamples samples. Have to convert to BPM now: if (averageLength <= 0) return; if (numSamples < 4) return; BeatsPointer pBeats = m_pBeats; if (!pBeats) return; // (60 seconds per minute) * (1000 milliseconds per second) / (X millis per // beat) = Y beats/minute double averageBpm = 60.0 * 1000.0 / averageLength / calcRateRatio(); pBeats->setBpm(averageBpm); }
void BpmControl::slotFileBpmChanged(double file_bpm) { // Adjust the file-bpm with the current setting of the rate to get the // engine BPM. We only do this for SYNC_NONE decks because EngineSync will // set our BPM if the file BPM changes. See SyncControl::fileBpmChanged(). //qDebug() << "BpmControl::slotFileBpmChanged" << file_bpm; BeatsPointer pBeats = m_pBeats; if (pBeats) { const double beats_bpm = pBeats->getBpmAroundPosition( getSampleOfTrack().current, kLocalBpmSpan); if (beats_bpm != -1) { m_pLocalBpm->set(beats_bpm); } else { m_pLocalBpm->set(file_bpm); } } else { m_pLocalBpm->set(file_bpm); } resetSyncAdjustment(); }
double BpmControl::getNearestPositionInPhase( double dThisPosition, bool respectLoops, bool playing) { // Without a beatgrid, we don't know the phase offset. BeatsPointer pBeats = m_pBeats; if (!pBeats) { return dThisPosition; } SyncMode syncMode = getSyncMode(); // Master buffer is always in sync! if (syncMode == SYNC_MASTER) { return dThisPosition; } // Get the current position of this deck. double dThisPrevBeat = m_pPrevBeat->get(); double dThisNextBeat = m_pNextBeat->get(); double dThisBeatLength; if (dThisPosition > dThisNextBeat || dThisPosition < dThisPrevBeat) { // There's a chance the COs might be out of date, so do a lookup. // TODO: figure out a way so that quantized control can take care of // this so this call isn't necessary. if (!getBeatContext(pBeats, dThisPosition, &dThisPrevBeat, &dThisNextBeat, &dThisBeatLength, NULL)) { return dThisPosition; } } else { if (!getBeatContextNoLookup(dThisPosition, dThisPrevBeat, dThisNextBeat, &dThisBeatLength, NULL)) { return dThisPosition; } } double dOtherBeatFraction; if (syncMode == SYNC_FOLLOWER) { // If we're a follower, it's easy to get the other beat fraction dOtherBeatFraction = m_dSyncTargetBeatDistance.getValue(); } else { // If not, we have to figure it out EngineBuffer* pOtherEngineBuffer = pickSyncTarget(); if (playing) { if (!pOtherEngineBuffer || pOtherEngineBuffer->getSpeed() == 0.0) { // "this" track is playing, or just starting // only match phase if the sync target is playing as well // else use the previouse phase of "this" track before the seek pOtherEngineBuffer = getEngineBuffer(); } } if (!pOtherEngineBuffer) { // no suitable sync buffer found return dThisPosition; } TrackPointer otherTrack = pOtherEngineBuffer->getLoadedTrack(); BeatsPointer otherBeats = otherTrack ? otherTrack->getBeats() : BeatsPointer(); // If either track does not have beats, then we can't adjust the phase. if (!otherBeats) { return dThisPosition; } double dOtherLength = ControlObject::getControl( ConfigKey(pOtherEngineBuffer->getGroup(), "track_samples"))->get(); double dOtherEnginePlayPos = pOtherEngineBuffer->getVisualPlayPos(); double dOtherPosition = dOtherLength * dOtherEnginePlayPos; if (!BpmControl::getBeatContext(otherBeats, dOtherPosition, NULL, NULL, NULL, &dOtherBeatFraction)) { return dThisPosition; } } bool this_near_next = dThisNextBeat - dThisPosition <= dThisPosition - dThisPrevBeat; bool other_near_next = dOtherBeatFraction >= 0.5; // We want our beat fraction to be identical to theirs. // If the two tracks have similar alignment, adjust phase is straight- // forward. Use the same fraction for both beats, starting from the previous // beat. But if This track is nearer to the next beat and the Other track // is nearer to the previous beat, use This Next beat as the starting point // for the phase. (ie, we pushed the sync button late). If This track // is nearer to the previous beat, but the Other track is nearer to the // next beat, we pushed the sync button early so use the double-previous // beat as the basis for the adjustment. // // This makes way more sense when you're actually mixing. // // TODO(XXX) Revisit this logic once we move away from tempo-locked, // infinite beatgrids because the assumption that findNthBeat(-2) always // works will be wrong then. double dNewPlaypos = (dOtherBeatFraction + m_dUserOffset.getValue()) * dThisBeatLength; if (this_near_next == other_near_next) { dNewPlaypos += dThisPrevBeat; } else if (this_near_next && !other_near_next) { dNewPlaypos += dThisNextBeat; } else { //!this_near_next && other_near_next dThisPrevBeat = pBeats->findNthBeat(dThisPosition, -2); dNewPlaypos += dThisPrevBeat; } if (respectLoops) { // We might be seeking outside the loop. const bool loop_enabled = m_pLoopEnabled->toBool(); const double loop_start_position = m_pLoopStartPosition->get(); const double loop_end_position = m_pLoopEndPosition->get(); // Cases for sanity: // // CASE 1 // Two identical 1-beat loops, out of phase by X samples. // Other deck is at its loop start. // This deck is half way through. We want to jump forward X samples to the loop end point. // // Two identical 1-beat loop, out of phase by X samples. // Other deck is // If sync target is 50% through the beat, // If we are at the loop end point and hit sync, jump forward X samples. // TODO(rryan): Revise this with something that keeps a broader number of // cases in sync. This at least prevents breaking out of the loop. if (loop_enabled && dThisPosition <= loop_end_position) { const double loop_length = loop_end_position - loop_start_position; const double end_delta = dNewPlaypos - loop_end_position; // Syncing to after the loop end. if (end_delta > 0 && loop_length > 0.0) { int i = end_delta / loop_length; dNewPlaypos = loop_start_position + end_delta - i * loop_length; // Move new position after loop jump into phase as well. // This is a recursive call, called only twice because of // respectLoops = false dNewPlaypos = getNearestPositionInPhase(dNewPlaypos, false, playing); } // Note: Syncing to before the loop beginning is allowed, because // loops are catching } } return dNewPlaypos; }
// TODO(XXX): Get rid of the horrible duplication here between initialise and // loadStored. bool AnalyserBeats::initialise(TrackPointer tio, int sampleRate, int totalSamples) { if (totalSamples == 0) { return false; } bool bPreferencesBeatDetectionEnabled = static_cast<bool>( m_pConfig->getValueString( ConfigKey(BPM_CONFIG_KEY, BPM_DETECTION_ENABLED)).toInt()); if (!bPreferencesBeatDetectionEnabled) { qDebug() << "Beat calculation is deactivated"; return false; } bool bpmLock = tio->hasBpmLock(); if (bpmLock) { qDebug() << "Track is BpmLocked: Beat calculation will not start"; return false; } bool allow_above = static_cast<bool>(m_pConfig->getValueString( ConfigKey(BPM_CONFIG_KEY, BPM_ABOVE_RANGE_ENABLED)).toInt()); if (allow_above) { m_iMinBpm = 0; m_iMaxBpm = 9999; } else { m_iMinBpm = m_pConfig->getValueString(ConfigKey(BPM_CONFIG_KEY, BPM_RANGE_START)).toInt(); m_iMaxBpm = m_pConfig->getValueString(ConfigKey(BPM_CONFIG_KEY, BPM_RANGE_END)).toInt(); } m_bPreferencesFixedTempo = static_cast<bool>( m_pConfig->getValueString( ConfigKey(BPM_CONFIG_KEY, BPM_FIXED_TEMPO_ASSUMPTION)).toInt()); m_bPreferencesOffsetCorrection = static_cast<bool>( m_pConfig->getValueString( ConfigKey(BPM_CONFIG_KEY, BPM_FIXED_TEMPO_OFFSET_CORRECTION)).toInt()); m_bPreferencesReanalyzeOldBpm = static_cast<bool>( m_pConfig->getValueString( ConfigKey(BPM_CONFIG_KEY, BPM_REANALYZE_WHEN_SETTINGS_CHANGE)).toInt()); m_bPreferencesFastAnalysis = static_cast<bool>( m_pConfig->getValueString( ConfigKey(BPM_CONFIG_KEY, BPM_FAST_ANALYSIS_ENABLED)).toInt()); QString library = m_pConfig->getValueString( ConfigKey(VAMP_CONFIG_KEY, VAMP_ANALYSER_BEAT_LIBRARY)); QString pluginID = m_pConfig->getValueString( ConfigKey(VAMP_CONFIG_KEY, VAMP_ANALYSER_BEAT_PLUGIN_ID)); m_pluginId = pluginID; m_iSampleRate = sampleRate; m_iTotalSamples = totalSamples; // If the track already has a Beats object then we need to decide whether to // analyze this track or not. bool bShouldAnalyze = false; BeatsPointer pBeats = tio->getBeats(); if (pBeats) { QString version = pBeats->getVersion(); QString subVersion = pBeats->getSubVersion(); QHash<QString, QString> extraVersionInfo = getExtraVersionInfo( m_pluginId, m_bPreferencesFastAnalysis); QString newVersion = BeatFactory::getPreferredVersion( m_bPreferencesFixedTempo); QString newSubVersion = BeatFactory::getPreferredSubVersion( m_bPreferencesFixedTempo, m_bPreferencesOffsetCorrection, m_iMinBpm, m_iMaxBpm, extraVersionInfo); if (version == newVersion && subVersion == newSubVersion) { // If the version and settings have not changed then if the world is // sane, re-analyzing will do nothing. bShouldAnalyze = false; qDebug() << "Beat sub-version has not changed since previous analysis so not analyzing."; } else if (m_bPreferencesReanalyzeOldBpm) { bShouldAnalyze = true; } else if (pBeats->getBpm() == 0.0) { bShouldAnalyze = true; qDebug() << "BPM is 0 for track so re-analyzing despite preference settings."; } else if (pBeats->findNextBeat(0) <= 0.0) { bShouldAnalyze = true; qDebug() << "First beat is 0 for grid so analyzing track to find first beat."; } else { bShouldAnalyze = false; qDebug() << "Beat calculation skips analyzing because the track has" << "a BPM computed by a previous Mixxx version and user" << "preferences indicate we should not change it."; } } else { // No valid keys stored so we want to analyze this track. bShouldAnalyze = true; } if (bShouldAnalyze) { m_pVamp = new VampAnalyser(m_pConfig); bShouldAnalyze = m_pVamp->Init(library, pluginID, m_iSampleRate, totalSamples, m_bPreferencesFastAnalysis); if (!bShouldAnalyze) { delete m_pVamp; m_pVamp = NULL; } } if (bShouldAnalyze) { qDebug() << "Beat calculation started with plugin" << pluginID; } else { qDebug() << "Beat calculation will not start"; } return bShouldAnalyze; }
void AnalyserBeats::finalise(TrackPointer tio) { if (m_pVamp == NULL) { return; } // Call End() here, because the number of total samples may have been // estimated incorrectly. bool success = m_pVamp->End(); qDebug() << "Beat Calculation" << (success ? "complete" : "failed"); QVector<double> beats = m_pVamp->GetInitFramesVector(); delete m_pVamp; m_pVamp = NULL; if (beats.isEmpty()) { qDebug() << "Could not detect beat positions from Vamp."; return; } QHash<QString, QString> extraVersionInfo = getExtraVersionInfo( m_pluginId, m_bPreferencesFastAnalysis); BeatsPointer pBeats = BeatFactory::makePreferredBeats( tio, beats, extraVersionInfo, m_bPreferencesFixedTempo, m_bPreferencesOffsetCorrection, m_iSampleRate, m_iTotalSamples, m_iMinBpm, m_iMaxBpm); BeatsPointer pCurrentBeats = tio->getBeats(); // If the track has no beats object then set our newly generated one // regardless of beat lock. if (!pCurrentBeats) { tio->setBeats(pBeats); return; } // If the track received the beat lock while we were analyzing it then we // abort setting it. if (tio->hasBpmLock()) { qDebug() << "Track was BPM-locked as we were analysing it. Aborting analysis."; return; } // If the user prefers to replace old beatgrids with newly generated ones or // the old beatgrid has 0-bpm then we replace it. bool zeroCurrentBpm = pCurrentBeats->getBpm() == 0.0f; if (m_bPreferencesReanalyzeOldBpm || zeroCurrentBpm) { if (zeroCurrentBpm) { qDebug() << "Replacing 0-BPM beatgrid with a" << pBeats->getBpm() << "beatgrid."; } tio->setBeats(pBeats); return; } // If we got here then the user doesn't want to replace the beatgrid but // since the first beat is zero we'll apply the offset we just detected. double currentFirstBeat = pCurrentBeats->findNextBeat(0); double newFirstBeat = pBeats->findNextBeat(0); if (currentFirstBeat == 0.0 && newFirstBeat > 0) { pCurrentBeats->translate(newFirstBeat); } }
// TODO(XXX): Get rid of the horrible duplication here between initialise and // loadStored. bool AnalyserBeats::loadStored(TrackPointer tio) const { int iMinBpm; int iMaxBpm; bool allow_above = static_cast<bool>(m_pConfig->getValueString( ConfigKey(BPM_CONFIG_KEY, BPM_ABOVE_RANGE_ENABLED)).toInt()); if (allow_above) { iMinBpm = 0; iMaxBpm = 9999; } else { iMinBpm = m_pConfig->getValueString(ConfigKey(BPM_CONFIG_KEY, BPM_RANGE_START)).toInt(); iMaxBpm = m_pConfig->getValueString(ConfigKey(BPM_CONFIG_KEY, BPM_RANGE_END)).toInt(); } bool bPreferencesFixedTempo = static_cast<bool>( m_pConfig->getValueString( ConfigKey(BPM_CONFIG_KEY, BPM_FIXED_TEMPO_ASSUMPTION)).toInt()); bool bPreferencesOffsetCorrection = static_cast<bool>( m_pConfig->getValueString( ConfigKey(BPM_CONFIG_KEY, BPM_FIXED_TEMPO_OFFSET_CORRECTION)).toInt()); bool bPreferencesReanalyzeOldBpm = static_cast<bool>( m_pConfig->getValueString( ConfigKey(BPM_CONFIG_KEY, BPM_REANALYZE_WHEN_SETTINGS_CHANGE)).toInt()); bool bPreferencesFastAnalysis = static_cast<bool>( m_pConfig->getValueString( ConfigKey(BPM_CONFIG_KEY, BPM_FAST_ANALYSIS_ENABLED)).toInt()); bool bpmLock = tio->hasBpmLock(); if (bpmLock) { qDebug() << "Track is BpmLocked: Beat calculation will not start"; return true; } QString library = m_pConfig->getValueString( ConfigKey(VAMP_CONFIG_KEY, VAMP_ANALYSER_BEAT_LIBRARY)); QString pluginID = m_pConfig->getValueString( ConfigKey(VAMP_CONFIG_KEY, VAMP_ANALYSER_BEAT_PLUGIN_ID)); // At first start config for QM and Vamp does not exist --> set default // TODO(XXX): This is no longer present in initialise. Remove? if (library.isEmpty() || library.isNull()) library = "libmixxxminimal"; if (pluginID.isEmpty() || pluginID.isNull()) pluginID="qm-tempotracker:0"; // If the track already has a Beats object then we need to decide whether to // analyze this track or not. BeatsPointer pBeats = tio->getBeats(); if (pBeats) { QString version = pBeats->getVersion(); QString subVersion = pBeats->getSubVersion(); QHash<QString, QString> extraVersionInfo = getExtraVersionInfo( pluginID, bPreferencesFastAnalysis); QString newVersion = BeatFactory::getPreferredVersion( bPreferencesOffsetCorrection); QString newSubVersion = BeatFactory::getPreferredSubVersion( bPreferencesFixedTempo, bPreferencesOffsetCorrection, iMinBpm, iMaxBpm, extraVersionInfo); if (version == newVersion && subVersion == newSubVersion) { // If the version and settings have not changed then if the world is // sane, re-analyzing will do nothing. qDebug() << "Beat sub-version has not changed since previous analysis so not analyzing."; return true; } else if (bPreferencesReanalyzeOldBpm) { return false; } else if (pBeats->getBpm() == 0.0) { qDebug() << "BPM is 0 for track so re-analyzing despite preference settings."; return false; } else if (pBeats->findNextBeat(0) <= 0.0) { qDebug() << "First beat is 0 for grid so analyzing track to find first beat."; return false; } else { qDebug() << "Beat calculation skips analyzing because the track has" << "a BPM computed by a previous Mixxx version and user" << "preferences indicate we should not change it."; return true; } } else { // If we got here, we want to analyze this track. return false; } }
bool BpmControl::syncPhase(EngineBuffer* pOtherEngineBuffer) { if (!pOtherEngineBuffer) { return false; } TrackPointer otherTrack = pOtherEngineBuffer->getLoadedTrack(); BeatsPointer otherBeats = otherTrack ? otherTrack->getBeats() : BeatsPointer(); // If either track does not have beats, then we can't adjust the phase. if (!m_pBeats || !otherBeats) { return false; } // Get the file BPM of each song. //double dThisBpm = m_pBeats->getBpm(); //double dOtherBpm = ControlObject::getControl( //ConfigKey(pOtherEngineBuffer->getGroup(), "file_bpm"))->get(); // Get the current position of both decks double dThisPosition = getCurrentSample(); double dOtherLength = ControlObject::getControl( ConfigKey(pOtherEngineBuffer->getGroup(), "track_samples"))->get(); double dOtherEnginePlayPos = ControlObject::getControl( ConfigKey(pOtherEngineBuffer->getGroup(), "visual_playposition"))->get(); double dOtherPosition = dOtherLength * dOtherEnginePlayPos; double dThisPrevBeat = m_pBeats->findPrevBeat(dThisPosition); double dThisNextBeat = m_pBeats->findNextBeat(dThisPosition); if (dThisPrevBeat == -1 || dThisNextBeat == -1 || dOtherEnginePlayPos == -1 || dOtherLength == 0) { return false; } // Protect against the case where we are sitting exactly on the beat. if (dThisPrevBeat == dThisNextBeat) { dThisNextBeat = m_pBeats->findNthBeat(dThisPosition, 2); } double dOtherPrevBeat = otherBeats->findPrevBeat(dOtherPosition); double dOtherNextBeat = otherBeats->findNextBeat(dOtherPosition); if (dOtherPrevBeat == -1 || dOtherNextBeat == -1) { return false; } // Protect against the case where we are sitting exactly on the beat. if (dOtherPrevBeat == dOtherNextBeat) { dOtherNextBeat = otherBeats->findNthBeat(dOtherPosition, 2); } double dThisBeatLength = fabs(dThisNextBeat - dThisPrevBeat); double dOtherBeatLength = fabs(dOtherNextBeat - dOtherPrevBeat); double dOtherBeatFraction = (dOtherPosition - dOtherPrevBeat) / dOtherBeatLength; double dNewPlaypos; bool this_near_next = dThisNextBeat - dThisPosition <= dThisPosition - dThisPrevBeat; bool other_near_next = dOtherNextBeat - dOtherPosition <= dOtherPosition - dOtherPrevBeat; // We want our beat fraction to be identical to theirs. // If the two tracks have similar alignment, adjust phase is straight- // forward. Use the same fraction for both beats, starting from the previous // beat. But if This track is nearer to the next beat and the Other track // is nearer to the previous beat, use This Next beat as the starting point // for the phase. (ie, we pushed the sync button late). If This track // is nearer to the previous beat, but the Other track is nearer to the // next beat, we pushed the sync button early so use the double-previous // beat as the basis for the adjustment. // // This makes way more sense when you're actually mixing. // // TODO(XXX) Revisit this logic once we move away from tempo-locked, // infinite beatgrids because the assumption that findNthBeat(-2) always // works will be wrong then. if (this_near_next == other_near_next) { dNewPlaypos = dThisPrevBeat + dOtherBeatFraction * dThisBeatLength; } else if (this_near_next && !other_near_next) { dNewPlaypos = dThisNextBeat + dOtherBeatFraction * dThisBeatLength; } else { //!this_near_next && other_near_next dThisPrevBeat = m_pBeats->findNthBeat(dThisPosition, -2); dNewPlaypos = dThisPrevBeat + dOtherBeatFraction * dThisBeatLength; } // We might be seeking outside the loop. const bool loop_enabled = m_pLoopEnabled->get() > 0.0; const double loop_start_position = m_pLoopStartPosition->get(); const double loop_end_position = m_pLoopEndPosition->get(); // Cases for sanity: // // CASE 1 // Two identical 1-beat loops, out of phase by X samples. // Other deck is at its loop start. // This deck is half way through. We want to jump forward X samples to the loop end point. // // Two identical 1-beat loop, out of phase by X samples. // Other deck is // If sync target is 50% through the beat, // If we are at the loop end point and hit sync, jump forward X samples. // TODO(rryan): Revise this with something that keeps a broader number of // cases in sync. This at least prevents breaking out of the loop. if (loop_enabled) { const double loop_length = loop_end_position - loop_start_position; if (loop_length <= 0.0) { return false; } // TODO(rryan): If loop_length is not a multiple of dThisBeatLength should // we bail and not sync phase? // Syncing to after the loop end. double end_delta = dNewPlaypos - loop_end_position; if (end_delta > 0) { int i = end_delta / loop_length; dNewPlaypos = loop_start_position + end_delta - i * loop_length; } // Syncing to before the loop beginning. double start_delta = loop_start_position - dNewPlaypos; if (start_delta > 0) { int i = start_delta / loop_length; dNewPlaypos = loop_end_position - start_delta + i * loop_length; } } seekAbs(dNewPlaypos); return true; }