Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework Song::processNextBuffer #5723

Merged
merged 2 commits into from
Nov 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions include/Song.h
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,10 @@ class LMMS_EXPORT Song : public TrackContainer
{
return m_playPos[pm];
}
inline PlayPos & getPlayPos()
{
return getPlayPos(m_playMode);
}
inline const PlayPos & getPlayPos() const
{
return getPlayPos(m_playMode);
Expand Down
245 changes: 90 additions & 155 deletions src/core/Song.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
#include <QFileInfo>
#include <QMessageBox>

#include <algorithm>
#include <cmath>
#include <functional>

#include "AutomationTrack.h"
Expand Down Expand Up @@ -191,226 +193,159 @@ void Song::savePos()

void Song::processNextBuffer()
{
m_vstSyncController.setPlaybackJumped( false );
m_vstSyncController.setPlaybackJumped(false);

// if not playing, nothing to do
if( m_playing == false )
// If nothing is playing, there is nothing to do
if (!m_playing) { return; }

// At the beginning of the song, we have to reset the LFOs
if (m_playMode == Mode_PlaySong && getPlayPos() == 0)
{
return;
EnvelopeAndLfoParameters::instances()->reset();
}

TrackList trackList;
int tcoNum = -1; // track content object number
int clipNum = -1; // The number of the clip that will be played

// determine the list of tracks to play and the track content object
// (TCO) number
switch( m_playMode )
// Determine the list of tracks to play and the clip number
switch (m_playMode)
{
case Mode_PlaySong:
trackList = tracks();
// at song-start we have to reset the LFOs
if( m_playPos[Mode_PlaySong] == 0 )
{
EnvelopeAndLfoParameters::instances()->reset();
}
break;

case Mode_PlayBB:
if( Engine::getBBTrackContainer()->numOfBBs() > 0 )
if (Engine::getBBTrackContainer()->numOfBBs() > 0)
{
tcoNum = Engine::getBBTrackContainer()->
currentBB();
trackList.push_back( BBTrack::findBBTrack(
tcoNum ) );
clipNum = Engine::getBBTrackContainer()->currentBB();
trackList.push_back(BBTrack::findBBTrack(clipNum));
}
break;

case Mode_PlayPattern:
if( m_patternToPlay != NULL )
if (m_patternToPlay)
{
tcoNum = m_patternToPlay->getTrack()->
getTCONum( m_patternToPlay );
trackList.push_back(
m_patternToPlay->getTrack() );
clipNum = m_patternToPlay->getTrack()->getTCONum(m_patternToPlay);
trackList.push_back(m_patternToPlay->getTrack());
}
break;

default:
return;

}

// if we have no tracks to play, nothing to do
if( trackList.empty() == true )
{
return;
}

// check for looping-mode and act if necessary
TimeLineWidget * tl = m_playPos[m_playMode].m_timeLine;
bool checkLoop =
tl != NULL && m_exporting == false && tl->loopPointsEnabled();
// If we have no tracks to play, there is nothing to do
if (trackList.empty()) { return; }

if( checkLoop )
// If the playback position is outside of the range [begin, end), move it to
DomClark marked this conversation as resolved.
Show resolved Hide resolved
// begin and inform interested parties.
// Returns true if the playback position was moved, else false.
const auto enforceLoop = [this](const MidiTime& begin, const MidiTime& end)
{
// if looping-mode is enabled and we are outside of the looping
// range, go to the beginning of the range
if( m_playPos[m_playMode] < tl->loopBegin() ||
m_playPos[m_playMode] >= tl->loopEnd() )
if (getPlayPos() < begin || getPlayPos() >= end)
{
setToTime(tl->loopBegin());
m_playPos[m_playMode].setTicks(
tl->loopBegin().getTicks() );

m_vstSyncController.setPlaybackJumped( true );

setToTime(begin);
m_vstSyncController.setPlaybackJumped(true);
emit updateSampleTracks();
return true;
}
}
return false;
};

if( m_playPos[m_playMode].jumped() )
const auto timeline = getPlayPos().m_timeLine;
const auto loopEnabled = !m_exporting && timeline && timeline->loopPointsEnabled();

// Ensure playback begins within the loop if it is enabled
if (loopEnabled) { enforceLoop(timeline->loopBegin(), timeline->loopEnd()); }

// Inform VST plugins if the user moved the play head
if (getPlayPos().jumped())
{
m_vstSyncController.setPlaybackJumped( true );
m_playPos[m_playMode].setJumped( false );
m_vstSyncController.setPlaybackJumped(true);
getPlayPos().setJumped(false);
}

f_cnt_t framesPlayed = 0;
const float framesPerTick = Engine::framesPerTick();
const auto framesPerTick = Engine::framesPerTick();
const auto framesPerPeriod = Engine::mixer()->framesPerPeriod();

f_cnt_t frameOffsetInPeriod = 0;

while( framesPlayed < Engine::mixer()->framesPerPeriod() )
while (frameOffsetInPeriod < framesPerPeriod)
{
m_vstSyncController.update();
auto frameOffsetInTick = getPlayPos().currentFrame();

float currentFrame = m_playPos[m_playMode].currentFrame();
// did we play a tick?
if( currentFrame >= framesPerTick )
// If a whole tick has elapsed, update the frame and tick count, and check any loops
if (frameOffsetInTick >= framesPerTick)
{
int ticks = m_playPos[m_playMode].getTicks() +
( int )( currentFrame / framesPerTick );

// did we play a whole bar?
if( ticks >= MidiTime::ticksPerBar() )
// Transfer any whole ticks from the frame count to the tick count
const auto elapsedTicks = static_cast<int>(frameOffsetInTick / framesPerTick);
getPlayPos().setTicks(getPlayPos().getTicks() + elapsedTicks);
frameOffsetInTick -= elapsedTicks * framesPerTick;
getPlayPos().setCurrentFrame(frameOffsetInTick);

// If we are playing a BB track, or a pattern with no loop enabled,
// loop back to the beginning when we reach the end
if (m_playMode == Mode_PlayBB)
{
// per default we just continue playing even if
// there's no more stuff to play
// (song-play-mode)
int maxBar = m_playPos[m_playMode].getBar()
+ 2;

// then decide whether to go over to next bar
// or to loop back to first bar
if( m_playMode == Mode_PlayBB )
{
maxBar = Engine::getBBTrackContainer()
->lengthOfCurrentBB();
}
else if( m_playMode == Mode_PlayPattern &&
m_loopPattern == true &&
tl != NULL &&
tl->loopPointsEnabled() == false )
{
maxBar = m_patternToPlay->length()
.getBar();
}

// end of played object reached?
if( m_playPos[m_playMode].getBar() + 1
>= maxBar )
{
// then start from beginning and keep
// offset
ticks %= ( maxBar * MidiTime::ticksPerBar() );

// wrap milli second counter
setToTimeByTicks(ticks);

m_vstSyncController.setPlaybackJumped( true );
}
enforceLoop(MidiTime{0}, MidiTime{Engine::getBBTrackContainer()->lengthOfCurrentBB(), 0});
}
else if (m_playMode == Mode_PlayPattern && m_loopPattern && !loopEnabled)
{
enforceLoop(MidiTime{0}, m_patternToPlay->length());
}
m_playPos[m_playMode].setTicks( ticks );

if (checkLoop || m_loopRenderRemaining > 1)
// Handle loop points, and inform VST plugins of the loop status
if (loopEnabled || m_loopRenderRemaining > 1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (loopEnabled || m_loopRenderRemaining > 1)
if (loopEnabled || (m_loopRenderRemaining > 1 && getPlayPos() >= timeline->loopBegin()))

This will be explained in the comment below.

{
m_vstSyncController.startCycle(
tl->loopBegin().getTicks(), tl->loopEnd().getTicks() );
timeline->loopBegin().getTicks(), timeline->loopEnd().getTicks());

// if looping-mode is enabled and we have got
// past the looping range, return to the
// beginning of the range
if( m_playPos[m_playMode] >= tl->loopEnd() )
// Loop if necessary, and decrement the remaining loops if we did
if (enforceLoop(timeline->loopBegin(), timeline->loopEnd())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds m_playPos[m_playMode] < timeline->loopBegin() condition for jumping back which is undesired in case m_loopRenderRemaining > 1, and make the area before the loop point be skipped or play wrongly. A relevant bug was reported on Discord.
Also, the startCycle() call should be checked by m_playPos[m_playMode] >= timeline->loopBegin(), though it was already wrong before this PR.
The proposed fix is in the comment above.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix looks good to me and seems to work properly; PR open at #5814.

&& m_loopRenderRemaining > 1)
{
if (m_loopRenderRemaining > 1)
m_loopRenderRemaining--;
ticks = tl->loopBegin().getTicks();
m_playPos[m_playMode].setTicks( ticks );
setToTime(tl->loopBegin());

m_vstSyncController.setPlaybackJumped( true );

emit updateSampleTracks();
m_loopRenderRemaining--;
}
}
else
{
m_vstSyncController.stopCycle();
}

currentFrame = fmodf( currentFrame, framesPerTick );
m_playPos[m_playMode].setCurrentFrame( currentFrame );
}

if( framesPlayed == 0 )
{
// update VST sync position after we've corrected frame/
// tick count but before actually playing any frames
m_vstSyncController.setAbsolutePosition(
m_playPos[m_playMode].getTicks()
+ m_playPos[m_playMode].currentFrame()
/ (double) framesPerTick );
}
const f_cnt_t framesUntilNextPeriod = framesPerPeriod - frameOffsetInPeriod;
const f_cnt_t framesUntilNextTick = static_cast<f_cnt_t>(std::ceil(framesPerTick - frameOffsetInTick));
Comment on lines +316 to +317
Copy link
Member

@PhysSong PhysSong Nov 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

frameOffsetInTick might be invalid at this point if enforceLoop() changed the play position.

Edit: It's not a regression can even be wrong, so you can ignore it at this moment.

Copy link
Member

@PhysSong PhysSong Nov 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A situation where it matters is:

  1. Add a note pattern starting at bar 3
  2. Add a note starting at the beginning of that pattern
  3. Locate the loop begin point at bar 3, but don't enable it yet
  4. Start playing song at bar 1
  5. Now enable looping
  6. The play head jumps to bar 3, but the note is not played

As I mentioned above, it's not a regression, but an old bug. You can ignore this.
If it's considered as a bug, I can open a new bug report.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

frameOffsetInTick is correct, in that it still corresponds to the ticks of the current PlayPos. The ticks of the PlayPos should be reset to zero/fmod'd with one though (can't decide which is correct). The issue is #3028. (Zonkmachine mentioned this here: #5723 (comment).)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see. Thanks for mentioning the relevant issue.


f_cnt_t framesToPlay =
Engine::mixer()->framesPerPeriod() - framesPlayed;
// We want to proceed to the next buffer or tick, whichever is closer
const auto framesToPlay = std::min(framesUntilNextPeriod, framesUntilNextTick);

f_cnt_t framesLeft = ( f_cnt_t )framesPerTick -
( f_cnt_t )currentFrame;
// skip last frame fraction
if( framesLeft == 0 )
if (frameOffsetInPeriod == 0)
{
++framesPlayed;
m_playPos[m_playMode].setCurrentFrame( currentFrame
+ 1.0f );
continue;
}
// do we have samples left in this tick but these are less
// than samples we have to play?
if( framesLeft < framesToPlay )
{
// then set framesToPlay to remaining samples, the
// rest will be played in next loop
framesToPlay = framesLeft;
// First frame of buffer: update VST sync position.
// This must be done after we've corrected the frame/tick count,
// but before actually playing any frames.
m_vstSyncController.setAbsolutePosition(getPlayPos().getTicks()
+ getPlayPos().currentFrame() / static_cast<double>(framesPerTick));
m_vstSyncController.update();
}

if( ( f_cnt_t ) currentFrame == 0 )
if (static_cast<f_cnt_t>(frameOffsetInTick) == 0)
Copy link
Member

@PhysSong PhysSong Nov 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the same reason, this check might go wrong. This can result in skipping some TCOs.

Edit: It's not a regression and can even be wrong, so you can ignore it at this moment.

{
processAutomations(trackList, m_playPos[m_playMode], framesToPlay);

// loop through all tracks and play them
for( int i = 0; i < trackList.size(); ++i )
// First frame of tick: process automation and play tracks
processAutomations(trackList, getPlayPos(), framesToPlay);
for (const auto track : trackList)
{
trackList[i]->play( m_playPos[m_playMode],
framesToPlay,
framesPlayed, tcoNum );
track->play(getPlayPos(), framesToPlay, frameOffsetInPeriod, clipNum);
}
}

// update frame-counters
framesPlayed += framesToPlay;
m_playPos[m_playMode].setCurrentFrame( framesToPlay +
currentFrame );
// Update frame counters
frameOffsetInPeriod += framesToPlay;
frameOffsetInTick += framesToPlay;
getPlayPos().setCurrentFrame(frameOffsetInTick);
m_elapsedMilliSeconds[m_playMode] += MidiTime::ticksToMilliseconds(framesToPlay / framesPerTick, getTempo());
m_elapsedBars = m_playPos[Mode_PlaySong].getBar();
m_elapsedTicks = ( m_playPos[Mode_PlaySong].getTicks() % ticksPerBar() ) / 48;
m_elapsedTicks = (m_playPos[Mode_PlaySong].getTicks() % ticksPerBar()) / 48;
}
}

Expand Down