dxx-rebirth/similar/arch/sdl/jukebox.cpp

375 lines
10 KiB
C++
Raw Normal View History

2014-06-01 17:55:23 +00:00
/*
* This file is part of the DXX-Rebirth project <http://www.dxx-rebirth.com/>.
* It is copyright by its individual contributors, as recorded in the
* project's Git history. See COPYING.txt at the top level for license
* terms and a link to the Git history.
*/
/*
* DXX Rebirth "jukebox" code
* MD 2211 <md2211@users.sourceforge.net>, 2007
*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "hudmsg.h"
#include "songs.h"
#include "jukebox.h"
#include "dxxerror.h"
#include "console.h"
#include "config.h"
2013-12-26 04:18:28 +00:00
#include "strutil.h"
#include "u_mem.h"
2015-04-19 04:18:49 +00:00
#include "physfs_list.h"
#include "digi.h"
2015-02-07 04:37:37 +00:00
#include "compiler-make_unique.h"
#include "partial_range.h"
2015-01-23 03:55:05 +00:00
2015-12-13 18:00:49 +00:00
namespace dcx {
#define MUSIC_HUDMSG_MAXLEN 40
#define JUKEBOX_HUDMSG_PLAYING "Now playing:"
#define JUKEBOX_HUDMSG_STOPPED "Jukebox stopped"
2015-01-23 03:55:05 +00:00
namespace {
2016-08-06 19:55:26 +00:00
struct m3u_bytes
{
using range_type = partial_range_t<char *>;
using ptr_range_type = partial_range_t<char **>;
2016-08-06 19:55:26 +00:00
using alloc_type = std::unique_ptr<char *[]>;
2016-08-06 19:55:26 +00:00
range_type range;
ptr_range_type ptr_range;
2016-08-06 19:55:26 +00:00
alloc_type alloc;
m3u_bytes() :
range(nullptr, nullptr),
ptr_range(nullptr, nullptr)
2016-08-06 19:55:26 +00:00
{
}
m3u_bytes(m3u_bytes &&) = default;
m3u_bytes(range_type &&r, ptr_range_type &&p, alloc_type &&b) :
range(std::move(r)),
ptr_range(std::move(p)),
alloc(std::move(b))
2016-08-06 19:55:26 +00:00
{
}
};
class FILE_deleter
{
public:
void operator()(FILE *const p) const
{
fclose(p);
}
};
2016-08-06 19:55:26 +00:00
class list_deleter : PHYSFS_list_deleter
2015-01-23 03:55:05 +00:00
{
public:
2016-08-06 19:55:26 +00:00
/* When `list_pointers` is a PHYSFS allocation, `buf` is nullptr.
* When `list_pointers` is a new[char *[]] allocation, `buf`
* points to the same location as `list_pointers`.
*/
std::unique_ptr<char *[]> buf;
2015-01-23 03:55:05 +00:00
void operator()(char **list)
{
if (buf)
{
2016-08-06 19:55:26 +00:00
assert(buf.get() == list);
2015-01-23 03:55:05 +00:00
buf.reset();
}
else
this->PHYSFS_list_deleter::operator()(list);
2015-01-23 03:55:05 +00:00
}
};
class list_pointers : public PHYSFSX_uncounted_list_template<list_deleter>
2015-01-23 03:55:05 +00:00
{
typedef PHYSFSX_uncounted_list_template<list_deleter> base_ptr;
2015-01-23 03:55:05 +00:00
public:
using base_ptr::reset;
2016-08-06 19:55:26 +00:00
void set_combined(std::unique_ptr<char *[]> &&buf)
2015-01-23 03:55:05 +00:00
noexcept(
2016-08-06 19:55:26 +00:00
noexcept(std::declval<base_ptr>().reset(buf.get())) &&
2015-01-23 03:55:05 +00:00
noexcept(std::declval<list_deleter>().buf = std::move(buf))
)
{
2016-08-06 19:55:26 +00:00
this->base_ptr::reset(buf.get());
2015-01-23 03:55:05 +00:00
get_deleter().buf = std::move(buf);
}
void reset(PHYSFSX_uncounted_list list)
noexcept(noexcept(std::declval<base_ptr>().reset(list.release())))
{
this->base_ptr::reset(list.release());
}
2015-01-23 03:55:05 +00:00
};
2015-01-23 03:55:05 +00:00
class jukebox_songs
{
2015-01-23 03:55:05 +00:00
public:
void unload();
2015-01-23 03:55:05 +00:00
list_pointers list; // the actual list
unsigned num_songs; // number of jukebox songs
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
};
2015-01-23 03:55:05 +00:00
}
2015-01-23 03:55:05 +00:00
static jukebox_songs JukeboxSongs;
void jukebox_songs::unload()
{
2015-01-23 03:55:05 +00:00
num_songs = 0;
list.reset();
2015-01-23 03:55:05 +00:00
}
void jukebox_unload()
{
JukeboxSongs.unload();
}
2015-03-22 18:49:21 +00:00
const array<file_extension_t, 5> jukebox_exts{{
SONG_EXT_HMP,
SONG_EXT_MID,
SONG_EXT_OGG,
SONG_EXT_FLAC,
SONG_EXT_MP3
}};
2016-08-06 19:55:26 +00:00
/* Open an m3u using fopen, not PHYSFS. If the path seems to be under
* PHYSFS, that will be preferred over a raw filesystem path.
*/
static std::unique_ptr<FILE, FILE_deleter> open_m3u_from_disk(const char *const cfgpath)
{
array<char, PATH_MAX> absbuf;
return std::unique_ptr<FILE, FILE_deleter>(fopen(
// it's a child of Sharepath, build full path
(PHYSFSX_exists(cfgpath, 0)
? (PHYSFSX_getRealPath(cfgpath, absbuf), absbuf.data())
: cfgpath), "rb")
);
}
2016-08-06 19:55:26 +00:00
static m3u_bytes read_m3u_bytes_from_disk(const char *const cfgpath)
{
const auto &&f = open_m3u_from_disk(cfgpath);
if (!f)
return {};
const auto fp = f.get();
fseek(fp, -1, SEEK_END);
const std::size_t length = ftell(fp) + 1;
const auto juke_max_songs = JukeboxSongs.max_songs;
if (length >= PATH_MAX * juke_max_songs)
2016-08-06 19:55:26 +00:00
return {};
fseek(fp, 0, SEEK_SET);
/* A file consisting only of single character records and newline
* separators, with no junk newlines, comments, or final terminator,
* will need one pointer per two bytes of file, rounded up. Any
* file that uses longer records, which most will use, will need
* fewer pointers. This expression usually overestimates, sometimes
* substantially. However, it is still more conservative than the
* previous expression, which was to allocate exactly
* `JukeboxSongs.max_songs` pointers without regard to the file size
* or contents.
*/
const auto required_alloc_size = 1 + (length / 2);
const auto max_songs = std::min(required_alloc_size, juke_max_songs);
/* Use T=`char*[]` to ensure alignment. Place pointers before file
* contents to keep the pointer array aligned.
*/
2016-08-06 19:55:26 +00:00
auto &&list_buf = make_unique<char*[]>(max_songs + 1 + (length / sizeof(char *)));
const auto p = reinterpret_cast<char *>(list_buf.get() + max_songs);
2016-08-06 19:55:26 +00:00
p[length] = '\0'; // make sure the last string is terminated
return fread(p, length, 1, fp)
? m3u_bytes(
unchecked_partial_range(p, length),
unchecked_partial_range(list_buf.get(), max_songs),
std::move(list_buf)
)
2016-08-06 19:55:26 +00:00
: m3u_bytes();
}
2013-10-27 22:00:14 +00:00
static int read_m3u(void)
{
2016-08-06 19:55:26 +00:00
auto &&m3u = read_m3u_bytes_from_disk(CGameCfg.CMLevelMusicPath.data());
auto &list_buf = m3u.alloc;
if (!list_buf)
return 0;
// The growing string list is allocated last, hopefully reducing memory fragmentation when it grows
const auto eol = [](char c) {
return c == '\n' || c == '\r' || !c;
};
2016-08-06 19:55:26 +00:00
JukeboxSongs.list.set_combined(std::move(list_buf));
2016-08-06 19:55:26 +00:00
const auto &range = m3u.range;
auto pp = m3u.ptr_range.begin();
for (auto buf = range.begin(); buf != range.end(); ++buf)
{
for (; buf != range.end() && eol(*buf);) // find new line - support DOS, Unix and Mac line endings
buf++;
if (buf == range.end())
break;
if (*buf != '#') // ignore comments / extra info
{
*pp++ = buf;
if (pp == m3u.ptr_range.end())
2015-01-23 03:55:05 +00:00
break;
}
for (; buf != range.end(); ++buf) // find end of line
if (eol(*buf))
{
*buf = 0;
break;
}
if (buf == range.end())
break;
}
JukeboxSongs.num_songs = std::distance(m3u.ptr_range.begin(), pp);
return 1;
}
2016-08-06 19:55:26 +00:00
}
namespace dsx {
/* Loads music file names from a given directory or M3U playlist */
void jukebox_load()
{
jukebox_unload();
// Check if it's an M3U file
2016-08-06 19:55:25 +00:00
auto &cfgpath = CGameCfg.CMLevelMusicPath;
size_t musiclen = strlen(cfgpath.data());
if (musiclen > 4 && !d_stricmp(&cfgpath[musiclen - 4], ".m3u"))
read_m3u();
else // a directory
{
class PHYSFS_path_deleter
{
public:
void operator()(const char *const p) const noexcept
{
PHYSFS_removeFromSearchPath(p);
}
};
std::unique_ptr<const char, PHYSFS_path_deleter> new_path;
const char *sep = PHYSFS_getDirSeparator();
2014-12-22 04:35:47 +00:00
size_t seplen = strlen(sep);
// stick a separator on the end if necessary.
2014-12-22 04:35:47 +00:00
if (musiclen >= seplen)
{
2016-08-06 19:55:25 +00:00
auto p = &cfgpath[musiclen - seplen];
if (strcmp(p, sep))
2016-08-06 19:55:25 +00:00
cfgpath.copy_if(musiclen, sep, seplen);
}
2016-08-06 19:55:25 +00:00
const auto p = cfgpath.data();
// Read directory using PhysicsFS
2016-08-06 19:55:25 +00:00
if (PHYSFS_isDirectory(p)) // find files in relative directory
JukeboxSongs.list.reset(PHYSFSX_findFiles(p, jukebox_exts));
else
{
if (PHYSFSX_isNewPath(p))
new_path.reset(p);
2016-08-06 19:55:25 +00:00
PHYSFS_addToSearchPath(p, 0);
// as mountpoints are no option (yet), make sure only files originating from GameCfg.CMLevelMusicPath are aded to the list.
2016-08-06 19:55:25 +00:00
JukeboxSongs.list.reset(PHYSFSX_findabsoluteFiles("", p, jukebox_exts));
}
if (!JukeboxSongs.list)
{
return;
}
JukeboxSongs.num_songs = std::distance(JukeboxSongs.list.begin(), JukeboxSongs.list.end());
}
if (JukeboxSongs.num_songs)
{
2016-08-06 19:55:25 +00:00
con_printf(CON_DEBUG,"Jukebox: %d music file(s) found in %s", JukeboxSongs.num_songs, cfgpath.data());
2016-08-06 19:55:25 +00:00
if (CGameCfg.CMLevelMusicTrack[1] != JukeboxSongs.num_songs)
{
2016-08-06 19:55:25 +00:00
CGameCfg.CMLevelMusicTrack[1] = JukeboxSongs.num_songs;
CGameCfg.CMLevelMusicTrack[0] = 0; // number of songs changed so start from beginning.
}
}
else
{
2016-08-06 19:55:25 +00:00
CGameCfg.CMLevelMusicTrack[0] = -1;
CGameCfg.CMLevelMusicTrack[1] = -1;
2013-12-07 00:47:27 +00:00
con_printf(CON_DEBUG,"Jukebox music could not be found!");
}
}
// To proceed tru our playlist. Usually used for continous play, but can loop as well.
2013-10-27 22:00:14 +00:00
static void jukebox_hook_next()
{
2016-08-06 19:55:25 +00:00
if (!JukeboxSongs.list || CGameCfg.CMLevelMusicTrack[0] == -1)
return;
if (GameCfg.CMLevelMusicPlayOrder == MUSIC_CM_PLAYORDER_RAND)
2016-08-06 19:55:25 +00:00
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!
else
2016-08-06 19:55:25 +00:00
CGameCfg.CMLevelMusicTrack[0]++;
if (CGameCfg.CMLevelMusicTrack[0] + 1 > CGameCfg.CMLevelMusicTrack[1])
CGameCfg.CMLevelMusicTrack[0] = 0;
jukebox_play();
}
// Play tracks from Jukebox directory. Play track specified in GameCfg.CMLevelMusicTrack[0] and loop depending on GameCfg.CMLevelMusicPlayOrder
int jukebox_play()
{
2013-12-03 22:20:09 +00:00
const char *music_filename;
2014-12-22 04:35:47 +00:00
uint_fast32_t size_full_filename = 0;
if (!JukeboxSongs.list)
return 0;
2016-08-06 19:55:25 +00:00
if (CGameCfg.CMLevelMusicTrack[0] < 0 ||
CGameCfg.CMLevelMusicTrack[0] + 1 > CGameCfg.CMLevelMusicTrack[1])
return 0;
2016-08-06 19:55:25 +00:00
music_filename = JukeboxSongs.list[CGameCfg.CMLevelMusicTrack[0]];
if (!music_filename)
return 0;
2013-12-03 22:12:42 +00:00
size_t size_music_filename = strlen(music_filename);
2016-08-06 19:55:25 +00:00
auto &cfgpath = CGameCfg.CMLevelMusicPath;
size_t musiclen = strlen(cfgpath.data());
2014-12-22 04:35:47 +00:00
size_full_filename = musiclen + size_music_filename + 1;
RAIIdmem<char[]> full_filename;
CALLOC(full_filename, char[], size_full_filename);
2013-12-03 22:12:42 +00:00
const char *LevelMusicPath;
2016-08-06 19:55:25 +00:00
if (musiclen > 4 && !d_stricmp(&cfgpath[musiclen - 4], ".m3u")) // if it's from an M3U playlist
2013-12-03 22:12:42 +00:00
LevelMusicPath = "";
else // if it's from a specified path
2016-08-06 19:55:25 +00:00
LevelMusicPath = cfgpath.data();
snprintf(full_filename.get(), size_full_filename, "%s%s", LevelMusicPath, music_filename);
int played = songs_play_file(full_filename.get(), (GameCfg.CMLevelMusicPlayOrder == MUSIC_CM_PLAYORDER_LEVEL ? 1 : 0), (GameCfg.CMLevelMusicPlayOrder == MUSIC_CM_PLAYORDER_LEVEL ? nullptr : jukebox_hook_next));
2015-03-28 01:16:10 +00:00
full_filename.reset();
2013-12-03 22:12:42 +00:00
if (!played)
{
return 0; // whoops, got an error
}
// Formatting a pretty message
2013-12-03 22:20:09 +00:00
const char *prefix = "...";
2013-12-03 22:12:42 +00:00
if (size_music_filename >= MUSIC_HUDMSG_MAXLEN) {
2013-12-03 22:20:09 +00:00
music_filename += size_music_filename - MUSIC_HUDMSG_MAXLEN;
} else {
2013-12-03 22:20:09 +00:00
prefix += 3;
}
2013-12-03 22:20:09 +00:00
HUD_init_message(HM_DEFAULT, "%s %s%s", JUKEBOX_HUDMSG_PLAYING, prefix, music_filename);
return 1;
}
}