Skip to content
Open
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
19 changes: 19 additions & 0 deletions source/app/powertabeditor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;

Expand Down
5 changes: 5 additions & 0 deletions source/app/powertabeditor.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
class AutoBackup;
class Caret;
class Command;
class ConstScoreLocation;
class DocumentManager;
class FileFormatManager;
class Instrument;
Expand Down Expand Up @@ -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.
Expand Down
88 changes: 69 additions & 19 deletions source/audio/midiplayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -313,7 +310,7 @@ MidiPlayer::playEvents(MidiFile &file, const SystemLocation &start_location,
clock_drift += actual_duration - sleep_duration;
}

return true;
return PlaybackResult::Finished;
}

void
Expand All @@ -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<std::mutex> 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<std::mutex> lock(mySeekMutex);
mySeekLocation = location;
mySeekRequested = true;
}

MidiFile
MidiPlayer::generateSingleNote(const ConstScoreLocation &location,
const SettingsManager &settings)
Expand All @@ -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();
}

Expand Down Expand Up @@ -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;
Expand Down
37 changes: 33 additions & 4 deletions source/audio/midiplayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,19 @@
#include <boost/signals2/connection.hpp>
#include <midi/midievent.h>
#include <midi/midifile.h>
#include <mutex>
#include <optional>
#include <QObject>
#include <score/generalmidi.h>
#include <score/scorelocation.h>
#include <score/systemlocation.h>
#include <vector>

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().
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
Expand All @@ -124,6 +146,13 @@ private slots:
/// Location where playback began.
std::optional<ConstScoreLocation> myStartLocation;

/// Set when a seek has been requested; polled by the playback loop.
std::atomic<bool> 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<std::atomic<uint8_t>, Midi::NUM_MIDI_CHANNELS_PER_PORT> myChannelMaxVolumes;
std::array<std::atomic<uint8_t>, Midi::NUM_MIDI_CHANNELS_PER_PORT> myChannelPans;
Expand Down
Loading