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

Refactor to move positionChanged signal to Timeline #7454

Open
wants to merge 25 commits into
base: master
Choose a base branch
from

Conversation

regulus79
Copy link
Contributor

@regulus79 regulus79 commented Aug 14, 2024

Previously, this PR simply added a new signal to the Timeline class and had it emitted by the TimeLineWidget class in order to solve #7351.

However, as messmerd pointed out in his review comments, that was quite a hacky solution, and ideally the positionChanged signal would be solely managed by the backend Timeline class, while the frontend TimeLineWidget class would connect to those signals, instead of the other way around.

This PR is no longer a simple bugfix, but instead a refactoring of the Timeline/TimeLineWidget signal/slots.

Changes

  • Moved definition of PlayPos to its own file, PlayPos.h.
  • The positionChanged signal and updatePosition slot were removed from TimeLineWidget and moved to Timeline.
  • The connections to Timeline::positionChanged were made into Qt::QueuedConnections to prevent errors when the audio thread changes the PlayPos which would immediately cause gui code to run if it were a direct connection.
  • The pos() method and PlayPos m_pos were removed from TimeLineWidget; PlayPos m_pos was added to Timeline, along with getter and setter methods which emit positionChanged.
  • The constructor for TimeLineWidget no longer requires a PlayPos reference.
  • Since each TimeLineWidget stores a reference to its Timeline, a new method was added, model(), for other classes to access it.
  • Removed array of PlayPoses in Song. Now each Timeline holds their respective PlayPos.
  • Song's methods for getPlayPos were changed to return the PlayPos of the respective Timeline. The non-const versions of the methods were removed because Timeline does not expose its PlayPos to write.
  • All of the places where Timelines are used were updated, along with their calls to access/modify the PlayPos. For example, occurrences of m_timeline->pos() were replaced with m_timeline->model()->getPlayPos(), and calls to m_timeline->pos().setTicks(ticks) were changed to m_timeline->model()->setTicks(ticks).

Minor note: The members m_timeline or m_timeLine in places like PianoRoll or SongEditor should honestly really be renamed something like m_timeLineWidget, as they are not Timeline objects.

Old PR Description This pull request fixes #7351 by changing the PianoRoll's connection from `m_timeline` to `&Engine::getSong()->getTimeline(Song::PlayMode::Song)`, and additionally adding the signal `positionChanged` to the `Timeline` class.

The new signal is necessary because getTimeline() returns a Timeline, which does not have a positionChanged signal, while m_timeline is a TimeLineWidget which does.

This pr is marked as a draft, since after fixing the bug I found another one: When pressing stop after record-play, the piano roll play head does not go back to the start, but instead stops at its current position. Edit: I have decided to leave fixing that bug to a future pr, as it appears that it existed prior to the regression. Instead, this pr simply fixes the pianoroll playhead movement.

@regulus79 regulus79 marked this pull request as ready for review August 25, 2024 16:29
@@ -290,7 +290,7 @@ PianoRoll::PianoRoll() :
this, SLOT( updatePositionStepRecording( const lmms::TimePos& ) ) );

// update timeline when in record-accompany mode
connect(m_timeLine, &TimeLineWidget::positionChanged, this, &PianoRoll::updatePositionAccompany);
connect(&Engine::getSong()->getTimeline(Song::PlayMode::Song), SIGNAL(positionChanged(const lmms::TimePos&)), this, SLOT(updatePositionAccompany(const lmms::TimePos&)));
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
connect(&Engine::getSong()->getTimeline(Song::PlayMode::Song), SIGNAL(positionChanged(const lmms::TimePos&)), this, SLOT(updatePositionAccompany(const lmms::TimePos&)));
connect(&Engine::getSong()->getTimeline(Song::PlayMode::Song), &Timeline::positionChanged, this, &PianoRoll::updatePositionAccompany);

We're trying to avoid using the SIGNAL and SLOT macros in new code

Copy link
Member

Choose a reason for hiding this comment

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

Actually, there may be a better approach than adding a signal to Timeline... Let me think this over some more

Copy link
Member

Choose a reason for hiding this comment

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

I don't really like how Timeline and TimeLineWidget have implemented their signals and slots for changes in position. I think ideally Timeline should be the model and only it should emit any positionChanged signals, while TimeLineWidget should be the view and its updatePosition slot should not emit any signals. The way it is currently, the TimeLineWidget is trying to do the job of both a model and a view, and that complicates things.

Copy link
Member

@messmerd messmerd Aug 31, 2024

Choose a reason for hiding this comment

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

@DomClark Do you agree with my assessment? It looks to me like a proper solution to this bug may require rethinking how the timeline position signals/slots are implemented.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have now updated to PR to refactor everything in order to move the positionChanged signal from TimeLineWidget to Timeline.

Co-authored-by: Dalton Messmer <[email protected]>
@Rossmaxx
Copy link
Contributor

Rossmaxx commented Oct 2, 2024

Fix looks simple. @messmerd did this fix your issue?

@regulus79 regulus79 changed the title Fix PianoRoll playhead not moving during Record-Play Refactor to move positionChanged signal to Timeline Dec 11, 2024
Copy link
Member

@messmerd messmerd left a comment

Choose a reason for hiding this comment

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

When I ran it, I saw this output in the console:

QObject::connect: Cannot queue arguments of type 'lmms::TimePos'
(Make sure 'lmms::TimePos' is registered using qRegisterMetaType().)

include/TimeLineWidget.h Outdated Show resolved Hide resolved
include/Timeline.h Outdated Show resolved Hide resolved
@@ -121,6 +121,7 @@ Song::Song() :

for (auto& scale : m_scales) {scale = std::make_shared<Scale>();}
for (auto& keymap : m_keymaps) {keymap = std::make_shared<Keymap>();}
for (auto& timeline : m_timelines) { connect(this, &Song::bufferProcessed, &timeline, &Timeline::updatePosition, Qt::QueuedConnection); }
Copy link
Member

Choose a reason for hiding this comment

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

This passes the Song's bufferProcessed signal to Timeline's updatePosition slot which does nothing except emit a positionChanged signal containing the play position which editors connect to for updating their position.

It seems to me that Timeline::updatePosition should not exist. When the setters of Timeline are called and its position changes, they would emit the positionChanged signal. Then any views connected to that signal would update as needed.

Maybe it would be a good idea to sketch out a tree diagram of the signal/slot connections between the different objects - might help with understanding and simplifying this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree, I think the bufferProcessed signal and updatePosition slot can probably be removed.

Except that, when I remove them and instead rely on setTicks(ticks) to emit the signal, the processNextBuffer function in Song does not update the playhead position, so it stays still during playback. I believe this may be due to processNextBuffer being on a different thread?

But then it somehow fixes itself when I specify Qt::DirectConnection in all the connect()s. I'm not sure why.

include/PlayPos.h Outdated Show resolved Hide resolved
src/core/Song.cpp Outdated Show resolved Hide resolved
src/gui/editors/AutomationEditor.cpp Outdated Show resolved Hide resolved
src/gui/editors/AutomationEditor.cpp Outdated Show resolved Hide resolved
src/gui/editors/AutomationEditor.cpp Outdated Show resolved Hide resolved
src/gui/editors/PianoRoll.cpp Outdated Show resolved Hide resolved
@regulus79
Copy link
Contributor Author

The QObject::connect: Cannot queue arguments of type 'lmms::TimePos' warnings appear to have gone away after I changed the connections to Qt::DirectConnection. However, I do not know if this will impact the performance of the audio thread, as it may cause the gui to be updated and interrupt the buffer processing(?) However, using Qt::QueuedConnection seems to fail to actually recieve the signals, and I'm not sure why.

@regulus79
Copy link
Contributor Author

regulus79 commented Dec 15, 2024

I found a solution to the warnings by simply removing the parameter from the signal and instead having each editor query the current position of the timeline themselves. The connections are all now Qt::QueuedConnection.

@sakertooth
Copy link
Contributor

Here's a sketch of how I think this functionality should work (for my own understanding and possibly anybody elses). I'll try to complete the review at some point.

image

@regulus79
Copy link
Contributor Author

Okay, this is difficult.

Originally, I had it so that Timeline emitted Timeline::positionChanged whenever Timeline::setPlayPos or Timeline::setTicks was called. Timeline::positionChanged was then connected to all the editors with a Qt::QueuedConnection. That worked well, but sakertooth experienced segmentation faults which I could not reproduce. Additionally, the audio thread causing the Timeline to send out signals to the gui thread whenever a buffer was processed didn't seem like a great idea. They were all queued connections, so theoretically it should be fine, but I don't know.

So then sakertooth had the idea of routing all the connections to the gui editors through TimeLineWidget, which would be passing along the signals from Timeline. This could have been an improvement by reducing the number of Qt::QueuedConnections, but it does not solve the issue of emitting signals across threads (which...may have been the cause of the segfaults? I do not know, I cannot reproduce them).

Another potential solution which sakertooth suggested is using std::atomic. This is a good idea to prevent race conditions, such as if the song is playing while the user drags on the timeline. However, removing all the positionChanged signals, while it simplifies the threading, means that we would have to reintroduce the QTimer to the TimeLineWidgets in order to update them. This is suboptimal. It is possible, and if it solves the segfaults, maybe it's the way to go. But it's not good.

What should be done?

@sakertooth
Copy link
Contributor

So then sakertooth had the idea of routing all the connections to the gui editors through TimeLineWidget, which would be passing along the signals from Timeline. This could have been an improvement by reducing the number of Qt::QueuedConnections, but it does not solve the issue of emitting signals across threads (which...may have been the cause of the segfaults? I do not know, I cannot reproduce them).

Coming up with sound designs is probably my biggest weakness. The idea I mentioned could work, but as more time passes I continue to fail to see a critical issue using queued connections that emit the positionChanged signal directly from Timeline. That being said, this is what we are currently doing already I believe, so I'll just work on completing my review since the refactor is working in a way that makes the most sense already.

Comment on lines 1 to 3
/*
* PlayPos.h - declaration of class PlayPos, a TimePos with additional features useful for Timelines
*
Copy link
Contributor

@sakertooth sakertooth Dec 15, 2024

Choose a reason for hiding this comment

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

I would suggest removing PlayPos and using TimePos directly in Timeline. This class mostly is just a container for data and does not do anything special really.

TimePos can be refactored to store frames as its smallest unit of time (or just store the frame position maybe? do we even need a class just to store the frame position within the tick?). The member functions should then be changed accordingly to account for that as necessary. This might be okay in a separate PR if you feel its out of scope, but I think its minor enough that it shouldn't matter too much to do it in here. Up to you.

Then for the jumped variable, from what I can tell, this just lets us know if the timeline didn't change through time progressively but instead jumped in time. What would it mean for the jumped variable to be true even when the timeline is not jumping? This should be a parameter for the positionChanged signal. You might have to refactor the VST sync controller code for its invocation to be event-based and not executed within Song::processNextBuffer though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure TimePos can easily be refactored to store frames as its smallest unit of time, unless we allow for fractional frames. If the number of frames in a bar is not a whole number, then it will have to be rounded. That's fine, but if the bpm is changed, then every single TimePos will have to be updated at once so that the number of frames matches the correct tick amount. I feel like a better long-term solution would be to allow float tick values, but that's just me.

But I agree, the jumped variable can probably be combined with the positionChanged signal.

Copy link
Contributor

Choose a reason for hiding this comment

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

What about just storing the frame position directly and still removing PlayPos?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have removed PlayPos. Now each Timeline stores it's current frame offset and jumped state. I will see if I can get jumped to be passed in the positionChanged signal instead.

include/Song.h Outdated Show resolved Hide resolved
include/TimeLineWidget.h Outdated Show resolved Hide resolved
src/core/Song.cpp Outdated Show resolved Hide resolved
src/core/Song.cpp Outdated Show resolved Hide resolved
src/core/Song.cpp Outdated Show resolved Hide resolved
@@ -273,23 +270,24 @@ void AutomationEditor::keyPressEvent(QKeyEvent * ke )
break;

case Qt::Key_Left:
if( ( m_timeLine->pos() -= 16 ) < 0 )
if(m_timeLine->model()->getTicks() - 16 > 0)
Copy link
Contributor

@sakertooth sakertooth Dec 15, 2024

Choose a reason for hiding this comment

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

Suggested change
if(m_timeLine->model()->getTicks() - 16 > 0)
if ((m_timeLine->model()->getTicks() -= 16) < 0)

Copy link
Contributor Author

@regulus79 regulus79 Dec 16, 2024

Choose a reason for hiding this comment

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

Timeline does not currently implement the -= operator. I suppose I could add it.

ke->accept();
break;

case Qt::Key_Right:
m_timeLine->pos() += 16;
m_timeLine->updatePosition();
m_timeLine->model()->setTicks(m_timeLine->model()->getTicks() + 16);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's fine to leave it as is?

Suggested change
m_timeLine->model()->setTicks(m_timeLine->model()->getTicks() + 16);
m_timeLine->model() += 16;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are you sure it makes sense to += an integer to a Timeline*? I get that it would just add to the internal TimePos, but it just seems weird to add a number to a timeline object. It would definitely make the code simpler in this situation, but idk.

src/gui/editors/PianoRoll.cpp Outdated Show resolved Hide resolved
src/gui/editors/AutomationEditor.cpp Show resolved Hide resolved
@regulus79
Copy link
Contributor Author

Partially following sakertooth's suggestion, I have removed PlayPos.h from the PR. Now each Timeline stores it's own TimePos along with a frame offset and jumped variable.

I have also removed m_elapsedMilliseconds, m_elapsedBars, and m_elapsedTicks from Song.

I could not find any code referencing m_elapsedBars or m_elapsedTicks in the entire codebase (outside of the code to update them), so removing those shouldn't cause any issues.

m_elapsedMilliseconds is used in displaying the current time LCD at the top. The current time can easily be calculated using the current frame, so removing this should also be fine in most cases, however, if you change the tempo mid-song, it will not display the correct time.

sakertooth also suggested getting rid of the jumped variable and instead passing it along with the positionChanged signal. I am still thinking of how that would be implemented, since Song would somehow have to store the jumped state when it receives the signal so that it can operate on it during the next buffer(?)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Piano Roll playhead not shown during Record-Play
4 participants