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 .
*/
2009-01-13 12:39:03 +00:00
/*
* DXX Rebirth " jukebox " code
* MD 2211 < md2211 @ users . sourceforge . net > , 2007
*/
# include <stdlib.h>
2010-09-26 13:15:20 +00:00
# include <stdio.h>
2009-01-13 12:39:03 +00:00
# include <string.h>
2010-09-02 00:07:37 +00:00
2009-01-13 12:39:03 +00:00
# include "hudmsg.h"
2012-06-10 12:02:43 +00:00
# include "songs.h"
2009-01-13 12:39:03 +00:00
# include "jukebox.h"
2012-07-07 18:35:06 +00:00
# include "dxxerror.h"
2009-01-13 12:39:03 +00:00
# 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"
2015-07-25 23:10:45 +00:00
# include "digi.h"
2009-01-13 12:39:03 +00:00
2015-02-07 04:37:37 +00:00
# include "compiler-make_unique.h"
2015-01-23 03:55:05 +00:00
# include "partial_range.h"
2015-01-23 03:55:05 +00:00
2015-12-13 18:00:49 +00:00
namespace dcx {
2015-12-13 18:00:48 +00:00
2009-01-13 12:39:03 +00:00
# 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 * > ;
2016-08-08 00:07:19 +00:00
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 ;
2016-08-08 00:07:19 +00:00
ptr_range_type ptr_range ;
2016-08-06 19:55:26 +00:00
alloc_type alloc ;
m3u_bytes ( ) :
2016-08-08 00:07:19 +00:00
range ( nullptr , nullptr ) ,
ptr_range ( nullptr , nullptr )
2016-08-06 19:55:26 +00:00
{
}
m3u_bytes ( m3u_bytes & & ) = default ;
2016-08-08 00:07:19 +00:00
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
2015-01-23 03:55:05 +00:00
this - > PHYSFS_list_deleter : : operator ( ) ( list ) ;
2015-01-23 03:55:05 +00:00
}
} ;
2015-05-09 17:38:57 +00:00
class list_pointers : public PHYSFSX_uncounted_list_template < list_deleter >
2015-01-23 03:55:05 +00:00
{
2015-05-09 17:38:57 +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 ) ;
}
2015-05-09 17:38:57 +00:00
void reset ( PHYSFSX_uncounted_list list )
2015-01-23 03:55:05 +00:00
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
2010-09-26 13:15:20 +00:00
{
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
2016-08-08 00:07:19 +00:00
unsigned num_songs ; // number of jukebox songs
2015-01-23 03:55:05 +00:00
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
2013-12-22 22:03:07 +00:00
} ;
2010-09-26 13:15:20 +00:00
2015-01-23 03:55:05 +00:00
}
2009-01-13 12:39:03 +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 ;
2016-08-08 00:07:19 +00:00
list . reset ( ) ;
2015-01-23 03:55:05 +00:00
}
void jukebox_unload ( )
{
JukeboxSongs . unload ( ) ;
2010-09-26 13:15:20 +00:00
}
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
} } ;
2009-01-13 12:39:03 +00:00
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 " )
) ;
2015-12-13 18:00:48 +00:00
}
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 ;
2016-08-08 00:07:19 +00:00
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 ) ;
2016-08-08 00:07:19 +00:00
/* 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 )
2016-08-08 00:07:19 +00:00
? 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 ( ) ;
}
2015-12-13 18:00:48 +00:00
2013-10-27 22:00:14 +00:00
static int read_m3u ( void )
2010-09-26 13:15:20 +00:00
{
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 )
2011-04-18 12:32:36 +00:00
return 0 ;
2010-09-26 13:15:20 +00:00
// The growing string list is allocated last, hopefully reducing memory fragmentation when it grows
2015-01-23 03:55:05 +00:00
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 ;
2016-08-08 00:07:19 +00:00
auto pp = m3u . ptr_range . begin ( ) ;
2015-01-23 03:55:05 +00:00
for ( auto buf = range . begin ( ) ; buf ! = range . end ( ) ; + + buf )
2010-09-26 13:15:20 +00:00
{
2015-01-23 03:55:05 +00:00
for ( ; buf ! = range . end ( ) & & eol ( * buf ) ; ) // find new line - support DOS, Unix and Mac line endings
2010-09-26 13:15:20 +00:00
buf + + ;
2015-01-23 03:55:05 +00:00
if ( buf = = range . end ( ) )
break ;
2010-09-26 13:15:20 +00:00
if ( * buf ! = ' # ' ) // ignore comments / extra info
{
2016-08-08 00:07:19 +00:00
* pp + + = buf ;
if ( pp = = m3u . ptr_range . end ( ) )
2015-01-23 03:55:05 +00:00
break ;
2010-09-26 13:15:20 +00:00
}
2015-01-23 03:55:05 +00:00
for ( ; buf ! = range . end ( ) ; + + buf ) // find end of line
if ( eol ( * buf ) )
{
* buf = 0 ;
break ;
}
if ( buf = = range . end ( ) )
break ;
2010-09-26 13:15:20 +00:00
}
2016-08-08 00:07:19 +00:00
JukeboxSongs . num_songs = std : : distance ( m3u . ptr_range . begin ( ) , pp ) ;
2011-04-18 12:32:36 +00:00
return 1 ;
2010-09-26 13:15:20 +00:00
}
2016-08-06 19:55:26 +00:00
}
namespace dsx {
2010-09-26 13:15:20 +00:00
/* Loads music file names from a given directory or M3U playlist */
2010-06-14 15:32:55 +00:00
void jukebox_load ( )
{
2010-11-21 11:55:35 +00:00
jukebox_unload ( ) ;
2009-01-13 12:39:03 +00:00
2011-04-18 12:32:36 +00:00
// 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 " ) )
2011-04-18 12:32:36 +00:00
read_m3u ( ) ;
else // a directory
2010-06-14 08:13:16 +00:00
{
2016-08-06 19:55:25 +00:00
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 ;
2011-04-18 12:32:36 +00:00
const char * sep = PHYSFS_getDirSeparator ( ) ;
2014-12-22 04:35:47 +00:00
size_t seplen = strlen ( sep ) ;
2010-06-14 08:13:16 +00:00
2011-04-18 12:32:36 +00:00
// stick a separator on the end if necessary.
2014-12-22 04:35:47 +00:00
if ( musiclen > = seplen )
2010-06-14 10:33:36 +00:00
{
2016-08-06 19:55:25 +00:00
auto p = & cfgpath [ musiclen - seplen ] ;
2011-04-18 12:32:36 +00:00
if ( strcmp ( p , sep ) )
2016-08-06 19:55:25 +00:00
cfgpath . copy_if ( musiclen , sep , seplen ) ;
2010-09-26 13:15:20 +00:00
}
2010-11-21 11:55:35 +00:00
2016-08-06 19:55:25 +00:00
const auto p = cfgpath . data ( ) ;
2010-11-21 11:55:35 +00:00
// 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 ) ) ;
2011-04-18 12:32:36 +00:00
else
{
2016-08-06 19:55:25 +00:00
if ( PHYSFSX_isNewPath ( p ) )
new_path . reset ( p ) ;
2016-08-06 19:55:25 +00:00
PHYSFS_addToSearchPath ( p , 0 ) ;
2011-04-18 12:32:36 +00:00
// 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 ) ) ;
2011-04-18 12:32:36 +00:00
}
2010-11-21 11:55:35 +00:00
if ( ! JukeboxSongs . list )
2010-09-26 13:15:20 +00:00
{
2010-11-21 11:55:35 +00:00
return ;
2009-01-13 12:39:03 +00:00
}
2015-04-26 20:15:51 +00:00
JukeboxSongs . num_songs = std : : distance ( JukeboxSongs . list . begin ( ) , JukeboxSongs . list . end ( ) ) ;
2010-11-21 11:55:35 +00:00
}
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 )
2010-11-21 11:55:35 +00:00
{
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.
2010-09-26 13:15:20 +00:00
}
2009-01-13 12:39:03 +00:00
}
2010-11-21 11:55:35 +00:00
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! " ) ;
2010-11-21 11:55:35 +00:00
}
2009-01-13 12:39:03 +00:00
}
2010-06-14 08:13:16 +00:00
// 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 ( )
2010-06-14 08:13:16 +00:00
{
2016-08-06 19:55:25 +00:00
if ( ! JukeboxSongs . list | | CGameCfg . CMLevelMusicTrack [ 0 ] = = - 1 )
return ;
2009-03-03 12:55:27 +00:00
2011-01-20 11:17:30 +00:00
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!
2011-01-20 11:17:30 +00:00
else
2016-08-06 19:55:25 +00:00
CGameCfg . CMLevelMusicTrack [ 0 ] + + ;
if ( CGameCfg . CMLevelMusicTrack [ 0 ] + 1 > CGameCfg . CMLevelMusicTrack [ 1 ] )
CGameCfg . CMLevelMusicTrack [ 0 ] = 0 ;
2010-06-14 08:13:16 +00:00
jukebox_play ( ) ;
}
2009-01-13 12:39:03 +00:00
2010-06-14 08:13:16 +00:00
// 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 ;
2009-03-03 12:55:27 +00:00
2010-09-26 13:15:20 +00:00
if ( ! JukeboxSongs . list )
2010-06-14 08:13:16 +00:00
return 0 ;
2009-03-03 12:55:27 +00:00
2016-08-06 19:55:25 +00:00
if ( CGameCfg . CMLevelMusicTrack [ 0 ] < 0 | |
CGameCfg . CMLevelMusicTrack [ 0 ] + 1 > CGameCfg . CMLevelMusicTrack [ 1 ] )
2010-06-14 08:13:16 +00:00
return 0 ;
2009-01-13 12:39:03 +00:00
2016-08-06 19:55:25 +00:00
music_filename = JukeboxSongs . list [ CGameCfg . CMLevelMusicTrack [ 0 ] ] ;
2010-02-27 13:05:34 +00:00
if ( ! music_filename )
return 0 ;
2009-01-13 12:39:03 +00:00
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 ;
2015-01-28 03:42:53 +00:00
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 = " " ;
2011-04-18 12:32:36 +00:00
else // if it's from a specified path
2016-08-06 19:55:25 +00:00
LevelMusicPath = cfgpath . data ( ) ;
2015-02-17 03:52:59 +00:00
snprintf ( full_filename . get ( ) , size_full_filename , " %s%s " , LevelMusicPath , music_filename ) ;
2010-11-21 11:55:35 +00:00
2015-02-17 03:52:59 +00:00
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 )
2010-11-21 11:55:35 +00:00
{
2010-04-05 11:54:23 +00:00
return 0 ; // whoops, got an error
2010-11-21 11:55:35 +00:00
}
2009-01-13 12:39:03 +00:00
// 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 ;
2009-01-13 12:39:03 +00:00
} else {
2013-12-03 22:20:09 +00:00
prefix + = 3 ;
2009-01-13 12:39:03 +00:00
}
2013-12-03 22:20:09 +00:00
HUD_init_message ( HM_DEFAULT , " %s %s%s " , JUKEBOX_HUDMSG_PLAYING , prefix , music_filename ) ;
2009-03-03 12:55:27 +00:00
return 1 ;
2009-01-13 12:39:03 +00:00
}
2015-12-13 18:00:48 +00:00
}