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 ------------------------------------------------------------
|
|
|
|
|
|
|
|
#include "i_musicinterns.h"
|
|
|
|
#include "c_cvars.h"
|
|
|
|
#include "critsec.h"
|
|
|
|
#include "v_text.h"
|
|
|
|
#include "templates.h"
|
2017-04-01 19:40:36 +00:00
|
|
|
#include "m_fixed.h"
|
2017-04-01 17:46:38 +00:00
|
|
|
|
|
|
|
// MACROS ------------------------------------------------------------------
|
|
|
|
|
|
|
|
// TYPES -------------------------------------------------------------------
|
|
|
|
|
|
|
|
class SndFileSong : public StreamSong
|
|
|
|
{
|
|
|
|
public:
|
2017-04-01 19:40:36 +00:00
|
|
|
SndFileSong(FileReader *reader, SoundDecoder *decoder, uint32_t loop_start, uint32_t loop_end, bool startass, bool endass);
|
2017-04-01 17:46:38 +00:00
|
|
|
~SndFileSong();
|
|
|
|
bool SetSubsong(int subsong);
|
|
|
|
void Play(bool looping, int subsong);
|
|
|
|
FString GetStats();
|
|
|
|
|
|
|
|
protected:
|
|
|
|
FCriticalSection CritSec;
|
|
|
|
FileReader *Reader;
|
|
|
|
SoundDecoder *Decoder;
|
|
|
|
int Channels;
|
|
|
|
int SampleRate;
|
|
|
|
|
|
|
|
uint32_t Loop_Start;
|
|
|
|
uint32_t Loop_End;
|
|
|
|
|
|
|
|
int CalcSongLength();
|
|
|
|
|
|
|
|
static bool Read(SoundStream *stream, void *buff, int len, void *userdata);
|
|
|
|
};
|
|
|
|
|
|
|
|
// 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
|
|
|
//
|
|
|
|
//==========================================================================
|
|
|
|
|
2018-02-11 13:32:14 +00:00
|
|
|
static void ParseVorbisComments(FileReader *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
|
|
|
|
if(fr->Read(vc_data, 4) != 4)
|
|
|
|
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
|
|
|
|
if(fr->Seek(vndr_len, SEEK_CUR) == -1)
|
|
|
|
return;
|
|
|
|
|
|
|
|
// Following the vendor string is a 32LE integer for the number of
|
|
|
|
// comments, followed by each comment.
|
|
|
|
if(fr->Read(vc_data, 4) != 4)
|
|
|
|
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!)
|
|
|
|
if(fr->Read(vc_data, 4) != 4)
|
|
|
|
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
|
|
|
|
if(fr->Seek(length, SEEK_CUR) == -1)
|
|
|
|
return;
|
2017-04-01 19:40:36 +00:00
|
|
|
continue;
|
|
|
|
}
|
2018-02-11 13:32:14 +00:00
|
|
|
|
|
|
|
char strdat[128];
|
|
|
|
if(fr->Read(strdat, length) != (long)length)
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
static void FindFlacComments(FileReader *fr, uint32_t *loop_start, bool *startass, uint32_t *loop_end, bool *endass)
|
|
|
|
{
|
|
|
|
// Already verified the fLaC marker, so we're 4 bytes into the file
|
|
|
|
bool lastblock = false;
|
|
|
|
uint8_t header[4];
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
if(fr->Seek(blocksize, SEEK_CUR) == -1)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void FindOggComments(FileReader *fr, uint32_t *loop_start, bool *startass, uint32_t *loop_end, bool *endass)
|
|
|
|
{
|
|
|
|
uint8_t ogghead[27];
|
|
|
|
|
|
|
|
// We already read and verified the OggS marker, so skip the first 4 bytes
|
|
|
|
// of the Ogg page header.
|
|
|
|
while(fr->Read(ogghead+4, 23) == 23)
|
|
|
|
{
|
|
|
|
// 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];
|
|
|
|
if(fr->Read(segsizes, ogg_segments) != ogg_segments)
|
|
|
|
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];
|
|
|
|
if(fr->Read(vorbhead, 7) != 7)
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
if(fr->Seek(segsize, SEEK_CUR) == -1)
|
|
|
|
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;
|
|
|
|
|
|
|
|
if(fr->Read(ogghead, 4) != 4 || memcmp(ogghead, "OggS", 4) != 0)
|
|
|
|
break;
|
2017-04-01 19:40:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-11 13:32:14 +00:00
|
|
|
void FindLoopTags(FileReader *fr, uint32_t *start, bool *startass, uint32_t *end, bool *endass)
|
|
|
|
{
|
|
|
|
uint8_t signature[4];
|
|
|
|
|
|
|
|
fr->Read(signature, 4);
|
|
|
|
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
|
|
|
//
|
|
|
|
//==========================================================================
|
|
|
|
|
|
|
|
MusInfo *SndFile_OpenSong(FileReader &fr)
|
|
|
|
{
|
|
|
|
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-02-11 13:32:14 +00:00
|
|
|
FindLoopTags(&fr, &loop_start, &startass, &loop_end, &endass);
|
|
|
|
|
2017-04-01 17:46:38 +00:00
|
|
|
fr.Seek(0, SEEK_SET);
|
|
|
|
auto decoder = SoundRenderer::CreateDecoder(&fr);
|
|
|
|
if (decoder == nullptr) return nullptr;
|
2017-04-01 19:40:36 +00:00
|
|
|
return new SndFileSong(&fr, decoder, loop_start, loop_end, startass, endass);
|
2017-04-01 17:46:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
//==========================================================================
|
|
|
|
//
|
|
|
|
// SndFileSong - Constructor
|
|
|
|
//
|
|
|
|
//==========================================================================
|
|
|
|
|
2017-04-01 19:40:36 +00:00
|
|
|
SndFileSong::SndFileSong(FileReader *reader, 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);
|
|
|
|
|
2017-04-01 17:46:38 +00:00
|
|
|
Loop_Start = loop_start;
|
|
|
|
Loop_End = clamp<uint32_t>(loop_end, 0, (uint32_t)decoder->getSampleLength());
|
|
|
|
Reader = reader;
|
|
|
|
Decoder = decoder;
|
|
|
|
Channels = iChannels == ChannelConfig_Stereo? 2:1;
|
2017-05-08 07:05:34 +00:00
|
|
|
m_Stream = GSnd->CreateStream(Read, snd_streambuffersize * 1024, iChannels == ChannelConfig_Stereo? 0 : SoundStream::Mono, SampleRate, this);
|
2017-04-01 17:46:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
//==========================================================================
|
|
|
|
//
|
|
|
|
// SndFileSong - Destructor
|
|
|
|
//
|
|
|
|
//==========================================================================
|
|
|
|
|
|
|
|
SndFileSong::~SndFileSong()
|
|
|
|
{
|
|
|
|
Stop();
|
|
|
|
if (m_Stream != nullptr)
|
|
|
|
{
|
|
|
|
delete m_Stream;
|
|
|
|
m_Stream = nullptr;
|
|
|
|
}
|
|
|
|
if (Decoder != nullptr)
|
|
|
|
{
|
|
|
|
delete Decoder;
|
|
|
|
}
|
|
|
|
if (Reader != nullptr)
|
|
|
|
{
|
|
|
|
delete Reader;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//==========================================================================
|
|
|
|
//
|
|
|
|
// SndFileSong :: Play
|
|
|
|
//
|
|
|
|
//==========================================================================
|
|
|
|
|
|
|
|
void SndFileSong::Play(bool looping, int track)
|
|
|
|
{
|
|
|
|
m_Status = STATE_Stopped;
|
|
|
|
m_Looping = looping;
|
|
|
|
if (m_Stream->Play(looping, 1))
|
|
|
|
{
|
|
|
|
m_Status = STATE_Playing;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//==========================================================================
|
|
|
|
//
|
|
|
|
// SndFileSong :: SetSubsong
|
|
|
|
//
|
|
|
|
//==========================================================================
|
|
|
|
|
|
|
|
bool SndFileSong::SetSubsong(int track)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
//==========================================================================
|
|
|
|
//
|
|
|
|
// SndFileSong :: GetStats
|
|
|
|
//
|
|
|
|
//==========================================================================
|
|
|
|
|
|
|
|
FString SndFileSong::GetStats()
|
|
|
|
{
|
|
|
|
FString out;
|
|
|
|
|
|
|
|
size_t SamplePos;
|
|
|
|
|
|
|
|
SamplePos = Decoder->getSampleOffset();
|
|
|
|
int time = int (SamplePos / SampleRate);
|
|
|
|
|
|
|
|
out.Format(
|
|
|
|
"Track: " TEXTCOLOR_YELLOW "%s, %dHz" TEXTCOLOR_NORMAL
|
|
|
|
" Time:" TEXTCOLOR_YELLOW "%02d:%02d" TEXTCOLOR_NORMAL,
|
|
|
|
Channels == 2? "Stereo" : "Mono", SampleRate,
|
|
|
|
time/60,
|
|
|
|
time % 60);
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
|
|
|
|
//==========================================================================
|
|
|
|
//
|
|
|
|
// SndFileSong :: Read STATIC
|
|
|
|
//
|
|
|
|
//==========================================================================
|
|
|
|
|
|
|
|
bool SndFileSong::Read(SoundStream *stream, void *vbuff, int ilen, void *userdata)
|
|
|
|
{
|
|
|
|
char *buff = (char*)vbuff;
|
|
|
|
SndFileSong *song = (SndFileSong *)userdata;
|
|
|
|
song->CritSec.Enter();
|
|
|
|
|
|
|
|
size_t len = size_t(ilen);
|
|
|
|
size_t currentpos = song->Decoder->getSampleOffset();
|
|
|
|
size_t framestoread = len / (song->Channels*2);
|
|
|
|
bool err = false;
|
|
|
|
if (!song->m_Looping)
|
|
|
|
{
|
|
|
|
size_t maxpos = song->Decoder->getSampleLength();
|
|
|
|
if (currentpos == maxpos)
|
|
|
|
{
|
|
|
|
memset(buff, 0, len);
|
|
|
|
song->CritSec.Leave();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (currentpos + framestoread > maxpos)
|
|
|
|
{
|
|
|
|
size_t got = song->Decoder->read(buff, (maxpos - currentpos) * song->Channels * 2);
|
|
|
|
memset(buff + got, 0, len - got);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
size_t got = song->Decoder->read(buff, len);
|
|
|
|
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.
|
2017-04-01 17:46:38 +00:00
|
|
|
if (currentpos + framestoread > song->Loop_End)
|
|
|
|
{
|
|
|
|
size_t endblock = (song->Loop_End - currentpos) * song->Channels * 2;
|
2017-07-09 10:28:16 +00:00
|
|
|
size_t endlen = song->Decoder->read(buff, endblock);
|
|
|
|
|
|
|
|
// Even if zero bytes was read give it a chance to start from the beginning
|
|
|
|
buff = buff + endlen;
|
|
|
|
len -= endlen;
|
|
|
|
song->Decoder->seek(song->Loop_Start, false, true);
|
2017-04-25 19:30:11 +00:00
|
|
|
}
|
|
|
|
while (len > 0)
|
|
|
|
{
|
|
|
|
size_t readlen = song->Decoder->read(buff, len);
|
|
|
|
if (readlen == 0)
|
|
|
|
{
|
|
|
|
song->CritSec.Leave();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
buff += readlen;
|
|
|
|
len -= readlen;
|
|
|
|
if (len > 0)
|
|
|
|
{
|
|
|
|
song->Decoder->seek(song->Loop_Start, false, true);
|
|
|
|
}
|
2017-04-01 17:46:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
song->CritSec.Leave();
|
2017-04-25 19:30:11 +00:00
|
|
|
return true;
|
2017-04-01 17:46:38 +00:00
|
|
|
}
|