2017-04-01 17:46:38 +00:00
/*
* * music_libsndfile . cpp
* * Uses libsndfile for streaming music formats
* *
* * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* * Copyright 2017 Christoph Oelckers
* * All rights reserved .
* *
* * Redistribution and use in source and binary forms , with or without
* * modification , are permitted provided that the following conditions
* * are met :
* *
* * 1. Redistributions of source code must retain the above copyright
* * notice , this list of conditions and the following disclaimer .
* * 2. Redistributions in binary form must reproduce the above copyright
* * notice , this list of conditions and the following disclaimer in the
* * documentation and / or other materials provided with the distribution .
* * 3. The name of the author may not be used to endorse or promote products
* * derived from this software without specific prior written permission .
* *
* * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ` ` AS IS ' ' AND ANY EXPRESS OR
* * IMPLIED WARRANTIES , INCLUDING , BUT NOT LIMITED TO , THE IMPLIED WARRANTIES
* * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED .
* * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT , INDIRECT ,
* * INCIDENTAL , SPECIAL , EXEMPLARY , OR CONSEQUENTIAL DAMAGES ( INCLUDING , BUT
* * NOT LIMITED TO , PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES ; LOSS OF USE ,
* * DATA , OR PROFITS ; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY
* * THEORY OF LIABILITY , WHETHER IN CONTRACT , STRICT LIABILITY , OR TORT
* * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF
* * THIS SOFTWARE , EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE .
* * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* *
*/
// HEADER FILES ------------------------------------------------------------
2019-02-22 19:24:24 +00:00
# include <mutex>
2017-04-01 17:46:38 +00:00
# include "i_musicinterns.h"
# include "c_cvars.h"
# include "v_text.h"
# include "templates.h"
2017-04-01 19:40:36 +00:00
# include "m_fixed.h"
2019-09-28 22:41:13 +00:00
# include "streamsource.h"
2019-09-29 10:48:12 +00:00
# include "zmusic/sounddecoder.h"
2017-04-01 17:46:38 +00:00
// MACROS ------------------------------------------------------------------
// TYPES -------------------------------------------------------------------
2019-09-28 18:33:25 +00:00
class SndFileSong : public StreamSource
2017-04-01 17:46:38 +00:00
{
public :
2019-09-29 10:48:12 +00:00
SndFileSong ( SoundDecoder * decoder , uint32_t loop_start , uint32_t loop_end , bool startass , bool endass ) ;
2017-04-01 17:46:38 +00:00
~ SndFileSong ( ) ;
2019-09-28 22:41:13 +00:00
std : : string GetStats ( ) override ;
2019-09-28 18:33:25 +00:00
SoundStreamInfo GetFormat ( ) override ;
bool GetData ( void * buffer , size_t len ) override ;
2017-04-01 17:46:38 +00:00
protected :
2019-02-22 19:24:24 +00:00
std : : mutex CritSec ;
2017-04-01 17:46:38 +00:00
SoundDecoder * Decoder ;
int Channels ;
int SampleRate ;
uint32_t Loop_Start ;
uint32_t Loop_End ;
int CalcSongLength ( ) ;
} ;
// EXTERNAL FUNCTION PROTOTYPES --------------------------------------------
// PUBLIC FUNCTION PROTOTYPES ----------------------------------------------
// PRIVATE FUNCTION PROTOTYPES ---------------------------------------------
// EXTERNAL DATA DECLARATIONS ----------------------------------------------
// PUBLIC DATA DEFINITIONS -------------------------------------------------
2017-05-08 07:05:34 +00:00
CUSTOM_CVAR ( Int , snd_streambuffersize , 64 , CVAR_ARCHIVE | CVAR_GLOBALCONFIG )
{
if ( self < 16 )
{
self = 16 ;
}
else if ( self > 1024 )
{
self = 1024 ;
}
}
2017-04-01 17:46:38 +00:00
// PRIVATE DATA DEFINITIONS ------------------------------------------------
// CODE --------------------------------------------------------------------
//==========================================================================
//
2018-02-11 13:32:14 +00:00
// Try to find the LOOP_START/LOOP_END tags in a Vorbis Comment block
2017-04-01 19:40:36 +00:00
//
2018-02-11 13:32:14 +00:00
// We have to parse through the FLAC or Ogg headers manually, since sndfile
// doesn't provide proper access to the comments and we'd rather not require
// using libFLAC and libvorbisfile directly.
2017-04-01 19:40:36 +00:00
//
//==========================================================================
2019-09-29 10:48:12 +00:00
static void ParseVorbisComments ( MusicIO : : FileInterface * fr , uint32_t * start , bool * startass , uint32_t * end , bool * endass )
2017-04-01 19:40:36 +00:00
{
2018-02-11 13:32:14 +00:00
uint8_t vc_data [ 4 ] ;
// The VC block starts with a 32LE integer for the vendor string length,
// followed by the vendor string
2019-09-29 10:48:12 +00:00
if ( fr - > read ( vc_data , 4 ) ! = 4 )
2018-02-11 13:32:14 +00:00
return ;
2018-02-19 10:28:24 +00:00
uint32_t vndr_len = vc_data [ 0 ] | ( vc_data [ 1 ] < < 8 ) | ( vc_data [ 2 ] < < 16 ) | ( vc_data [ 3 ] < < 24 ) ;
2018-02-11 13:32:14 +00:00
// Skip vendor string
2019-09-29 10:48:12 +00:00
if ( fr - > seek ( vndr_len , SEEK_CUR ) = = - 1 )
2018-02-11 13:32:14 +00:00
return ;
// Following the vendor string is a 32LE integer for the number of
// comments, followed by each comment.
2019-09-29 10:48:12 +00:00
if ( fr - > read ( vc_data , 4 ) ! = 4 )
2018-02-11 13:32:14 +00:00
return ;
size_t count = vc_data [ 0 ] | ( vc_data [ 1 ] < < 8 ) | ( vc_data [ 2 ] < < 16 ) | ( vc_data [ 3 ] < < 24 ) ;
2017-04-01 19:40:36 +00:00
2018-02-11 13:32:14 +00:00
for ( size_t i = 0 ; i < count ; i + + )
2017-04-01 19:40:36 +00:00
{
2018-02-11 13:32:14 +00:00
// Each comment is a 32LE integer for the comment length, followed by
// the comment text (not null terminated!)
2019-09-29 10:48:12 +00:00
if ( fr - > read ( vc_data , 4 ) ! = 4 )
2018-02-11 13:32:14 +00:00
return ;
2018-02-19 10:28:24 +00:00
uint32_t length = vc_data [ 0 ] | ( vc_data [ 1 ] < < 8 ) | ( vc_data [ 2 ] < < 16 ) | ( vc_data [ 3 ] < < 24 ) ;
2017-04-01 19:40:36 +00:00
2018-02-11 13:32:14 +00:00
if ( length > = 128 )
2017-04-01 19:40:36 +00:00
{
2018-02-11 13:32:14 +00:00
// If the comment is "big", skip it
2019-09-29 10:48:12 +00:00
if ( fr - > seek ( length , SEEK_CUR ) = = - 1 )
2018-02-11 13:32:14 +00:00
return ;
2017-04-01 19:40:36 +00:00
continue ;
}
2018-02-11 13:32:14 +00:00
char strdat [ 128 ] ;
2019-09-29 10:48:12 +00:00
if ( fr - > read ( strdat , length ) ! = ( long ) length )
2018-02-11 13:32:14 +00:00
return ;
strdat [ length ] = 0 ;
if ( strnicmp ( strdat , " LOOP_START= " , 11 ) = = 0 )
S_ParseTimeTag ( strdat + 11 , startass , start ) ;
else if ( strnicmp ( strdat , " LOOP_END= " , 9 ) = = 0 )
S_ParseTimeTag ( strdat + 9 , endass , end ) ;
2017-04-01 19:40:36 +00:00
}
2018-02-11 13:32:14 +00:00
}
2019-09-29 10:48:12 +00:00
static void FindFlacComments ( MusicIO : : FileInterface * fr , uint32_t * loop_start , bool * startass , uint32_t * loop_end , bool * endass )
2018-02-11 13:32:14 +00:00
{
// Already verified the fLaC marker, so we're 4 bytes into the file
bool lastblock = false ;
uint8_t header [ 4 ] ;
2019-09-29 10:48:12 +00:00
while ( ! lastblock & & fr - > read ( header , 4 ) = = 4 )
2017-04-01 19:40:36 +00:00
{
2018-02-11 13:32:14 +00:00
// The first byte of the block header contains the type and a flag
// indicating the last metadata block
char blocktype = header [ 0 ] & 0x7f ;
2018-02-19 10:28:24 +00:00
lastblock = ! ! ( header [ 0 ] & 0x80 ) ;
2018-02-11 13:32:14 +00:00
// Following the type is a 24BE integer for the size of the block
2018-02-19 10:28:24 +00:00
uint32_t blocksize = ( header [ 1 ] < < 16 ) | ( header [ 2 ] < < 8 ) | header [ 3 ] ;
2018-02-11 13:32:14 +00:00
// FLAC__METADATA_TYPE_VORBIS_COMMENT is 4
if ( blocktype = = 4 )
2017-04-01 19:40:36 +00:00
{
2018-02-11 13:32:14 +00:00
ParseVorbisComments ( fr , loop_start , startass , loop_end , endass ) ;
return ;
2017-04-01 19:40:36 +00:00
}
2018-02-11 13:32:14 +00:00
2019-09-29 10:48:12 +00:00
if ( fr - > seek ( blocksize , SEEK_CUR ) = = - 1 )
2018-02-11 13:32:14 +00:00
break ;
}
}
2019-09-29 10:48:12 +00:00
static void FindOggComments ( MusicIO : : FileInterface * fr , uint32_t * loop_start , bool * startass , uint32_t * loop_end , bool * endass )
2018-02-11 13:32:14 +00:00
{
uint8_t ogghead [ 27 ] ;
// We already read and verified the OggS marker, so skip the first 4 bytes
// of the Ogg page header.
2019-09-29 10:48:12 +00:00
while ( fr - > read ( ogghead + 4 , 23 ) = = 23 )
2018-02-11 13:32:14 +00:00
{
// The 19th byte of the Ogg header is a 32LE integer for the page
// number, and the 27th is a uint8 for the number of segments in the
// page.
uint32_t ogg_pagenum = ogghead [ 18 ] | ( ogghead [ 19 ] < < 8 ) | ( ogghead [ 20 ] < < 16 ) |
( ogghead [ 21 ] < < 24 ) ;
uint8_t ogg_segments = ogghead [ 26 ] ;
// Following the Ogg page header is a series of uint8s for the length of
// each segment in the page. The page segment data follows contiguously
// after.
uint8_t segsizes [ 256 ] ;
2019-09-29 10:48:12 +00:00
if ( fr - > read ( segsizes , ogg_segments ) ! = ogg_segments )
2018-02-11 13:32:14 +00:00
break ;
// Find the segment with the Vorbis Comment packet (type 3)
for ( int i = 0 ; i < ogg_segments ; + + i )
2017-04-01 19:40:36 +00:00
{
2018-02-19 10:28:24 +00:00
uint8_t segsize = segsizes [ i ] ;
2018-02-11 13:32:14 +00:00
if ( segsize > 16 )
{
uint8_t vorbhead [ 7 ] ;
2019-09-29 10:48:12 +00:00
if ( fr - > read ( vorbhead , 7 ) ! = 7 )
2018-02-11 13:32:14 +00:00
return ;
if ( vorbhead [ 0 ] = = 3 & & memcmp ( vorbhead + 1 , " vorbis " , 6 ) = = 0 )
{
// If the packet is 'laced', it spans multiple segments (a
// segment size of 255 indicates the next segment continues
// the packet, ending with a size less than 255). Vorbis
// packets always start and end on segment boundaries. A
// packet that's an exact multiple of 255 ends with a
// segment of 0 size.
while ( segsize = = 255 & & + + i < ogg_segments )
segsize = segsizes [ i ] ;
// TODO: A Vorbis packet can theoretically span multiple
// Ogg pages (e.g. start in the last segment of one page
// and end in the first segment of a following page). That
// will require extra logic to decode as the VC block will
// be broken up with non-Vorbis data in-between. For now,
// just handle the common case where it's all in one page.
if ( i < ogg_segments )
ParseVorbisComments ( fr , loop_start , startass , loop_end , endass ) ;
return ;
}
segsize - = 7 ;
}
2019-09-29 10:48:12 +00:00
if ( fr - > seek ( segsize , SEEK_CUR ) = = - 1 )
2018-02-11 13:32:14 +00:00
return ;
2017-04-01 19:40:36 +00:00
}
2018-02-11 13:32:14 +00:00
// Don't keep looking after the third page
if ( ogg_pagenum > = 2 )
break ;
2019-09-29 10:48:12 +00:00
if ( fr - > read ( ogghead , 4 ) ! = 4 | | memcmp ( ogghead , " OggS " , 4 ) ! = 0 )
2018-02-11 13:32:14 +00:00
break ;
2017-04-01 19:40:36 +00:00
}
}
2019-09-29 10:48:12 +00:00
void FindLoopTags ( MusicIO : : FileInterface * fr , uint32_t * start , bool * startass , uint32_t * end , bool * endass )
2018-02-11 13:32:14 +00:00
{
uint8_t signature [ 4 ] ;
2019-09-29 10:48:12 +00:00
fr - > read ( signature , 4 ) ;
2018-02-11 13:32:14 +00:00
if ( memcmp ( signature , " fLaC " , 4 ) = = 0 )
FindFlacComments ( fr , start , startass , end , endass ) ;
else if ( memcmp ( signature , " OggS " , 4 ) = = 0 )
FindOggComments ( fr , start , startass , end , endass ) ;
}
2017-04-01 19:40:36 +00:00
//==========================================================================
//
// SndFile_OpenSong
2017-04-01 17:46:38 +00:00
//
//==========================================================================
2019-09-29 10:48:12 +00:00
StreamSource * SndFile_OpenSong ( MusicIO : : FileInterface * fr )
2017-04-01 17:46:38 +00:00
{
2019-09-29 10:48:12 +00:00
fr - > seek ( 0 , SEEK_SET ) ;
2018-02-11 13:32:14 +00:00
2017-04-01 17:46:38 +00:00
uint32_t loop_start = 0 , loop_end = ~ 0u ;
2017-04-01 19:40:36 +00:00
bool startass = false , endass = false ;
2018-03-10 17:45:11 +00:00
FindLoopTags ( fr , & loop_start , & startass , & loop_end , & endass ) ;
2018-02-11 13:32:14 +00:00
2019-09-29 10:48:12 +00:00
fr - > seek ( 0 , FileReader : : SeekSet ) ;
auto decoder = SoundDecoder : : CreateDecoder ( fr ) ;
if ( decoder = = nullptr ) return nullptr ; // If this fails the file reader has not been taken over and the caller needs to clean up. This is to allow further analysis of the passed file.
return new SndFileSong ( decoder , loop_start , loop_end , startass , endass ) ;
2017-04-01 17:46:38 +00:00
}
//==========================================================================
//
// SndFileSong - Constructor
//
//==========================================================================
2019-09-29 10:48:12 +00:00
SndFileSong : : SndFileSong ( SoundDecoder * decoder , uint32_t loop_start , uint32_t loop_end , bool startass , bool endass )
2017-04-01 17:46:38 +00:00
{
ChannelConfig iChannels ;
SampleType Type ;
decoder - > getInfo ( & SampleRate , & iChannels , & Type ) ;
2017-04-01 19:40:36 +00:00
if ( ! startass ) loop_start = Scale ( loop_start , SampleRate , 1000 ) ;
if ( ! endass ) loop_end = Scale ( loop_end , SampleRate , 1000 ) ;
2018-03-20 16:01:35 +00:00
const uint32_t sampleLength = ( uint32_t ) decoder - > getSampleLength ( ) ;
2017-04-01 17:46:38 +00:00
Loop_Start = loop_start ;
2018-03-20 16:01:35 +00:00
Loop_End = sampleLength = = 0 ? loop_end : clamp < uint32_t > ( loop_end , 0 , sampleLength ) ;
2017-04-01 17:46:38 +00:00
Decoder = decoder ;
Channels = iChannels = = ChannelConfig_Stereo ? 2 : 1 ;
2019-09-28 18:33:25 +00:00
}
SoundStreamInfo SndFileSong : : GetFormat ( )
{
return { snd_streambuffersize * 1024 , SampleRate , - Channels } ;
2017-04-01 17:46:38 +00:00
}
//==========================================================================
//
// SndFileSong - Destructor
//
//==========================================================================
SndFileSong : : ~ SndFileSong ( )
{
if ( Decoder ! = nullptr )
{
delete Decoder ;
}
}
//==========================================================================
//
// SndFileSong :: GetStats
//
//==========================================================================
2019-09-28 22:41:13 +00:00
std : : string SndFileSong : : GetStats ( )
2017-04-01 17:46:38 +00:00
{
2019-09-28 22:41:13 +00:00
char out [ 80 ] ;
2017-04-01 17:46:38 +00:00
size_t SamplePos ;
SamplePos = Decoder - > getSampleOffset ( ) ;
int time = int ( SamplePos / SampleRate ) ;
2019-09-28 22:41:13 +00:00
snprintf ( out , 80 ,
" Track: %s, %dHz Time: %02d:%02d " ,
2017-04-01 17:46:38 +00:00
Channels = = 2 ? " Stereo " : " Mono " , SampleRate ,
time / 60 ,
time % 60 ) ;
return out ;
}
//==========================================================================
//
// SndFileSong :: Read STATIC
//
//==========================================================================
2019-09-28 18:33:25 +00:00
bool SndFileSong : : GetData ( void * vbuff , size_t len )
2017-04-01 17:46:38 +00:00
{
char * buff = ( char * ) vbuff ;
2019-09-28 18:33:25 +00:00
std : : lock_guard < std : : mutex > lock ( CritSec ) ;
2017-04-01 17:46:38 +00:00
2019-09-28 18:33:25 +00:00
size_t currentpos = Decoder - > getSampleOffset ( ) ;
size_t framestoread = len / ( Channels * 2 ) ;
2017-04-01 17:46:38 +00:00
bool err = false ;
2019-09-28 18:33:25 +00:00
if ( ! m_Looping )
2017-04-01 17:46:38 +00:00
{
2019-09-28 18:33:25 +00:00
size_t maxpos = Decoder - > getSampleLength ( ) ;
2017-04-01 17:46:38 +00:00
if ( currentpos = = maxpos )
{
memset ( buff , 0 , len ) ;
return false ;
}
if ( currentpos + framestoread > maxpos )
{
2019-09-28 18:33:25 +00:00
size_t got = Decoder - > read ( buff , ( maxpos - currentpos ) * Channels * 2 ) ;
2017-04-01 17:46:38 +00:00
memset ( buff + got , 0 , len - got ) ;
}
else
{
2019-09-28 18:33:25 +00:00
size_t got = Decoder - > read ( buff , len ) ;
2017-04-01 17:46:38 +00:00
err = ( got ! = len ) ;
}
}
else
{
2017-04-25 19:30:11 +00:00
// This looks a bit more complicated than necessary because libmpg123 will not read the full requested length for the last block in the file.
2019-09-28 18:33:25 +00:00
if ( currentpos + framestoread > Loop_End )
2017-04-01 17:46:38 +00:00
{
2018-03-21 10:28:12 +00:00
// Loop can be very short, make sure the current position doesn't exceed it
2019-09-28 18:33:25 +00:00
if ( currentpos < Loop_End )
2018-03-21 10:28:12 +00:00
{
2019-09-28 18:33:25 +00:00
size_t endblock = ( Loop_End - currentpos ) * Channels * 2 ;
size_t endlen = Decoder - > read ( buff , endblock ) ;
2018-03-21 10:28:12 +00:00
// Even if zero bytes was read give it a chance to start from the beginning
buff + = endlen ;
len - = endlen ;
}
2017-07-09 10:28:16 +00:00
2019-09-28 18:33:25 +00:00
Decoder - > seek ( Loop_Start , false , true ) ;
2017-04-25 19:30:11 +00:00
}
while ( len > 0 )
{
2019-09-28 18:33:25 +00:00
size_t readlen = Decoder - > read ( buff , len ) ;
2017-04-25 19:30:11 +00:00
if ( readlen = = 0 )
{
return false ;
}
buff + = readlen ;
len - = readlen ;
if ( len > 0 )
{
2019-09-28 18:33:25 +00:00
Decoder - > seek ( Loop_Start , false , true ) ;
2017-04-25 19:30:11 +00:00
}
2017-04-01 17:46:38 +00:00
}
}
2017-04-25 19:30:11 +00:00
return true ;
2017-04-01 17:46:38 +00:00
}