diff options
author | Jean-Francois Mauguit <jfmauguit@mac.com> | 2024-09-24 09:03:25 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-09-24 09:03:25 -0400 |
commit | bab614c421ed7ae329d26bf028c4a3b1d2450f5a (patch) | |
tree | 12f17f78986871dd2cfb0a56e5e93b545c1ae0d0 /Src/Plugins/Input/in_mp3/DecodeThread.cpp | |
parent | 4bde6044fddf053f31795b9eaccdd2a5a527d21f (diff) | |
parent | 20d28e80a5c861a9d5f449ea911ab75b4f37ad0d (diff) | |
download | winamp-bab614c421ed7ae329d26bf028c4a3b1d2450f5a.tar.gz |
Merge pull request #5 from WinampDesktop/community
Merge to main
Diffstat (limited to 'Src/Plugins/Input/in_mp3/DecodeThread.cpp')
-rw-r--r-- | Src/Plugins/Input/in_mp3/DecodeThread.cpp | 810 |
1 files changed, 810 insertions, 0 deletions
diff --git a/Src/Plugins/Input/in_mp3/DecodeThread.cpp b/Src/Plugins/Input/in_mp3/DecodeThread.cpp new file mode 100644 index 00000000..99654069 --- /dev/null +++ b/Src/Plugins/Input/in_mp3/DecodeThread.cpp @@ -0,0 +1,810 @@ +#include "DecodeThread.h" +#include "giofile.h" +#include "main.h" +#include "pdtimer.h" +#include "mpegutil.h" +#include "../Winamp/wa_ipc.h" +#include "config.h" +#include <shlwapi.h> +#include "adts.h" +#include "adts_vlb.h" +#include <foundation/error.h> + +// {19450308-90D7-4E45-8A9D-DC71E67123E2} +static const GUID adts_aac_guid = +{ 0x19450308, 0x90d7, 0x4e45, { 0x8a, 0x9d, 0xdc, 0x71, 0xe6, 0x71, 0x23, 0xe2 } }; + +// {4192FE3F-E843-445c-8D62-51BE5EE5E68C} +static const GUID adts_mp2_guid = +{ 0x4192fe3f, 0xe843, 0x445c, { 0x8d, 0x62, 0x51, 0xbe, 0x5e, 0xe5, 0xe6, 0x8c } }; + +extern int m_is_stream; +extern bool m_is_stream_seekable; + +// post this to the main window at end of file (after playback as stopped) +#define WM_WA_MPEG_EOF WM_USER+2 + +/* public data */ +int last_decode_pos_ms; +int decode_pos_ms; // current decoding position, in milliseconds. +volatile int seek_needed; // if != -1, it is the point that the decode +// thread should seek to, in ms. +int g_ds; + +size_t g_bits; +int g_sndopened; +int g_bufferstat; +int g_length = -1000; +int g_vis_enabled; +volatile int g_closeaudio = 0; + +CGioFile *g_playing_file=0; +/* private data */ +static size_t g_samplebuf_used; +static int need_prebuffer; +static int g_srate, g_nch, g_br_add, g_br_div, g_avg_vbr_br; +int g_br; + +class EndCutter +{ +public: + EndCutter() : buffer(0), cutSize(0), filledSize(0), preCutSize(0), preCut(0), decoderDelay(0) + {} + ~EndCutter() + { + free(buffer); + } + void SetEndSize(int postSize) + { + postSize -= decoderDelay; + if (postSize < 0) + postSize = 0; + else if (postSize) + { + free(buffer); + buffer = (char *)calloc(postSize, sizeof(char)); + cutSize = postSize; + } + } + + void SetSize(int decoderDelaySize, int preSize, int postSize) + { + decoderDelay = decoderDelaySize; + SetEndSize(postSize); + + preCutSize = preSize; + preCut = preCutSize + decoderDelay; + } + + void Flush(int time_in_ms) + { + if (time_in_ms == 0) // TODO: calculate actual delay if we seek within the encoder delay area + preCut = preCutSize; // reset precut size if we seek to the start + + filledSize = 0; + mod.outMod->Flush(time_in_ms); + } + + void Write(char *out, int outSize) + { + if (!out && (!outSize)) + { + mod.outMod->Write(0, 0); + return ; + } + + // cut pre samples, if necessary + int pre = min(preCut, outSize); + out += pre; + outSize -= pre; + preCut -= pre; + + if (!outSize) + return ; + + int remainingFill = cutSize - filledSize; + int fillWrite = min(outSize - remainingFill, filledSize); // only write fill buffer if we've got enough left to fill it up + + if (fillWrite > 0) + { + mod.outMod->Write((char *)buffer, fillWrite); + if (cutSize - fillWrite) + memmove(buffer, buffer + fillWrite, cutSize - fillWrite); + filledSize -= fillWrite; + + } + remainingFill = cutSize - filledSize; + int outWrite = max(0, outSize - remainingFill); + if (outWrite) + mod.outMod->Write((char *)out, outWrite); + out += outWrite; + outSize -= outWrite; + + if (outSize) + { + memcpy(buffer + filledSize, out, outSize); + filledSize += outSize; + } + + + } + char *buffer; + int cutSize; + int filledSize; + int preCut, preCutSize, decoderDelay; +}; + +class DecodeLoop +{ +public: + DecodeLoop() : decoder(0) + { + isAac = 0; + isEAAC = 0; + + last_bpos = -1; + need_synclight = true; + done = 0; + br = 0; + + g_framesize = 0; + maxlatency = 0; + sampleFrameSize = 0; + memset(&g_samplebuf, 0, sizeof(g_samplebuf)); + } + + ~DecodeLoop() + { + if (decoder) + { + decoder->Close(); + decoder->Release(); + } + decoder=0; + + } + + DWORD Loop(); + DWORD OpenDecoder(); + void Seek(int seekPosition); + void PreBuffer(); + void Decode(); + void Viz(); + void CalculateCodecDelay(); + DWORD OpenOutput(int numChannels, int sampleRate, int bitsPerSample); + void SetupStream(); + + BYTE g_samplebuf[6*3*2*2*1152]; + + int g_framesize; + int isAac; + int isEAAC; + + CGioFile file; + + int maxlatency; + int last_bpos; + bool need_synclight; + int done; // set to TRUE if decoding has finished, 2 if all has been written + size_t br; + + EndCutter endCutter; + int sampleFrameSize; + adts *decoder; +}; + +static int CalcPreBuffer(int buffer_setting, int bitrate) +{ + if (bitrate < 8) + bitrate = 8; + else if (bitrate > 320) + bitrate = 320; + int prebuffer = (buffer_setting * bitrate) / 128; + if (prebuffer > 100) + prebuffer=100; + return prebuffer; +} + +void DecodeLoop::SetupStream() +{ + char buf[1024] = {0}; + int len; + + m_is_stream = file.IsStream(); + + //Wait until we have data... + while (!killDecodeThread && file.Peek(buf, 1024, &len) == NErr_Success && !len) + Sleep(50); + + m_is_stream_seekable = file.IsStreamSeekable(); + char *content_type = file.m_content_type; + if (content_type) + { + if (!_strnicmp(content_type, "misc/ultravox", 13)) + { + switch (file.uvox_last_message) + { + case 0x8001: + case 0x8003: + isEAAC = 1; + isAac = 1; + break; + + case 0x8000: + isAac = 1; + break; + } + } + else if (!_strnicmp(content_type, "audio/aac", 9)) + { + isEAAC = 1; + isAac = 1; + } + else if (!_strnicmp(content_type, "audio/aacp", 10)) + { + isEAAC = 1; + isAac = 1; + } + else if (!_strnicmp(content_type, "audio/apl", 10)) + { + isEAAC = 1; + isAac = 1; + } + } + + // todo: poll until connected to see if we get aac uvox frames or a content-type:aac header +} + +DWORD DecodeLoop::OpenOutput(int numChannels, int sampleRate, int bitsPerSample) +{ + maxlatency = mod.outMod->Open(sampleRate, numChannels, bitsPerSample, -1, -1); + + // maxlatency is the maxium latency between a outMod->Write() call and + // when you hear those samples. In ms. Used primarily by the visualization + // system. + + if (maxlatency < 0) // error opening device + { + PostMessage(mod.hMainWindow, WM_COMMAND, 40047, 0); + return 0; + } + g_sndopened = 1; + if (maxlatency == 0 && file.IsStream() == 2) // can't use with disk writer + { + if (!killDecodeThread) + { + EnterCriticalSection(&g_lfnscs); + WASABI_API_LNGSTRING_BUF(IDS_CANNOT_WRITE_STREAMS_TO_DISK,lastfn_status,256); + LeaveCriticalSection(&g_lfnscs); + PostMessage(mod.hMainWindow, WM_USER, 0, IPC_UPDTITLE); + } + if (!killDecodeThread) Sleep(200); + if (!killDecodeThread) PostMessage(mod.hMainWindow, WM_WA_MPEG_EOF, 0, 0); + g_bufferstat = 0; + g_closeaudio = 1; + + return 0; + } + + if (paused) mod.outMod->Pause(1); + + // set the output plug-ins default volume. + // volume is 0-255, -666 is a token for + // current volume. + + mod.outMod->SetVolume(-666); + return 1; +} + +void DecodeLoop::CalculateCodecDelay() +{ + int decoderDelaySamples = (int)decoder->GetDecoderDelay(); + + endCutter.SetSize(decoderDelaySamples*sampleFrameSize, + file.prepad*sampleFrameSize, + file.postpad*sampleFrameSize); +} + +void DecodeLoop::Viz() +{ + if (!config_fastvis || (decoder->GetLayer() != 3 || g_ds)) + { + int vis_waveNch; + int vis_specNch; + int csa = mod.SAGetMode(); + int is_vis_running = mod.VSAGetMode(&vis_specNch, &vis_waveNch); + if (csa || is_vis_running) + { + int l = 576 * sampleFrameSize; + int ti = decode_pos_ms; + { + if (g_ds == 2) + { + memcpy(g_samplebuf + g_samplebuf_used, g_samplebuf, g_samplebuf_used); + } + size_t pos = 0; + while (pos < g_samplebuf_used) + { + int a, b; + if (mod.SAGetMode()) mod.SAAddPCMData((char *)g_samplebuf + pos, g_nch, (int)g_bits, ti); + if (mod.VSAGetMode(&a, &b)) mod.VSAAddPCMData((char *)g_samplebuf + pos, g_nch, (int)g_bits, ti); + ti += ((l / sampleFrameSize * 1000) / g_srate); + pos += l >> g_ds; + } + } + } + } + else + { + int l = (576 * (int)g_bits * g_nch); + int ti = decode_pos_ms; + size_t pos = 0; + int x = 0; + while (pos < g_samplebuf_used) + { + do_layer3_vis((short*)(g_samplebuf + pos), &g_vis_table[x++][0][0][0], g_nch, ti); + ti += (l / g_nch / 2 * 1000) / g_srate; + pos += l; + } + } +} + +void DecodeLoop::Decode() +{ + while (g_samplebuf_used < (size_t)g_framesize && !killDecodeThread && seek_needed == -1) + { + size_t newl = 0; + size_t br=0; + size_t endCut=0; + int res = decoder->Decode(&file, g_samplebuf + g_samplebuf_used, sizeof(g_samplebuf) / 2 - g_samplebuf_used, &newl, &br, &endCut); + + if (config_gapless && endCut) + endCutter.SetEndSize((int)endCut* sampleFrameSize); + + // we're not using switch here because we sometimes need to break out of the while loop + if (res == adts::SUCCESS) + { + if (!file.m_vbr_frames) + { + if (br) { + bool do_real_br=false; + if (!(config_miscopts&2) && br != decoder->GetCurrentBitrate()) + { + do_real_br=true; + } + + int r = (int)br; + g_br_add += r; + g_br_div++; + r = (g_br_add + g_br_div / 2) / g_br_div; + if (g_br != r) + { + need_synclight = false; + g_br = r; + if (!file.m_vbr_frames && file.IsSeekable()) g_length = MulDiv(file.GetContentLength(), 8, g_br); + if (!do_real_br) + mod.SetInfo(g_br, -1, -1, 1); + } + if (do_real_br) + mod.SetInfo((int)br, -1, -1, 1); + } + } + else + { + if (br) { + int r; + if (!(config_miscopts&2) || !g_avg_vbr_br) + r = (int)br; + else r = g_avg_vbr_br; + if (g_br != r) + { + need_synclight = false; + g_br = r; + mod.SetInfo(g_br, -1, -1, 1); + } + } + } + if (need_synclight) + { + need_synclight = false; + mod.SetInfo(-1, -1, -1, 1); + } + g_samplebuf_used += newl; + } + else if (res == adts::ENDOFFILE) + { + done = 1; + break; + } + else if (res == adts::NEEDMOREDATA) + { + if (file.IsStream() && !need_synclight) + { + need_synclight = true; mod.SetInfo(-1, -1, -1, 0); + } + if (file.IsStream() && !mod.outMod->IsPlaying()) + { + need_prebuffer = CalcPreBuffer(config_http_prebuffer_underrun, (int)br); + } + break; + } + else + { + if (!need_synclight) mod.SetInfo(-1, -1, -1, 0); + need_synclight = true; + break; + } + } +} + +void DecodeLoop::PreBuffer() +{ + int p = file.RunStream(); + int pa = file.PercentAvailable(); + if (pa >= need_prebuffer || p == 2) + { + EnterCriticalSection(&g_lfnscs); + lastfn_status[0] = 0; + LeaveCriticalSection(&g_lfnscs); + PostMessage(mod.hMainWindow, WM_USER, 0, IPC_UPDTITLE); + need_prebuffer = 0; + g_bufferstat = 0; + last_bpos = -1; + } + else + { + int bpos = pa * 100 / need_prebuffer; + if (!g_bufferstat) g_bufferstat = decode_pos_ms; + if (bpos != last_bpos) + { + last_bpos = bpos; + EnterCriticalSection(&g_lfnscs); + if (stricmp(lastfn_status, "stream temporarily interrupted")) + { + char langbuf[512] = {0}; + wsprintfA(lastfn_status, WASABI_API_LNGSTRING_BUF(IDS_BUFFER_X,langbuf,512), bpos); + } + LeaveCriticalSection(&g_lfnscs); + + int csa = mod.SAGetMode(); + char tempdata[75*2] = {0, }; + int x; + if (csa&1) + { + for (x = 0; x < bpos*75 / 100; x ++) + { + tempdata[x] = x * 16 / 75; + } + } + if (csa&2) + { + int offs = (csa & 1) ? 75 : 0; + x = 0; + while (x < bpos*75 / 100) + { + tempdata[offs + x++] = -6 + x * 14 / 75; + } + while (x < 75) + { + tempdata[offs + x++] = 0; + } + } + if (csa == 4) + { + tempdata[0] = tempdata[1] = (bpos * 127 / 100); + } + + if (csa) mod.SAAdd(tempdata, ++g_bufferstat, (csa == 3) ? 0x80000003 : csa); + PostMessage(mod.hMainWindow, WM_USER, 0, IPC_UPDTITLE); + } + } +} + +void DecodeLoop::Seek(int seekPosition) +{ + if (done == 3) + return; + done=0; + int br = (int)decoder->GetCurrentBitrate(); + + need_prebuffer = CalcPreBuffer(config_http_prebuffer_underrun, br); + if (need_prebuffer < 1) need_prebuffer = 5; + + last_decode_pos_ms = decode_pos_ms = seekPosition; + + seek_needed = -1; + endCutter.Flush(decode_pos_ms); + decoder->Flush(&file); + done = 0; + g_samplebuf_used = 0; + + int r = g_br; + if (g_br_div) r = (g_br_add + g_br_div / 2) / g_br_div; + file.Seek(decode_pos_ms, r); + // need_prebuffer=config_http_prebuffer/8; + // g_br_add=g_br_div=0; + +} + +DWORD DecodeLoop::OpenDecoder() +{ + mod.UsesOutputPlug &= ~8; + if (isAac) + { + if (isEAAC) + { + waServiceFactory *factory = mod.service->service_getServiceByGuid(adts_aac_guid); + if (factory) + decoder = (adts *)factory->getInterface(); + + mod.UsesOutputPlug|=8; + } + if (!decoder) + { + decoder = new ADTS_VLB; + mod.UsesOutputPlug &= ~8; + } + } + else + { + waServiceFactory *factory = mod.service->service_getServiceByGuid(adts_mp2_guid); + if (factory) + decoder = (adts *)factory->getInterface(); + + mod.UsesOutputPlug|=8; + } + + if (decoder) { + decoder->SetDecoderHooks(mp3GiveVisData, mp2Equalize, mp3Equalize); + } + + if (decoder + && decoder->Initialize(AGAVE_API_CONFIG->GetBool(playbackConfigGroupGUID, L"mono", false), + config_downmix == 2, + AGAVE_API_CONFIG->GetBool(playbackConfigGroupGUID, L"surround", true), + (int)AGAVE_API_CONFIG->GetUnsigned(playbackConfigGroupGUID, L"bits", 16), true, false, + (config_miscopts&1)/*crc*/) == adts::SUCCESS + && decoder->Open(&file)) + { + // sync to stream + while (1) + { + switch (decoder->Sync(&file, g_samplebuf, sizeof(g_samplebuf), &g_samplebuf_used, &br)) + { + case adts::SUCCESS: + return 1; + case adts::FAILURE: + case adts::ENDOFFILE: + if (!killDecodeThread) + { + if (!lastfn_status_err) + { + EnterCriticalSection(&g_lfnscs); + WASABI_API_LNGSTRING_BUF(IDS_ERROR_SYNCING_TO_STREAM,lastfn_status,256); + LeaveCriticalSection(&g_lfnscs); + PostMessage(mod.hMainWindow, WM_USER, 0, IPC_UPDTITLE); + } + } + if (!killDecodeThread) Sleep(200); + if (!killDecodeThread) PostMessage(mod.hMainWindow, WM_WA_MPEG_EOF, 0, 0); + return 0; + case adts::NEEDMOREDATA: + if (!killDecodeThread && file.IsStream()) Sleep(25); + if (killDecodeThread) return 0; + } + } + } + + return 0; +} + +DWORD DecodeLoop::Loop() +{ + last_decode_pos_ms = 0; + + if (file.Open(lastfn, config_max_bufsize_k) != NErr_Success) + { + if (!killDecodeThread) Sleep(200); + if (!killDecodeThread) PostMessage(mod.hMainWindow, WM_WA_MPEG_EOF, 0, 0); + return 0; + } + + if (file.IsSeekable()) mod.is_seekable = 1; + + wchar_t *ext = PathFindExtension(lastfn); + if (!_wcsicmp(ext, L".aac") + || !_wcsicmp(ext, L".vlb") + || !_wcsicmp(ext, L".apl")) + { + if (file.IsStream()) + SetupStream(); + else + { + isAac = 1; + if (!_wcsicmp(ext, L".aac") || !_wcsicmp(ext, L".apl")) isEAAC = 1; + } + } + else if (file.IsStream()) + SetupStream(); + + if (OpenDecoder() == 0) + return 0; + + EnterCriticalSection(&streamInfoLock); + g_playing_file = &file; + if (file.uvox_3901) + { + PostMessage(mod.hMainWindow, WM_WA_IPC, (WPARAM) "0x3901", IPC_METADATA_CHANGED); + PostMessage(mod.hMainWindow, WM_WA_IPC, 0, IPC_UPDTITLE); + } + LeaveCriticalSection(&streamInfoLock); + + + EnterCriticalSection(&g_lfnscs); + lastfn_status[0] = 0; + LeaveCriticalSection(&g_lfnscs); + + lastfn_data_ready = 1; + +// TODO? if (decoder != &aacp) // hack because aac+ bitrate isn't accurate at this point + br = decoder->GetCurrentBitrate(); + + need_prebuffer = CalcPreBuffer(config_http_prebuffer, (int)br); + + if (((!(config_eqmode&4) && decoder->GetLayer() == 3) || + ((config_eqmode&8) && decoder->GetLayer() < 3))) + { + mod.UsesOutputPlug |= 2; + } + else + mod.UsesOutputPlug &= ~2; + + decoder->CalculateFrameSize(&g_framesize); + decoder->GetOutputParameters(&g_bits, &g_nch, &g_srate); + + if (!killDecodeThread && file.IsStream() == 1) + { + DWORD_PTR dw; + if (!killDecodeThread) SendMessageTimeout(mod.hMainWindow, WM_USER, 0, IPC_UPDTITLE, SMTO_BLOCK, 100, &dw); + if (!killDecodeThread) SendMessageTimeout(mod.hMainWindow, WM_TIMER, 38, 0, SMTO_BLOCK, 100, &dw); + } + + sampleFrameSize = g_nch * ((int)g_bits/8); + + if (config_gapless) + CalculateCodecDelay(); + + if (OpenOutput(g_nch, g_srate, (int)g_bits) == 0) + return 0; + + /* ----- send info to winamp and vis: bitrate, etc ----- */ + g_br = (int)decoder->GetCurrentBitrate(); + + g_br_add = g_br; + g_br_div = 1; + g_avg_vbr_br = file.GetAvgVBRBitrate(); + mod.SetInfo(g_br, g_srate / 1000, g_nch, 0); + + // initialize visualization stuff + mod.SAVSAInit((maxlatency << g_ds), g_srate); + mod.VSASetInfo(g_srate, g_nch); + /* ----- end send info to winamp and vis ----- */ + + if (file.IsSeekable() && g_br) + { + mod.is_seekable = 1; + if (!file.m_vbr_frames) g_length = MulDiv(file.GetContentLength(), 8, g_br); + else g_length = file.m_vbr_ms; + } + + if (file.IsStream()) + { + if (need_prebuffer < config_http_prebuffer / 2) + need_prebuffer = config_http_prebuffer / 2; + } + + while (!killDecodeThread) + { + if (seek_needed != -1) + Seek(seek_needed); + + if (need_prebuffer && file.IsStream() && maxlatency && !file.EndOf()) + PreBuffer(); + + int needsleep = 1; + + if (done == 2) // done was set to TRUE during decoding, signaling eof + { + mod.outMod->CanWrite(); // some output drivers need CanWrite + // to be called on a regular basis. + + if (!mod.outMod->IsPlaying()) + { + // we're done playing, so tell Winamp and quit the thread. + if (!killDecodeThread) PostMessage(mod.hMainWindow, WM_WA_MPEG_EOF, 0, 0); + done=3; + break; + } + } + else + { + int fs = (g_framesize * ((mod.dsp_isactive() == 1) ? 2 : 1)); + // TODO: we should really support partial writes, there's no gaurantee that CanWrite() will EVER get big enough + if (mod.outMod->CanWrite() >= fs && (!need_prebuffer || !file.IsStream() || !maxlatency)) + // CanWrite() returns the number of bytes you can write, so we check that + // to the block size. the reason we multiply the block size by two if + // mod.dsp_isactive() is that DSP plug-ins can change it by up to a + // factor of two (for tempo adjustment). + { + int p = mod.SAGetMode(); + g_vis_enabled = ((p & 1) || p == 4); + if (!g_vis_enabled) + { + int s, a; + mod.VSAGetMode(&s, &a); + if (s) g_vis_enabled = 1; + } + + Decode(); + + if ((g_samplebuf_used >= (size_t)g_framesize || (done && g_samplebuf_used > 0)) && seek_needed == -1) + { + // adjust decode position variable + if (file.isSeekReset()) + last_decode_pos_ms = decode_pos_ms = 0; + else + decode_pos_ms += ((int)g_samplebuf_used / sampleFrameSize * 1000) / g_srate; + + // if we have a DSP plug-in, then call it on our samples + if (mod.dsp_isactive()) + { + g_samplebuf_used = mod.dsp_dosamples((short *)g_samplebuf, (int)g_samplebuf_used / sampleFrameSize, (int)g_bits, g_nch, g_srate) * sampleFrameSize; + } + Viz(); + endCutter.Write((char *)g_samplebuf, (int)g_samplebuf_used); + g_samplebuf_used = 0; + needsleep = 0; + //memcpy(g_samplebuf,g_samplebuf+r,g_samplebuf_used); + } + if (done) + { + endCutter.Write(0, 0); + done = 2; + } + } + } + if (decode_pos_ms > last_decode_pos_ms + 1000) + { + last_decode_pos_ms = decode_pos_ms; + } + + if (needsleep) Sleep(10); + // if we can't write data, wait a little bit. Otherwise, continue + // through the loop writing more data (without sleeping) + } + + /* ---- change some globals to let everyone know we're done */ + EnterCriticalSection(&g_lfnscs); + lastfn_status[0] = 0; + LeaveCriticalSection(&g_lfnscs); + g_bufferstat = 0; + g_closeaudio = 1; + /* ---- */ + + return 0; +} + +DWORD WINAPI DecodeThread(LPVOID b) +{ + DecodeLoop loop; + + + + DWORD ret = loop.Loop(); + + EnterCriticalSection(&streamInfoLock); + g_playing_file = 0; + LeaveCriticalSection(&streamInfoLock); + return ret; +} + |