Skip to content

Commit

Permalink
repeat: after last track sample, fill buffer with samples from track …
Browse files Browse the repository at this point in the history
…start ...

with samples from end if playing in reverse.

Previously, the last buffer at track end was padded with silence which makes the track longer than
it actually is. With loop tracks (length is exactly n beats) this causes beat offsets.

NOTE This is just a minimal invasive POC hack that abuses LoopingControl::nextTrigger because that
is called anyways, can already read track samples and does basically the same wrapping for loops.
Maybe there is a better place or a separate function is required.
  • Loading branch information
ronso0 committed May 3, 2023
1 parent 2b15819 commit b92320d
Show file tree
Hide file tree
Showing 13 changed files with 88 additions and 45 deletions.
5 changes: 5 additions & 0 deletions src/engine/bufferscalers/enginebufferscale.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ class EngineBufferScale : public QObject {
m_dPitchRatio = *pPitchRatio;
}

virtual void setRepeat(bool repeat) {
m_repeat = repeat;
}

// Set the desired output sample rate.
void setSampleRate(
mixxx::audio::SampleRate sampleRate);
Expand Down Expand Up @@ -73,4 +77,5 @@ class EngineBufferScale : public QObject {
bool m_bSpeedAffectsPitch;
double m_dTempoRatio;
double m_dPitchRatio;
bool m_repeat;
};
10 changes: 6 additions & 4 deletions src/engine/bufferscalers/enginebufferscalelinear.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ double EngineBufferScaleLinear::scaleBuffer(
//qDebug() << "extra samples" << extra_samples;

SINT next_samples_read = m_pReadAheadManager->getNextSamples(
rate_add_new, m_bufferInt, extra_samples);
rate_add_new, m_bufferInt, extra_samples, m_repeat);
frames_read += getOutputSignal().samples2frames(next_samples_read);
}
// force a buffer read:
Expand Down Expand Up @@ -145,8 +145,8 @@ SINT EngineBufferScaleLinear::do_copy(CSAMPLE* buf, SINT buf_size) {
// to call getNextSamples until you receive the number of samples you
// wanted.
while (samples_needed > 0) {
SINT read_size = m_pReadAheadManager->getNextSamples(m_dRate, write_buf,
samples_needed);
SINT read_size = m_pReadAheadManager->getNextSamples(
m_dRate, write_buf, samples_needed, m_repeat);
if (read_size == 0) {
if (++read_failed_count > 1) {
break;
Expand Down Expand Up @@ -287,7 +287,9 @@ SINT EngineBufferScaleLinear::do_scale(CSAMPLE* buf, SINT buf_size) {

m_bufferIntSize = m_pReadAheadManager->getNextSamples(
rate_new == 0 ? rate_old : rate_new,
m_bufferInt, samples_to_read);
m_bufferInt,
samples_to_read,
m_repeat);

if (m_bufferIntSize == 0) {
if (++read_failed_count > 1) {
Expand Down
11 changes: 6 additions & 5 deletions src/engine/bufferscalers/enginebufferscalerubberband.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,12 @@ double EngineBufferScaleRubberBand::scaleBuffer(

if (remaining_frames > 0 && iLenFramesRequired > 0) {
SINT iAvailSamples = m_pReadAheadManager->getNextSamples(
// The value doesn't matter here. All that matters is we
// are going forward or backward.
(m_bBackwards ? -1.0 : 1.0) * m_dBaseRate * m_dTempoRatio,
m_buffer_back,
getOutputSignal().frames2samples(iLenFramesRequired));
// The value doesn't matter here. All that matters is we
// are going forward or backward.
(m_bBackwards ? -1.0 : 1.0) * m_dBaseRate * m_dTempoRatio,
m_buffer_back,
getOutputSignal().frames2samples(iLenFramesRequired),
m_repeat);
SINT iAvailFrames = getOutputSignal().samples2frames(iAvailSamples);

if (iAvailFrames > 0) {
Expand Down
11 changes: 6 additions & 5 deletions src/engine/bufferscalers/enginebufferscalest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,12 @@ double EngineBufferScaleST::scaleBuffer(

if (remaining_frames > 0) {
SINT iAvailSamples = m_pReadAheadManager->getNextSamples(
// The value doesn't matter here. All that matters is we
// are going forward or backward.
(m_bBackwards ? -1.0 : 1.0) * m_dBaseRate * m_dTempoRatio,
buffer_back.data(),
buffer_back.size());
// The value doesn't matter here. All that matters is we
// are going forward or backward.
(m_bBackwards ? -1.0 : 1.0) * m_dBaseRate * m_dTempoRatio,
buffer_back.data(),
buffer_back.size(),
m_repeat);
SINT iAvailFrames = getOutputSignal().samples2frames(iAvailSamples);

if (iAvailFrames > 0) {
Expand Down
16 changes: 15 additions & 1 deletion src/engine/controls/loopingcontrol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,8 @@ void LoopingControl::process(const double dRate,

double LoopingControl::nextTrigger(bool reverse,
const double currentSample,
double *pTarget) {
double* pTarget,
bool repeat) {
*pTarget = kNoTrigger;

LoopSamples loopSamples = m_loopSamples.getValue();
Expand Down Expand Up @@ -423,6 +424,19 @@ double LoopingControl::nextTrigger(bool reverse,
return loopSamples.end;
}
}

// TEST hack: Abuse LoopingControl for wrapping around at track end / start (reverse)
// if repeat is enabled
if (repeat) {
if (reverse) {
*pTarget = m_pTrackSamples->get();
return 0.0;
} else {
*pTarget = 0.0;
return m_pTrackSamples->get();
}
}

return kNoTrigger;
}

Expand Down
5 changes: 3 additions & 2 deletions src/engine/controls/loopingcontrol.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ class LoopingControl : public EngineControl {
// nextTrigger returns the sample at which the engine will be triggered to
// take a loop, given the value of currentSample and dRate.
virtual double nextTrigger(bool reverse,
const double currentSample,
double *pTarget);
const double currentSample,
double* pTarget,
bool repeat);

// hintReader will add to hintList hints both the loop in and loop out
// sample, if set.
Expand Down
19 changes: 13 additions & 6 deletions src/engine/enginebuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,12 @@ void EngineBuffer::processTrackLocked(

m_rate_old = rate;

bool repeat = m_pRepeat->toBool();
if (m_repeat_old != repeat) {
m_pScale->setRepeat(repeat);
m_repeat_old = repeat;
}

// If the buffer is not paused, then scale the audio.
if (!bCurBufferPaused) {
// Perform scaling of Reader buffer into buffer.
Expand All @@ -973,6 +979,8 @@ void EngineBuffer::processTrackLocked(
// Note: The last buffer of a track is padded with silence.
// This silence is played together with the last samples in the last
// callback and the m_filepos_play is advanced behind the end of the track.
// This does not apply when repeat is enabled, sclaer->scaleBuffer()
// wraps around at end/start.

if (m_bCrossfadeReady) {
// Bring pOutput with the new parameters in and fade out the old one,
Expand Down Expand Up @@ -1005,20 +1013,19 @@ void EngineBuffer::processTrackLocked(
m_scratching_old = is_scratching;

// Handle repeat mode
bool at_start = m_filepos_play <= 0;
// bool at_start = m_filepos_play <= 0;
at_end = m_filepos_play >= m_trackSamplesOld;

bool repeat_enabled = m_pRepeat->toBool();

bool end_of_track = //(at_start && backwards) ||
(at_end && !backwards);

// If playbutton is pressed, check if we are at start or end of track
if ((m_playButton->toBool() || (m_fwdButton->toBool() || m_backButton->toBool()))
&& end_of_track) {
if (repeat_enabled) {
double fractionalPos = at_start ? 1.0 : 0;
doSeekFractional(fractionalPos, SEEK_STANDARD);
// TODO Remove this, ReadAheadManager should already have seeked to start/end
if (repeat) {
// double fractionalPos = at_start ? 1.0 : 0;
// doSeekFractional(fractionalPos, SEEK_STANDARD);
} else {
m_playButton->set(0.);
}
Expand Down
3 changes: 3 additions & 0 deletions src/engine/enginebuffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,9 @@ class EngineBuffer : public EngineObject {
// True if the previous callback was reverse.
bool m_reverse_old;

// True if repeat was enabled in the previous callback.
bool m_repeat_old;

// The previous callback's pitch. Used to check if the scaler parameters
// need updating.
double m_pitch_old;
Expand Down
6 changes: 3 additions & 3 deletions src/engine/readaheadmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ ReadAheadManager::~ReadAheadManager() {
SampleUtil::free(m_pCrossFadeBuffer);
}

SINT ReadAheadManager::getNextSamples(double dRate, CSAMPLE* pOutput,
SINT requested_samples) {
SINT ReadAheadManager::getNextSamples(
double dRate, CSAMPLE* pOutput, SINT requested_samples, bool repeat) {
// TODO(XXX): Remove implicit assumption of 2 channels
if (!even(requested_samples)) {
qDebug() << "ERROR: Non-even requested_samples to ReadAheadManager::getNextSamples";
Expand All @@ -49,7 +49,7 @@ SINT ReadAheadManager::getNextSamples(double dRate, CSAMPLE* pOutput,
double target;
// A loop will only limit the amount we can read in one shot.
const double loop_trigger = m_pLoopingControl->nextTrigger(
in_reverse, m_currentPosition, &target);
in_reverse, m_currentPosition, &target, repeat);

SINT preloop_samples = 0;
double samplesToLoopTrigger = 0.0;
Expand Down
2 changes: 1 addition & 1 deletion src/engine/readaheadmanager.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class ReadAheadManager {
/// direction the audio is progressing in. Returns the total number of
/// samples read into buffer. Note that it is very common that the total
/// samples read is less than the requested number of samples.
virtual SINT getNextSamples(double dRate, CSAMPLE* buffer, SINT requested_samples);
virtual SINT getNextSamples(double dRate, CSAMPLE* buffer, SINT requested_samples, bool repeat);

/// Used to add a new EngineControls that ReadAheadManager will use to decide
/// which samples to return.
Expand Down
25 changes: 16 additions & 9 deletions src/test/enginebufferscalelineartest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ class ReadAheadManagerMock : public ReadAheadManager {
m_pBuffer(NULL),
m_iBufferSize(0),
m_iReadPosition(0),
m_iSamplesRead(0) {
m_iSamplesRead(0),
m_repeat(false) {
}

SINT getNextSamplesFake(double dRate, CSAMPLE* buffer, SINT requested_samples) {
SINT getNextSamplesFake(double dRate, CSAMPLE* buffer, SINT requested_samples, bool repeat) {
Q_UNUSED(dRate);
Q_UNUSED(repeat);
bool hasBuffer = m_pBuffer != NULL;
// You forgot to set the mock read buffer.
EXPECT_TRUE(hasBuffer);
Expand All @@ -51,12 +53,17 @@ class ReadAheadManagerMock : public ReadAheadManager {
return m_iSamplesRead;
}

MOCK_METHOD3(getNextSamples, SINT(double dRate, CSAMPLE* buffer, SINT requested_samples));
MOCK_METHOD4(getNextSamples,
SINT(double dRate,
CSAMPLE* buffer,
SINT requested_samples,
bool repeat));

CSAMPLE* m_pBuffer;
SINT m_iBufferSize;
SINT m_iReadPosition;
SINT m_iSamplesRead;
bool m_repeat;
};

class EngineBufferScaleLinearTest : public MixxxTest {
Expand Down Expand Up @@ -137,7 +144,7 @@ TEST_F(EngineBufferScaleLinearTest, ScaleConstant) {
m_pReadAheadMock->setReadBuffer(readBuffer, 1);

// Tell the RAMAN mock to invoke getNextSamplesFake
EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _))
EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _, _))
.WillRepeatedly(Invoke(m_pReadAheadMock, &ReadAheadManagerMock::getNextSamplesFake));

CSAMPLE* pOutput = SampleUtil::alloc(kiLinearScaleReadAheadLength);
Expand All @@ -157,7 +164,7 @@ TEST_F(EngineBufferScaleLinearTest, UnityRateIsSamplePerfect) {
SetRateNoLerp(1.0);

// Tell the RAMAN mock to invoke getNextSamplesFake
EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _))
EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _, _))
.WillRepeatedly(Invoke(m_pReadAheadMock, &ReadAheadManagerMock::getNextSamplesFake));

QVector<CSAMPLE> readBuffer;
Expand Down Expand Up @@ -189,7 +196,7 @@ TEST_F(EngineBufferScaleLinearTest, TestRateLERPMonotonicallyProgresses) {
m_pReadAheadMock->setReadBuffer(readBuffer, 1);

// Tell the RAMAN mock to invoke getNextSamplesFake
EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _))
EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _, _))
.WillRepeatedly(Invoke(m_pReadAheadMock, &ReadAheadManagerMock::getNextSamplesFake));

CSAMPLE* pOutput = SampleUtil::alloc(kiLinearScaleReadAheadLength);
Expand All @@ -214,7 +221,7 @@ TEST_F(EngineBufferScaleLinearTest, TestDoubleSpeedSmoothlyHalvesSamples) {
m_pReadAheadMock->setReadBuffer(readBuffer, 8);

// Tell the RAMAN mock to invoke getNextSamplesFake
EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _))
EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _, _))
.WillRepeatedly(Invoke(m_pReadAheadMock, &ReadAheadManagerMock::getNextSamplesFake));

CSAMPLE* pOutput = SampleUtil::alloc(kiLinearScaleReadAheadLength);
Expand Down Expand Up @@ -243,7 +250,7 @@ TEST_F(EngineBufferScaleLinearTest, TestHalfSpeedSmoothlyDoublesSamples) {
m_pReadAheadMock->setReadBuffer(readBuffer, 4);

// Tell the RAMAN mock to invoke getNextSamplesFake
EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _))
EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _, _))
.WillRepeatedly(Invoke(m_pReadAheadMock, &ReadAheadManagerMock::getNextSamplesFake));

CSAMPLE* pOutput = SampleUtil::alloc(kiLinearScaleReadAheadLength);
Expand Down Expand Up @@ -275,7 +282,7 @@ TEST_F(EngineBufferScaleLinearTest, TestRepeatedScaleCalls) {
m_pReadAheadMock->setReadBuffer(readBuffer, 4);

// Tell the RAMAN mock to invoke getNextSamplesFake
EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _))
EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _, _))
.WillRepeatedly(Invoke(m_pReadAheadMock, &ReadAheadManagerMock::getNextSamplesFake));

CSAMPLE expectedResult[] = { -101.0, 101.0,
Expand Down
2 changes: 1 addition & 1 deletion src/test/looping_control_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ TEST_F(LoopingControlTest, LoopScale_HalvesLoop) {
// the current sample should reseek based on the new loop size.
double target;
double trigger = m_pChannel1->getEngineBuffer()->m_pLoopingControl->nextTrigger(
false, 1800, &target);
false, 1800, &target, /* repeat */ false);
EXPECT_EQ(300, target);
EXPECT_EQ(1800, trigger);
}
Expand Down
18 changes: 10 additions & 8 deletions src/test/readaheadmanager_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,13 @@ class StubLoopControl : public LoopingControl {
}

double nextTrigger(bool reverse,
const double currentSample,
double* pTarget) override {
const double currentSample,
double* pTarget,
bool repeat) override {
Q_UNUSED(reverse);
Q_UNUSED(currentSample);
Q_UNUSED(pTarget);
Q_UNUSED(repeat);
RELEASE_ASSERT(!m_targetReturnValues.isEmpty());
*pTarget = m_targetReturnValues.takeFirst();
RELEASE_ASSERT(!m_triggerReturnValues.isEmpty());
Expand Down Expand Up @@ -129,17 +131,17 @@ TEST_F(ReadAheadManagerTest, FractionalFrameLoop) {
m_pLoopControl->pushTargetReturnValue(3.3);
m_pLoopControl->pushTargetReturnValue(kNoTrigger);
// read from start to loop trigger, overshoot 0.3
EXPECT_EQ(20, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 100));
EXPECT_EQ(20, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 100, false));
// read loop
EXPECT_EQ(18, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 80));
EXPECT_EQ(18, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 80, false));
// read loop
EXPECT_EQ(16, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 62));
EXPECT_EQ(16, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 62, false));
// read loop
EXPECT_EQ(18, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 46));
EXPECT_EQ(18, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 46, false));
// read loop
EXPECT_EQ(16, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 28));
EXPECT_EQ(16, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 28, false));
// read loop
EXPECT_EQ(12, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 12));
EXPECT_EQ(12, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 12, false));

// start 0.5 to 20.2 = 19.7
// loop 3.3 to 20.2 = 16.9
Expand Down

0 comments on commit b92320d

Please sign in to comment.