477 lines
10 KiB
C++
477 lines
10 KiB
C++
// Game_Music_Emu $vers. http://www.slack.net/~ant/
|
|
|
|
#include "M3u_Playlist.h"
|
|
#include "Music_Emu.h"
|
|
|
|
/* Copyright (C) 2006 Shay Green. This module is free software; you
|
|
can redistribute it and/or modify it under the terms of the GNU Lesser
|
|
General Public License as published by the Free Software Foundation; either
|
|
version 2.1 of the License, or (at your option) any later version. This
|
|
module is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
|
|
details. You should have received a copy of the GNU Lesser General Public
|
|
License along with this module; if not, write to the Free Software Foundation,
|
|
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */
|
|
|
|
#include "blargg_source.h"
|
|
|
|
// gme functions defined here to avoid linking in m3u code unless it's used
|
|
|
|
blargg_err_t Gme_File::load_m3u_( blargg_err_t err )
|
|
{
|
|
if ( !err )
|
|
{
|
|
require( raw_track_count_ ); // file must be loaded first
|
|
if ( playlist.size() )
|
|
track_count_ = playlist.size();
|
|
|
|
int line = playlist.first_error();
|
|
if ( line )
|
|
{
|
|
// avoid using bloated printf()
|
|
char* out = &playlist_warning [sizeof playlist_warning];
|
|
*--out = 0;
|
|
do {
|
|
*--out = line % 10 + '0';
|
|
} while ( (line /= 10) > 0 );
|
|
|
|
static const char str [] = "Problem in m3u at line ";
|
|
out -= sizeof str - 1;
|
|
memcpy( out, str, sizeof str - 1 );
|
|
set_warning( out );
|
|
}
|
|
}
|
|
return err;
|
|
}
|
|
|
|
blargg_err_t Gme_File::load_m3u( const char path [] ) { return load_m3u_( playlist.load( path ) ); }
|
|
|
|
blargg_err_t Gme_File::load_m3u( Data_Reader& in ) { return load_m3u_( playlist.load( in ) ); }
|
|
|
|
gme_err_t gme_load_m3u( Music_Emu* me, const char path [] ) { return me->load_m3u( path ); }
|
|
|
|
gme_err_t gme_load_m3u_data( Music_Emu* me, const void* data, long size )
|
|
{
|
|
Mem_File_Reader in( data, size );
|
|
return me->load_m3u( in );
|
|
}
|
|
|
|
static char* skip_white( char* in )
|
|
{
|
|
while ( unsigned (*in - 1) <= ' ' - 1 )
|
|
in++;
|
|
return in;
|
|
}
|
|
|
|
inline unsigned from_dec( unsigned n ) { return n - '0'; }
|
|
|
|
static char* parse_filename( char* in, M3u_Playlist::entry_t& entry )
|
|
{
|
|
entry.file = in;
|
|
entry.type = "";
|
|
char* out = in;
|
|
while ( 1 )
|
|
{
|
|
int c = *in;
|
|
if ( !c ) break;
|
|
in++;
|
|
|
|
if ( c == ',' ) // commas in filename
|
|
{
|
|
char* p = skip_white( in );
|
|
if ( *p == '$' || from_dec( *p ) <= 9 )
|
|
{
|
|
in = p;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( c == ':' && in [0] == ':' && in [1] && in [2] != ',' ) // ::type suffix
|
|
{
|
|
entry.type = ++in;
|
|
while ( (c = *in) != 0 && c != ',' )
|
|
in++;
|
|
if ( c == ',' )
|
|
{
|
|
*in++ = 0; // terminate type
|
|
in = skip_white( in );
|
|
}
|
|
break;
|
|
}
|
|
|
|
if ( c == '\\' ) // \ prefix for special characters
|
|
{
|
|
c = *in;
|
|
if ( !c ) break;
|
|
in++;
|
|
}
|
|
*out++ = (char) c;
|
|
}
|
|
*out = 0; // terminate string
|
|
return in;
|
|
}
|
|
|
|
static char* next_field( char* in, int* result )
|
|
{
|
|
while ( 1 )
|
|
{
|
|
in = skip_white( in );
|
|
|
|
if ( !*in )
|
|
break;
|
|
|
|
if ( *in == ',' )
|
|
{
|
|
in++;
|
|
break;
|
|
}
|
|
|
|
*result = 1;
|
|
in++;
|
|
}
|
|
return skip_white( in );
|
|
}
|
|
|
|
static char* parse_int_( char* in, int* out )
|
|
{
|
|
int n = 0;
|
|
while ( 1 )
|
|
{
|
|
unsigned d = from_dec( *in );
|
|
if ( d > 9 )
|
|
break;
|
|
in++;
|
|
n = n * 10 + d;
|
|
*out = n;
|
|
}
|
|
return in;
|
|
}
|
|
|
|
static char* parse_int( char* in, int* out, int* result )
|
|
{
|
|
return next_field( parse_int_( in, out ), result );
|
|
}
|
|
|
|
// Returns 16 or greater if not hex
|
|
inline int from_hex_char( int h )
|
|
{
|
|
h -= 0x30;
|
|
if ( (unsigned) h > 9 )
|
|
h = ((h - 0x11) & 0xDF) + 10;
|
|
return h;
|
|
}
|
|
|
|
static char* parse_track( char* in, M3u_Playlist::entry_t& entry, int* result )
|
|
{
|
|
if ( *in == '$' )
|
|
{
|
|
in++;
|
|
int n = 0;
|
|
while ( 1 )
|
|
{
|
|
int h = from_hex_char( *in );
|
|
if ( h > 15 )
|
|
break;
|
|
in++;
|
|
n = n * 16 + h;
|
|
entry.track = n;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
in = parse_int_( in, &entry.track );
|
|
if ( entry.track >= 0 )
|
|
entry.decimal_track = 1;
|
|
}
|
|
return next_field( in, result );
|
|
}
|
|
|
|
static char* parse_time_( char* in, int* out )
|
|
{
|
|
*out = -1;
|
|
int n = -1;
|
|
in = parse_int_( in, &n );
|
|
if ( n >= 0 )
|
|
{
|
|
*out = n;
|
|
while ( *in == ':' )
|
|
{
|
|
n = -1;
|
|
in = parse_int_( in + 1, &n );
|
|
if ( n >= 0 )
|
|
*out = *out * 60 + n;
|
|
}
|
|
*out *= 1000;
|
|
if ( *in == '.' )
|
|
{
|
|
n = -1;
|
|
in = parse_int_( in + 1, &n );
|
|
if ( n >= 0 )
|
|
*out = *out + n;
|
|
}
|
|
}
|
|
return in;
|
|
}
|
|
|
|
static char* parse_time( char* in, int* out, int* result )
|
|
{
|
|
return next_field( parse_time_( in, out ), result );
|
|
}
|
|
|
|
static char* parse_name( char* in )
|
|
{
|
|
char* out = in;
|
|
while ( 1 )
|
|
{
|
|
int c = *in;
|
|
if ( !c ) break;
|
|
in++;
|
|
|
|
if ( c == ',' ) // commas in string
|
|
{
|
|
char* p = skip_white( in );
|
|
if ( *p == ',' || *p == '-' || from_dec( *p ) <= 9 )
|
|
{
|
|
in = p;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( c == '\\' ) // \ prefix for special characters
|
|
{
|
|
c = *in;
|
|
if ( !c ) break;
|
|
in++;
|
|
}
|
|
*out++ = (char) c;
|
|
}
|
|
*out = 0; // terminate string
|
|
return in;
|
|
}
|
|
|
|
static int parse_line( char* in, M3u_Playlist::entry_t& entry )
|
|
{
|
|
int result = 0;
|
|
|
|
// file
|
|
entry.file = in;
|
|
entry.type = "";
|
|
in = parse_filename( in, entry );
|
|
|
|
// track
|
|
entry.track = -1;
|
|
entry.decimal_track = 0;
|
|
in = parse_track( in, entry, &result );
|
|
|
|
// name
|
|
entry.name = in;
|
|
in = parse_name( in );
|
|
|
|
// time
|
|
entry.length = -1;
|
|
in = parse_time( in, &entry.length, &result );
|
|
|
|
// loop
|
|
entry.intro = -1;
|
|
entry.loop = -1;
|
|
if ( *in == '-' )
|
|
{
|
|
entry.loop = entry.length;
|
|
in++;
|
|
}
|
|
else
|
|
{
|
|
in = parse_time_( in, &entry.loop );
|
|
if ( entry.loop >= 0 )
|
|
{
|
|
entry.intro = entry.length - entry.loop;
|
|
if ( *in == '-' ) // trailing '-' means that intro length was specified
|
|
{
|
|
in++;
|
|
entry.intro = entry.loop;
|
|
entry.loop = entry.length - entry.intro;
|
|
}
|
|
}
|
|
}
|
|
in = next_field( in, &result );
|
|
|
|
// fade
|
|
entry.fade = -1;
|
|
in = parse_time( in, &entry.fade, &result );
|
|
|
|
// repeat
|
|
entry.repeat = -1;
|
|
in = parse_int( in, &entry.repeat, &result );
|
|
|
|
return result;
|
|
}
|
|
|
|
static void parse_comment( char* in, M3u_Playlist::info_t& info, char *& last_comment_value, bool first )
|
|
{
|
|
in = skip_white( in + 1 );
|
|
const char* field = in;
|
|
if ( *field != '@' )
|
|
while ( *in && *in != ':' )
|
|
in++;
|
|
|
|
if ( *in == ':' )
|
|
{
|
|
const char* text = skip_white( in + 1 );
|
|
if ( *text )
|
|
{
|
|
*in = 0;
|
|
if ( !strcmp( "Composer" , field ) ) info.composer = text;
|
|
else if ( !strcmp( "Engineer" , field ) ) info.engineer = text;
|
|
else if ( !strcmp( "Ripping" , field ) ) info.ripping = text;
|
|
else if ( !strcmp( "Tagging" , field ) ) info.tagging = text;
|
|
else if ( !strcmp( "Game" , field ) ) info.title = text;
|
|
else if ( !strcmp( "Artist" , field ) ) info.artist = text;
|
|
else if ( !strcmp( "Copyright", field ) ) info.copyright = text;
|
|
else
|
|
text = 0;
|
|
if ( text )
|
|
return;
|
|
*in = ':';
|
|
}
|
|
}
|
|
else if ( *field == '@' )
|
|
{
|
|
++field;
|
|
in = (char*)field;
|
|
while ( *in && *in > ' ' )
|
|
in++;
|
|
const char* text = skip_white( in );
|
|
if ( *text )
|
|
{
|
|
char saved = *in;
|
|
*in = 0;
|
|
if ( !strcmp( "TITLE" , field ) ) info.title = text;
|
|
else if ( !strcmp( "ARTIST", field ) ) info.artist = text;
|
|
else if ( !strcmp( "DATE", field ) ) info.date = text;
|
|
else if ( !strcmp( "COMPOSER", field ) ) info.composer = text;
|
|
else if ( !strcmp( "SEQUENCER", field ) ) info.sequencer = text;
|
|
else if ( !strcmp( "ENGINEER", field ) ) info.engineer = text;
|
|
else if ( !strcmp( "RIPPER", field ) ) info.ripping = text;
|
|
else if ( !strcmp( "TAGGER", field ) ) info.tagging = text;
|
|
else
|
|
text = 0;
|
|
if ( text )
|
|
{
|
|
last_comment_value = (char*)text;
|
|
return;
|
|
}
|
|
*in = saved;
|
|
}
|
|
}
|
|
else if ( last_comment_value )
|
|
{
|
|
size_t len = strlen( last_comment_value );
|
|
last_comment_value[ len ] = ',';
|
|
last_comment_value[ len + 1 ] = ' ';
|
|
size_t field_len = strlen( field );
|
|
memmove( last_comment_value + len + 2, field, field_len );
|
|
last_comment_value[ len + 2 + field_len ] = 0;
|
|
return;
|
|
}
|
|
|
|
if ( first )
|
|
info.title = field;
|
|
}
|
|
|
|
blargg_err_t M3u_Playlist::parse_()
|
|
{
|
|
info_.title = "";
|
|
info_.artist = "";
|
|
info_.date = "";
|
|
info_.composer = "";
|
|
info_.sequencer = "";
|
|
info_.engineer = "";
|
|
info_.ripping = "";
|
|
info_.tagging = "";
|
|
info_.copyright = "";
|
|
|
|
int const CR = 13;
|
|
int const LF = 10;
|
|
|
|
data.end() [-1] = LF; // terminate input
|
|
|
|
first_error_ = 0;
|
|
bool first_comment = true;
|
|
int line = 0;
|
|
int count = 0;
|
|
char* in = data.begin();
|
|
char* last_comment_value = 0;
|
|
while ( in < data.end() )
|
|
{
|
|
// find end of line and terminate it
|
|
line++;
|
|
char* begin = in;
|
|
while ( *in != CR && *in != LF )
|
|
{
|
|
if ( !*in )
|
|
return blargg_err_file_type;
|
|
in++;
|
|
}
|
|
if ( in [0] == CR && in [1] == LF ) // treat CR,LF as a single line
|
|
*in++ = 0;
|
|
*in++ = 0;
|
|
|
|
// parse line
|
|
if ( *begin == '#' )
|
|
{
|
|
parse_comment( begin, info_, last_comment_value, first_comment );
|
|
first_comment = false;
|
|
}
|
|
else if ( *begin )
|
|
{
|
|
if ( (int) entries.size() <= count )
|
|
RETURN_ERR( entries.resize( count * 2 + 64 ) );
|
|
|
|
if ( !parse_line( begin, entries [count] ) )
|
|
count++;
|
|
else if ( !first_error_ )
|
|
first_error_ = line;
|
|
first_comment = false;
|
|
}
|
|
else last_comment_value = 0;
|
|
}
|
|
if ( count <= 0 )
|
|
return blargg_err_file_type;
|
|
|
|
// Treat first comment as title only if another field is also specified
|
|
if ( !(info_.artist [0] | info_.composer [0] | info_.date [0] | info_.engineer [0] | info_.ripping [0] | info_.sequencer [0] | info_.tagging [0] | info_.copyright[0]) )
|
|
info_.title = "";
|
|
|
|
return entries.resize( count );
|
|
}
|
|
|
|
blargg_err_t M3u_Playlist::parse()
|
|
{
|
|
blargg_err_t err = parse_();
|
|
if ( err )
|
|
clear_();
|
|
return err;
|
|
}
|
|
|
|
blargg_err_t M3u_Playlist::load( Data_Reader& in )
|
|
{
|
|
RETURN_ERR( data.resize( in.remain() + 1 ) );
|
|
RETURN_ERR( in.read( data.begin(), data.size() - 1 ) );
|
|
return parse();
|
|
}
|
|
|
|
blargg_err_t M3u_Playlist::load( const char path [] )
|
|
{
|
|
GME_FILE_READER in;
|
|
RETURN_ERR( in.open( path ) );
|
|
return load( in );
|
|
}
|
|
|
|
blargg_err_t M3u_Playlist::load( void const* in, long size )
|
|
{
|
|
RETURN_ERR( data.resize( size + 1 ) );
|
|
memcpy( data.begin(), in, size );
|
|
return parse();
|
|
}
|