EngineBuffer* EngineControl::pickSyncTarget() { EngineMaster* pMaster = getEngineMaster(); if (!pMaster) { return NULL; } QString group = getGroup(); EngineBuffer* pFirstNonplayingDeck = NULL; for (int i = 0; i < m_numDecks.get(); ++i) { QString deckGroup = PlayerManager::groupForDeck(i); if (deckGroup == group) { continue; } EngineChannel* pChannel = pMaster->getChannel(deckGroup); // Only consider channels that have a track loaded and are in the master // mix. if (pChannel && pChannel->isActive() && pChannel->isMaster()) { EngineBuffer* pBuffer = pChannel->getEngineBuffer(); if (pBuffer && pBuffer->getBpm() > 0) { // If the deck is playing then go with it immediately. if (fabs(pBuffer->getSpeed()) > 0) { return pBuffer; } // Otherwise hold out for a deck that might be playing but // remember the first deck that matched our criteria. if (pFirstNonplayingDeck == NULL) { pFirstNonplayingDeck = pBuffer; } } } } // No playing decks have a BPM. Go with the first deck that was stopped but // had a BPM. return pFirstNonplayingDeck; }
void EngineMaster::addChannel(EngineChannel* pChannel) { ChannelInfo* pChannelInfo = new ChannelInfo(m_channels.size()); pChannelInfo->m_pChannel = pChannel; const QString& group = pChannel->getGroup(); pChannelInfo->m_handle = m_channelHandleFactory.getOrCreateHandle(group); pChannelInfo->m_pVolumeControl = new ControlAudioTaperPot( ConfigKey(group, "volume"), -20, 0, 1); pChannelInfo->m_pVolumeControl->setDefaultValue(1.0); pChannelInfo->m_pVolumeControl->set(1.0); pChannelInfo->m_pMuteControl = new ControlPushButton( ConfigKey(group, "mute")); pChannelInfo->m_pMuteControl->setButtonMode(ControlPushButton::POWERWINDOW); pChannelInfo->m_pBuffer = SampleUtil::alloc(MAX_BUFFER_LEN); SampleUtil::clear(pChannelInfo->m_pBuffer, MAX_BUFFER_LEN); m_channels.append(pChannelInfo); const GainCache gainCacheDefault = {0, false}; m_channelHeadphoneGainCache.append(gainCacheDefault); m_channelTalkoverGainCache.append(gainCacheDefault); m_channelMasterGainCache.append(gainCacheDefault); // Pre-allocate scratch buffers to avoid memory allocation in the // callback. QVarLengthArray does nothing if reserve is called with a size // smaller than its pre-allocation. m_activeChannels.reserve(m_channels.size()); m_activeBusChannels[EngineChannel::LEFT].reserve(m_channels.size()); m_activeBusChannels[EngineChannel::CENTER].reserve(m_channels.size()); m_activeBusChannels[EngineChannel::RIGHT].reserve(m_channels.size()); m_activeHeadphoneChannels.reserve(m_channels.size()); m_activeTalkoverChannels.reserve(m_channels.size()); EngineBuffer* pBuffer = pChannelInfo->m_pChannel->getEngineBuffer(); if (pBuffer != NULL) { pBuffer->bindWorkers(m_pWorkerScheduler); } }
TrackPointer BaseTrackPlayerImpl::loadFakeTrack(bool bPlay, double filebpm) { TrackPointer pTrack(Track::newTemporary()); pTrack->setSampleRate(44100); // 10 seconds pTrack->setDuration(10); if (filebpm > 0) { pTrack->setBpm(filebpm); } TrackPointer pOldTrack = m_pLoadedTrack; m_pLoadedTrack = pTrack; if (m_pLoadedTrack) { // Listen for updates to the file's BPM connect(m_pLoadedTrack.get(), SIGNAL(bpmUpdated(double)), m_pFileBPM.get(), SLOT(set(double))); connect(m_pLoadedTrack.get(), SIGNAL(keyUpdated(double)), m_pKey.get(), SLOT(set(double))); // Listen for updates to the file's Replay Gain connect(m_pLoadedTrack.get(), SIGNAL(ReplayGainUpdated(mixxx::ReplayGain)), this, SLOT(slotSetReplayGain(mixxx::ReplayGain))); } // Request a new track from EngineBuffer EngineBuffer* pEngineBuffer = m_pChannel->getEngineBuffer(); pEngineBuffer->loadFakeTrack(pTrack, bPlay); // await slotTrackLoaded()/slotLoadFailed() emit(loadingTrack(pTrack, pOldTrack)); return pTrack; }
void EngineMaster::addChannel(EngineChannel* pChannel) { ChannelInfo* pChannelInfo = new ChannelInfo(); pChannelInfo->m_pChannel = pChannel; pChannelInfo->m_pVolumeControl = new ControlLogpotmeter( ConfigKey(pChannel->getGroup(), "volume"), 1.0); pChannelInfo->m_pVolumeControl->setDefaultValue(1.0); pChannelInfo->m_pVolumeControl->set(1.0); pChannelInfo->m_pBuffer = SampleUtil::alloc(MAX_BUFFER_LEN); SampleUtil::applyGain(pChannelInfo->m_pBuffer, 0, MAX_BUFFER_LEN); m_channels.push_back(pChannelInfo); EngineBuffer* pBuffer = pChannelInfo->m_pChannel->getEngineBuffer(); if (pBuffer != NULL) { pBuffer->bindWorkers(m_pWorkerScheduler); pBuffer->setEngineMaster(this); } }
void EngineMaster::addChannel(EngineChannel* pChannel) { ChannelInfo* pChannelInfo = new ChannelInfo(); pChannelInfo->m_pChannel = pChannel; pChannelInfo->m_pVolumeControl = new ControlLogpotmeter( ConfigKey(pChannel->getGroup(), "volume"), 1.0); pChannelInfo->m_pVolumeControl->setDefaultValue(1.0); pChannelInfo->m_pVolumeControl->set(1.0); pChannelInfo->m_pMuteControl = new ControlPushButton( ConfigKey(pChannel->getGroup(), "mute")); pChannelInfo->m_pMuteControl->setButtonMode(ControlPushButton::POWERWINDOW); pChannelInfo->m_pBuffer = SampleUtil::alloc(MAX_BUFFER_LEN); SampleUtil::clear(pChannelInfo->m_pBuffer, MAX_BUFFER_LEN); m_channels.push_back(pChannelInfo); m_channelHeadphoneGainCache.push_back(0); for (int o = EngineChannel::LEFT; o <= EngineChannel::RIGHT; o++) { m_channelMasterGainCache.push_back(0); } EngineBuffer* pBuffer = pChannelInfo->m_pChannel->getEngineBuffer(); if (pBuffer != NULL) { pBuffer->bindWorkers(m_pWorkerScheduler); pBuffer->setEngineMaster(this); } }
double BpmControl::getNearestPositionInPhase(double dThisPosition, bool respectLoops, bool playing) { // Without a beatgrid, we don't know the phase offset. if (!m_pBeats) { return dThisPosition; } // Master buffer is always in sync! if (getSyncMode() == 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(m_pBeats, dThisPosition, &dThisPrevBeat, &dThisNextBeat, &dThisBeatLength, NULL)) { return dThisPosition; } } else { if (!getBeatContextNoLookup(dThisPosition, dThisPrevBeat, dThisNextBeat, &dThisBeatLength, NULL)) { return dThisPosition; } } double dOtherBeatFraction; if (getSyncMode() == SYNC_FOLLOWER) { // If we're a follower, it's easy to get the other beat fraction dOtherBeatFraction = m_dSyncTargetBeatDistance; } else { // If not, we have to figure it out EngineBuffer* pOtherEngineBuffer = pickSyncTarget(); if (pOtherEngineBuffer == NULL) { return dThisPosition; } if (playing) { // "this" track is playing, or just starting // only match phase if the sync target is playing as well if (pOtherEngineBuffer->getSpeed() == 0.0) { 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) * 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 = m_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; }
bool BpmControl::syncTempo() { EngineBuffer* pOtherEngineBuffer = pickSyncTarget(); if (!pOtherEngineBuffer) { return false; } double fThisBpm = m_pEngineBpm->get(); double fThisLocalBpm = m_pLocalBpm->get(); double fOtherBpm = pOtherEngineBuffer->getBpm(); double fOtherLocalBpm = pOtherEngineBuffer->getLocalBpm(); //qDebug() << "this" << "bpm" << fThisBpm << "filebpm" << fThisFileBpm; //qDebug() << "other" << "bpm" << fOtherBpm << "filebpm" << fOtherFileBpm; //////////////////////////////////////////////////////////////////////////// // Rough proof of how syncing works -- rryan 3/2011 // ------------------------------------------------ // // Let this and other denote this deck versus the sync-target deck. // // The goal is for this deck's effective BPM to equal the other decks. // // thisBpm = otherBpm // // The overall rate is the product of range, direction, and scale plus 1: // // rate = 1.0 + rateDir * rateRange * rateScale // // An effective BPM is the file-bpm times the rate: // // bpm = fileBpm * rate // // So our goal is to tweak thisRate such that this equation is true: // // thisFileBpm * (1.0 + thisRate) = otherFileBpm * (1.0 + otherRate) // // so rearrange this equation in terms of thisRate: // // thisRate = (otherFileBpm * (1.0 + otherRate)) / thisFileBpm - 1.0 // // So the new rateScale to set is: // // thisRateScale = ((otherFileBpm * (1.0 + otherRate)) / thisFileBpm - 1.0) / (thisRateDir * thisRateRange) if (fOtherBpm > 0.0 && fThisBpm > 0.0) { // The desired rate is the other decks effective rate divided by this // deck's file BPM. This gives us the playback rate that will produce an // effective BPM equivalent to the other decks. double desiredRate = fOtherBpm / fThisLocalBpm; // Test if this buffer's bpm is the double of the other one, and adjust // the rate scale. I believe this is intended to account for our BPM // algorithm sometimes finding double or half BPMs. This avoids drastic // scales. float fFileBpmDelta = fabs(fThisLocalBpm - fOtherLocalBpm); if (fabs(fThisLocalBpm * 2.0 - fOtherLocalBpm) < fFileBpmDelta) { desiredRate /= 2.0; } else if (fabs(fThisLocalBpm - 2.0 * fOtherLocalBpm) < fFileBpmDelta) { desiredRate *= 2.0; } // Subtract the base 1.0, now fDesiredRate is the percentage // increase/decrease in playback rate, not the playback rate. double desiredRateShift = desiredRate - 1.0; // Ensure the rate is within resonable boundaries. Remember, this is the // percent to scale the rate, not the rate itself. If fDesiredRate was -1, // that would mean the deck would be completely stopped. If fDesiredRate // is 1, that means it is playing at 2x speed. This limit enforces that // we are scaled between 0.5x and 2x. if (desiredRateShift < 1.0 && desiredRateShift > -0.5) { m_pEngineBpm->set(m_pLocalBpm->get() * desiredRate); // Adjust the rateScale. We have to divide by the range and // direction to get the correct rateScale. double desiredRateSlider = desiredRateShift / (m_pRateRange->get() * m_pRateDir->get()); // And finally, set the slider m_pRateSlider->set(desiredRateSlider); return true; } } return false; }