Subversion Repositories Games.Descent

Rev

Blame | Last modification | View Log | Download | RSS feed

  1. /*
  2.  * This file is part of the DXX-Rebirth project <https://www.dxx-rebirth.com/>.
  3.  * It is copyright by its individual contributors, as recorded in the
  4.  * project's Git history.  See COPYING.txt at the top level for license
  5.  * terms and a link to the Git history.
  6.  */
  7. /*
  8.  * DXX Rebirth "jukebox" code
  9.  * MD 2211 <md2211@users.sourceforge.net>, 2007
  10.  */
  11.  
  12. #include <stdlib.h>
  13. #include <stdio.h>
  14. #include <string.h>
  15.  
  16. #include "hudmsg.h"
  17. #include "songs.h"
  18. #include "jukebox.h"
  19. #include "dxxerror.h"
  20. #include "console.h"
  21. #include "config.h"
  22. #include "strutil.h"
  23. #include "u_mem.h"
  24. #include "physfs_list.h"
  25. #include "digi.h"
  26.  
  27. #include "partial_range.h"
  28. #include <memory>
  29.  
  30. namespace dcx {
  31.  
  32. extern bool isDirectory(const char *fname); // Pierre-Marie Baty -- work around PHYSFS_isDirectory() deprecation
  33.  
  34. #define MUSIC_HUDMSG_MAXLEN 40
  35. #define JUKEBOX_HUDMSG_PLAYING "Now playing:"
  36. #define JUKEBOX_HUDMSG_STOPPED "Jukebox stopped"
  37.  
  38. namespace {
  39.  
  40. struct m3u_bytes
  41. {
  42.         using range_type = partial_range_t<char *>;
  43.         using ptr_range_type = partial_range_t<char **>;
  44.         using alloc_type = std::unique_ptr<char *[]>;
  45.         range_type range = {nullptr, nullptr};
  46.         ptr_range_type ptr_range = {nullptr, nullptr};
  47.         alloc_type alloc;
  48.         m3u_bytes() = default;
  49.         m3u_bytes(m3u_bytes &&) = default;
  50.         m3u_bytes(range_type &&r, ptr_range_type &&p, alloc_type &&b) :
  51.                 range(std::move(r)),
  52.                 ptr_range(std::move(p)),
  53.                 alloc(std::move(b))
  54.         {
  55.         }
  56. };
  57.  
  58. class FILE_deleter
  59. {
  60. public:
  61.         void operator()(FILE *const p) const
  62.         {
  63.                 fclose(p);
  64.         }
  65. };
  66.  
  67. class list_deleter : PHYSFS_list_deleter
  68. {
  69. public:
  70.         /* When `list_pointers` is a PHYSFS allocation, `buf` is nullptr.
  71.          * When `list_pointers` is a new[char *[]] allocation, `buf`
  72.          * points to the same location as `list_pointers`.
  73.          */
  74.         std::unique_ptr<char *[]> buf;
  75.         void operator()(char **list)
  76.         {
  77.                 if (buf)
  78.                 {
  79.                         assert(buf.get() == list);
  80.                         buf.reset();
  81.                 }
  82.                 else
  83.                         this->PHYSFS_list_deleter::operator()(list);
  84.         }
  85. };
  86.  
  87. class list_pointers : public PHYSFSX_uncounted_list_template<list_deleter>
  88. {
  89.         typedef PHYSFSX_uncounted_list_template<list_deleter> base_ptr;
  90. public:
  91.         using base_ptr::reset;
  92.         void set_combined(std::unique_ptr<char *[]> &&buf)
  93.                 noexcept(
  94.                         noexcept(std::declval<base_ptr>().reset(buf.get())) &&
  95.                         noexcept(std::declval<list_deleter>().buf = std::move(buf))
  96.                 )
  97.         {
  98.                 this->base_ptr::reset(buf.get());
  99.                 get_deleter().buf = std::move(buf);
  100.         }
  101.         void reset(PHYSFSX_uncounted_list list)
  102.                 noexcept(noexcept(std::declval<base_ptr>().reset(list.release())))
  103.         {
  104.                 this->base_ptr::reset(list.release());
  105.         }
  106. };
  107.  
  108. class jukebox_songs
  109. {
  110. public:
  111.         void unload();
  112.         list_pointers list;     // the actual list
  113.         unsigned num_songs;     // number of jukebox songs
  114.         static const std::size_t max_songs = 1024;      // maximum number of pointers that 'list' can hold, i.e. size of list / size of one pointer
  115. };
  116.  
  117. }
  118.  
  119. static jukebox_songs JukeboxSongs;
  120.  
  121. void jukebox_songs::unload()
  122. {
  123.         num_songs = 0;
  124.         list.reset();
  125. }
  126.  
  127. void jukebox_unload()
  128. {
  129.         JukeboxSongs.unload();
  130. }
  131.  
  132. const std::array<file_extension_t, 5> jukebox_exts{{
  133.         SONG_EXT_HMP,
  134.         SONG_EXT_MID,
  135.         SONG_EXT_OGG,
  136.         SONG_EXT_FLAC,
  137.         SONG_EXT_MP3
  138. }};
  139.  
  140. /* Open an m3u using fopen, not PHYSFS.  If the path seems to be under
  141.  * PHYSFS, that will be preferred over a raw filesystem path.
  142.  */
  143. static std::unique_ptr<FILE, FILE_deleter> open_m3u_from_disk(const char *const cfgpath)
  144. {
  145.         std::array<char, PATH_MAX> absbuf;
  146.         return std::unique_ptr<FILE, FILE_deleter>(fopen(
  147.         // it's a child of Sharepath, build full path
  148.                 (PHYSFSX_exists(cfgpath, 0)
  149.                         ? (PHYSFSX_getRealPath(cfgpath, absbuf), absbuf.data())
  150.                         : cfgpath), "rb")
  151.         );
  152. }
  153.  
  154. static m3u_bytes read_m3u_bytes_from_disk(const char *const cfgpath)
  155. {
  156.         const auto &&f = open_m3u_from_disk(cfgpath);
  157.         if (!f)
  158.                 return {};
  159.         const auto fp = f.get();
  160.         fseek(fp, -1, SEEK_END);
  161.         const std::size_t length = ftell(fp) + 1;
  162.         const auto juke_max_songs = JukeboxSongs.max_songs;
  163.         if (length >= PATH_MAX * juke_max_songs)
  164.                 return {};
  165.         fseek(fp, 0, SEEK_SET);
  166.         /* A file consisting only of single character records and newline
  167.          * separators, with no junk newlines, comments, or final terminator,
  168.          * will need one pointer per two bytes of file, rounded up.  Any
  169.          * file that uses longer records, which most will use, will need
  170.          * fewer pointers.  This expression usually overestimates, sometimes
  171.          * substantially.  However, it is still more conservative than the
  172.          * previous expression, which was to allocate exactly
  173.          * `JukeboxSongs.max_songs` pointers without regard to the file size
  174.          * or contents.
  175.          */
  176.         const auto required_alloc_size = 1 + (length / 2);
  177.         const auto max_songs = std::min(required_alloc_size, juke_max_songs);
  178.         /* Use T=`char*[]` to ensure alignment.  Place pointers before file
  179.          * contents to keep the pointer array aligned.
  180.          */
  181.         auto &&list_buf = std::make_unique<char*[]>(max_songs + 1 + (length / sizeof(char *)));
  182.         const auto p = reinterpret_cast<char *>(list_buf.get() + max_songs);
  183.         p[length] = '\0';       // make sure the last string is terminated
  184.         return fread(p, length, 1, fp)
  185.                 ? m3u_bytes(
  186.                         unchecked_partial_range(p, length),
  187.                         unchecked_partial_range(list_buf.get(), max_songs),
  188.                         std::move(list_buf)
  189.                 )
  190.                 : m3u_bytes();
  191. }
  192.  
  193. static int read_m3u(void)
  194. {
  195.         auto &&m3u = read_m3u_bytes_from_disk(CGameCfg.CMLevelMusicPath.data());
  196.         auto &list_buf = m3u.alloc;
  197.         if (!list_buf)
  198.                 return 0;
  199.  
  200.         // The growing string list is allocated last, hopefully reducing memory fragmentation when it grows
  201.         const auto eol = [](char c) {
  202.                 return c == '\n' || c == '\r' || !c;
  203.         };
  204.         JukeboxSongs.list.set_combined(std::move(list_buf));
  205.         const auto &range = m3u.range;
  206.         auto pp = m3u.ptr_range.begin();
  207.         for (auto buf = range.begin(); buf != range.end(); ++buf)
  208.         {
  209.                 for (; buf != range.end() && eol(*buf);)        // find new line - support DOS, Unix and Mac line endings
  210.                         buf++;
  211.                 if (buf == range.end())
  212.                         break;
  213.                 if (*buf != '#')        // ignore comments / extra info
  214.                 {
  215.                         *pp++ = buf;
  216.                         if (pp == m3u.ptr_range.end())
  217.                                 break;
  218.                 }
  219.                 for (; buf != range.end(); ++buf)       // find end of line
  220.                         if (eol(*buf))
  221.                         {
  222.                                 *buf = 0;
  223.                                 break;
  224.                         }
  225.                 if (buf == range.end())
  226.                         break;
  227.         }
  228.         JukeboxSongs.num_songs = std::distance(m3u.ptr_range.begin(), pp);
  229.         return 1;
  230. }
  231.  
  232. /* Loads music file names from a given directory or M3U playlist */
  233. void jukebox_load()
  234. {
  235.         jukebox_unload();
  236.  
  237.         // Check if it's an M3U file
  238.         auto &cfgpath = CGameCfg.CMLevelMusicPath;
  239.         size_t musiclen = strlen(cfgpath.data());
  240.         if (musiclen > 4 && !d_stricmp(&cfgpath[musiclen - 4], ".m3u"))
  241.                 read_m3u();
  242.         else    // a directory
  243.         {
  244.                 class PHYSFS_path_deleter
  245.                 {
  246.                 public:
  247.                         void operator()(const char *const p) const noexcept
  248.                         {
  249.                                 PHYSFS_unmount(p); // Pierre-Marie Baty -- work around PHYSFS_removeFromSearchPath() deprecation
  250.                         }
  251.                 };
  252.                 std::unique_ptr<const char, PHYSFS_path_deleter> new_path;
  253.                 const char *sep = PHYSFS_getDirSeparator();
  254.                 size_t seplen = strlen(sep);
  255.  
  256.                 // stick a separator on the end if necessary.
  257.                 if (musiclen >= seplen)
  258.                 {
  259.                         auto p = &cfgpath[musiclen - seplen];
  260.                         if (strcmp(p, sep))
  261.                                 cfgpath.copy_if(musiclen, sep, seplen);
  262.                 }
  263.  
  264.                 const auto p = cfgpath.data();
  265.                 // Read directory using PhysicsFS
  266.                 if (/*PHYSFS_*/isDirectory(p))  // find files in relative directory // Pierre-Marie Baty -- work around PHYSFS_isDirectory() deprecation
  267.                         JukeboxSongs.list.reset(PHYSFSX_findFiles(p, jukebox_exts));
  268.                 else
  269.                 {
  270.                         if (PHYSFSX_isNewPath(p))
  271.                                 new_path.reset(p);
  272.                         PHYSFS_mount(p, NULL, 0); // Pierre-Marie Baty -- work around PHYSFS_addToSearchPath() deprecation
  273.  
  274.                         // as mountpoints are no option (yet), make sure only files originating from GameCfg.CMLevelMusicPath are aded to the list.
  275.                         JukeboxSongs.list.reset(PHYSFSX_findabsoluteFiles("", p, jukebox_exts));
  276.                 }
  277.  
  278.                 if (!JukeboxSongs.list)
  279.                 {
  280.                         return;
  281.                 }
  282.                 JukeboxSongs.num_songs = std::distance(JukeboxSongs.list.begin(), JukeboxSongs.list.end());
  283.         }
  284.  
  285.         if (JukeboxSongs.num_songs)
  286.         {
  287.                 con_printf(CON_DEBUG,"Jukebox: %d music file(s) found in %s", JukeboxSongs.num_songs, cfgpath.data());
  288.                 if (CGameCfg.CMLevelMusicTrack[1] != JukeboxSongs.num_songs)
  289.                 {
  290.                         CGameCfg.CMLevelMusicTrack[1] = JukeboxSongs.num_songs;
  291.                         CGameCfg.CMLevelMusicTrack[0] = 0; // number of songs changed so start from beginning.
  292.                 }
  293.         }
  294.         else
  295.         {
  296.                 CGameCfg.CMLevelMusicTrack[0] = -1;
  297.                 CGameCfg.CMLevelMusicTrack[1] = -1;
  298.                 con_puts(CON_DEBUG,"Jukebox music could not be found!");
  299.         }
  300. }
  301.  
  302. // To proceed tru our playlist. Usually used for continous play, but can loop as well.
  303. static void jukebox_hook_next()
  304. {
  305.         if (!JukeboxSongs.list || CGameCfg.CMLevelMusicTrack[0] == -1)
  306.                 return;
  307.  
  308.         if (CGameCfg.CMLevelMusicPlayOrder == LevelMusicPlayOrder::Random)
  309.                 CGameCfg.CMLevelMusicTrack[0] = d_rand() % CGameCfg.CMLevelMusicTrack[1]; // simply a random selection - no check if this song has already been played. But that's how I roll!
  310.         else
  311.                 CGameCfg.CMLevelMusicTrack[0]++;
  312.         if (CGameCfg.CMLevelMusicTrack[0] + 1 > CGameCfg.CMLevelMusicTrack[1])
  313.                 CGameCfg.CMLevelMusicTrack[0] = 0;
  314.  
  315.         jukebox_play();
  316. }
  317.  
  318. }
  319.  
  320. namespace dsx {
  321.  
  322. // Play tracks from Jukebox directory. Play track specified in GameCfg.CMLevelMusicTrack[0] and loop depending on CGameCfg.CMLevelMusicPlayOrder
  323. int jukebox_play()
  324. {
  325.         const char *music_filename;
  326.         uint_fast32_t size_full_filename = 0;
  327.  
  328.         if (!JukeboxSongs.list)
  329.                 return 0;
  330.  
  331.         if (CGameCfg.CMLevelMusicTrack[0] < 0 ||
  332.                 CGameCfg.CMLevelMusicTrack[0] + 1 > CGameCfg.CMLevelMusicTrack[1])
  333.                 return 0;
  334.  
  335.         music_filename = JukeboxSongs.list[CGameCfg.CMLevelMusicTrack[0]];
  336.         if (!music_filename)
  337.                 return 0;
  338.  
  339.         size_t size_music_filename = strlen(music_filename);
  340.         auto &cfgpath = CGameCfg.CMLevelMusicPath;
  341.         size_t musiclen = strlen(cfgpath.data());
  342.         size_full_filename = musiclen + size_music_filename + 1;
  343.         RAIIdmem<char[]> full_filename;
  344.         CALLOC(full_filename, char[], size_full_filename);
  345.         const char *LevelMusicPath;
  346.         if (musiclen > 4 && !d_stricmp(&cfgpath[musiclen - 4], ".m3u")) // if it's from an M3U playlist
  347.                 LevelMusicPath = "";
  348.         else                                                                                    // if it's from a specified path
  349.                 LevelMusicPath = cfgpath.data();
  350.         snprintf(full_filename.get(), size_full_filename, "%s%s", LevelMusicPath, music_filename);
  351.  
  352.         int played = songs_play_file(full_filename.get(), (CGameCfg.CMLevelMusicPlayOrder == LevelMusicPlayOrder::Level ? 1 : 0), (CGameCfg.CMLevelMusicPlayOrder == LevelMusicPlayOrder::Level ? nullptr : jukebox_hook_next));
  353.         full_filename.reset();
  354.         if (!played)
  355.         {
  356.                 return 0;       // whoops, got an error
  357.         }
  358.  
  359.         // Formatting a pretty message
  360.         const char *prefix = "...";
  361.         if (size_music_filename >= MUSIC_HUDMSG_MAXLEN) {
  362.                 music_filename += size_music_filename - MUSIC_HUDMSG_MAXLEN;
  363.         } else {
  364.                 prefix += 3;
  365.         }
  366.  
  367.         HUD_init_message(HM_DEFAULT, "%s %s%s", JUKEBOX_HUDMSG_PLAYING, prefix, music_filename);
  368.  
  369.         return 1;
  370. }
  371.  
  372. }
  373.