diff --git a/Changelog.md b/Changelog.md index 1291ed5..1ce9316 100644 --- a/Changelog.md +++ b/Changelog.md @@ -7,7 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +* Support Plugin V4 API which allows selecting a playlist entry by index. + **Note:** The CD Player volume will use the new `mstarPlayerVolumeChanged` instead of `mstarTrackVolumeChanged` like in previous API versions. + +### Fixes + * The dialogs to edit or remove files of a track work again. +* Actually call `mstarPlayerVolumeChanged` for plugins. ## [1.3.0] - 2026-02-07 diff --git a/Source/CDPlayer.cpp b/Source/CDPlayer.cpp index 0165d61..58fdabc 100644 --- a/Source/CDPlayer.cpp +++ b/Source/CDPlayer.cpp @@ -249,6 +249,10 @@ void CDPlayer::previousEntry() { m_tracksTable.previous(); } +void CDPlayer::selectEntry(int playlist_index) +{ + m_tracksTable.selectRow(playlist_index); +} void CDPlayer::paint(juce::Graphics& g) { g.fillAll(m_color); @@ -296,7 +300,7 @@ void CDPlayer::setGain(float gain) { m_gain = gain; updateGain(); - m_pluginLoader.trackVolumeChanged(getName().toRawUTF8(), "", m_gain); + m_pluginLoader.cdPlayerVolumeChanged(getName().toRawUTF8(), gain); std::for_each(m_listeners.begin(), m_listeners.end(), std::bind(&MixerControlableChangeListener::gainChanged, std::placeholders::_1, gain)); diff --git a/Source/CDPlayer.h b/Source/CDPlayer.h index bc53db1..78e2dc6 100644 --- a/Source/CDPlayer.h +++ b/Source/CDPlayer.h @@ -40,6 +40,7 @@ class CDPlayer void stop() override; void nextEntry(bool onlyIfEntrySaysSo = false) override; void previousEntry() override; + void selectEntry(int playlist_index) override; // XML serialization public: diff --git a/Source/Player.cpp b/Source/Player.cpp index d017e2b..3bc8c3e 100644 --- a/Source/Player.cpp +++ b/Source/Player.cpp @@ -99,6 +99,7 @@ void Player::setType(PlayerType type) void Player::setGain(float gain) { m_tracksContainer.setGain(gain); + m_pluginLoader.playerVolumeChanged(getName().toRawUTF8(), gain); std::for_each(m_listeners.begin(), m_listeners.end(), std::bind(&MixerControlableChangeListener::gainChanged, std::placeholders::_1, gain)); @@ -419,6 +420,11 @@ void Player::previousEntry() m_pluginLoader.previousEntrySelected(getName().toRawUTF8()); } +void Player::selectEntry(int playlist_index) +{ + m_playlistPlayer.selectPlaylistEntry(playlist_index); +} + void Player::playlistEntryChanged(const std::vector& trackConfigs, bool play, int index) { currentPlaylistEntry = index; diff --git a/Source/Player.h b/Source/Player.h index 205eb0e..54aba92 100644 --- a/Source/Player.h +++ b/Source/Player.h @@ -41,6 +41,7 @@ class Player void stop() override; void nextEntry(bool onlyIfEntrySaysSo = false) override; void previousEntry() override; + void selectEntry(int playlist_index) override; void playlistEntryChanged(const std::vector& trackConfigs, bool play, int index); void gainChangedCallback(const char* track_name, float gain); diff --git a/Source/PlayerComponent.h b/Source/PlayerComponent.h index a5420e4..069d152 100644 --- a/Source/PlayerComponent.h +++ b/Source/PlayerComponent.h @@ -14,4 +14,5 @@ class PlayerComponent virtual void stop() = 0; virtual void nextEntry(bool onlyIfEntrySaysSo = false) = 0; virtual void previousEntry() = 0; + virtual void selectEntry(int playlist_index) = 0; }; diff --git a/Source/PlaylistPlayerWindow.cpp b/Source/PlaylistPlayerWindow.cpp index 9b742e3..56f7373 100644 --- a/Source/PlaylistPlayerWindow.cpp +++ b/Source/PlaylistPlayerWindow.cpp @@ -319,3 +319,8 @@ void PlaylistPlayerWindow::previousPlaylistEntry() { m_tableListBox.previous(); } + +void PlaylistPlayerWindow::selectPlaylistEntry(int playlist_index) +{ + m_tableListBox.selectRow(playlist_index); +} diff --git a/Source/PlaylistPlayerWindow.h b/Source/PlaylistPlayerWindow.h index 1d67290..f759dd8 100644 --- a/Source/PlaylistPlayerWindow.h +++ b/Source/PlaylistPlayerWindow.h @@ -44,6 +44,8 @@ class PlaylistPlayerWindow void previousPlaylistEntry(); + void selectPlaylistEntry(int playlist_index); + int getResizerBarPosition() const; void setResizerBarPosition(int position); diff --git a/Source/PluginInterfaceV4.h b/Source/PluginInterfaceV4.h new file mode 100644 index 0000000..c2c994a --- /dev/null +++ b/Source/PluginInterfaceV4.h @@ -0,0 +1,48 @@ +#pragma once + +namespace PluginInterface::V4 +{ +using ListPlayersCallbackFunction = void (*)(const char* player_name, void* userData); +using ListPlayersFunction = void (*)(ListPlayersCallbackFunction callback, void* userData); +using PlayFunction = void (*)(const char* player_name); +using StopFunction = void (*)(const char* player_name); +using NextEntryFunction = void (*)(const char* player_name); +using PreviousEntryFunction = void (*)(const char* player_name); +using SelectEntryFunction = void (*)(const char* player_name, int playlist_index); +using ListTracksCallbackFunction = void (*)(const char* track_name, void* userData); +using ListTracksFunction = void (*)(const char* player_name, ListTracksCallbackFunction callback, void* userData); +using SetPlayerVolumeFunction = void (*)(const char* player_name, float volume); +using SetTrackVolumeFunction = void (*)(const char* player_name, const char* track_name, float volume); + +struct Init +{ + ListPlayersFunction listPlayers; + PlayFunction play; + StopFunction stop; + NextEntryFunction next; + PreviousEntryFunction previous; + SelectEntryFunction select; + ListTracksFunction listTracks; + SetTrackVolumeFunction setTrackVolume; + SetPlayerVolumeFunction setPlayerVolume; +}; + +using InitFunction = void (*)(const Init&); +using PlayingStateChangedFunction = void (*)(const char* playerName, bool isPlaying); +using NextEntrySelectedFunction = void (*)(const char* playerName); +using PreviousEntrySelectedFunction = void (*)(const char* playerName); +using PlaylistEntrySelectedFunction + = void (*)(const char* playerName, int playlist_index, const char* playlistEntryName, double duration); +using PlaylistEntryDurationChangedFunction = void (*)(const char* playerName, int playlist_index, double duration); +using PlaylistEntryNameChangedFunction + = void (*)(const char* playerName, int playlist_index, const char* playlistEntryName); +using TrackVolumeChangedFunction = void (*)(const char* playerName, const char* track_name, double volume); +using PlayerVolumeChangedFunction = void (*)(const char* playerName, double volume); +using PositionChangedFunction = void (*)(const char* playerName, double position); +using ConfigureFunction = void (*)(); +using ShutdownFunction = void (*)(); + +using LoadConfigurationFunction = void (*)(const char* configurationText); +using GetConfigurationFunction = char* (*)(); +using FreeConfigurationTextFunction = void (*)(const char* configurationText); +} diff --git a/Source/PluginLoader.cpp b/Source/PluginLoader.cpp index 560e17f..bfefbed 100644 --- a/Source/PluginLoader.cpp +++ b/Source/PluginLoader.cpp @@ -99,6 +99,14 @@ void previousEntry(const char* playerName) player->previousEntry(); } +void selectEntry(const char* playerName, int playlist_index) +{ + const juce::MessageManagerLock mmLock; + PlayerComponent* player = getPlayer(playerName); + if (player != nullptr) + player->selectEntry(playlist_index); +} + void listTracksV1(const char* playerName, PluginInterface::V1::ListTracksCallbackFunction callback, void* userData) { const PlayerComponent* player = getPlayer(playerName); @@ -267,6 +275,29 @@ PluginLoader::PluginLoader(MyMultiDocumentPanel* pComponent) } break; } + case 4: + { + auto loadResult = loadPluginV4(*dynamicLibrary); + switch (loadResult.index()) + { + case 0: + { + PluginV4 plugin = std::move(std::get<0>(loadResult)); + plugin.name = dirEntry.getFile().getFileNameWithoutExtension().toStdString(); + plugin.dynamicLibrary = std::move(dynamicLibrary); + pluginsV4.emplace_back(std::move(plugin)); + break; + } + case 1: + { + failedPlugins.add({dirEntry.getFile().getFileName(), std::get<1>(loadResult)}); + break; + } + default: + std::terminate(); + } + break; + } default: failedPlugins.add({dirEntry.getFile().getFileName(), "Plugin has version " + juce::String(version) + " but M*Player requires version 1, 2 or 3."}); @@ -323,6 +354,24 @@ PluginLoader::PluginLoader(MyMultiDocumentPanel* pComponent) } } + { + PluginInterface::V4::Init init; + init.listPlayers = &listPlayersV2; + init.listTracks = &listTracksV2; + init.play = &play; + init.stop = &stop; + init.next = &nextEntry; + init.previous = &previousEntry; + init.select = &selectEntry; + init.setPlayerVolume = &playerVolume; + init.setTrackVolume = &trackVolume; + + for (const auto& plugin : pluginsV4) + { + plugin.initFunction(init); + } + } + if (failedPlugins.size() > 0) { juce::String lines; @@ -350,11 +399,15 @@ PluginLoader::~PluginLoader() { plugin.shutdownFunction(); } + for (const auto& plugin : pluginsV4) + { + plugin.shutdownFunction(); + } } size_t PluginLoader::count() { - return pluginsV1.size() + pluginsV2.size() + pluginsV3.size(); + return pluginsV1.size() + pluginsV2.size() + pluginsV3.size() + pluginsV4.size(); } juce::String PluginLoader::pluginName(size_t index) @@ -369,7 +422,12 @@ juce::String PluginLoader::pluginName(size_t index) index -= pluginsV2.size(); - return pluginsV3[index].name; + if (index < pluginsV3.size()) + return pluginsV3[index].name; + + index -= pluginsV3.size(); + + return pluginsV4[index].name; } void PluginLoader::configure(size_t index) @@ -386,6 +444,11 @@ void PluginLoader::configure(size_t index) if (index < pluginsV3.size()) pluginsV3[index].configureFunction(); + + index -= pluginsV3.size(); + + if (index < pluginsV4.size()) + pluginsV4[index].configureFunction(); } void PluginLoader::playingStateChanged(const char* playerName, bool isPlaying) @@ -402,6 +465,10 @@ void PluginLoader::playingStateChanged(const char* playerName, bool isPlaying) { plugin.playingStateChangedFunction(playerName, isPlaying); } + for (const auto& plugin : pluginsV4) + { + plugin.playingStateChangedFunction(playerName, isPlaying); + } } void PluginLoader::nextEntrySelected(const char* playerName) @@ -418,6 +485,10 @@ void PluginLoader::nextEntrySelected(const char* playerName) { plugin.nextEntrySelectedFunction(playerName); } + for (const auto& plugin : pluginsV4) + { + plugin.nextEntrySelectedFunction(playerName); + } } void PluginLoader::previousEntrySelected(const char* playerName) @@ -434,6 +505,10 @@ void PluginLoader::previousEntrySelected(const char* playerName) { plugin.previousEntrySelectedFunction(playerName); } + for (const auto& plugin : pluginsV4) + { + plugin.previousEntrySelectedFunction(playerName); + } } void PluginLoader::playlistEntrySelected( @@ -451,6 +526,10 @@ void PluginLoader::playlistEntrySelected( { plugin.playlistEntrySelectedFunction(playerName, entryIndex, playlistEntryName, duration); } + for (const auto& plugin : pluginsV4) + { + plugin.playlistEntrySelectedFunction(playerName, entryIndex, playlistEntryName, duration); + } } void PluginLoader::playlistEntryNameChanged(const char* playerName, int entryIndex, const char* playlistEntryName) @@ -463,6 +542,10 @@ void PluginLoader::playlistEntryNameChanged(const char* playerName, int entryInd { plugin.playlistEntryNameChangedFunction(playerName, entryIndex, playlistEntryName); } + for (const auto& plugin : pluginsV4) + { + plugin.playlistEntryNameChangedFunction(playerName, entryIndex, playlistEntryName); + } } void PluginLoader::playlistEntryDurationChanged(const char* playerName, int entryIndex, double duration) @@ -475,6 +558,10 @@ void PluginLoader::playlistEntryDurationChanged(const char* playerName, int entr { plugin.playlistEntryDurationChangedFunction(playerName, entryIndex, duration); } + for (const auto& plugin : pluginsV4) + { + plugin.playlistEntryDurationChangedFunction(playerName, entryIndex, duration); + } } void PluginLoader::trackVolumeChanged(const char* playerName, const char* trackName, float volume) @@ -491,6 +578,42 @@ void PluginLoader::trackVolumeChanged(const char* playerName, const char* trackN { plugin.trackVolumeChangedFunction(playerName, trackName, volume); } + for (const auto& plugin : pluginsV4) + { + plugin.trackVolumeChangedFunction(playerName, trackName, volume); + } +} + +void PluginLoader::playerVolumeChanged(const char* playerName, float volume) +{ + for (const auto& plugin : pluginsV3) + { + plugin.playerVolumeChangedFunction(playerName, volume); + } + for (const auto& plugin : pluginsV4) + { + plugin.playerVolumeChangedFunction(playerName, volume); + } +} + +void PluginLoader::cdPlayerVolumeChanged(const char* playerName, float volume) +{ + for (const auto& plugin : pluginsV1) + { + plugin.trackVolumeChangedFunction(playerName, "", volume); + } + for (const auto& plugin : pluginsV2) + { + plugin.trackVolumeChangedFunction(playerName, "", volume); + } + for (const auto& plugin : pluginsV3) + { + plugin.trackVolumeChangedFunction(playerName, "", volume); + } + for (const auto& plugin : pluginsV4) + { + plugin.playerVolumeChangedFunction(playerName, volume); + } } void PluginLoader::positionChanged(const char* playerName, double position) @@ -507,6 +630,10 @@ void PluginLoader::positionChanged(const char* playerName, double position) { plugin.positionChangedFunction(playerName, position); } + for (const auto& plugin : pluginsV4) + { + plugin.positionChangedFunction(playerName, position); + } } void PluginLoader::saveConfigurations(juce::XmlElement* pluginsElement) @@ -538,6 +665,15 @@ void PluginLoader::saveConfigurations(juce::XmlElement* pluginsElement) plugin.freeConfigurationTextFunction(data); pluginsElement->addChildElement(pluginElement); } + for (auto&& plugin : pluginsV4) + { + juce::XmlElement* pluginElement = new juce::XmlElement("Plugin"); + pluginElement->setAttribute("name", plugin.name); + const char* data = plugin.getConfigurationFunction(); + pluginElement->addTextElement(juce::String::fromUTF8(data)); + plugin.freeConfigurationTextFunction(data); + pluginsElement->addChildElement(pluginElement); + } } void PluginLoader::loadConfigurations(juce::XmlElement* pluginsElement) @@ -569,6 +705,13 @@ void PluginLoader::loadConfigurations(juce::XmlElement* pluginsElement) { it3->loadConfigurationFunction(configurationText.toRawUTF8()); } + auto it4 = std::find_if(pluginsV4.begin(), + pluginsV4.end(), + [pluginName](const PluginV4& plugin) { return plugin.name == pluginName; }); + if (it4 != pluginsV4.end()) + { + it4->loadConfigurationFunction(configurationText.toRawUTF8()); + } } } @@ -879,6 +1022,118 @@ std::variant PluginLoader::loadPluginV3(juc return plugin; } +std::variant PluginLoader::loadPluginV4(juce::DynamicLibrary& dynamicLibrary) +{ + PluginV4 plugin; + + plugin.initFunction = reinterpret_cast(dynamicLibrary.getFunction("mstarInit")); + if (!plugin.initFunction) + { + return "Missing function 'init'."; + } + + plugin.playingStateChangedFunction = reinterpret_cast( + dynamicLibrary.getFunction("mstarPlayingStateChanged")); + if (!plugin.playingStateChangedFunction) + { + return "Missing function 'playingStateChanged'."; + } + + plugin.nextEntrySelectedFunction = reinterpret_cast( + dynamicLibrary.getFunction("mstarNextEntrySelected")); + if (!plugin.nextEntrySelectedFunction) + { + return "Missing function 'nextEntrySelected'."; + } + + plugin.previousEntrySelectedFunction = reinterpret_cast( + dynamicLibrary.getFunction("mstarPreviousEntrySelected")); + if (!plugin.previousEntrySelectedFunction) + { + return "Missing function 'previousEntrySelected'."; + } + + plugin.playlistEntrySelectedFunction = reinterpret_cast( + dynamicLibrary.getFunction("mstarPlaylistEntrySelected")); + if (!plugin.playlistEntrySelectedFunction) + { + return "Missing function 'playlistEntrySelected'."; + } + + plugin.playlistEntryDurationChangedFunction + = reinterpret_cast( + dynamicLibrary.getFunction("mstarPlaylistEntryDurationChanged")); + if (!plugin.playlistEntryDurationChangedFunction) + { + return "Missing function 'playlistEntryDurationChanged'."; + } + + plugin.playlistEntryNameChangedFunction = reinterpret_cast( + dynamicLibrary.getFunction("mstarPlaylistEntryNameChanged")); + if (!plugin.playlistEntrySelectedFunction) + { + return "Missing function 'playlistEntryNameChanged'."; + } + + plugin.playerVolumeChangedFunction = reinterpret_cast( + dynamicLibrary.getFunction("mstarPlayerVolumeChanged")); + if (!plugin.playerVolumeChangedFunction) + { + return "Missing function 'playerVolumeChanged'."; + } + + plugin.trackVolumeChangedFunction = reinterpret_cast( + dynamicLibrary.getFunction("mstarTrackVolumeChanged")); + if (!plugin.trackVolumeChangedFunction) + { + return "Missing function 'trackVolumeChanged'."; + } + + plugin.positionChangedFunction = reinterpret_cast( + dynamicLibrary.getFunction("mstarPositionChanged")); + if (!plugin.positionChangedFunction) + { + return "Missing function 'positionChanged'."; + } + + plugin.configureFunction + = reinterpret_cast(dynamicLibrary.getFunction("mstarConfigure")); + if (!plugin.configureFunction) + { + return "Missing function 'configure'."; + } + + plugin.shutdownFunction + = reinterpret_cast(dynamicLibrary.getFunction("mstarShutdown")); + if (!plugin.shutdownFunction) + { + return "Missing function 'shutdown'."; + } + + plugin.loadConfigurationFunction = reinterpret_cast( + dynamicLibrary.getFunction("mstarLoadConfiguration")); + if (!plugin.loadConfigurationFunction) + { + return "Missing function 'loadConfiguration'."; + } + + plugin.getConfigurationFunction = reinterpret_cast( + dynamicLibrary.getFunction("mstarGetConfiguration")); + if (!plugin.getConfigurationFunction) + { + return "Missing function 'getConfiguration'."; + } + + plugin.freeConfigurationTextFunction = reinterpret_cast( + dynamicLibrary.getFunction("mstarFreeConfigurationText")); + if (!plugin.freeConfigurationTextFunction) + { + return "Missing function 'freeConfigurationText'."; + } + + return plugin; +} + PluginLoader::PluginV1::PluginV1() {} PluginLoader::PluginV1::PluginV1(PluginV1&& other) @@ -906,3 +1161,7 @@ PluginLoader::PluginV2::PluginV2(PluginV2&& other) = default; PluginLoader::PluginV3::PluginV3() {} PluginLoader::PluginV3::PluginV3(PluginV3&& other) = default; + +PluginLoader::PluginV4::PluginV4() {} + +PluginLoader::PluginV4::PluginV4(PluginV4&& other) = default; diff --git a/Source/PluginLoader.h b/Source/PluginLoader.h index 21fdbdd..b8040f5 100644 --- a/Source/PluginLoader.h +++ b/Source/PluginLoader.h @@ -9,6 +9,7 @@ #include "PluginInterfaceV1.h" #include "PluginInterfaceV2.h" #include "PluginInterfaceV3.h" +#include "PluginInterfaceV4.h" class PluginLoader { @@ -33,6 +34,10 @@ class PluginLoader void trackVolumeChanged(const char* playerName, const char* trackName, float volume); + void playerVolumeChanged(const char* playerName, float volume); + + void cdPlayerVolumeChanged(const char* playerName, float volume); + void positionChanged(const char* playerName, double position); size_t count(); @@ -109,11 +114,36 @@ class PluginLoader PluginInterface::V3::FreeConfigurationTextFunction freeConfigurationTextFunction; }; + struct PluginV4 + { + PluginV4(); + PluginV4(PluginV4&& other); + juce::String name; + std::unique_ptr dynamicLibrary; + PluginInterface::V4::InitFunction initFunction; + PluginInterface::V4::PlayingStateChangedFunction playingStateChangedFunction; + PluginInterface::V4::NextEntrySelectedFunction nextEntrySelectedFunction; + PluginInterface::V4::PreviousEntrySelectedFunction previousEntrySelectedFunction; + PluginInterface::V4::PlaylistEntrySelectedFunction playlistEntrySelectedFunction; + PluginInterface::V4::PlaylistEntryDurationChangedFunction playlistEntryDurationChangedFunction; + PluginInterface::V4::PlaylistEntryNameChangedFunction playlistEntryNameChangedFunction; + PluginInterface::V4::PlayerVolumeChangedFunction playerVolumeChangedFunction; + PluginInterface::V4::TrackVolumeChangedFunction trackVolumeChangedFunction; + PluginInterface::V4::PositionChangedFunction positionChangedFunction; + PluginInterface::V4::ConfigureFunction configureFunction; + PluginInterface::V4::ShutdownFunction shutdownFunction; + PluginInterface::V4::LoadConfigurationFunction loadConfigurationFunction; + PluginInterface::V4::GetConfigurationFunction getConfigurationFunction; + PluginInterface::V4::FreeConfigurationTextFunction freeConfigurationTextFunction; + }; + std::variant loadPluginV1(juce::DynamicLibrary& library); std::variant loadPluginV2(juce::DynamicLibrary& library); std::variant loadPluginV3(juce::DynamicLibrary& library); + std::variant loadPluginV4(juce::DynamicLibrary& library); std::vector pluginsV1; std::vector pluginsV2; std::vector pluginsV3; + std::vector pluginsV4; };