Add next/previous/last functions and playback history recording.

This commit is contained in:
Kai Blaschke
2022-11-26 23:47:04 +01:00
parent 03f81ff36f
commit a1ffd93e31
7 changed files with 479 additions and 11 deletions

View File

@ -34,6 +34,7 @@ bool Playlist::Empty() const
void Playlist::Clear()
{
m_presetHistory.clear();
m_items.clear();
}
@ -59,6 +60,7 @@ bool Playlist::AddItem(const std::string& filename, uint32_t index, bool allowDu
}
}
m_presetHistory.clear();
if (index >= m_items.size())
{
m_items.emplace_back(filename);
@ -76,6 +78,7 @@ auto Playlist::AddPath(const std::string& path, uint32_t index, bool recursive,
{
uint32_t presetsAdded{0};
m_presetHistory.clear();
if (recursive)
{
for (const auto& entry : recursive_directory_iterator(path))
@ -124,6 +127,7 @@ auto Playlist::RemoveItem(uint32_t index) -> bool
return false;
}
m_presetHistory.clear();
m_items.erase(m_items.cbegin() + index);
return true;
@ -145,6 +149,8 @@ auto Playlist::Shuffle() const -> bool
void Playlist::Sort(uint32_t startIndex, uint32_t count,
Playlist::SortPredicate predicate, Playlist::SortOrder order)
{
m_presetHistory.clear();
std::sort(m_items.begin() + startIndex,
m_items.begin() + startIndex + count,
[predicate, order](const Item& left, const Item& right) {
@ -187,6 +193,8 @@ auto Playlist::NextPresetIndex() -> size_t
throw PlaylistEmptyException();
}
AddCurrentPresetIndexToHistory();
if (m_shuffle)
{
std::uniform_int_distribution<size_t> randomDistribution(0, m_items.size());
@ -205,6 +213,58 @@ auto Playlist::NextPresetIndex() -> size_t
}
auto Playlist::PreviousPresetIndex() -> size_t
{
if (m_items.empty())
{
throw PlaylistEmptyException();
}
AddCurrentPresetIndexToHistory();
if (m_shuffle)
{
std::uniform_int_distribution<size_t> randomDistribution(0, m_items.size());
m_currentPosition = randomDistribution(m_randomGenerator);
}
else
{
if (m_currentPosition == 0)
{
m_currentPosition = m_items.size() -1;
}
else
{
m_currentPosition--;
}
}
return m_currentPosition;
}
auto Playlist::LastPresetIndex() -> size_t
{
if (m_items.empty())
{
throw PlaylistEmptyException();
}
if (!m_presetHistory.empty())
{
m_currentPosition = m_presetHistory.back();
m_presetHistory.pop_back();
}
else
{
m_currentPosition = PreviousPresetIndex();
// Remove added history item again to prevent ping-pong behavior
m_presetHistory.pop_back();
}
return m_currentPosition;
}
auto Playlist::PresetIndex() const -> size_t
{
if (m_items.empty())
@ -223,6 +283,13 @@ auto Playlist::SetPresetIndex(size_t presetIndex) -> size_t
throw PlaylistEmptyException();
}
AddCurrentPresetIndexToHistory();
if (presetIndex == m_currentPosition)
{
return m_currentPosition;
}
m_currentPosition = presetIndex;
if (m_currentPosition >= m_items.size())
@ -234,5 +301,30 @@ auto Playlist::SetPresetIndex(size_t presetIndex) -> size_t
}
void Playlist::AddCurrentPresetIndexToHistory()
{
// No duplicate entries.
if (!m_presetHistory.empty() && m_currentPosition == m_presetHistory.back())
{
return;
}
m_presetHistory.push_back(m_currentPosition);
if (m_presetHistory.size() > MaxHistoryItems)
{
m_presetHistory.pop_front();
}
}
void Playlist::RemoveLastHistoryEntry()
{
if (!m_presetHistory.empty())
{
m_presetHistory.pop_back();
}
}
} // namespace Playlist
} // namespace ProjectM

View File

@ -4,6 +4,7 @@
#include <cstdint>
#include <limits>
#include <list>
#include <random>
#include <string>
#include <vector>
@ -39,6 +40,11 @@ public:
*/
static constexpr auto InsertAtEnd = std::numeric_limits<uint32_t>::max();
/**
* Maximum number of items in the playback history.
*/
static constexpr size_t MaxHistoryItems = 1000;
/**
* Sort predicate.
*/
@ -172,6 +178,28 @@ public:
*/
virtual auto NextPresetIndex() -> size_t;
/**
* @brief Returns the previous preset index in the playlist.
*
* Each call will either decrement the current index, or select a random preset, depending on
* the shuffle setting.
*
* @throws PlaylistEmptyException Thrown if the playlist is currently empty.
* @return The index of the previous playlist item.
*/
virtual auto PreviousPresetIndex() -> size_t;
/**
* @brief Returns the last preset index that has been played.
*
* Each call will pop the last history item. If the history is empty, it will internally call
* PreviousPresetIndex(), but not add a history item.
*
* @throws PlaylistEmptyException Thrown if the playlist is currently empty.
* @return The index of the last (or previous) playlist item.
*/
virtual auto LastPresetIndex() -> size_t;
/**
* @brief Returns the current playlist/preset index without changing the position.
* @throws PlaylistEmptyException Thrown if the playlist is currently empty.
@ -191,10 +219,22 @@ public:
*/
virtual auto SetPresetIndex(size_t presetIndex) -> size_t;
/**
* @brief Removes the newest entry in the playback history.
* Useful if the last playlist item failed to load, so it won't get selected again.
*/
virtual void RemoveLastHistoryEntry();
private:
std::vector<Item> m_items; //!< Items in the current playlist.
bool m_shuffle{false}; //!< True if shuffle mode is enabled, false to play presets in order.
size_t m_currentPosition{0}; //!< Current playlist position.
/**
* @brief Adds a preset to the history and trims the list if it gets too long.
*/
void AddCurrentPresetIndexToHistory();
std::vector<Item> m_items; //!< Items in the current playlist.
bool m_shuffle{false}; //!< True if shuffle mode is enabled, false to play presets in order.
size_t m_currentPosition{0}; //!< Current playlist position.
std::list<size_t> m_presetHistory; //!< The playback history.
std::default_random_engine m_randomGenerator;
};

View File

@ -59,6 +59,11 @@ void PlaylistCWrapper::OnPresetSwitchFailed(const char* presetFilename, const ch
auto* playlist = reinterpret_cast<PlaylistCWrapper*>(userData);
// ToDo: Add different retry behavior for set/next/previous/last calls.
// Don't go back to a broken preset.
playlist->RemoveLastHistoryEntry();
// Preset switch may fail due to broken presets, retry a few times before giving up.
if (playlist->m_presetSwitchFailedCount < playlist->m_presetSwitchRetryCount)
{
@ -419,10 +424,57 @@ auto projectm_playlist_set_position(projectm_playlist_handle instance, uint32_t
{
auto newIndex = playlist->SetPresetIndex(new_position);
playlist->PlayPresetIndex(newIndex, hard_cut, true);
return newIndex;
return playlist->PresetIndex();
}
catch (ProjectM::Playlist::PlaylistEmptyException&)
{
return 0;
}
}
uint32_t projectm_playlist_play_next(projectm_playlist_handle instance, bool hard_cut)
{
auto* playlist = playlist_handle_to_instance(instance);
try
{
auto newIndex = playlist->NextPresetIndex();
playlist->PlayPresetIndex(newIndex, hard_cut, true);
return playlist->PresetIndex();
}
catch (ProjectM::Playlist::PlaylistEmptyException&)
{
return 0;
}
}
uint32_t projectm_playlist_play_previous(projectm_playlist_handle instance, bool hard_cut)
{
auto* playlist = playlist_handle_to_instance(instance);
try
{
auto newIndex = playlist->PreviousPresetIndex();
playlist->PlayPresetIndex(newIndex, hard_cut, true);
return playlist->PresetIndex();
}
catch (ProjectM::Playlist::PlaylistEmptyException&)
{
return 0;
}
}
uint32_t projectm_playlist_play_last(projectm_playlist_handle instance, bool hard_cut)
{
auto* playlist = playlist_handle_to_instance(instance);
try
{
auto newIndex = playlist->LastPresetIndex();
playlist->PlayPresetIndex(newIndex, hard_cut, true);
return playlist->PresetIndex();
}
catch (ProjectM::Playlist::PlaylistEmptyException&)
{
return 0;
}
}

View File

@ -373,6 +373,52 @@ uint32_t projectm_playlist_get_position(projectm_playlist_handle instance);
uint32_t projectm_playlist_set_position(projectm_playlist_handle instance, uint32_t new_position,
bool hard_cut);
/**
* @brief Plays the next playlist item and returns the index of the new preset.
*
* If shuffle is on, it will select a random preset, otherwise the next in the playlist. If the
* end of the playlist is reached in continuous mode, it will wrap back to 0.
*
* The old playlist item is added to the history.
*
* @param instance The playlist manager instance.
* @param hard_cut If true, the preset transition is instant. If true, a smooth transition is played.
* @return The new playlist position. If the playlist is empty, 0 will be returned.
*/
uint32_t projectm_playlist_play_next(projectm_playlist_handle instance, bool hard_cut);
/**
* @brief Plays the previous playlist item and returns the index of the new preset.
*
* If shuffle is on, it will select a random preset, otherwise the next in the playlist. If the
* end of the playlist is reached in continuous mode, it will wrap back to 0.
*
* The old playlist item is added to the history.
*
* @param instance The playlist manager instance.
* @param hard_cut If true, the preset transition is instant. If true, a smooth transition is played.
* @return The new playlist position. If the playlist is empty, 0 will be returned.
*/
uint32_t projectm_playlist_play_previous(projectm_playlist_handle instance, bool hard_cut);
/**
* @brief Plays the last preset played in the history and returns the index of the preset.
*
* The history keeps track of the last 1000 presets and will go back in the history. The
* playback history will be cleared whenever the playlist items are changed.
*
* If the history is empty, this call behaves identical to projectm_playlist_play_previous(),
* but the item is not added to the history.
*
* Presets which failed to load are not recorded in the history and thus will be skipped when
* calling this method.
*
* @param instance The playlist manager instance.
* @param hard_cut If true, the preset transition is instant. If true, a smooth transition is played.
* @return The new playlist position. If the playlist is empty, 0 will be returned.
*/
uint32_t projectm_playlist_play_last(projectm_playlist_handle instance, bool hard_cut);
#ifdef __cplusplus
}
// extern "C"

View File

@ -372,6 +372,9 @@ TEST(projectMPlaylistAPI, SetPosition)
.Times(1);
EXPECT_CALL(mockPlaylist, PlayPresetIndex(512, false, true))
.Times(1);
EXPECT_CALL(mockPlaylist, PresetIndex())
.Times(2)
.WillRepeatedly(Return(512));
EXPECT_EQ(projectm_playlist_set_position(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), 256, true), 512);
EXPECT_EQ(projectm_playlist_set_position(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), 256, false), 512);
@ -389,3 +392,118 @@ TEST(projectMPlaylistAPI, SetPositionException)
EXPECT_EQ(projectm_playlist_set_position(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), 256, true), 0);
EXPECT_EQ(projectm_playlist_set_position(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), 256, false), 0);
}
TEST(projectMPlaylistAPI, PlayNext)
{
PlaylistCWrapperMock mockPlaylist;
EXPECT_CALL(mockPlaylist, NextPresetIndex())
.Times(2)
.WillRepeatedly(Return(512));
EXPECT_CALL(mockPlaylist, PlayPresetIndex(512, true, true))
.Times(1);
EXPECT_CALL(mockPlaylist, PlayPresetIndex(512, false, true))
.Times(1);
EXPECT_CALL(mockPlaylist, PresetIndex())
.Times(2)
.WillRepeatedly(Return(512));
EXPECT_EQ(projectm_playlist_play_next(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), true), 512);
EXPECT_EQ(projectm_playlist_play_next(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), false), 512);
}
TEST(projectMPlaylistAPI, PlayNextException)
{
PlaylistCWrapperMock mockPlaylist;
EXPECT_CALL(mockPlaylist, NextPresetIndex())
.Times(2)
.WillRepeatedly(Throw(ProjectM::Playlist::PlaylistEmptyException()));
EXPECT_EQ(projectm_playlist_play_next(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), true), 0);
EXPECT_EQ(projectm_playlist_play_next(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), false), 0);
}
TEST(projectMPlaylistAPI, PlayPrevious)
{
PlaylistCWrapperMock mockPlaylist;
EXPECT_CALL(mockPlaylist, PreviousPresetIndex())
.Times(2)
.WillRepeatedly(Return(512));
EXPECT_CALL(mockPlaylist, PlayPresetIndex(512, true, true))
.Times(1);
EXPECT_CALL(mockPlaylist, PlayPresetIndex(512, false, true))
.Times(1);
EXPECT_CALL(mockPlaylist, PresetIndex())
.Times(2)
.WillRepeatedly(Return(512));
EXPECT_EQ(projectm_playlist_play_previous(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), true), 512);
EXPECT_EQ(projectm_playlist_play_previous(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), false), 512);
}
TEST(projectMPlaylistAPI, PlayPreviousException)
{
PlaylistCWrapperMock mockPlaylist;
EXPECT_CALL(mockPlaylist, PreviousPresetIndex())
.Times(2)
.WillRepeatedly(Throw(ProjectM::Playlist::PlaylistEmptyException()));
EXPECT_EQ(projectm_playlist_play_previous(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), true), 0);
EXPECT_EQ(projectm_playlist_play_previous(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), false), 0);
}
TEST(projectMPlaylistAPI, PlayLast)
{
PlaylistCWrapperMock mockPlaylist;
EXPECT_CALL(mockPlaylist, PreviousPresetIndex())
.Times(2)
.WillRepeatedly(Return(512));
EXPECT_CALL(mockPlaylist, PlayPresetIndex(512, true, true))
.Times(1);
EXPECT_CALL(mockPlaylist, PlayPresetIndex(512, false, true))
.Times(1);
EXPECT_CALL(mockPlaylist, PresetIndex())
.Times(2)
.WillRepeatedly(Return(512));
EXPECT_EQ(projectm_playlist_play_previous(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), true), 512);
EXPECT_EQ(projectm_playlist_play_previous(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), false), 512);
}
TEST(projectMPlaylistAPI, PlayLastException)
{
PlaylistCWrapperMock mockPlaylist;
EXPECT_CALL(mockPlaylist, LastPresetIndex())
.Times(2)
.WillRepeatedly(Throw(ProjectM::Playlist::PlaylistEmptyException()));
EXPECT_EQ(projectm_playlist_play_last(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), true), 0);
EXPECT_EQ(projectm_playlist_play_last(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), false), 0);
}
TEST(projectMPlaylistAPI, SetPresetSwitchFailedCallback)
{
PlaylistCWrapperMock mockPlaylist;
projectm_playlist_preset_switch_failed_event dummyCallback = [](const char* preset_filename,
const char* message,
void* user_data) {};
void* dummyData{reinterpret_cast<void*>(348564)};
EXPECT_CALL(mockPlaylist, SetPresetSwitchFailedCallback(dummyCallback, dummyData))
.Times(1);
projectm_playlist_set_preset_switch_failed_event_callback(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), dummyCallback, dummyData);
}

View File

@ -7,7 +7,8 @@
class PlaylistCWrapperMock : public PlaylistCWrapper
{
public:
PlaylistCWrapperMock() : PlaylistCWrapper(nullptr) {};
PlaylistCWrapperMock()
: PlaylistCWrapper(nullptr){};
// PlaylistCWrapper members
MOCK_METHOD(void, Connect, (projectm_handle));
@ -17,14 +18,19 @@ public:
MOCK_METHOD(bool, Empty, (), (const));
MOCK_METHOD(void, Clear, ());
MOCK_METHOD(const std::vector<ProjectM::Playlist::Item>&, Items, (), (const));
MOCK_METHOD(bool, AddItem, (const std::string&, uint32_t, bool));
MOCK_METHOD(uint32_t, AddPath, (const std::string&, uint32_t, bool, bool));
MOCK_METHOD(bool, AddItem, (const std::string&, uint32_t, bool) );
MOCK_METHOD(uint32_t, AddPath, (const std::string&, uint32_t, bool, bool) );
MOCK_METHOD(bool, RemoveItem, (uint32_t));
MOCK_METHOD(void, SetShuffle, (bool));
MOCK_METHOD(void, SetShuffle, (bool) );
MOCK_METHOD(void, Sort, (uint32_t, uint32_t, SortPredicate, SortOrder));
MOCK_METHOD(uint32_t, RetryCount, ());
MOCK_METHOD(void, SetRetryCount, (uint32_t));
MOCK_METHOD(size_t, NextPresetIndex, (), ());
MOCK_METHOD(size_t, PreviousPresetIndex, (), ());
MOCK_METHOD(size_t, LastPresetIndex, (), ());
MOCK_METHOD(size_t, PresetIndex, (), (const));
MOCK_METHOD(size_t, SetPresetIndex, (size_t));
MOCK_METHOD(void, PlayPresetIndex, (size_t, bool, bool));
MOCK_METHOD(void, PlayPresetIndex, (size_t, bool, bool) );
MOCK_METHOD(void, RemoveLastHistoryEntry, ());
MOCK_METHOD(void, SetPresetSwitchFailedCallback, (projectm_playlist_preset_switch_failed_event, void*));
};

View File

@ -459,6 +459,101 @@ TEST(projectMPlaylistPlaylist, NextPresetIndexSequential)
}
TEST(projectMPlaylistPlaylist, PreviousPresetIndexEmptyPlaylist)
{
Playlist playlist;
EXPECT_THROW(playlist.PreviousPresetIndex(), ProjectM::Playlist::PlaylistEmptyException);
}
TEST(projectMPlaylistPlaylist, PreviousPresetIndexShuffle)
{
Playlist playlist;
playlist.SetShuffle(true);
EXPECT_TRUE(playlist.AddItem("/some/PresetZ.milk", Playlist::InsertAtEnd, false));
EXPECT_TRUE(playlist.AddItem("/some/PresetA.milk", Playlist::InsertAtEnd, false));
// Shuffle 100 times, this will have an (almost) 100% chance that both presets were played.
std::set<size_t> playlistIndices;
for (int i = 0; i < 100; i++)
{
EXPECT_NO_THROW(playlistIndices.insert(playlist.PreviousPresetIndex()));
}
EXPECT_TRUE(playlistIndices.find(0) != playlistIndices.end());
EXPECT_TRUE(playlistIndices.find(1) != playlistIndices.end());
}
TEST(projectMPlaylistPlaylist, PreviousPresetIndexSequential)
{
Playlist playlist;
playlist.SetShuffle(false);
EXPECT_TRUE(playlist.AddItem("/some/PresetZ.milk", Playlist::InsertAtEnd, false));
EXPECT_TRUE(playlist.AddItem("/some/PresetA.milk", Playlist::InsertAtEnd, false));
EXPECT_TRUE(playlist.AddItem("/some/other/PresetC.milk", Playlist::InsertAtEnd, false));
EXPECT_EQ(playlist.PreviousPresetIndex(), 2);
EXPECT_EQ(playlist.PreviousPresetIndex(), 1);
EXPECT_EQ(playlist.PreviousPresetIndex(), 0);
EXPECT_EQ(playlist.PreviousPresetIndex(), 2);
}
TEST(projectMPlaylistPlaylist, LastPresetIndex)
{
Playlist playlist;
playlist.SetShuffle(false);
EXPECT_TRUE(playlist.AddItem("/some/PresetZ.milk", Playlist::InsertAtEnd, false));
EXPECT_TRUE(playlist.AddItem("/some/PresetA.milk", Playlist::InsertAtEnd, false));
EXPECT_TRUE(playlist.AddItem("/some/other/PresetC.milk", Playlist::InsertAtEnd, false));
EXPECT_TRUE(playlist.AddItem("/the/last/Preset.milk", Playlist::InsertAtEnd, false));
EXPECT_EQ(playlist.SetPresetIndex(1), 1);
EXPECT_EQ(playlist.SetPresetIndex(2), 2);
EXPECT_EQ(playlist.SetPresetIndex(1), 1);
EXPECT_EQ(playlist.SetPresetIndex(1), 1);
EXPECT_EQ(playlist.SetPresetIndex(0), 0);
EXPECT_EQ(playlist.PresetIndex(), 0);
// Index 1 should only be added once here, even if played twice.
EXPECT_EQ(playlist.LastPresetIndex(), 1);
EXPECT_EQ(playlist.PresetIndex(), 1);
EXPECT_EQ(playlist.LastPresetIndex(), 2);
EXPECT_EQ(playlist.PresetIndex(), 2);
EXPECT_EQ(playlist.LastPresetIndex(), 1);
EXPECT_EQ(playlist.PresetIndex(), 1);
// Starting index 0 is always be in the history.
EXPECT_EQ(playlist.LastPresetIndex(), 0);
EXPECT_EQ(playlist.PresetIndex(), 0);
// History empty, wrap back to last item.
EXPECT_EQ(playlist.LastPresetIndex(), 3);
EXPECT_EQ(playlist.PresetIndex(), 3);
// History should stell be empty, go back one item.
EXPECT_EQ(playlist.LastPresetIndex(), 2);
EXPECT_EQ(playlist.PresetIndex(), 2);
}
TEST(projectMPlaylistPlaylist, LastPresetIndexEmptyPlaylist)
{
Playlist playlist;
EXPECT_THROW(playlist.LastPresetIndex(), ProjectM::Playlist::PlaylistEmptyException);
}
TEST(projectMPlaylistPlaylist, SetPresetIndex)
{
Playlist playlist;
@ -489,7 +584,7 @@ TEST(projectMPlaylistPlaylist, SetPresetIndexException)
{
Playlist playlist;
EXPECT_THROW(playlist.SetPresetIndex(0), ProjectM::Playlist::PlaylistEmptyException);;
EXPECT_THROW(playlist.SetPresetIndex(0), ProjectM::Playlist::PlaylistEmptyException);
}
@ -511,5 +606,24 @@ TEST(projectMPlaylistPlaylist, PresetIndexException)
{
Playlist playlist;
EXPECT_THROW(playlist.PresetIndex(), ProjectM::Playlist::PlaylistEmptyException);;
EXPECT_THROW(playlist.PresetIndex(), ProjectM::Playlist::PlaylistEmptyException);
}
TEST(projectMPlaylistPlaylist, RemoveLastHistoryEntry)
{
Playlist playlist;
EXPECT_TRUE(playlist.AddItem("/some/PresetZ.milk", Playlist::InsertAtEnd, false));
EXPECT_TRUE(playlist.AddItem("/some/PresetA.milk", Playlist::InsertAtEnd, false));
EXPECT_TRUE(playlist.AddItem("/some/other/PresetC.milk", Playlist::InsertAtEnd, false));
EXPECT_EQ(playlist.SetPresetIndex(1), 1);
EXPECT_EQ(playlist.SetPresetIndex(2), 2);
// History: 0,1
playlist.RemoveLastHistoryEntry();
// History: 0
EXPECT_EQ(playlist.LastPresetIndex(), 0);
}