From 2a6c1229583776737d10a4077a1dfcd34091fd95 Mon Sep 17 00:00:00 2001 From: laszloh Date: Tue, 20 Feb 2024 17:13:58 +0100 Subject: [PATCH 1/5] Add Playlist class (1/5): Add Playlist interface and MediaItem classes Add the interface of the new playlist class and the MediaItem support classes. --- src/MediaItem.hpp | 317 ++++++++++++++++++++++++++++++++++++++++++++++ src/Playlist.h | 197 ++++++++++++++++++++++++++-- 2 files changed, 502 insertions(+), 12 deletions(-) create mode 100644 src/MediaItem.hpp diff --git a/src/MediaItem.hpp b/src/MediaItem.hpp new file mode 100644 index 00000000..b738bf45 --- /dev/null +++ b/src/MediaItem.hpp @@ -0,0 +1,317 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace media { + +/// @brief Class for providing +class Artwork : public Stream { +public: + Artwork() = default; + virtual ~Artwork() = default; + + // copy operators + Artwork(const Artwork &) = default; + Artwork &operator=(const Artwork &) = default; + + // move operators + Artwork(Artwork &&) noexcept = default; + Artwork &operator=(Artwork &&) noexcept = default; + + String mimeType {String()}; +}; + +class RamArtwork : public Artwork { +public: + RamArtwork() = default; + RamArtwork(std::vector &data) + : data(data) { } + RamArtwork(std::vector &&data) + : data(std::move(data)) { } + RamArtwork(const uint8_t *data, size_t len) { setContent(data, len); } + virtual ~RamArtwork() override = default; + + // copy operators + RamArtwork(const RamArtwork &) = default; + RamArtwork &operator=(const RamArtwork &) = default; + + // move operators + RamArtwork(RamArtwork &&) noexcept = default; + RamArtwork &operator=(RamArtwork &&) noexcept = default; + + void setContent(const uint8_t *data, size_t len) { + this->data.resize(len); + std::copy(data, data + len, this->data.begin()); + readIt = this->data.cbegin(); + } + + void setContent(const std::vector &data) { + this->data = data; + } + + void setContent(std::vector &&data) { + this->data = std::move(data); + } + + // overrides for Stream + + /// @see https://www.arduino.cc/reference/en/language/functions/communication/stream/streamavailable/ + virtual int available() override { + return std::distance(readIt, data.cend()); + } + + /// @see https://www.arduino.cc/reference/en/language/functions/communication/stream/streamread/ + virtual int read() override { + int c = *readIt; + readIt++; + return c; + } + + ///@see https://www.arduino.cc/reference/en/language/functions/communication/stream/streampeek/ + virtual int peek() override { + return *readIt; + } + +protected: + std::vector data {std::vector()}; + std::vector::const_iterator readIt {}; +}; + +class Metadata { +public: + Metadata() = default; + virtual ~Metadata() = default; + + // copy operators for base class are deleted because std::unique_ptr + Metadata(const Metadata &) = default; + Metadata &operator=(const Metadata &) = default; + + // move operators + Metadata(Metadata &&) noexcept = default; + Metadata &operator=(Metadata &&) noexcept = default; + + enum MetaDataType { + title, //< track title + artist, //< track artist + duration, //< track lenght in seconds + albumTitle, //< album title + albumArtist, //< album artist, use artist if not set + displayTitle, //< overrides title if set + trackNumber, //< track number _in the album_ + totalTrackCount, //< total number of track _in the album_ + chapter, //< the chapter number if this is a audiobook + releaseYear, //< year of the track/album release + artwork, //< album artwork + }; + using MetaDataVariant = std::variant>; //< variant ot hold all types of metadata + + /** + * @brief Returns if the field with the requested type is in the map + * @param type The field to find + * @return true when the field is in the map + * @return false when the field is not presen + */ + virtual bool containsField(MetaDataType type) const { + return metadata.find(type) != metadata.cend(); + } + + /** + * @brief Get a field from the metadata + * @param type The requeszed field + * @return MetaDataVariant The vlaue of the field (or std::monostate if the field is not set) + */ + virtual std::optional getMetadataField(MetaDataType type) const { + const auto it = metadata.find(type); + if (it == metadata.end()) { + return std::nullopt; + } + return it->second; + } + + /** + * @brief Set a selected field of the metadata + * @param type The field to be modified + * @param value The value of the field + */ + virtual void setMetadataField(MetaDataType type, const MetaDataVariant &value) { + metadata.insert_or_assign(type, value); + } + + /** + * @see setMetadataField(MetaDataType type, const MetaDataVariant &value) + */ + virtual void setMetadataField(MetaDataType type, MetaDataVariant &&value) { + metadata.insert_or_assign(type, value); + } + + /** + * @brief Compare two Metatdata object + * @param a The first object + * @param b The second object + * @return true if all fields match + */ + friend bool operator==(const Metadata &a, const Metadata &b) { + return a.metadata == b.metadata; + } + +protected: + std::map metadata; +}; + +} // namespace media + +/// @brief Object representing a single entry in the playlist +struct MediaItem { + MediaItem() = default; + MediaItem(const String &path) { + if (fileValid(path)) { + parseMetaData(path); + this->uri = path; + } + } + MediaItem(String &&path) { + if (fileValid(path)) { + parseMetaData(path); + this->uri = std::move(path); + } + } + virtual ~MediaItem() noexcept = default; + + // copy constructor / operator + MediaItem(const MediaItem &) = default; + MediaItem &operator=(const MediaItem &) = default; + + // move constructor / operator + MediaItem(MediaItem &&rhs) noexcept = default; + MediaItem &operator=(MediaItem &&rhs) noexcept = default; + + /** + * @brief Get the playable path to the MediaItem + * @return String a playlable path + */ + virtual String getUri() const { + return uri; + } + + virtual const media::Metadata &getMetadata() const { + return metadata; + } + + /// @brief Compare operator overload for MediaItem, returns true if lhs==rhs + friend bool operator==(MediaItem const &lhs, MediaItem const &rhs) { + return lhs.uri == rhs.uri && lhs.metadata == rhs.metadata; + } + + /** + * @brief Returns the status of the MediaItem + * @return true when the uri is filles out + * @return false when uri is empty + */ + bool isValid() const { + return !uri.isEmpty(); + } + + /// @brief bool operator override + explicit operator bool() const { return isValid(); } + +protected: + String uri {String()}; //< playable uri of the entry + media::Metadata metadata {media::Metadata()}; //< optional metadata for the entry + + /** + * @brief Check the path against a fixed set of urles to determine if this is (propably) a playlable media files + * @param path The uri to be checked + * @return true if all rules pass + * @return false if a single rule fails + * @todo This function could be improved to check against the content instead of the file name + */ + static bool fileValid(const String &path) { + // clang-format off + // all supported extension + constexpr std::array audioFileSufix = { + "mp3", + "aac", + "m4a", + "wav", + "flac", + "ogg", + "oga", + "opus", + // playlists + "m3u", + "m3u8", + "pls", + "asx" + }; + // clang-format on + constexpr size_t maxExtLen = strlen(*std::max_element(audioFileSufix.begin(), audioFileSufix.end(), [](const char *a, const char *b) { + return strlen(a) < strlen(b); + })); + + if (!path || path.isEmpty()) { + // invalid entry + // log_n("Empty path"); + return false; + } + + // check for streams + if (path.startsWith("http://") || path.startsWith("https")) { + return true; + } + + // check for files which start with "/." + int lastSlashPos = path.lastIndexOf('/'); + if (path[lastSlashPos + 1] == '.') { + // this is a hidden file + // log_n("File is hidden: %s", path.c_str()); + return false; + } + + // extract the file extension + int extStartPos = path.lastIndexOf('.'); + if (extStartPos == -1) { + // no extension found + // log_n( "File has no extension: %s", path.c_str()); + return false; + } + extStartPos++; + if ((path.length() - extStartPos) > maxExtLen) { + // the extension is too long + // log_n("File not supported (extension to long): %s", path.c_str()); + return false; + } + String extension = path.substring(extStartPos); + extension.toLowerCase(); + + // check extension against all supported values + for (const auto &e : audioFileSufix) { + if (extension.equals(e)) { + // hit we found the extension + return true; + } + } + // miss, we did not find the extension + // log_n( "File not supported: %s", path.c_str()); + return false; + } + + /** + * @brief Parse meta data from a Stream + * @attention not yet implemented + */ + void parseMetaData(Stream &s) { + } + + /** + * @brief Parse meta data from an uri + * @attention not yet implemented + */ + void parseMetaData(const String &path) { + } +}; diff --git a/src/Playlist.h b/src/Playlist.h index ce587912..2ea8dfd3 100644 --- a/src/Playlist.h +++ b/src/Playlist.h @@ -1,18 +1,191 @@ #pragma once +#include "MediaItem.hpp" + #include -#include -using Playlist = std::vector; +/// @brief Interface class representing a playlist +class Playlist { +public: + /** + * @brief signature for the compare function + * @param a First element for the compare + * @param a Second element for the compare + * @return true if the expression a; -// Release previously allocated memory -inline void freePlaylist(Playlist *(&playlist)) { - if (playlist == nullptr) { - return; - } - for (auto e : *playlist) { - free(e); + Playlist() = default; + virtual ~Playlist() = default; + + // copy constructor / operators + Playlist(const Playlist &) = default; + Playlist &operator=(const Playlist &) = default; + + // move constructor / operators + Playlist(Playlist &&) noexcept = default; + Playlist &operator=(Playlist &&) noexcept = default; + + /** + * @brief get the status of the playlist + * @return true If the playlist has at least 1 playable entry + * @return false If the playlist is invalid + */ + virtual bool isValid() const = 0; + + /** + * @brief Allow explicit bool conversions, like when calling `if(playlist)` + * @see isValid() + * @return A bool conversion representing the status of the playlist + */ + explicit operator bool() const { return isValid(); } + + /** + * @brief Get the number of entries in the playlist + * @return size_t The number of MediaItem elemenets in the underlying container + */ + virtual size_t size() const = 0; + + /** + * @brief Get the element at index + * @param idx The queried index + * @return MediaItem the data at the index + */ + virtual MediaItem &at(size_t idx) = 0; + + /// @see MediaItem &at(size_t idx) + const MediaItem &at(size_t idx) const { return at(idx); } + + /** + * @brief Add an item at the end of the container + * @param item The new item ot be added + * @return true when the operation succeeded + * @return false on error + */ + virtual bool addMediaItem(MediaItem &&item) = 0; + + /** + * @brief Add an item at the end of the container + * @param item The new item ot be added + * @param idx after which entry it'll be added + * @return true when the operation succeeded + * @return false on error + */ + virtual bool addMediaItem(MediaItem &&item, size_t idx) = 0; + + /** + * @brief Remove free space in underlying container + */ + virtual void compress() { } + + /** + * @brief Remove a single item from the container + * @param idx The idex to be removed + */ + virtual void removeMediaItem(size_t idx) = 0; + + /** + * @brief Remove a range of items from the underlying container + * @param startIdx The first element to be reomved + * @param endIdx The first element, which shall _not_ be removed + * @attention This function removes the range (first, last] + */ + virtual void removeMediaItem(size_t startIdx, size_t endIdx) { + for (size_t i = startIdx; i < endIdx; i++) { + removeMediaItem(i); + } } - delete playlist; - playlist = nullptr; -} + + /** + * @brief Sort the underlying container according to the supplied sort functions + * @param comp The compare function to use, defaults to strcmp between the two uri objects + */ + virtual void sort(CompareFunc comp = [](const MediaItem &a, const MediaItem &b) -> bool { return a.getUri() < b.getUri(); }) { } + + /** + * @brief Randomize the underlying entries + */ + virtual void shuffle() { } + + /** + * @brief Array opertor override for item access + * @param idx the queried index + * @return const MediaItem& Reference to the MediaItem at the index + */ + MediaItem &operator[](size_t idx) { return at(idx); }; + + /// @see MediaItem &operator[](size_t idx) + const MediaItem &operator[](size_t idx) const { return at(idx); }; + + ///@brief Iterator class to access playlist items + class Iterator { + public: + // define what we can do + using value_type = MediaItem; + using iterator_category = std::random_access_iterator_tag; //< support random access iterators + using difference_type = typename std::iterator::difference_type; + using pointer = value_type *; + using reference = value_type &; + + // Lifecycle + Iterator() = default; + Iterator(Playlist *playlist, size_t idx) + : m_playlist(playlist) + , m_idx(idx) { } + + // Operators: Access + inline reference operator*() { return m_playlist->at(m_idx); } + inline pointer operator->() { return &m_playlist->at(m_idx); } + inline reference operator[](difference_type rhs) { return m_playlist->at(rhs); } + + // Operators: arithmetic + // clang-format off + inline Iterator& operator++() { ++m_idx; return *this; } + inline Iterator& operator--() { --m_idx; return *this; } + inline Iterator operator++(int) { Iterator tmp(*this); ++m_idx; return tmp; } + inline Iterator operator--(int) { Iterator tmp(*this); --m_idx; return tmp; } + inline difference_type operator-(const Iterator &rhs) const { return m_idx - rhs.m_idx; } + inline Iterator operator+(difference_type rhs) const { return Iterator(m_playlist, m_idx + rhs); } + inline Iterator operator-(difference_type rhs) const { return Iterator(m_playlist, m_idx - rhs); } + inline Iterator& operator+=(difference_type rhs) { m_idx += rhs; return *this; } + inline Iterator& operator-=(difference_type rhs) { m_idx -= rhs; return *this; } + friend inline Iterator operator+(difference_type lhs, const Iterator &rhs) { return Iterator(rhs.m_playlist, rhs.m_idx + lhs); } + friend inline Iterator operator-(difference_type lhs, const Iterator &rhs) { return Iterator(rhs.m_playlist, lhs - rhs.m_idx); } + // clang-format on + + // Operators: comparison + inline bool operator==(const Iterator &rhs) { return m_idx == rhs.m_idx; } + inline bool operator!=(const Iterator &rhs) { return m_idx != rhs.m_idx; } + inline bool operator>(const Iterator &rhs) { return m_idx > rhs.m_idx; } + inline bool operator<(const Iterator &rhs) { return m_idx < rhs.m_idx; } + inline bool operator>=(const Iterator &rhs) { return m_idx >= rhs.m_idx; } + inline bool operator<=(const Iterator &rhs) { return m_idx <= rhs.m_idx; } + + protected: + Playlist *m_playlist {nullptr}; + size_t m_idx {0}; + }; + + Iterator begin() { return Iterator(this, 0); } + Iterator end() { return Iterator(this, size()); } + + // convenience functions with iterators + + /** + * @brief Add an item to the container at the position marked by the iterator + * @see Playlist::addMediaItem(const MediaItem &&item, int idx) + */ + bool addMediaItem(MediaItem &&item, Iterator pos) { return addMediaItem(std::move(item), std::distance(begin(), pos)); } + + /** + * @brief Remove the item at the position marked by the iterator + * @see Playlist::removeMediaItem(int idx) + */ + void removeMediaItem(Iterator pos) { removeMediaItem(std::distance(begin(), pos)); } + + /** + * @brief Remove the range (first, last] defined by the iterators + * @see Playlist::removeMediaItemsize_t startIdx, size_t endIdx) + */ + void removeMediaItem(Iterator first, Iterator last) { removeMediaItem(std::distance(begin(), first), std::distance(begin(), last)); } +}; From 8ab3aec185844d076b790f686a5e644731452523 Mon Sep 17 00:00:00 2001 From: laszloh Date: Wed, 21 Feb 2024 11:06:35 +0100 Subject: [PATCH 2/5] Add Playlist class (2/5): Add basic Playlist implementation Add a basic implementation of the Playlist interface, which holds a list of MediaItems. --- src/Playlist/BasicPlaylist.hpp | 83 ++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/Playlist/BasicPlaylist.hpp diff --git a/src/Playlist/BasicPlaylist.hpp b/src/Playlist/BasicPlaylist.hpp new file mode 100644 index 00000000..b6be0e2e --- /dev/null +++ b/src/Playlist/BasicPlaylist.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include + +#include "../Playlist.h" + +#include +#include +#include +#include + +class BasicPlaylist : public Playlist { +public: + BasicPlaylist() = default; + BasicPlaylist(size_t initialCap) { + container.reserve(initialCap); + } + virtual ~BasicPlaylist() = default; + + // copy operators + BasicPlaylist(const BasicPlaylist &) = default; + BasicPlaylist &operator=(const BasicPlaylist &) = default; + + // move operators + BasicPlaylist(BasicPlaylist &&) noexcept = default; + BasicPlaylist &operator=(BasicPlaylist &&) noexcept = default; + + // implementation fo the virtual funtions + + /// @see Playlist::isValid() + virtual bool isValid() const { return !container.empty(); } + + /// @see Playlist::size() + virtual size_t size() const { return container.size(); } + + /// @see Playlist::at(size_t idx) + virtual MediaItem &at(size_t idx) { return container.at(idx); } + + /// @see Playlist::addMediaItem(const MediaItem &&item) + virtual bool addMediaItem(MediaItem &&item) { return addMediaItem(std::move(item), container.size()); } + + /// @see Playlist::addMediaItem(const MediaItem &&item, int idx) + virtual bool addMediaItem(MediaItem &&item, size_t idx) { + if (!item.isValid()) { + return false; + } + container.emplace(container.begin() + idx, std::move(item)); + return true; + } + + /// @see Playlist::compress() + virtual void compress() override { container.shrink_to_fit(); } + + /** + * @brief Remove a single item from the container + * @param idx The idex to be removed + */ + virtual void removeMediaItem(size_t idx) override { + container.erase(container.begin() + idx); + } + + /// @see Playlist::sort(CompareFunc comp) + virtual void sort(CompareFunc comp) { + if (container.size() < 2) { + // we can not sort less than two items + return; + } + std::sort(container.begin(), container.end(), comp); + } + + /// @see Playlist::shuffle() + virtual void shuffle() { + if (container.size() < 2) { + // we can not shuffle less than two items + return; + } + auto rng = std::default_random_engine {}; + std::shuffle(container.begin(), container.end(), rng); + } + +protected: + std::vector container; +}; From e165ca2da9dec7e6014bcd4c6f5b7cc124ee2446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laszlo=20Heged=C3=BCs?= Date: Sat, 23 Mar 2024 12:31:30 +0100 Subject: [PATCH 3/5] Add Playlist class (3/5): Embed playlist into the AudioPlayer Embed the playlist class into the AudiPlayer and move to use std::unique_ptr to reduce manual memory managment overhead --- src/AudioPlayer.cpp | 202 ++++++++++++++++++-------------------------- src/AudioPlayer.h | 37 +++++++- 2 files changed, 114 insertions(+), 125 deletions(-) diff --git a/src/AudioPlayer.cpp b/src/AudioPlayer.cpp index 02adaa31..2f652e52 100644 --- a/src/AudioPlayer.cpp +++ b/src/AudioPlayer.cpp @@ -12,6 +12,7 @@ #include "Log.h" #include "MemX.h" #include "Mqtt.h" +#include "Playlist/BasicPlaylist.hpp" #include "Port.h" #include "Queues.h" #include "Rfid.h" @@ -31,7 +32,7 @@ #define AUDIOPLAYER_VOLUME_MIN 0u #define AUDIOPLAYER_VOLUME_INIT 3u -playProps gPlayProperties; +PlayProps gPlayProperties; TaskHandle_t AudioTaskHandle; // uint32_t cnt123 = 0; @@ -64,13 +65,9 @@ static uint8_t AudioPlayer_MaxVolumeHeadphone = 11u; // Maximum volume that can static void AudioPlayer_Task(void *parameter); static void AudioPlayer_HeadphoneVolumeManager(void); -static std::optional AudioPlayer_ReturnPlaylistFromWebstream(const char *_webUrl); -static bool AudioPlayer_ArrSortHelper_strcmp(const char *a, const char *b); -static bool AudioPlayer_ArrSortHelper_strnatcmp(const char *a, const char *b); -static bool AudioPlayer_ArrSortHelper_strnatcasecmp(const char *a, const char *b); -static void AudioPlayer_SortPlaylist(Playlist *playlist); -static void AudioPlayer_RandomizePlaylist(Playlist *playlist); -static size_t AudioPlayer_NvsRfidWriteWrapper(const char *_rfidCardId, const char *_track, const uint32_t _playPosition, const uint8_t _playMode, const uint16_t _trackLastPlayed, const uint16_t _numberOfTracks); +static Playlist *AudioPlayer_ReturnPlaylistFromWebstream(const char *_webUrl); +static void AudioPlayer_SortPlaylist(Playlist &playlist); +static size_t AudioPlayer_NvsRfidWriteWrapper(const char *_rfidCardId, const Playlist &playlist, uint32_t currentTrack, uint32_t _playPosition, uint8_t _playMode, uint16_t _trackLastPlayed); static void AudioPlayer_ClearCover(void); void AudioPlayer_Init(void) { @@ -132,15 +129,8 @@ void AudioPlayer_Init(void) { // Adjust volume depending on headphone is connected and volume-adjustment is enabled AudioPlayer_SetupVolumeAndAmps(); - // initialize gPlayProperties - memset(&gPlayProperties, 0, sizeof(gPlayProperties)); - gPlayProperties.playlistFinished = true; - // clear title and cover image - gPlayProperties.title[0] = '\0'; - gPlayProperties.coverFilePos = 0; AudioPlayer_StationLogoUrl = ""; - gPlayProperties.playlist = new Playlist(); // Don't start audio-task in BT-speaker mode! if ((System_GetOperationMode() == OPMODE_NORMAL) || (System_GetOperationMode() == OPMODE_BLUETOOTH_SOURCE)) { @@ -455,8 +445,7 @@ void AudioPlayer_Task(void *parameter) { audio->stopSong(); // destroy the old playlist and assign the new - freePlaylist(gPlayProperties.playlist); - gPlayProperties.playlist = newPlaylist; + gPlayProperties.playlist.reset(newPlaylist); Log_Printf(LOGLEVEL_NOTICE, newPlaylistReceived, gPlayProperties.playlist->size()); Log_Printf(LOGLEVEL_DEBUG, "Free heap: %u", ESP.getFreeHeap()); playbackTimeoutStart = millis(); @@ -483,7 +472,7 @@ void AudioPlayer_Task(void *parameter) { if (gPlayProperties.saveLastPlayPosition) { // Don't save for AUDIOBOOK_LOOP because not necessary if (gPlayProperties.currentTrackNumber + 1 < gPlayProperties.playlist->size()) { // Only save if there's another track, otherwise it will be saved at end of playlist anyway - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber + 1, gPlayProperties.playlist->size()); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *gPlayProperties.playlist, gPlayProperties.currentTrackNumber, 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber + 1); } } if (gPlayProperties.sleepAfterCurrentTrack) { // Go to sleep if "sleep after track" was requested @@ -532,7 +521,7 @@ void AudioPlayer_Task(void *parameter) { } if (gPlayProperties.saveLastPlayPosition && !gPlayProperties.pausePlay) { Log_Printf(LOGLEVEL_INFO, trackPausedAtPos, audio->getFilePos(), audio->getFilePos() - audio->inBufferFilled()); - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber), audio->getFilePos() - audio->inBufferFilled(), gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.playlist->size()); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *gPlayProperties.playlist, gPlayProperties.currentTrackNumber, audio->getFilePos() - audio->inBufferFilled(), gPlayProperties.playMode, gPlayProperties.currentTrackNumber); } gPlayProperties.pausePlay = !gPlayProperties.pausePlay; Web_SendWebsocketData(0, 30); @@ -559,7 +548,7 @@ void AudioPlayer_Task(void *parameter) { gPlayProperties.currentTrackNumber++; } if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.playlist->size()); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *gPlayProperties.playlist, gPlayProperties.currentTrackNumber, 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber); Log_Println(trackStartAudiobook, LOGLEVEL_INFO); } Log_Println(cmndNextTrack, LOGLEVEL_INFO); @@ -608,7 +597,7 @@ void AudioPlayer_Task(void *parameter) { } if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.playlist->size()); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *gPlayProperties.playlist, gPlayProperties.currentTrackNumber, 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber); Log_Println(trackStartAudiobook, LOGLEVEL_INFO); } @@ -618,11 +607,11 @@ void AudioPlayer_Task(void *parameter) { } } else { if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.playlist->size()); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *gPlayProperties.playlist, gPlayProperties.currentTrackNumber, 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber); } audio->stopSong(); Led_Indicate(LedIndicatorType::Rewind); - audioReturnCode = audio->connecttoFS(gFSystem, gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber)); + audioReturnCode = audio->connecttoFS(gFSystem, gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber).getUri().c_str()); // consider track as finished, when audio lib call was not successful if (!audioReturnCode) { System_IndicateError(); @@ -642,7 +631,7 @@ void AudioPlayer_Task(void *parameter) { } gPlayProperties.currentTrackNumber = 0; if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.playlist->size()); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *gPlayProperties.playlist, gPlayProperties.currentTrackNumber, 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber); Log_Println(trackStartAudiobook, LOGLEVEL_INFO); } Log_Println(cmndFirstTrack, LOGLEVEL_INFO); @@ -660,7 +649,7 @@ void AudioPlayer_Task(void *parameter) { if (gPlayProperties.currentTrackNumber + 1 < gPlayProperties.playlist->size()) { gPlayProperties.currentTrackNumber = gPlayProperties.playlist->size() - 1; if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.playlist->size()); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *gPlayProperties.playlist, gPlayProperties.currentTrackNumber, 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber); Log_Println(trackStartAudiobook, LOGLEVEL_INFO); } Log_Println(cmndLastTrack, LOGLEVEL_INFO); @@ -686,7 +675,7 @@ void AudioPlayer_Task(void *parameter) { if (gPlayProperties.playUntilTrackNumber == gPlayProperties.currentTrackNumber && gPlayProperties.playUntilTrackNumber > 0) { if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber), 0, gPlayProperties.playMode, 0, gPlayProperties.playlist->size()); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *gPlayProperties.playlist, gPlayProperties.currentTrackNumber, 0, gPlayProperties.playMode, 0); } gPlayProperties.playlistFinished = true; gPlayProperties.playMode = NO_PLAYLIST; @@ -699,7 +688,7 @@ void AudioPlayer_Task(void *parameter) { if (!gPlayProperties.repeatPlaylist) { if (gPlayProperties.saveLastPlayPosition) { // Set back to first track - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->at(0), 0, gPlayProperties.playMode, 0, gPlayProperties.playlist->size()); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *gPlayProperties.playlist, 0, 0, gPlayProperties.playMode, 0); } gPlayProperties.playlistFinished = true; gPlayProperties.playMode = NO_PLAYLIST; @@ -723,12 +712,12 @@ void AudioPlayer_Task(void *parameter) { Log_Println(repeatPlaylistDueToPlaymode, LOGLEVEL_NOTICE); gPlayProperties.currentTrackNumber = 0; if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->at(0), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.playlist->size()); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *gPlayProperties.playlist, 0, 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber); } } } - if (!strncmp("http", gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber), 4)) { + if (!strncmp("http", gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber).getUri().c_str(), 4)) { gPlayProperties.isWebstream = true; } else { gPlayProperties.isWebstream = false; @@ -737,17 +726,17 @@ void AudioPlayer_Task(void *parameter) { audioReturnCode = false; if (gPlayProperties.playMode == WEBSTREAM || (gPlayProperties.playMode == LOCAL_M3U && gPlayProperties.isWebstream)) { // Webstream - audioReturnCode = audio->connecttohost(gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber)); + audioReturnCode = audio->connecttohost(gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber).getUri().c_str()); gPlayProperties.playlistFinished = false; gTriedToConnectToHost = true; } else if (gPlayProperties.playMode != WEBSTREAM && !gPlayProperties.isWebstream) { // Files from SD - if (!gFSystem.exists(gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber))) { // Check first if file/folder exists - Log_Printf(LOGLEVEL_ERROR, dirOrFileDoesNotExist, gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber)); + if (!gFSystem.exists(gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber).getUri().c_str())) { // Check first if file/folder exists + Log_Printf(LOGLEVEL_ERROR, dirOrFileDoesNotExist, gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber).getUri().c_str()); gPlayProperties.trackFinished = true; continue; } else { - audioReturnCode = audio->connecttoFS(gFSystem, gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber)); + audioReturnCode = audio->connecttoFS(gFSystem, gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber).getUri().c_str()); // consider track as finished, when audio lib call was not successful } } @@ -765,7 +754,7 @@ void AudioPlayer_Task(void *parameter) { Log_Printf(LOGLEVEL_NOTICE, trackStartatPos, gPlayProperties.startAtFilePos); gPlayProperties.startAtFilePos = 0; } - const char *title = gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber); + const char *title = gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber).getUri().c_str(); if (gPlayProperties.isWebstream) { title = "Webradio"; } @@ -775,7 +764,7 @@ void AudioPlayer_Task(void *parameter) { Audio_setTitle("%s", title); } AudioPlayer_ClearCover(); - Log_Printf(LOGLEVEL_NOTICE, currentlyPlaying, gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber), (gPlayProperties.currentTrackNumber + 1), gPlayProperties.playlist->size()); + Log_Printf(LOGLEVEL_NOTICE, currentlyPlaying, gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber).getUri().c_str(), (gPlayProperties.currentTrackNumber + 1), gPlayProperties.playlist->size()); gPlayProperties.playlistFinished = false; } } @@ -996,27 +985,24 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l gPlayProperties.startAtFilePos = _lastPlayPos; gPlayProperties.currentTrackNumber = _trackLastPlayed; - std::optional musicFiles; + Playlist *playlist = nullptr; String folderPath = _itemToPlay; if (_playMode != WEBSTREAM) { if (_playMode == RANDOM_SUBDIRECTORY_OF_DIRECTORY || _playMode == RANDOM_SUBDIRECTORY_OF_DIRECTORY_ALL_TRACKS_OF_DIR_RANDOM) { folderPath = SdCard_pickRandomSubdirectory(_itemToPlay); - if (!folderPath) { - // If error occured while extracting random subdirectory - musicFiles = std::nullopt; - } else { - musicFiles = SdCard_ReturnPlaylist(folderPath.c_str(), _playMode); // Provide random subdirectory in order to enter regular playlist-generation + if (folderPath) { + playlist = SdCard_ReturnPlaylist(folderPath.c_str(), _playMode); // Provide random subdirectory in order to enter regular playlist-generation } } else { - musicFiles = SdCard_ReturnPlaylist(_itemToPlay, _playMode); + playlist = SdCard_ReturnPlaylist(_itemToPlay, _playMode); } } else { - musicFiles = AudioPlayer_ReturnPlaylistFromWebstream(_itemToPlay); + playlist = AudioPlayer_ReturnPlaylistFromWebstream(_itemToPlay); } // Catch if error occured (e.g. file not found) - if (!musicFiles) { + if (!playlist) { Log_Println(errorOccured, LOGLEVEL_ERROR); System_IndicateError(); if (gPlayProperties.playMode != NO_PLAYLIST) { @@ -1026,8 +1012,7 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l } gPlayProperties.playMode = BUSY; // Show @Neopixel, if uC is busy with creating playlist - Playlist *list = musicFiles.value(); - if (!list->size()) { + if (!playlist->isValid()) { Log_Println(noMp3FilesInDir, LOGLEVEL_NOTICE); System_IndicateError(); if (!gPlayProperties.pausePlay) { @@ -1038,7 +1023,7 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l } gPlayProperties.playMode = NO_PLAYLIST; - freePlaylist(list); + delete playlist; return; } @@ -1075,20 +1060,16 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l gPlayProperties.playUntilTrackNumber = 0; Led_SetNightmode(true); Log_Println(modeSingleTrackRandom, LOGLEVEL_NOTICE); - AudioPlayer_RandomizePlaylist(list); + playlist->shuffle(); // we have a random order, so pick the first entry and scrap the rest - auto first = list->at(0); - list->at(0) = nullptr; // prevent our entry from being destroyed - freePlaylist(list); // this also scrapped our vector --> recreate it - list = new Playlist(); - list->push_back(first); + playlist->removeMediaItem(std::next(playlist->begin()), playlist->end()); break; } case AUDIOBOOK: { // Tracks need to be sorted! gPlayProperties.saveLastPlayPosition = true; Log_Println(modeSingleAudiobook, LOGLEVEL_NOTICE); - AudioPlayer_SortPlaylist(list); + AudioPlayer_SortPlaylist(*playlist); break; } @@ -1096,35 +1077,35 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l gPlayProperties.repeatPlaylist = true; gPlayProperties.saveLastPlayPosition = true; Log_Println(modeSingleAudiobookLoop, LOGLEVEL_NOTICE); - AudioPlayer_SortPlaylist(list); + AudioPlayer_SortPlaylist(*playlist); break; } case ALL_TRACKS_OF_DIR_SORTED: case RANDOM_SUBDIRECTORY_OF_DIRECTORY: { Log_Printf(LOGLEVEL_NOTICE, modeAllTrackAlphSorted, folderPath.c_str()); - AudioPlayer_SortPlaylist(list); + AudioPlayer_SortPlaylist(*playlist); break; } case ALL_TRACKS_OF_DIR_RANDOM: case RANDOM_SUBDIRECTORY_OF_DIRECTORY_ALL_TRACKS_OF_DIR_RANDOM: { Log_Printf(LOGLEVEL_NOTICE, modeAllTrackRandom, folderPath.c_str()); - AudioPlayer_RandomizePlaylist(list); + playlist->shuffle(); break; } case ALL_TRACKS_OF_DIR_SORTED_LOOP: { gPlayProperties.repeatPlaylist = true; Log_Println(modeAllTrackAlphSortedLoop, LOGLEVEL_NOTICE); - AudioPlayer_SortPlaylist(list); + AudioPlayer_SortPlaylist(*playlist); break; } case ALL_TRACKS_OF_DIR_RANDOM_LOOP: { gPlayProperties.repeatPlaylist = true; Log_Println(modeAllTrackRandomLoop, LOGLEVEL_NOTICE); - AudioPlayer_RandomizePlaylist(list); + playlist->shuffle(); break; } @@ -1149,47 +1130,49 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l if (!error) { gPlayProperties.playMode = _playMode; - xQueueSend(gTrackQueue, &list, 0); + xQueueSend(gTrackQueue, &playlist, 0); return; } // we had an error, blink and destroy playlist gPlayProperties.playMode = NO_PLAYLIST; System_IndicateError(); - freePlaylist(list); + delete playlist; } -/* Wraps putString for writing settings into NVS for RFID-cards. - Returns number of characters written. */ -size_t AudioPlayer_NvsRfidWriteWrapper(const char *_rfidCardId, const char *_track, const uint32_t _playPosition, const uint8_t _playMode, const uint16_t _trackLastPlayed, const uint16_t _numberOfTracks) { - if (_playMode == NO_PLAYLIST) { +/** + * @brief Write current play position and settings for RFID-Card into NVS + * @param _rfidCardId The card ID to be modified + * @param playlist The playlist to be saved + * @param currentTrack currently playing track + * @param _playPosition The resume position in to file + * @param _playMode The playback modus + * @param _trackLastPlayed Track number for restart + * @return number of characters written + */ +static size_t AudioPlayer_NvsRfidWriteWrapper(const char *_rfidCardId, const Playlist &playlist, uint32_t currentTrack, uint32_t _playPosition, uint8_t _playMode, uint16_t _trackLastPlayed) { + if (_playMode == NO_PLAYLIST || !playlist) { // writing back to NVS with NO_PLAYLIST seems to be a bug - Todo: Find the cause here Log_Printf(LOGLEVEL_ERROR, modeInvalid, _playMode); return 0; } - Led_SetPause(true); // Workaround to prevent exceptions due to Neopixel-signalisation while NVS-write - char prefBuf[275]; - char trackBuf[255]; - snprintf(trackBuf, sizeof(trackBuf) / sizeof(trackBuf[0]), _track); - - // If it's a directory we just want to play/save basename(path) - if (_numberOfTracks > 1) { - const char s = '/'; - const char *last = strrchr(_track, s); - const char *first = strchr(_track, s); - unsigned long substr = last - first + 1; - if (substr <= sizeof(trackBuf) / sizeof(trackBuf[0])) { - snprintf(trackBuf, substr, _track); // save substring basename(_track) - } else { - return 0; // Filename too long! - } + const MediaItem &item = playlist.at(currentTrack); + String uri = item.getUri(); + + int lastSlashIdx = uri.lastIndexOf('/'); + if (playlist.size() > 1 && lastSlashIdx > 0) { + // only save the basename in directory mode + uri = uri.substring(0, lastSlashIdx); } - snprintf(prefBuf, sizeof(prefBuf) / sizeof(prefBuf[0]), "%s%s%s%u%s%d%s%u", stringDelimiter, trackBuf, stringDelimiter, _playPosition, stringDelimiter, _playMode, stringDelimiter, _trackLastPlayed); - Log_Printf(LOGLEVEL_INFO, wroteLastTrackToNvs, prefBuf, _rfidCardId, _playMode, _trackLastPlayed); - Log_Println(prefBuf, LOGLEVEL_INFO); - Led_SetPause(false); - return gPrefsRfid.putString(_rfidCardId, prefBuf); + size_t prefBufLen = uri.length() + 20; + auto prefBuf = std::make_unique(prefBufLen); + constexpr const char *prefTmplt = "%s%s%s%u%s%d%s%u"; + + snprintf(prefBuf.get(), prefBufLen, prefTmplt, stringDelimiter, uri.c_str(), stringDelimiter, _playPosition, stringDelimiter, _playMode, stringDelimiter, _trackLastPlayed); + Log_Printf(LOGLEVEL_INFO, wroteLastTrackToNvs, prefBuf.get(), _rfidCardId, _playMode, _trackLastPlayed); + Log_Println(prefBuf.get(), LOGLEVEL_INFO); + return gPrefsRfid.putString(_rfidCardId, prefBuf.get()); // Examples for serialized RFID-actions that are stored in NVS // #### @@ -1202,20 +1185,9 @@ size_t AudioPlayer_NvsRfidWriteWrapper(const char *_rfidCardId, const char *_tra } // Adds webstream to playlist; same like SdCard_ReturnPlaylist() but always only one entry -std::optional AudioPlayer_ReturnPlaylistFromWebstream(const char *_webUrl) { - Playlist *playlist = new Playlist(); - const size_t len = strlen(_webUrl) + 1; - char *entry = static_cast(x_malloc(len)); - if (!entry) { - // OOM - Log_Println(unableToAllocateMemForLinearPlaylist, LOGLEVEL_ERROR); - freePlaylist(playlist); - return std::nullopt; - } - strncpy(entry, _webUrl, len); - entry[len - 1] = '\0'; - playlist->push_back(entry); - +Playlist *AudioPlayer_ReturnPlaylistFromWebstream(const char *_webUrl) { + Playlist *playlist = new BasicPlaylist(1); + playlist->addMediaItem(MediaItem(_webUrl)); return playlist; } @@ -1224,36 +1196,24 @@ void AudioPlayer_TrackControlToQueueSender(const uint8_t trackCommand) { xQueueSend(gTrackControlQueue, &trackCommand, 0); } -// Knuth-Fisher-Yates-algorithm to randomize playlist -void AudioPlayer_RandomizePlaylist(Playlist *playlist) { - if (playlist->size() < 2) { - // we can not randomize less than 2 entries - return; - } - - // randomize using the "normal" random engine and shuffle - std::default_random_engine rnd(millis()); - std::shuffle(playlist->begin(), playlist->end(), rnd); -} - // Helper to sort playlist - standard string comparison -static bool AudioPlayer_ArrSortHelper_strcmp(const char *a, const char *b) { - return strcmp(a, b) < 0; +static bool AudioPlayer_ArrSortHelper_strcmp(const MediaItem &a, const MediaItem &b) { + return strcmp(a.getUri().c_str(), b.getUri().c_str()) < 0; } // Helper to sort playlist - natural case-sensitive -static bool AudioPlayer_ArrSortHelper_strnatcmp(const char *a, const char *b) { - return strnatcmp(a, b) < 0; +static bool AudioPlayer_ArrSortHelper_strnatcmp(const MediaItem &a, const MediaItem &b) { + return strnatcmp(a.getUri().c_str(), b.getUri().c_str()) < 0; } // Helper to sort playlist - natural case-insensitive -static bool AudioPlayer_ArrSortHelper_strnatcasecmp(const char *a, const char *b) { - return strnatcasecmp(a, b) < 0; +static bool AudioPlayer_ArrSortHelper_strnatcasecmp(const MediaItem &a, const MediaItem &b) { + return strnatcasecmp(a.getUri().c_str(), b.getUri().c_str()) < 0; } // Sort playlist -void AudioPlayer_SortPlaylist(Playlist *playlist) { - std::function cmpFunc; +void AudioPlayer_SortPlaylist(Playlist &playlist) { + Playlist::CompareFunc cmpFunc; const char *mode; switch (AudioPlayer_PlaylistSortMode) { case playlistSortMode::STRCMP: @@ -1272,7 +1232,7 @@ void AudioPlayer_SortPlaylist(Playlist *playlist) { } Log_Printf(LOGLEVEL_INFO, "Sorting files using %s", mode); - std::sort(playlist->begin(), playlist->end(), cmpFunc); + playlist.sort(cmpFunc); } // Clear cover send notification diff --git a/src/AudioPlayer.h b/src/AudioPlayer.h index 689356cc..6922de5b 100644 --- a/src/AudioPlayer.h +++ b/src/AudioPlayer.h @@ -2,6 +2,7 @@ #include "Playlist.h" +#include #include #ifndef AUDIOPLAYER_PLAYLIST_SORT_MODE_DEFAULT @@ -14,9 +15,9 @@ enum class playlistSortMode : uint8_t { STRNATCASECMP = 3, }; -typedef struct { // Bit field +struct PlayProps { // Bit field uint8_t playMode : 4; // playMode - Playlist *playlist; // playlist + std::unique_ptr playlist; // playlist char title[255]; // current title bool repeatCurrentTrack : 1; // If current track should be looped bool repeatPlaylist : 1; // If whole playlist should be looped @@ -42,9 +43,37 @@ typedef struct { // Bit field size_t coverFilePos; // current cover file position size_t coverFileSize; // current cover file size size_t audioFileSize; // file size of current audio file -} playProps; -extern playProps gPlayProperties; + PlayProps() + : playMode(NO_PLAYLIST) + , playlist(nullptr) + , title {} + , repeatCurrentTrack(false) + , repeatPlaylist(false) + , currentTrackNumber(0) + , startAtFilePos(0) + , currentRelPos(0.0f) + , sleepAfterCurrentTrack(false) + , sleepAfterPlaylist(false) + , sleepAfter5Tracks(false) + , playRfidTag {} + , pausePlay(false) + , trackFinished(false) + , playlistFinished(true) + , playUntilTrackNumber(0) + , seekmode(SEEK_NORMAL) + , newPlayMono(false) + , currentPlayMono(false) + , isWebstream(false) + , tellMode(TTS_NONE) + , currentSpeechActive(false) + , lastSpeechActive(false) + , coverFilePos(0) + , coverFileSize(0) + , audioFileSize(0) { } +}; + +extern PlayProps gPlayProperties; void AudioPlayer_Init(void); void AudioPlayer_Exit(void); From 04e462915d9e7bdaa0a298d141571ef192d8a102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laszlo=20Heged=C3=BCs?= Date: Wed, 21 Feb 2024 20:49:10 +0100 Subject: [PATCH 4/5] Add Playlist class (4/5): Embed playlist class into the SdCard --- src/SdCard.cpp | 156 ++++++++----------------------------------------- src/SdCard.h | 2 +- 2 files changed, 24 insertions(+), 134 deletions(-) diff --git a/src/SdCard.cpp b/src/SdCard.cpp index 09c9cdb0..8a3f1db4 100644 --- a/src/SdCard.cpp +++ b/src/SdCard.cpp @@ -7,6 +7,7 @@ #include "Led.h" #include "Log.h" #include "MemX.h" +#include "Playlist/BasicPlaylist.hpp" #include "System.h" #include @@ -123,87 +124,6 @@ void SdCard_PrintInfo() { Log_Printf(LOGLEVEL_NOTICE, sdInfo, cardSize, freeSize); } -// Check if file-type is correct -bool fileValid(const char *_fileItem) { - // clang-format off - // all supported extension - constexpr std::array audioFileSufix = { - ".mp3", - ".aac", - ".m4a", - ".wav", - ".flac", - ".ogg", - ".oga", - ".opus", - // playlists - ".m3u", - ".m3u8", - ".pls", - ".asx" - }; - // clang-format on - constexpr size_t maxExtLen = strlen(*std::max_element(audioFileSufix.begin(), audioFileSufix.end(), [](const char *a, const char *b) { - return strlen(a) < strlen(b); - })); - - if (!_fileItem || !strlen(_fileItem)) { - // invalid entry - return false; - } - - // check for streams - if (strncmp(_fileItem, "http://", strlen("http://")) == 0 || strncmp(_fileItem, "https://", strlen("https://")) == 0) { - // this is a stream - return true; - } - - // check for files which start with "/." - const char *lastSlashPtr = strrchr(_fileItem, '/'); - if (lastSlashPtr == nullptr) { - // we have a relative filename without any slashes... - // set the pointer so that it points to the first character AFTER a +1 - lastSlashPtr = _fileItem - 1; - } - if (*(lastSlashPtr + 1) == '.') { - // we have a hidden file - // Log_Printf(LOGLEVEL_DEBUG, "File is hidden: %s", _fileItem); - return false; - } - - // extract the file extension - const char *extStartPtr = strrchr(_fileItem, '.'); - if (extStartPtr == nullptr) { - // no extension found - // Log_Printf(LOGLEVEL_DEBUG, "File has no extension: %s", _fileItem); - return false; - } - const size_t extLen = strlen(extStartPtr); - if (extLen > maxExtLen) { - // extension too long, we do not care anymore - // Log_Printf(LOGLEVEL_DEBUG, "File not supported (extension to long): %s", _fileItem); - return false; - } - char extBuffer[maxExtLen + 1] = {0}; - memcpy(extBuffer, extStartPtr, extLen); - - // make the extension lower case (without using non standard C functions) - for (size_t i = 0; i < extLen; i++) { - extBuffer[i] = tolower(extBuffer[i]); - } - - // check extension against all supported values - for (const auto &e : audioFileSufix) { - if (strcmp(extBuffer, e) == 0) { - // hit we found the extension - return true; - } - } - // miss, we did not find the extension - // Log_Printf(LOGLEVEL_DEBUG, "File not supported: %s", _fileItem); - return false; -} - // Takes a directory as input and returns a random subdirectory from it const String SdCard_pickRandomSubdirectory(const char *_directory) { // Look if folder requested really exists and is a folder. If not => break. @@ -252,27 +172,12 @@ const String SdCard_pickRandomSubdirectory(const char *_directory) { return String(); } -static bool SdCard_allocAndSave(Playlist *playlist, const String &s) { - const size_t len = s.length() + 1; - char *entry = static_cast(x_malloc(len)); - if (!entry) { - // OOM, free playlist and return - Log_Println(unableToAllocateMemForLinearPlaylist, LOGLEVEL_ERROR); - freePlaylist(playlist); - return false; - } - s.toCharArray(entry, len); - playlist->push_back(entry); - return true; -}; - -static std::optional SdCard_ParseM3UPlaylist(File f, bool forceExtended = false) { +static Playlist *SdCard_ParseM3UPlaylist(File f, bool forceExtended = false) { const String line = f.readStringUntil('\n'); const bool extended = line.startsWith("#EXTM3U") || forceExtended; - Playlist *playlist = new Playlist(); + Playlist *playlist = new BasicPlaylist(64); // reserve a sane amount of memory to reduce heap fragmentation - playlist->reserve(64); if (extended) { // extended m3u file format // ignore all lines starting with '#' @@ -282,14 +187,11 @@ static std::optional SdCard_ParseM3UPlaylist(File f, bool forceExten if (!line.startsWith("#")) { // this something we have to save line.trim(); - // save string - if (!SdCard_allocAndSave(playlist, line)) { - return std::nullopt; - } + // save entry + playlist->addMediaItem(MediaItem(line)); } } - // resize std::vector memory to fit our count - playlist->shrink_to_fit(); + playlist->compress(); return playlist; } @@ -298,23 +200,22 @@ static std::optional SdCard_ParseM3UPlaylist(File f, bool forceExten while (f.available()) { String line = f.readStringUntil('\n'); // save string - if (!SdCard_allocAndSave(playlist, line)) { - return std::nullopt; - } + line.trim(); + playlist->addMediaItem(MediaItem(line)); } // resize memory to fit our count - playlist->shrink_to_fit(); + playlist->compress(); return playlist; } /* Puts SD-file(s) or directory into a playlist First element of array always contains the number of payload-items. */ -std::optional SdCard_ReturnPlaylist(const char *fileName, const uint32_t _playMode) { +Playlist *SdCard_ReturnPlaylist(const char *fileName, const uint32_t _playMode) { // Look if file/folder requested really exists. If not => break. File fileOrDirectory = gFSystem.open(fileName); if (!fileOrDirectory) { Log_Printf(LOGLEVEL_ERROR, dirOrFileDoesNotExist, fileName); - return std::nullopt; + return new BasicPlaylist(); } Log_Printf(LOGLEVEL_DEBUG, freeMemory, ESP.getFreeHeap()); @@ -329,43 +230,32 @@ std::optional SdCard_ReturnPlaylist(const char *fileName, const uint // if we reach here, this was not a m3u Log_Println(playlistGen, LOGLEVEL_NOTICE); - Playlist *playlist = new Playlist; + + Playlist *playlist = new BasicPlaylist(64); // File-mode if (!fileOrDirectory.isDirectory()) { - if (!SdCard_allocAndSave(playlist, fileOrDirectory.path())) { - // OOM, function already took care of house cleaning - return std::nullopt; - } + playlist->addMediaItem(MediaItem(fileOrDirectory.path())); + playlist->compress(); return playlist; } - // Directory-mode (linear-playlist) - playlist->reserve(64); // reserve a sane amount of memory to reduce the number of reallocs - size_t hiddenFiles = 0; while (true) { bool isDir; - const String name = fileOrDirectory.getNextFileName(&isDir); - if (name.isEmpty()) { + const String path = fileOrDirectory.getNextFileName(&isDir); + if (path.isEmpty()) { + // reached end of the folder enumeration break; } if (isDir) { + // we ignore folders continue; } - // Don't support filenames that start with "." and only allow .mp3 and other supported audio file formats - if (fileValid(name.c_str())) { - // save it to the vector - if (!SdCard_allocAndSave(playlist, name)) { - // OOM, function already took care of house cleaning - return std::nullopt; - } - } else { - hiddenFiles++; - } + // no need to check against the rules here, MediaItem is doing it for us + playlist->addMediaItem(MediaItem(path)); } - playlist->shrink_to_fit(); - + playlist->compress(); Log_Printf(LOGLEVEL_NOTICE, numberOfValidFiles, playlist->size()); - Log_Printf(LOGLEVEL_DEBUG, "Hidden files: %u", hiddenFiles); + return playlist; } diff --git a/src/SdCard.h b/src/SdCard.h index 25874c5b..f5a84915 100644 --- a/src/SdCard.h +++ b/src/SdCard.h @@ -18,5 +18,5 @@ sdcard_type_t SdCard_GetType(void); uint64_t SdCard_GetSize(); uint64_t SdCard_GetFreeSize(); void SdCard_PrintInfo(); -std::optional SdCard_ReturnPlaylist(const char *fileName, const uint32_t _playMode); +Playlist *SdCard_ReturnPlaylist(const char *fileName, const uint32_t _playMode); const String SdCard_pickRandomSubdirectory(const char *_directory); From 1e7d69da7a0b04b2eb319af31979567cef6902bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laszlo=20Heged=C3=BCs?= Date: Sat, 23 Mar 2024 12:31:56 +0100 Subject: [PATCH 5/5] Add Playlist class (5/5): Embed playlist into Web.cpp --- src/Web.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Web.cpp b/src/Web.cpp index 22afffc6..8ff9ce8b 100644 --- a/src/Web.cpp +++ b/src/Web.cpp @@ -2103,7 +2103,7 @@ static void handleCoverImageRequest(AsyncWebServerRequest *request) { } return; } - const char *coverFileName = gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber); + const char *coverFileName = gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber).getUri().c_str(); String decodedCover = "/.cache"; decodedCover.concat(coverFileName);