#include #include #include "vinylcontrol/vinylcontrolxwax.h" #include "util/timer.h" #include "control/controlproxy.h" #include "control/controlobject.h" #include "util/math.h" #include "util/defs.h" /****** TODO ******* Stuff to maybe implement here 1) The smoothing thing that xwax does 2) Tons of cleanup 3) Speed up needle dropping 4) Extrapolate small dropouts and keep track of "dynamics" ********************/ namespace { constexpr int kChannels = 2; } // namespace // Sample threshold below which we consider there to be no signal. const double kMinSignal = 75.0 / SAMPLE_MAXIMUM; bool VinylControlXwax::s_bLUTInitialized = false; QMutex VinylControlXwax::s_xwaxLUTMutex; VinylControlXwax::VinylControlXwax(UserSettingsPointer pConfig, const QString& group) : VinylControl(pConfig, group), m_dVinylPositionOld(0.0), m_pWorkBuffer(new short[MAX_BUFFER_LEN]), m_workBufferSize(MAX_BUFFER_LEN), m_iQualPos(0), m_iQualFilled(0), m_iPosition(-1), m_bAtRecordEnd(false), m_bForceResync(false), m_iVCMode(static_cast(mode->get())), m_iOldVCMode(MIXXX_VCMODE_ABSOLUTE), m_dOldFilePos(0.0), m_dOldDuration(0.0), m_dOldDurationInaccurate(-1.0), m_bWasReversed(false), m_pPitchRing(nullptr), m_iPitchRingSize(0), m_iPitchRingPos(0), m_iPitchRingFilled(0), m_dDisplayPitch(0.0), m_pSteadySubtle(nullptr), m_pSteadyGross(nullptr), m_bCDControl(false), m_bTrackSelectMode(false), m_pControlTrackSelector(nullptr), m_pControlTrackLoader(nullptr), m_dLastTrackSelectPos(0.0), m_dCurTrackSelectPos(0.0), m_dDriftAmt(0.0), m_dUiUpdateTime(-1.0) { // TODO(rryan): Should probably live in VinylControlManager since it's not // specific to a VC deck. signalenabled->slotSet(m_pConfig->getValueString( ConfigKey(VINYL_PREF_KEY, "show_signal_quality")).toInt()); // Get the vinyl type and speed. QString strVinylType = m_pConfig->getValueString( ConfigKey(group,"vinylcontrol_vinyl_type")); QString strVinylSpeed = m_pConfig->getValueString( ConfigKey(group,"vinylcontrol_speed_type")); // libxwax indexes by C-strings so we pass libxwax string literals so we // don't have to deal with freeing the strings later char* timecode = nullptr; if (strVinylType == MIXXX_VINYL_SERATOCV02VINYLSIDEA) { timecode = (char*)"serato_2a"; } else if (strVinylType == MIXXX_VINYL_SERATOCV02VINYLSIDEB) { timecode = (char*)"serato_2b"; } else if (strVinylType == MIXXX_VINYL_SERATOCD) { timecode = (char*)"serato_cd"; m_bCDControl = true; // Set up very sensitive steady monitors for CDJs. m_pSteadySubtle = new SteadyPitch(0.06, true); m_pSteadyGross = new SteadyPitch(0.25, true); } else if (strVinylType == MIXXX_VINYL_TRAKTORSCRATCHSIDEA) { timecode = (char*)"traktor_a"; } else if (strVinylType == MIXXX_VINYL_TRAKTORSCRATCHSIDEB) { timecode = (char*)"traktor_b"; } else if (strVinylType == MIXXX_VINYL_MIXVIBESDVS) { timecode = (char*)"mixvibes_v2"; } else { qDebug() << "Unknown vinyl type, defaulting to serato_2a"; timecode = (char*)"serato_2a"; } // If we didn't set up the steady monitors already (not CDJ), do it now. if (m_pSteadySubtle == nullptr) { m_pSteadySubtle = new SteadyPitch(0.12, false); } if (m_pSteadyGross == nullptr) { m_pSteadyGross = new SteadyPitch(0.5, false); } timecode_def* tc_def = timecoder_find_definition(timecode); if (tc_def == nullptr) { qDebug() << "Error finding timecode definition for " << timecode << ", defaulting to serato_2a"; timecode = (char*)"serato_2a"; tc_def = timecoder_find_definition(timecode); } double speed = 1.0; double rpm = 100.0 / 3.0; if (strVinylSpeed == MIXXX_VINYL_SPEED_45) { rpm = 45.0; speed = 1.35; } double latency = ControlObject::get( ConfigKey("[Master]", "latency")); if (latency <= 0 || latency > 200) { qDebug() << "Failed to get sane latency, assuming 20 as a reasonable value"; latency = 20; } int iSampleRate = m_pConfig->getValueString( ConfigKey("[Soundcard]","Samplerate")).toULong(); // Set pitch ring size to 1/4 of one revolution -- a full revolution adds // too much stickiness to the pitch. m_iPitchRingSize = static_cast(60000 / (rpm * latency * 4)); m_pPitchRing = new double[m_iPitchRingSize]; qDebug() << "Xwax Vinyl control starting with a sample rate of:" << iSampleRate; qDebug() << "Building timecode lookup tables for" << strVinylType << "with speed" << strVinylSpeed; // Initialize the timecoder structure. Use the static mutex so that we only // do this once across the VinylControlXwax instances. s_xwaxLUTMutex.lock(); timecoder_init(&timecoder, tc_def, speed, iSampleRate, /* phono */ false); timecoder_monitor_init(&timecoder, MIXXX_VINYL_SCOPE_SIZE); //Note that timecoder_init will not double-malloc the LUTs, and after this we are guaranteed //that the LUT has been generated unless we ran out of memory. s_bLUTInitialized = true; m_uiSafeZone = timecoder_get_safe(&timecoder); //} s_xwaxLUTMutex.unlock(); qDebug() << "Starting vinyl control xwax thread"; } VinylControlXwax::~VinylControlXwax() { delete m_pSteadySubtle; delete m_pSteadyGross; delete [] m_pPitchRing; delete [] m_pWorkBuffer; // Cleanup xwax nicely timecoder_monitor_clear(&timecoder); timecoder_clear(&timecoder); // TODO(rryan): This looks wrong. freeLUTs is called later by // VinylControlProcessor so we are probably leaking the LUTs. s_bLUTInitialized = false; m_pVCRate->set(0.0); } //static void VinylControlXwax::freeLUTs() { s_xwaxLUTMutex.lock(); //Static mutex! We don't want two threads doing this! if (s_bLUTInitialized) { timecoder_free_lookup(); //Frees all the LUTs in xwax. s_bLUTInitialized = false; } s_xwaxLUTMutex.unlock(); } bool VinylControlXwax::writeQualityReport(VinylSignalQualityReport* pReport) { if (pReport) { pReport->timecode_quality = m_fTimecodeQuality; pReport->angle = getAngle(); memcpy(pReport->scope, timecoder.mon, sizeof(pReport->scope)); return true; } return false; } void VinylControlXwax::analyzeSamples(CSAMPLE* pSamples, size_t nFrames) { ScopedTimer t("VinylControlXwax::analyzeSamples"); auto gain = static_cast(m_pVinylControlInputGain->get()); // We only support amplifying with the VC pre-amp. if (gain < 1.0f) { gain = 1.0f; } size_t samplesSize = nFrames * kChannels; if (samplesSize > m_workBufferSize) { delete [] m_pWorkBuffer; m_pWorkBuffer = new short[samplesSize]; m_workBufferSize = samplesSize; } // Convert CSAMPLE samples to shorts, preventing overflow. for (int i = 0; i < static_cast(samplesSize); ++i) { CSAMPLE sample = pSamples[i] * gain * SAMPLE_MAXIMUM; if (sample > SAMPLE_MAXIMUM) { m_pWorkBuffer[i] = SAMPLE_MAXIMUM; } else if (sample < SAMPLE_MINIMUM) { m_pWorkBuffer[i] = SAMPLE_MINIMUM; } else { m_pWorkBuffer[i] = static_cast(sample); } } // Submit the samples to the xwax timecode processor. The size argument is // in stereo frames. timecoder_submit(&timecoder, m_pWorkBuffer, nFrames); bool bHaveSignal = fabs(pSamples[0]) + fabs(pSamples[1]) > kMinSignal; //qDebug() << "signal?" << bHaveSignal; //TODO: Move all these config object get*() calls to an "updatePrefs()" function, // and make that get called when any options get changed in the preferences dialog, rather than // polling everytime we get a buffer. // Check if vinyl control is enabled... m_bIsEnabled = enabled && checkEnabled(m_bIsEnabled, enabled->toBool()); if(bHaveSignal) { // Always analyze the input samples m_iPosition = timecoder_get_position(&timecoder, nullptr); //Notify the UI if the timecode quality is good establishQuality(m_iPosition != -1); } //are we even playing and enabled at all? if (!m_bIsEnabled) { return; } double dVinylPitch = timecoder_get_pitch(&timecoder); // Has a new track been loaded? Currently we use track duration which is // integer seconds in the song. However, for calculations we need the // higher-accuracy duration found by dividing the track samples by the // samplerate. // TODO(XXX): we should really sync on all track changes // TODO(rryan): Should we calculate the true duration to check if it // changed? It's just an extra division by trackSampleRate. double duration_inaccurate = duration->get(); if (duration_inaccurate != m_dOldDurationInaccurate) { m_bForceResync = true; m_bTrackSelectMode = false; //just in case m_dOldDurationInaccurate = duration_inaccurate; m_dOldDuration = trackSamples->get() / 2 / trackSampleRate->get(); // we were at record end, so turn it off and restore mode if(m_bAtRecordEnd) { disableRecordEndMode(); if (m_iOldVCMode == MIXXX_VCMODE_CONSTANT) { m_iVCMode = MIXXX_VCMODE_RELATIVE; } else { m_iVCMode = m_iOldVCMode; } } } // make sure m_dVinylPosition only has good values if (m_iPosition != -1) { m_dVinylPosition = static_cast(m_iPosition) / 1000.0 - m_iLeadInTime; } // Initialize drift control to zero in case we don't get any position data // to calculate it with. double dDriftControl = 0.0; // Get the playback position in the file in seconds. double filePosition = playPos->get() * m_dOldDuration; int reportedMode = static_cast(mode->get()); bool reportedPlayButton = playButton->toBool(); if (m_iVCMode != reportedMode) { //if we are playing, don't allow change //to absolute mode (would cause sudden track skip) if (reportedPlayButton && reportedMode == MIXXX_VCMODE_ABSOLUTE) { m_iVCMode = MIXXX_VCMODE_RELATIVE; mode->slotSet((double)m_iVCMode); } else { // go ahead and switch m_iVCMode = reportedMode; if (reportedMode == MIXXX_VCMODE_ABSOLUTE) { m_bForceResync = true; } } //if we are out of error mode... if (vinylStatus->get() == VINYL_STATUS_ERROR && m_iVCMode == MIXXX_VCMODE_RELATIVE) { vinylStatus->slotSet(VINYL_STATUS_OK); } } //if looping has been enabled, don't allow absolute mode if (loopEnabled->toBool() && m_iVCMode == MIXXX_VCMODE_ABSOLUTE) { m_iVCMode = MIXXX_VCMODE_RELATIVE; mode->slotSet((double)m_iVCMode); } // Don't allow cueing mode to be enabled in absolute mode. if (m_iVCMode == MIXXX_VCMODE_ABSOLUTE && cueing->get() != MIXXX_RELATIVE_CUE_OFF) { cueing->set(MIXXX_RELATIVE_CUE_OFF); } //are we newly playing near the end of the record? (in absolute mode, this happens //when the filepos is past safe (more accurate), //but it can also happen in relative mode if the vinylpos is nearing the end //If so, change to constant mode so DJ can move the needle safely if (!m_bAtRecordEnd && reportedPlayButton) { if (m_iVCMode == MIXXX_VCMODE_ABSOLUTE && (filePosition + m_iLeadInTime) * 1000.0 > m_uiSafeZone && !m_bForceResync) { // corner case: we are waiting for resync so don't enable just yet enableRecordEndMode(); } else if (m_iVCMode != MIXXX_VCMODE_ABSOLUTE && m_iPosition != -1 && m_iPosition > static_cast(m_uiSafeZone)) { enableRecordEndMode(); } } if (m_bAtRecordEnd) { //if m_bAtRecordEnd was true, maybe it no longer applies: if (!reportedPlayButton) { //if we turned off play button, also disable disableRecordEndMode(); } else if (m_iPosition != -1 && m_iPosition <= static_cast(m_uiSafeZone) && m_dVinylPosition > 0 && checkSteadyPitch(dVinylPitch, filePosition) > 0.5) { //if good position, and safe, and not in leadin, and steady, //disable disableRecordEndMode(); } if (m_bAtRecordEnd) { //ok, it's still valid, blink if ((reportedPlayButton && (int)(filePosition * 2.0) % 2) || (!reportedPlayButton && (int)(m_iPosition / 500.0) % 2)) { vinylStatus->slotSet(VINYL_STATUS_WARNING); } else { vinylStatus->slotSet(VINYL_STATUS_DISABLED); } } } //check here for position > safe, and if no record end mode, //then trigger track selection mode. just pass position to it //and ignore pitch if (!m_bAtRecordEnd) { if (m_iPosition != -1 && m_iPosition > static_cast(m_uiSafeZone)) { //only enable if pitch is steady, though. Heavy scratching can //produce crazy results and trigger this mode if (m_bTrackSelectMode || checkSteadyPitch(dVinylPitch, filePosition) > 0.1) { //until I can figure out how to detect "track 2" on serato CD, //don't try track selection if (!m_bCDControl) { if (!m_bTrackSelectMode) { qDebug() << "position greater than safe, select mode" << m_iPosition << m_uiSafeZone; m_bTrackSelectMode = true; togglePlayButton(false); resetSteadyPitch(0.0, 0.0); m_pVCRate->set(0.0); } doTrackSelection(true, dVinylPitch, m_iPosition); } //hm I wonder if track will keep playing while this happens? //not sure what we want to do here... probably enforce //stopped deck. //but if constant mode... nah, force stop. return; } //if it's not steady yet we process as normal } else { //so we're not unsafe.... but //if no position, but we were in select mode, do select mode if (m_iPosition == -1 && m_bTrackSelectMode) { //qDebug() << "no position, but were in select mode"; doTrackSelection(false, dVinylPitch, m_iPosition); //again, force stop? return; } else if (m_bTrackSelectMode) { //qDebug() << "discontinuing select mode, selecting track"; if (m_pControlTrackLoader == nullptr) { m_pControlTrackLoader = new ControlProxy( m_group, "LoadSelectedTrack", this); } m_pControlTrackLoader->slotSet(1.0); m_pControlTrackLoader->slotSet(0.0); // I think I have to do this... // if position is known and safe then no track select mode m_bTrackSelectMode = false; } } } if (m_iVCMode == MIXXX_VCMODE_CONSTANT) { // when we enabled constant mode we set the rate slider // now we just either set scratch val to 0 (stops playback) // or 1 (plays back at that rate) double newScratch = reportedPlayButton ? m_pRateRatio->get() : 0.0; m_pVCRate->set(newScratch); // is there any reason we'd need to do anything else? return; } // CONSTANT MODE NO LONGER APPLIES... // When there's a timecode signal available // This is set when we analyze samples (no need for lock I think) if(bHaveSignal) { //POSITION: MAYBE PITCH: YES //We have pitch, but not position. so okay signal but not great (scratching / cueing?) //qDebug() << "Pitch" << dVinylPitch; if (m_iPosition != -1) { //POSITION: YES PITCH: YES //add a value to the pitch ring (for averaging / smoothing the pitch) //qDebug() << fabs(((m_dVinylPosition - m_dVinylPositionOld) * (dVinylPitch / fabs(dVinylPitch)))); bool reversed = static_cast(reverseButton->get()); if (!reversed && m_bWasReversed) { resetSteadyPitch(dVinylPitch, m_dVinylPosition); } m_bWasReversed = reversed; //save the absolute amount of drift for when we need to estimate vinyl position m_dDriftAmt = m_dVinylPosition - filePosition; //qDebug() << "drift" << m_dDriftAmt; if (m_bForceResync) { //if forceresync was set but we're no longer absolute, //it no longer applies //if we're in relative mode then we'll do a sync //because it might select a cue if (m_iVCMode == MIXXX_VCMODE_ABSOLUTE || (m_iVCMode == MIXXX_VCMODE_RELATIVE && cueing->toBool())) { syncPosition(); resetSteadyPitch(dVinylPitch, m_dVinylPosition); } m_bForceResync = false; } else if (fabs(m_dVinylPosition - filePosition) > 0.1 && m_dVinylPosition < -2.0) { //At first I thought it was a bug to resync to leadin in relative mode, //but after using it that way it's actually pretty convenient. //qDebug() << "Vinyl leadin"; syncPosition(); resetSteadyPitch(dVinylPitch, m_dVinylPosition); if (uiUpdateTime(filePosition)) { m_pRateRatio->set(fabs(dVinylPitch)); } } else if (m_iVCMode == MIXXX_VCMODE_ABSOLUTE && (fabs(m_dVinylPosition - m_dVinylPositionOld) >= 5.0)) { //If the position from the timecode is more than a few seconds off, resync the position. //qDebug() << "resync position (>15.0 sec)"; //qDebug() << m_dVinylPosition << m_dVinylPositionOld << m_dVinylPosition - m_dVinylPositionOld; syncPosition(); resetSteadyPitch(dVinylPitch, m_dVinylPosition); } else if (m_iVCMode == MIXXX_VCMODE_ABSOLUTE && m_bCDControl && fabs(m_dVinylPosition - m_dVinylPositionOld) >= 0.1) { //qDebug() << "CDJ resync position (>0.1 sec)"; syncPosition(); resetSteadyPitch(dVinylPitch, m_dVinylPosition); } else if (playPos->get() >= 1.0 && dVinylPitch > 0) { //end of track, force stop togglePlayButton(false); resetSteadyPitch(0.0, 0.0); m_pVCRate->set(0.0); m_iPitchRingPos = 0; m_iPitchRingFilled = 0; return; } else { togglePlayButton(checkSteadyPitch(dVinylPitch, filePosition) > 0.5); } // Calculate how much the vinyl's position has drifted from it's timecode and compensate for it. // (This is caused by the manufacturing process of the vinyl.) if (m_iVCMode == MIXXX_VCMODE_ABSOLUTE && fabs(m_dDriftAmt) > 0.1 && fabs(m_dDriftAmt) < 5.0) { dDriftControl = m_dDriftAmt * .01; } else { dDriftControl = 0.0; } m_dVinylPositionOld = m_dVinylPosition; } else { //POSITION: NO PITCH: YES //if we don't have valid position, we're not playing so reset time to current //estimate vinyl position if (playPos->get() >= 1.0 && dVinylPitch > 0) { //end of track, force stop togglePlayButton(false); resetSteadyPitch(0.0, 0.0); m_pVCRate->set(0.0); m_iPitchRingPos = 0; m_iPitchRingFilled = 0; return; } if (m_iVCMode == MIXXX_VCMODE_ABSOLUTE && fabs(dVinylPitch) < 0.05 && fabs(m_dDriftAmt) >= 0.3) { //qDebug() << "slow, out of sync, syncing position"; syncPosition(); } m_dVinylPositionOld = filePosition + m_dDriftAmt; if (dVinylPitch > 0.2) { togglePlayButton(checkSteadyPitch(dVinylPitch, filePosition) > 0.5); } } //playbutton status may have changed reportedPlayButton = playButton->toBool(); if (reportedPlayButton) { // Only add to the ring if pitch is stable m_pPitchRing[m_iPitchRingPos] = dVinylPitch; if (m_iPitchRingFilled < m_iPitchRingSize) { m_iPitchRingFilled++; } m_iPitchRingPos = (m_iPitchRingPos + 1) % m_iPitchRingSize; } else { // Reset ring if pitch isn't steady m_iPitchRingPos = 0; m_iPitchRingFilled = 0; } //only smooth when we have good position (no smoothing for scratching) double averagePitch = 0.0; if (m_iPosition != -1 && reportedPlayButton) { for (int i = 0; i < m_iPitchRingFilled; ++i) { averagePitch += m_pPitchRing[i]; } averagePitch /= m_iPitchRingFilled; // Round out some of the noise averagePitch = round(averagePitch * 10000.0); averagePitch /= 10000.0; } else { averagePitch = dVinylPitch; } m_pVCRate->set(averagePitch + dDriftControl); if (uiUpdateTime(filePosition)) { double true_pitch = averagePitch + dDriftControl; double pitch_difference = true_pitch - m_dDisplayPitch; // The true pitch can show a misleading amount of variance -- // differences of .1% or less can show up as 1 or 2 bpm changes. // Therefore we react slowly to bpm changes to show a more steady // number to the user. if (fabs(pitch_difference) > 0.5) { // For large changes in pitch (start/stop, usually), immediately // update the display. m_dDisplayPitch = true_pitch; } else if (fabs(pitch_difference) > 0.005) { // For medium changes in pitch, take 4 callback loops to // converge on the correct amount. m_dDisplayPitch += pitch_difference * .25; } else { // For extremely small changes, converge very slowly. m_dDisplayPitch += pitch_difference * .01; } // Don't show extremely high or low speeds in the UI. if (reportedPlayButton && !scratching->toBool() && m_dDisplayPitch < 1.9 && m_dDisplayPitch > 0.2) { m_pRateRatio->set(m_dDisplayPitch); } else { m_pRateRatio->set(1.0); } m_dUiUpdateTime = filePosition; } m_dOldFilePos = filePosition; } else { // No pitch data available (the needle is up/stopped.... or *really* // crappy signal) //POSITION: NO PITCH: NO //if it's been a long time, we're stopped. //if it hasn't been long, //let the track play a wee bit more before deciding we've stopped m_pRateRatio->set(1.0); if (m_iVCMode == MIXXX_VCMODE_ABSOLUTE && fabs(m_dVinylPosition - filePosition) >= 0.1) { //qDebug() << "stopped, out of sync, syncing position"; syncPosition(); } if(fabs(filePosition - m_dOldFilePos) >= 0.1 || filePosition == m_dOldFilePos) { //We are not playing any more togglePlayButton(false); resetSteadyPitch(0.0, 0.0); m_pVCRate->set(0.0); //resetSteadyPitch(dVinylPitch, filePosition); // Notify the UI that the timecode quality is garbage/missing. m_fTimecodeQuality = 0.0f; m_iPitchRingPos = 0; m_iPitchRingFilled = 0; m_iQualPos = 0; m_iQualFilled = 0; m_bForceResync = true; vinylStatus->slotSet(VINYL_STATUS_OK); } } } void VinylControlXwax::enableRecordEndMode() { qDebug() << "record end, setting constant mode"; vinylStatus->slotSet(VINYL_STATUS_WARNING); enableConstantMode(); m_bAtRecordEnd = true; } void VinylControlXwax::enableConstantMode() { m_iOldVCMode = m_iVCMode; m_iVCMode = MIXXX_VCMODE_CONSTANT; mode->slotSet((double)m_iVCMode); togglePlayButton(true); double rate = m_pVCRate->get(); m_pRateRatio->set(fabs(rate)); m_pVCRate->set(rate); } void VinylControlXwax::enableConstantMode(double rate) { m_iOldVCMode = m_iVCMode; m_iVCMode = MIXXX_VCMODE_CONSTANT; mode->slotSet((double)m_iVCMode); togglePlayButton(true); m_pRateRatio->set(fabs(rate)); m_pVCRate->set(rate); } void VinylControlXwax::disableRecordEndMode() { vinylStatus->slotSet(VINYL_STATUS_OK); m_bAtRecordEnd = false; m_iVCMode = MIXXX_VCMODE_RELATIVE; mode->slotSet((double)m_iVCMode); } void VinylControlXwax::togglePlayButton(bool on) { if (m_bIsEnabled && (playButton->get() > 0) != on) { playButton->slotSet((float)on); //and we all float on all right } } void VinylControlXwax::doTrackSelection(bool valid_pos, double pitch, double position) { //compare positions, fabricating if we don't have position data, and //move the selector every so often //track will be selected when the needle is moved back to play area //track selection can be cancelled by loading a track manually const int SELECT_INTERVAL = 150; const double NOPOS_SPEED = 0.50; if (m_pControlTrackSelector == nullptr) { // this isn't done in the constructor because this object // doesn't seem to be created yet m_pControlTrackSelector = new ControlProxy( "[Playlist]","SelectTrackKnob", this); } if (!valid_pos) { if (fabs(pitch) > 0.1) { //how to estimate how far the record has moved when we don't have a valid //position and no mp3 track to compare with??? just add a bullshit amount? m_dCurTrackSelectPos += pitch * NOPOS_SPEED; //MADE UP CONSTANT, needs to be based on frames per second I think } else { // too slow, do nothing return; } } else { // if we have valid pos, use it m_dCurTrackSelectPos = position; } //we have position or at least record is moving, so check if we should //change location if (fabs(m_dCurTrackSelectPos - m_dLastTrackSelectPos) > 10.0 * 1000) { //yeah probably not a valid value //qDebug() << "large change in track position, resetting"; m_dLastTrackSelectPos = m_dCurTrackSelectPos; } else if (fabs(m_dCurTrackSelectPos - m_dLastTrackSelectPos) > SELECT_INTERVAL) { //only adjust by one at a time. It's no help jumping around m_pControlTrackSelector->set((int)(m_dCurTrackSelectPos - m_dLastTrackSelectPos) / fabs(m_dCurTrackSelectPos - m_dLastTrackSelectPos)); m_dLastTrackSelectPos = m_dCurTrackSelectPos; } } void VinylControlXwax::resetSteadyPitch(double pitch, double time) { m_pSteadySubtle->reset(pitch, time); m_pSteadyGross->reset(pitch, time); } double VinylControlXwax::checkSteadyPitch(double pitch, double time) { // If the track is in reverse we can't really know what's going on. if (m_bWasReversed) { return 0; } if (m_pSteadyGross->check(pitch, time) < 0.5) { scratching->slotSet(1.0); } else { scratching->slotSet(0.0); } return m_pSteadySubtle->check(pitch, time); } //Synchronize Mixxx's position to the position of the timecoded vinyl. void VinylControlXwax::syncPosition() { //qDebug() << "sync position" << m_dVinylPosition / m_dOldDuration; // VinylPos in seconds / total length of song. vinylSeek->slotSet(m_dVinylPosition / m_dOldDuration); } bool VinylControlXwax::checkEnabled(bool was, bool is) { // if we're not enabled, but the last object was, try turning ourselves on // XXX: is this just a race that's working right now? if (!is && wantenabled->get() > 0) { enabled->slotSet(true); wantenabled->slotSet(false); //don't try to do this over and over return true; //optimism! } if (was != is) { // we reset the scratch value, but we don't reset the rate slider. // This means if we are playing, and we disable vinyl control, // the track will keep playing at the previous rate. // This allows for single-deck control, dj handoffs, etc. togglePlayButton(playButton->toBool() || fabs(m_pVCRate->get()) > 0.05); m_pVCRate->set(m_pRateRatio->get()); resetSteadyPitch(0.0, 0.0); m_bForceResync = true; if (!was) { m_dOldFilePos = 0.0; } m_iVCMode = static_cast(mode->get()); m_bAtRecordEnd = false; } if (is && !was) { vinylStatus->slotSet(VINYL_STATUS_OK); } else if (!is) { vinylStatus->slotSet(VINYL_STATUS_DISABLED); } return is; } bool VinylControlXwax::uiUpdateTime(double now) { if (m_dUiUpdateTime > now || now - m_dUiUpdateTime > 0.05) { m_dUiUpdateTime = now; return true; } return false; } void VinylControlXwax::establishQuality(bool quality_sample) { m_bQualityRing[m_iQualPos] = quality_sample; if (m_iQualFilled < QUALITY_RING_SIZE) { m_iQualFilled++; } int quality = 0; for (int i = 0; i < m_iQualFilled; ++i) { if (m_bQualityRing[i]) { quality++; } } m_fTimecodeQuality = static_cast(quality) / static_cast(m_iQualFilled); m_iQualPos = (m_iQualPos + 1) % QUALITY_RING_SIZE; } float VinylControlXwax::getAngle() { float pos = timecoder_get_position(&timecoder, nullptr); if (pos == -1) { return -1.0; } const auto rps = static_cast(timecoder_revs_per_sec(&timecoder)); // Invert angle to make vinyl spin direction correct. return 360 - (static_cast(pos / 1000.0f * 360.0f * rps) % 360); }