diff --git a/source/app/powertabeditor.cpp b/source/app/powertabeditor.cpp index 0d1b0dda..f57146b7 100644 --- a/source/app/powertabeditor.cpp +++ b/source/app/powertabeditor.cpp @@ -682,6 +682,19 @@ void PowerTabEditor::startStopPlayback(bool from_measure_start) } } +void PowerTabEditor::seekPlaybackToLocation(const ConstScoreLocation &location) +{ + Caret &caret = getCaret(); + caret.moveToLocation(location); + caret.setSelectedItem(ScoreItem::Staff); + + const ScoreLocation &new_location = caret.getLocation(); + // Called directly rather than queued, since the MIDI thread's event loop is + // blocked while playback is running. + myMidiPlayer->seekToLocation(SystemLocation( + new_location.getSystemIndex(), new_location.getPositionIndex())); +} + void PowerTabEditor::redrawSystem(int index) { getCaret().moveToValidPosition(); @@ -3687,8 +3700,14 @@ void PowerTabEditor::setupNewTab() connect(scorearea, &ScoreArea::itemClicked, [&](ScoreItem item, const ConstScoreLocation &location, ScoreItemAction action) { + // While playing, clicking on the staff seeks playback to that + // location instead of editing the score. if (getCaret().isInPlaybackMode()) + { + if (item == ScoreItem::Staff) + seekPlaybackToLocation(location); return; + } myIsHandlingClick = true; diff --git a/source/app/powertabeditor.h b/source/app/powertabeditor.h index 9411e07e..f8b9cddd 100644 --- a/source/app/powertabeditor.h +++ b/source/app/powertabeditor.h @@ -29,6 +29,7 @@ class AutoBackup; class Caret; class Command; +class ConstScoreLocation; class DocumentManager; class FileFormatManager; class Instrument; @@ -301,6 +302,10 @@ private slots: /// Set up the MIDI thread. void createMidiThread(); + /// Moves the caret to the given location and jumps playback there, without + /// interrupting playback. + void seekPlaybackToLocation(const ConstScoreLocation &location); + /// Load any custom keyboard shortcuts. void loadKeyboardShortcuts(); /// Save any custom keyboard shortcuts. diff --git a/source/audio/midiplayer.cpp b/source/audio/midiplayer.cpp index b12f91cd..398bb456 100644 --- a/source/audio/midiplayer.cpp +++ b/source/audio/midiplayer.cpp @@ -169,21 +169,14 @@ mergeMidiEvents(MidiFile &file) return events; } -bool -MidiPlayer::playEvents(MidiFile &file, const SystemLocation &start_location, +MidiPlayer::PlaybackResult +MidiPlayer::playEvents(const MidiEventList &events, int ticks_per_beat, + const SystemLocation &start_location, const MidiPlaybackSettings &initial_settings, bool allow_count_in, const Score *score) { setPlaybackSettings(initial_settings); - myIsPlaying = true; - Util::ScopeExit on_exit([&]() { - myIsPlaying = false; - }); - - MidiEventList events = mergeMidiEvents(file); - const int ticks_per_beat = file.getTicksPerBeat(); - bool started = false; Midi::Tempo beat_duration = Midi::BEAT_DURATION_120_BPM; SystemLocation current_location = start_location; @@ -195,7 +188,11 @@ MidiPlayer::playEvents(MidiFile &file, const SystemLocation &start_location, for (const MidiEvent &event : events) { if (!myIsPlaying) - return false; + return PlaybackResult::Stopped; + + // A seek was requested, so restart the loop from the new location. + if (mySeekRequested) + return PlaybackResult::Seek; if (event.isTempoChange()) beat_duration = event.getTempo(); @@ -313,7 +310,7 @@ MidiPlayer::playEvents(MidiFile &file, const SystemLocation &start_location, clock_drift += actual_duration - sleep_duration; } - return true; + return PlaybackResult::Finished; } void @@ -331,16 +328,61 @@ MidiPlayer::playScore(const ConstScoreLocation &start_score_location, MidiFile file; file.load(score, options); - const SystemLocation start_location(myStartLocation->getSystemIndex(), - myStartLocation->getPositionIndex()); + // The events can only be merged once, since doing so converts the file's + // tracks to absolute ticks. + const MidiEventList events = mergeMidiEvents(file); + const int ticks_per_beat = file.getTicksPerBeat(); + + SystemLocation start_location(myStartLocation->getSystemIndex(), + myStartLocation->getPositionIndex()); + + // Keep myIsPlaying set for the whole session so that stopPlayback() can't + // miss the window in between seeks. + mySeekRequested = false; + myIsPlaying = true; + Util::ScopeExit on_exit([&]() { myIsPlaying = false; }); - if (playEvents(file, start_location, initial_settings, true, - &score)) + // Restart the event loop whenever a seek is requested. + bool allow_count_in = true; + while (true) { - emit playbackFinished(); + const PlaybackResult result = + playEvents(events, ticks_per_beat, start_location, + initial_settings, allow_count_in, &score); + + if (result != PlaybackResult::Seek) + { + if (result == PlaybackResult::Finished) + emit playbackFinished(); + break; + } + + { + std::lock_guard lock(mySeekMutex); + start_location = mySeekLocation; + mySeekRequested = false; + } + + // Silence anything that was sounding at the old location, and don't + // repeat the count-in when jumping around mid-playback. + myDevice->stopAllNotes(); + allow_count_in = false; } } +void +MidiPlayer::seekToLocation(const SystemLocation &location) +{ + if (!myIsPlaying) + return; + + // Set both under the lock so that a request can't be lost if it races with + // the playback thread consuming the previous one. + std::lock_guard lock(mySeekMutex); + mySeekLocation = location; + mySeekRequested = true; +} + MidiFile MidiPlayer::generateSingleNote(const ConstScoreLocation &location, const SettingsManager &settings) @@ -364,8 +406,13 @@ MidiPlayer::playSingleNote(MidiFile &file, const ConstScoreLocation &location, SystemLocation start_location(location.getSystemIndex(), location.getPositionIndex()); - playEvents(file, start_location, initial_settings, - /* allow_count_in */ false, nullptr); + const MidiEventList events = mergeMidiEvents(file); + + myIsPlaying = true; + Util::ScopeExit on_exit([&]() { myIsPlaying = false; }); + + playEvents(events, file.getTicksPerBeat(), start_location, + initial_settings, /* allow_count_in */ false, nullptr); myDevice->stopAllNotes(); } @@ -420,6 +467,9 @@ MidiPlayer::performCountIn(const Score &score, const SystemLocation &location, void MidiPlayer::stopPlayback() { + // Discard any pending seek so it can't restart the loop. + mySeekRequested = false; + if (myIsPlaying) { myIsPlaying = false; diff --git a/source/audio/midiplayer.h b/source/audio/midiplayer.h index 1961e24d..1ddcdeb0 100644 --- a/source/audio/midiplayer.h +++ b/source/audio/midiplayer.h @@ -23,17 +23,19 @@ #include #include #include +#include #include #include #include #include +#include #include +class MidiEventList; class MidiFile; class MidiOutputDevice; class Score; class SettingsManager; -class SystemLocation; /// Initial values for playback settings which can be updated live from the /// mixer, see liveChangePlayerSettings() and liveChangePlaybackSpeed(). @@ -67,6 +69,12 @@ class MidiPlayer : public QObject void stopPlayback(); + /// Jumps playback to the given location without interrupting the playback + /// session. Has no effect if playback isn't running. + /// Thread-safe, and must be called directly rather than through the event + /// loop since the MIDI thread is busy while playback is running. + void seekToLocation(const SystemLocation &location); + static MidiFile generateSingleNote(const ConstScoreLocation &location, const SettingsManager &settings); @@ -102,14 +110,28 @@ private slots: void updateLiveSettings(); private: + /// How a playEvents() run ended. + enum class PlaybackResult + { + /// Reached the end of the event list. + Finished, + /// Aborted via stopPlayback(). + Stopped, + /// Interrupted by seekToLocation(); should be restarted from the + /// requested location. + Seek + }; + void setPlaybackSettings(const MidiPlaybackSettings &settings); void performCountIn(const Score &score, const SystemLocation &location, Midi::Tempo beat_duration); - bool playEvents(MidiFile &file, const SystemLocation &start_location, - const MidiPlaybackSettings &initial_settings, - bool allow_count_in = false, const Score *score = nullptr); + PlaybackResult playEvents(const MidiEventList &events, int ticks_per_beat, + const SystemLocation &start_location, + const MidiPlaybackSettings &initial_settings, + bool allow_count_in = false, + const Score *score = nullptr); const SettingsManager &mySettingsManager; boost::signals2::scoped_connection mySettingsListener; @@ -124,6 +146,13 @@ private slots: /// Location where playback began. std::optional myStartLocation; + /// Set when a seek has been requested; polled by the playback loop. + std::atomic mySeekRequested = false; + /// Guards mySeekLocation, which isn't trivially copyable. + std::mutex mySeekMutex; + /// Location requested by the most recent seekToLocation() call. + SystemLocation mySeekLocation; + /// Max volume and pan for each channel. std::array, Midi::NUM_MIDI_CHANNELS_PER_PORT> myChannelMaxVolumes; std::array, Midi::NUM_MIDI_CHANNELS_PER_PORT> myChannelPans;