Merge remote-tracking branch 'origin/miami' into lcs

This commit is contained in:
Sergeanur 2021-06-27 14:33:39 +03:00
commit 883d8172b0
4 changed files with 678 additions and 247 deletions

View File

@ -1,8 +1,6 @@
#include "common.h"
#ifdef AUDIO_OAL
#include "stream.h"
#include "sampman.h"
#if defined _MSC_VER && !defined CMAKE_NO_AUTOLINK
#ifdef AUDIO_OAL_USE_SNDFILE
@ -22,6 +20,28 @@
#include <opusfile.h>
#endif
#include <queue>
#include <utility>
#ifdef MULTITHREADED_AUDIO
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include "MusicManager.h"
#include "stream.h"
std::thread gAudioThread;
std::mutex gAudioThreadQueueMutex;
std::condition_variable gAudioThreadCv;
bool gAudioThreadTerm = false;
std::queue<CStream*> gStreamsToProcess; // values are not unique, we will handle that ourself
#else
#include "stream.h"
#endif
#include "sampman.h"
#ifndef _WIN32
#include "crossplatform.h"
#endif
@ -39,6 +59,10 @@ class CSortStereoBuffer
{
uint16* PcmBuf;
size_t BufSize;
//#ifdef MULTITHREADED_AUDIO
// std::mutex Mutex;
//#endif
public:
CSortStereoBuffer() : PcmBuf(nil), BufSize(0) {}
~CSortStereoBuffer()
@ -65,6 +89,9 @@ public:
void SortStereo(void* buf, size_t size)
{
//#ifdef MULTITHREADED_AUDIO
// std::lock_guard<std::mutex> lock(Mutex);
//#endif
uint16* InBuf = (uint16*)buf;
uint16* OutBuf = GetBuffer(size);
@ -279,6 +306,10 @@ public:
#undef CLOSE_ON_ERROR
}
void FileOpen()
{
}
~CWavFile()
{
Close();
@ -289,6 +320,7 @@ public:
return m_bIsOpen;
}
uint32 GetSampleSize()
{
return sizeof(uint16);
@ -405,6 +437,10 @@ public:
m_pfSound = sf_open(path, SFM_READ, &m_soundInfo);
}
void FileOpen()
{
}
~CSndFile()
{
if ( m_pfSound )
@ -464,8 +500,6 @@ public:
#endif
#ifdef AUDIO_OAL_USE_MPG123
// fuzzy seek eliminates stutter when playing ADF but spams errors a lot (and breaks radio sometimes)
//#define MP3_USE_FUZZY_SEEK
class CMP3File : public IDecoder
{
@ -474,45 +508,58 @@ protected:
bool m_bOpened;
uint32 m_nRate;
uint32 m_nChannels;
const char* m_pPath;
bool m_bFileNotOpenedYet;
CMP3File() :
m_pMH(nil),
m_bOpened(false),
m_nRate(0),
m_bFileNotOpenedYet(false),
m_nChannels(0) {}
public:
CMP3File(const char *path) :
m_pMH(nil),
m_bOpened(false),
m_nRate(0),
m_nChannels(0)
m_nChannels(0),
m_pPath(path),
m_bFileNotOpenedYet(false)
{
m_pMH = mpg123_new(nil, nil);
if ( m_pMH )
{
#ifdef MP3_USE_FUZZY_SEEK
mpg123_param(m_pMH, MPG123_FLAGS, MPG123_FUZZY | MPG123_SEEKBUFFER | MPG123_GAPLESS | MPG123_QUIET, 0.0);
#else
mpg123_param(m_pMH, MPG123_FLAGS, MPG123_SEEKBUFFER | MPG123_GAPLESS, 0.0);
#endif
long rate = 0;
int channels = 0;
int encoding = 0;
m_bOpened = mpg123_open(m_pMH, path) == MPG123_OK
&& mpg123_getformat(m_pMH, &rate, &channels, &encoding) == MPG123_OK;
m_nRate = rate;
m_nChannels = channels;
if ( IsOpened() )
{
mpg123_format_none(m_pMH);
mpg123_format(m_pMH, rate, channels, encoding);
}
m_bOpened = true;
m_bFileNotOpenedYet = true;
// It's possible to move this to audioFileOpsThread(), but effect isn't noticable + probably not compatible with our current cutscene audio handling
#if 1
FileOpen();
#endif
}
}
void FileOpen()
{
if(!m_bFileNotOpenedYet) return;
long rate = 0;
int channels = 0;
int encoding = 0;
m_bOpened = mpg123_open(m_pMH, m_pPath) == MPG123_OK
&& mpg123_getformat(m_pMH, &rate, &channels, &encoding) == MPG123_OK;
m_nRate = rate;
m_nChannels = channels;
if(IsOpened()) {
mpg123_format_none(m_pMH);
mpg123_format(m_pMH, rate, channels, encoding);
}
m_bFileNotOpenedYet = false;
}
~CMP3File()
{
if ( m_pMH )
@ -535,7 +582,7 @@ public:
uint32 GetSampleCount()
{
if ( !IsOpened() ) return 0;
if ( !IsOpened() || m_bFileNotOpenedYet ) return 0;
return mpg123_length(m_pMH);
}
@ -551,19 +598,19 @@ public:
void Seek(uint32 milliseconds)
{
if ( !IsOpened() ) return;
if ( !IsOpened() || m_bFileNotOpenedYet ) return;
mpg123_seek(m_pMH, ms2samples(milliseconds), SEEK_SET);
}
uint32 Tell()
{
if ( !IsOpened() ) return 0;
if ( !IsOpened() || m_bFileNotOpenedYet ) return 0;
return samples2ms(mpg123_tell(m_pMH));
}
uint32 Decode(void *buffer)
{
if ( !IsOpened() ) return 0;
if ( !IsOpened() || m_bFileNotOpenedYet ) return 0;
size_t size;
int err = mpg123_read(m_pMH, (unsigned char *)buffer, GetBufferSize(), &size);
@ -602,29 +649,42 @@ public:
m_pMH = mpg123_new(nil, nil);
if (m_pMH)
{
#ifdef MP3_USE_FUZZY_SEEK
mpg123_param(m_pMH, MPG123_FLAGS, MPG123_FUZZY | MPG123_SEEKBUFFER | MPG123_GAPLESS | MPG123_QUIET, 0.0);
#else
mpg123_param(m_pMH, MPG123_FLAGS, MPG123_SEEKBUFFER | MPG123_GAPLESS, 0.0);
m_bOpened = true;
m_bFileNotOpenedYet = true;
m_pPath = path;
// It's possible to move this to audioFileOpsThread(), but effect isn't noticable + probably not compatible with our current cutscene audio handling
#if 1
FileOpen();
#endif
long rate = 0;
int channels = 0;
int encoding = 0;
FILE* f = fopen(path, "rb");
m_bOpened = mpg123_replace_reader_handle(m_pMH, r_read, r_seek, r_close) == MPG123_OK
&& mpg123_open_handle(m_pMH, f) == MPG123_OK && mpg123_getformat(m_pMH, &rate, &channels, &encoding) == MPG123_OK;
m_nRate = rate;
m_nChannels = channels;
if (IsOpened())
{
mpg123_format_none(m_pMH);
mpg123_format(m_pMH, rate, channels, encoding);
}
}
}
void FileOpen()
{
if(!m_bFileNotOpenedYet) return;
long rate = 0;
int channels = 0;
int encoding = 0;
FILE *f = fopen(m_pPath, "rb");
m_bOpened = f && mpg123_replace_reader_handle(m_pMH, r_read, r_seek, r_close) == MPG123_OK
&& mpg123_open_handle(m_pMH, f) == MPG123_OK && mpg123_getformat(m_pMH, &rate, &channels, &encoding) == MPG123_OK;
m_nRate = rate;
m_nChannels = channels;
if(IsOpened()) {
mpg123_format_none(m_pMH);
mpg123_format(m_pMH, rate, channels, encoding);
}
m_bFileNotOpenedYet = false;
}
};
#endif
@ -744,6 +804,10 @@ public:
m_ppVagBuffers[i] = new uint8[VB_BLOCK_SIZE];
}
void FileOpen()
{
}
~CVbFile()
{
if (m_pFile)
@ -896,6 +960,10 @@ public:
m_bOpened = true;
}
}
void FileOpen()
{
}
~COpusFile()
{
@ -961,11 +1029,169 @@ public:
};
#endif
// For multi-thread: Someone always acquire stream's mutex before entering here
void
CStream::BuffersShouldBeFilled()
{
#ifdef MULTITHREADED_AUDIO
if (MusicManager.m_nMusicMode != MUSICMODE_CUTSCENE) {
std::queue<std::pair<ALuint, ALuint>> tempQueue;
for(int i = 0; i < NUM_STREAMBUFFERS / 2; i++) {
tempQueue.push(std::pair<ALuint, ALuint>(m_alBuffers[i * 2], m_alBuffers[i * 2 + 1]));
}
m_fillBuffers.swap(tempQueue);
FlagAsToBeProcessed();
m_bActive = true; // to allow Update() to queue the filled buffers & play
return;
}
std::queue<std::pair<ALuint, ALuint>>().swap(m_fillBuffers);
#endif
if ( FillBuffers() != 0 )
{
SetPlay(true);
}
}
// returns whether it's queued (not on multi-thread)
bool
CStream::BufferShouldBeFilledAndQueued(std::pair<ALuint, ALuint>* bufs)
{
#ifdef MULTITHREADED_AUDIO
if (MusicManager.m_nMusicMode != MUSICMODE_CUTSCENE)
m_fillBuffers.push(*bufs);
else
#endif
{
ALuint alBuffers[2] = {(*bufs).first, (*bufs).second}; // left - right
if (FillBuffer(alBuffers)) {
alSourceQueueBuffers(m_pAlSources[0], 1, &alBuffers[0]);
alSourceQueueBuffers(m_pAlSources[1], 1, &alBuffers[1]);
return true;
}
}
return false;
}
#ifdef MULTITHREADED_AUDIO
void
CStream::FlagAsToBeProcessed(bool close)
{
if (!close && MusicManager.m_nMusicMode == MUSICMODE_CUTSCENE)
return;
gAudioThreadQueueMutex.lock();
gStreamsToProcess.push(this);
gAudioThreadQueueMutex.unlock();
gAudioThreadCv.notify_one();
}
void audioFileOpsThread()
{
do
{
CStream *stream;
{
// Just a semaphore
std::unique_lock<std::mutex> queueMutex(gAudioThreadQueueMutex);
gAudioThreadCv.wait(queueMutex, [] { return gStreamsToProcess.size() > 0 || gAudioThreadTerm; });
if (gAudioThreadTerm)
return;
if (!gStreamsToProcess.empty()) {
stream = gStreamsToProcess.front();
gStreamsToProcess.pop();
} else
continue;
}
std::unique_lock<std::mutex> lock(stream->m_mutex);
std::pair<ALuint, ALuint> buffers, *lastBufAddr;
bool insertBufsAfterCheck = false;
do {
if (!stream->IsOpened()) {
// We MUST do that here, because we release mutex for m_pSoundFile->Seek() and m_pSoundFile->Decode() since they're costly
if (stream->m_pSoundFile) {
delete stream->m_pSoundFile;
stream->m_pSoundFile = nil;
}
if (stream->m_pBuffer) {
free(stream->m_pBuffer);
stream->m_pBuffer = nil;
}
lock.unlock();
stream->m_closeCv.notify_one();
break;
}
if (stream->m_bReset)
break;
// We gave up this idea for now
/*
stream->m_pSoundFile->FileOpen();
// Deffered allocation, do it now
if (stream->m_pBuffer == nil) {
stream->m_pBuffer = malloc(stream->m_pSoundFile->GetBufferSize());
ASSERT(stream->m_pBuffer != nil);
}
*/
if (stream->m_bDoSeek) {
stream->m_bDoSeek = false;
int pos = stream->m_SeekPos;
lock.unlock();
stream->m_pSoundFile->Seek(pos);
lock.lock();
continue; // let's do the checks again, make sure we didn't miss anything while Seeking
}
if (insertBufsAfterCheck) {
stream->m_queueBuffers.push(buffers);
insertBufsAfterCheck = false;
}
if (!stream->m_fillBuffers.empty()) {
lastBufAddr = &stream->m_fillBuffers.front();
buffers = *lastBufAddr;
lock.unlock();
ALuint alBuffers[2] = {buffers.first, buffers.second}; // left - right
bool filled = stream->FillBuffer(alBuffers);
lock.lock();
// Make sure queue isn't touched after we released mutex
if (!stream->m_fillBuffers.empty() && lastBufAddr == &stream->m_fillBuffers.front()) {
stream->m_fillBuffers.pop();
if (filled)
insertBufsAfterCheck = true; // Also make sure stream's properties aren't changed. So make one more pass, and push it to m_queueBuffers only if it pass checks again.
}
} else
break;
} while (true);
} while(true);
}
#endif
void CStream::Initialise()
{
#ifdef AUDIO_OAL_USE_MPG123
mpg123_init();
#endif
#ifdef MULTITHREADED_AUDIO
gAudioThread = std::thread(audioFileOpsThread);
#endif
}
void CStream::Terminate()
@ -973,14 +1199,27 @@ void CStream::Terminate()
#ifdef AUDIO_OAL_USE_MPG123
mpg123_exit();
#endif
#ifdef MULTITHREADED_AUDIO
gAudioThreadQueueMutex.lock();
gAudioThreadTerm = true;
gAudioThreadQueueMutex.unlock();
gAudioThreadCv.notify_one();
gAudioThread.join();
#endif
}
CStream::CStream(char *filename, ALuint *sources, ALuint (&buffers)[NUM_STREAMBUFFERS], uint32 overrideSampleRate) :
CStream::CStream(ALuint *sources, ALuint (&buffers)[NUM_STREAMBUFFERS]) :
m_pAlSources(sources),
m_alBuffers(buffers),
m_pBuffer(nil),
m_bPaused(false),
m_bActive(false),
#ifdef MULTITHREADED_AUDIO
m_bIExist(false),
m_bDoSeek(false),
m_SeekPos(0),
#endif
m_pSoundFile(nil),
m_bReset(false),
m_nVolume(0),
@ -989,6 +1228,31 @@ CStream::CStream(char *filename, ALuint *sources, ALuint (&buffers)[NUM_STREAMBU
m_nLoopCount(1)
{
}
bool CStream::Open(const char* filename, uint32 overrideSampleRate)
{
if (IsOpened()) return false;
#ifdef MULTITHREADED_AUDIO
std::unique_lock<std::mutex> lock(m_mutex);
CStream *stream = this;
// Wait for thread to close old one. We can't close it here, because the thread might be running Decode() or Seek(), while mutex is released
m_closeCv.wait(lock, [this] { return m_pSoundFile == nil && m_pBuffer == nil; });
m_bDoSeek = false;
m_SeekPos = 0;
#endif
m_bPaused = false;
m_bActive = false;
m_bReset = false;
m_nVolume = 0;
m_nPan = 0;
m_nPosBeforeReset = 0;
m_nLoopCount = 1;
// Be case-insensitive on linux (from https://github.com/OneSadCookie/fcaseopen/)
#if !defined(_WIN32)
char *real = casepath(filename);
@ -1025,44 +1289,66 @@ CStream::CStream(char *filename, ALuint *sources, ALuint (&buffers)[NUM_STREAMBU
else
m_pSoundFile = nil;
if ( IsOpened() )
if ( m_pSoundFile && m_pSoundFile->IsOpened() )
{
m_pBuffer = malloc(m_pSoundFile->GetBufferSize());
ASSERT(m_pBuffer!=nil);
DEV("AvgSamplesPerSec: %d\n", m_pSoundFile->GetAvgSamplesPerSec());
DEV("SampleCount: %d\n", m_pSoundFile->GetSampleCount());
DEV("SampleRate: %d\n", m_pSoundFile->GetSampleRate());
DEV("Channels: %d\n", m_pSoundFile->GetChannels());
DEV("Buffer Samples: %d\n", m_pSoundFile->GetBufferSamples());
DEV("Buffer sec: %f\n", (float(m_pSoundFile->GetBufferSamples()) / float(m_pSoundFile->GetChannels())/ float(m_pSoundFile->GetSampleRate())));
DEV("Length MS: %02d:%02d\n", (m_pSoundFile->GetLength() / 1000) / 60, (m_pSoundFile->GetLength() / 1000) % 60);
return;
uint32 bufSize = m_pSoundFile->GetBufferSize();
if(bufSize != 0) { // Otherwise it's deferred
m_pBuffer = malloc(bufSize);
ASSERT(m_pBuffer != nil);
DEV("AvgSamplesPerSec: %d\n", m_pSoundFile->GetAvgSamplesPerSec());
DEV("SampleCount: %d\n", m_pSoundFile->GetSampleCount());
DEV("SampleRate: %d\n", m_pSoundFile->GetSampleRate());
DEV("Channels: %d\n", m_pSoundFile->GetChannels());
DEV("Buffer Samples: %d\n", m_pSoundFile->GetBufferSamples());
DEV("Buffer sec: %f\n", (float(m_pSoundFile->GetBufferSamples()) / float(m_pSoundFile->GetChannels())/ float(m_pSoundFile->GetSampleRate())));
DEV("Length MS: %02d:%02d\n", (m_pSoundFile->GetLength() / 1000) / 60, (m_pSoundFile->GetLength() / 1000) % 60);
}
#ifdef MULTITHREADED_AUDIO
m_bIExist = true;
#endif
return true;
}
return false;
}
CStream::~CStream()
{
Delete();
assert(!IsOpened());
}
void CStream::Delete()
void CStream::Close()
{
if(!IsOpened()) return;
#ifdef MULTITHREADED_AUDIO
{
std::lock_guard<std::mutex> lock(m_mutex);
Stop();
ClearBuffers();
m_bIExist = false;
// clearing buffer queues are not needed. after m_bIExist is cleared, this stream is ded
}
FlagAsToBeProcessed(true);
#else
Stop();
ClearBuffers();
if ( m_pSoundFile )
{
delete m_pSoundFile;
m_pSoundFile = nil;
}
if ( m_pBuffer )
{
free(m_pBuffer);
m_pBuffer = nil;
}
#endif
}
bool CStream::HasSource()
@ -1070,9 +1356,14 @@ bool CStream::HasSource()
return (m_pAlSources[0] != AL_NONE) && (m_pAlSources[1] != AL_NONE);
}
// m_bIExist only written in main thread, thus mutex is not needed on main thread
bool CStream::IsOpened()
{
#ifdef MULTITHREADED_AUDIO
return m_bIExist;
#else
return m_pSoundFile && m_pSoundFile->IsOpened();
#endif
}
bool CStream::IsPlaying()
@ -1086,6 +1377,14 @@ bool CStream::IsPlaying()
alGetSourcei(m_pAlSources[1], AL_SOURCE_STATE, &sourceState[1]);
if (sourceState[0] == AL_PLAYING || sourceState[1] == AL_PLAYING)
return true;
#ifdef MULTITHREADED_AUDIO
std::lock_guard<std::mutex> lock(m_mutex);
// Streams are designed in such a way that m_fillBuffers and m_queueBuffers will be *always* filled if audio is playing, and mutex is acquired
if (!m_fillBuffers.empty() || !m_queueBuffers.emptyNts())
return true;
#endif
}
return false;
@ -1160,8 +1459,24 @@ void CStream::SetPan(uint8 nPan)
void CStream::SetPosMS(uint32 nPos)
{
if ( !IsOpened() ) return;
m_pSoundFile->Seek(nPos);
#ifdef MULTITHREADED_AUDIO
std::lock_guard<std::mutex> lock(m_mutex);
std::queue<std::pair<ALuint, ALuint>>().swap(m_fillBuffers);
tsQueue<std::pair<ALuint, ALuint>>().swapNts(m_queueBuffers); // TSness not required, second thread always access it when stream mutex acquired
if (MusicManager.m_nMusicMode != MUSICMODE_CUTSCENE) {
m_bDoSeek = true;
m_SeekPos = nPos;
} else
#endif
{
m_pSoundFile->Seek(nPos);
}
ClearBuffers();
// adding to gStreamsToProcess not needed, someone always calls Start() / BuffersShouldBeFilled() after SetPosMS
}
uint32 CStream::GetPosMS()
@ -1169,10 +1484,16 @@ uint32 CStream::GetPosMS()
if ( !HasSource() ) return 0;
if ( !IsOpened() ) return 0;
// Deferred init causes division by zero
if (m_pSoundFile->GetChannels() == 0)
return 0;
ALint offset;
//alGetSourcei(m_alSource, AL_SAMPLE_OFFSET, &offset);
alGetSourcei(m_pAlSources[0], AL_BYTE_OFFSET, &offset);
//std::lock_guard<std::mutex> lock(m_mutex);
return m_pSoundFile->Tell()
- m_pSoundFile->samples2ms(m_pSoundFile->GetBufferSamples() * (NUM_STREAMBUFFERS/2-1)) / m_pSoundFile->GetChannels()
+ m_pSoundFile->samples2ms(offset/m_pSoundFile->GetSampleSize()) / m_pSoundFile->GetChannels();
@ -1186,6 +1507,7 @@ uint32 CStream::GetLengthMS()
bool CStream::FillBuffer(ALuint *alBuffer)
{
#ifndef MULTITHREADED_AUDIO
if ( !HasSource() )
return false;
if ( !IsOpened() )
@ -1194,13 +1516,14 @@ bool CStream::FillBuffer(ALuint *alBuffer)
return false;
if ( !(alBuffer[1] != AL_NONE && alIsBuffer(alBuffer[1])) )
return false;
#endif
uint32 size = m_pSoundFile->Decode(m_pBuffer);
if( size == 0 )
return false;
uint32 channelSize = size / m_pSoundFile->GetChannels();
uint32 channelSize = size / m_pSoundFile->GetChannels();
alBufferData(alBuffer[0], AL_FORMAT_MONO16, m_pBuffer, channelSize, m_pSoundFile->GetSampleRate());
// TODO: use just one buffer if we play mono
if (m_pSoundFile->GetChannels() == 1)
@ -1210,6 +1533,26 @@ bool CStream::FillBuffer(ALuint *alBuffer)
return true;
}
#ifdef MULTITHREADED_AUDIO
bool CStream::QueueBuffers()
{
bool buffersQueued = false;
std::pair<ALuint, ALuint> buffers;
while (m_queueBuffers.peekPop(&buffers)) // beware: m_queueBuffers is tsQueue
{
ALuint leftBuf = buffers.first;
ALuint rightBuf = buffers.second;
alSourceQueueBuffers(m_pAlSources[0], 1, &leftBuf);
alSourceQueueBuffers(m_pAlSources[1], 1, &rightBuf);
buffersQueued = true;
}
return buffersQueued;
}
#endif
// Only used in single-threaded audio or cutscene audio
int32 CStream::FillBuffers()
{
int32 i = 0;
@ -1239,17 +1582,33 @@ void CStream::ClearBuffers()
alSourceUnqueueBuffers(m_pAlSources[1], 1, &value);
}
bool CStream::Setup(bool imSureQueueIsEmpty)
bool CStream::Setup(bool imSureQueueIsEmpty, bool lock)
{
if ( IsOpened() )
{
alSourcei(m_pAlSources[0], AL_LOOPING, AL_FALSE);
alSourcei(m_pAlSources[1], AL_LOOPING, AL_FALSE);
#ifdef MULTITHREADED_AUDIO
if (lock)
m_mutex.lock();
#endif
if (!imSureQueueIsEmpty) {
SetPlay(false);
Stop();
ClearBuffers();
}
#ifdef MULTITHREADED_AUDIO
if (MusicManager.m_nMusicMode == MUSICMODE_CUTSCENE) {
m_pSoundFile->Seek(0);
} else {
m_bDoSeek = true;
m_SeekPos = 0;
}
if (lock)
m_mutex.unlock();
#else
m_pSoundFile->Seek(0);
#endif
//SetPosition(0.0f, 0.0f, 0.0f);
SetPitch(1.0f);
//SetPan(m_nPan);
@ -1302,8 +1661,12 @@ void CStream::SetPlay(bool state)
void CStream::Start()
{
if ( !HasSource() ) return;
if ( FillBuffers() != 0 )
SetPlay(true);
#ifdef MULTITHREADED_AUDIO
std::lock_guard<std::mutex> lock(m_mutex);
tsQueue<std::pair<ALuint, ALuint>>().swapNts(m_queueBuffers); // TSness not required, second thread always access it when stream mutex acquired
#endif
BuffersShouldBeFilled();
}
void CStream::Stop()
@ -1325,9 +1688,23 @@ void CStream::Update()
if ( !m_bPaused )
{
ALint totalBuffers[2] = { 0, 0 };
ALint buffersProcessed[2] = { 0, 0 };
bool buffersQueuedAndStarted = false;
bool buffersQueuedButNotStarted = false;
#ifdef MULTITHREADED_AUDIO
// Put it in here because we need totalBuffers after queueing to decide when to loop audio
if (m_bActive)
{
buffersQueuedAndStarted = QueueBuffers();
if(buffersQueuedAndStarted) {
SetPlay(true);
}
}
#endif
ALint totalBuffers[2] = {0, 0};
ALint buffersProcessed[2] = {0, 0};
// Relying a lot on left buffer states in here
do
@ -1339,44 +1716,66 @@ void CStream::Update()
alGetSourcei(m_pAlSources[1], AL_BUFFERS_QUEUED, &totalBuffers[1]);
alGetSourcei(m_pAlSources[1], AL_BUFFERS_PROCESSED, &buffersProcessed[1]);
} while (buffersProcessed[0] != buffersProcessed[1]);
assert(buffersProcessed[0] == buffersProcessed[1]);
// Correcting OpenAL concepts here:
// AL_BUFFERS_QUEUED = Number of *all* buffers in queue, including processed, processing and pending
// AL_BUFFERS_PROCESSED = Index of the buffer being processing right now. Buffers coming after that(have greater index) are pending buffers.
// which means: totalBuffers[0] - buffersProcessed[0] = pending buffers
bool buffersRefilled = false;
// We should wait queue to be cleared to loop track, because position calculation relies on queue.
if (m_nLoopCount != 1 && m_bActive && totalBuffers[0] == 0)
{
Setup(true);
buffersRefilled = FillBuffers() != 0;
if (m_nLoopCount != 0)
m_nLoopCount--;
#ifdef MULTITHREADED_AUDIO
std::lock_guard<std::mutex> lock(m_mutex);
if (m_fillBuffers.empty() && m_queueBuffers.emptyNts()) // we already acquired stream mutex, which is enough for second thread. thus Nts variant
#endif
{
Setup(true, false);
BuffersShouldBeFilled(); // will also call SetPlay(true)
if (m_nLoopCount != 0)
m_nLoopCount--;
}
}
else
{
while( buffersProcessed[0]-- )
static std::queue<std::pair<ALuint, ALuint>> tempFillBuffer;
while ( buffersProcessed[0]-- )
{
ALuint buffer[2];
alSourceUnqueueBuffers(m_pAlSources[0], 1, &buffer[0]);
alSourceUnqueueBuffers(m_pAlSources[1], 1, &buffer[1]);
if (m_bActive && FillBuffer(buffer))
if (m_bActive)
{
buffersRefilled = true;
alSourceQueueBuffers(m_pAlSources[0], 1, &buffer[0]);
alSourceQueueBuffers(m_pAlSources[1], 1, &buffer[1]);
tempFillBuffer.push(std::pair<ALuint, ALuint>(buffer[0], buffer[1]));
}
}
if (m_bActive && buffersProcessed[1])
{
#ifdef MULTITHREADED_AUDIO
m_mutex.lock();
#endif
while (!tempFillBuffer.empty()) {
auto elem = tempFillBuffer.front();
tempFillBuffer.pop();
buffersQueuedButNotStarted = BufferShouldBeFilledAndQueued(&elem);
}
#ifdef MULTITHREADED_AUDIO
m_mutex.unlock();
FlagAsToBeProcessed();
#endif
}
}
// Two reasons: 1-Source may be starved to audio and stopped itself, 2- We're already waiting it to starve and die for looping track!
if (m_bActive && (buffersRefilled || (totalBuffers[1] - buffersProcessed[1] != 0)))
// Source may be starved to audio and stopped itself
if (m_bActive && !buffersQueuedAndStarted && (buffersQueuedButNotStarted || (totalBuffers[1] - buffersProcessed[1] != 0)))
SetPlay(true);
}
}
@ -1385,28 +1784,45 @@ void CStream::ProviderInit()
{
if ( m_bReset )
{
if ( Setup(true) )
if ( Setup(true, false) ) // lock not needed, thread can't process streams with m_bReset set
{
SetPan(m_nPan);
SetVolume(m_nVolume);
SetLoopCount(m_nLoopCount);
SetPosMS(m_nPosBeforeReset);
if (m_bActive)
FillBuffers();
SetPlay(m_bActive);
if ( m_bPaused )
#ifdef MULTITHREADED_AUDIO
std::unique_lock<std::mutex> lock(m_mutex);
#endif
if(m_bActive)
BuffersShouldBeFilled();
if (m_bPaused)
Pause();
m_bReset = false;
} else {
#ifdef MULTITHREADED_AUDIO
std::unique_lock<std::mutex> lock(m_mutex);
#endif
m_bReset = false;
}
m_bReset = false;
}
}
void CStream::ProviderTerm()
{
#ifdef MULTITHREADED_AUDIO
std::lock_guard<std::mutex> lock(m_mutex);
// unlike Close() we will reuse this stream, so clearing queues are important.
std::queue<std::pair<ALuint, ALuint>>().swap(m_fillBuffers);
tsQueue<std::pair<ALuint, ALuint>>().swapNts(m_queueBuffers); // stream mutex is already acquired, thus Nts variant
#endif
m_bReset = true;
m_nPosBeforeReset = GetPosMS();
Stop();
ClearBuffers();
}

View File

@ -11,6 +11,7 @@ public:
virtual ~IDecoder() { }
virtual bool IsOpened() = 0;
virtual void FileOpen() = 0;
virtual uint32 GetSampleSize() = 0;
virtual uint32 GetSampleCount() = 0;
@ -48,12 +49,70 @@ public:
uint32 GetLength()
{
FileOpen(); // abort deferred init, we need length now - game has to cache audio file sizes
return float(GetSampleCount()) * 1000.0f / float(GetSampleRate());
}
virtual uint32 Decode(void *buffer) = 0;
};
#ifdef MULTITHREADED_AUDIO
template <typename T> class tsQueue
{
public:
tsQueue() : count(0) { }
void push(const T &value)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_queue.push(value);
count++;
}
bool peekPop(T *retVal)
{
std::lock_guard<std::mutex> lock(m_mutex);
if (count == 0)
return false;
*retVal = m_queue.front();
m_queue.pop();
count--;
return true;
}
void swapNts(tsQueue<T> &replaceWith)
{
m_queue.swap(replaceWith.m_queue);
replaceWith.count = count;
}
/*
void swapTs(tsQueue<T> &replaceWith)
{
std::lock_guard<std::mutex> lock(m_mutex);
std::lock_guard<std::mutex> lock2(replaceWith.m_mutex);
swapNts(replaceWith);
}
*/
bool emptyNts()
{
return count == 0;
}
/*
bool emptyTs()
{
std::lock_guard<std::mutex> lock(m_mutex);
return emptyNts();
}
*/
std::queue<T> m_queue;
int count;
mutable std::mutex m_mutex;
};
#endif
class CStream
{
char m_aFilename[128];
@ -63,6 +122,17 @@ class CStream
bool m_bPaused;
bool m_bActive;
public:
#ifdef MULTITHREADED_AUDIO
std::mutex m_mutex;
std::queue<std::pair<ALuint, ALuint>> m_fillBuffers; // left and right buffer
tsQueue<std::pair<ALuint, ALuint>> m_queueBuffers;
std::condition_variable m_closeCv;
bool m_bDoSeek;
uint32 m_SeekPos;
bool m_bIExist;
#endif
void *m_pBuffer;
bool m_bReset;
@ -72,7 +142,14 @@ class CStream
int32 m_nLoopCount;
IDecoder *m_pSoundFile;
void BuffersShouldBeFilled(); // all
bool BufferShouldBeFilledAndQueued(std::pair<ALuint, ALuint>*); // two (left-right)
#ifdef MULTITHREADED_AUDIO
void FlagAsToBeProcessed(bool close = false);
bool QueueBuffers();
#endif
bool HasSource();
void SetPosition(int i, float x, float y, float z);
void SetPitch(float pitch);
@ -81,15 +158,17 @@ class CStream
void SetPlay(bool state);
bool FillBuffer(ALuint *alBuffer);
int32 FillBuffers();
int32 FillBuffers();
void ClearBuffers();
public:
//public:
static void Initialise();
static void Terminate();
CStream(char *filename, ALuint *sources, ALuint (&buffers)[NUM_STREAMBUFFERS], uint32 overrideSampleRate = 32000);
CStream(ALuint *sources, ALuint (&buffers)[NUM_STREAMBUFFERS]);
~CStream();
void Delete();
bool Open(const char *filename, uint32 overrideSampleRate = 32000);
void Close();
bool IsOpened();
bool IsPlaying();
@ -100,12 +179,11 @@ public:
uint32 GetPosMS();
uint32 GetLengthMS();
bool Setup(bool imSureQueueIsEmpty = false);
bool Setup(bool imSureQueueIsEmpty = false, bool lock = true);
void Start();
void Stop();
void Update(void);
void SetLoopCount(int32);
void ProviderInit();
void ProviderTerm();

View File

@ -34,6 +34,13 @@
#include "oal/oal_utils.h"
#include "oal/aldlist.h"
#include "oal/channel.h"
#include <utility>
#ifdef MULTITHREADED_AUDIO
#include <mutex>
#include <queue>
#include <condition_variable>
#endif
#include "oal/stream.h"
#include "AudioManager.h"
@ -590,13 +597,10 @@ _FindMP3s(void)
} else
bShortcut = FALSE;
aStream[0] = new CStream(filepath, ALStreamSources[0], ALStreamBuffers[0]);
if (aStream[0] && aStream[0]->IsOpened())
if (aStream[0] && aStream[0]->Open(filepath))
{
total_ms = aStream[0]->GetLengthMS();
delete aStream[0];
aStream[0] = NULL;
aStream[0]->Close();
OutputDebugString(fd.cFileName);
@ -664,13 +668,10 @@ _FindMP3s(void)
continue;
}
}
aStream[0] = new CStream(filepath, ALStreamSources[0], ALStreamBuffers[0]);
if (aStream[0] && aStream[0]->IsOpened())
if (aStream[0] && aStream[0]->Open(filepath))
{
total_ms = aStream[0]->GetLengthMS();
delete aStream[0];
aStream[0] = NULL;
aStream[0]->Close();
OutputDebugString(fd.cFileName);
@ -724,13 +725,10 @@ _FindMP3s(void)
} else
bShortcut = FALSE;
aStream[0] = new CStream(filepath, ALStreamSources[0], ALStreamBuffers[0]);
if (aStream[0] && aStream[0]->IsOpened())
if (aStream[0] && aStream[0]->Open(filepath))
{
total_ms = aStream[0]->GetLengthMS();
delete aStream[0];
aStream[0] = NULL;
aStream[0]->Close();
OutputDebugString(fd.cFileName);
@ -885,6 +883,10 @@ cSampleManager::Initialise(void)
return TRUE;
SetUpDebugBanksInfo();
EFXInit();
for(int i = 0; i < MAX_STREAMS; i++)
aStream[i] = new CStream(ALStreamSources[i], ALStreamBuffers[i]);
CStream::Initialise();
{
@ -1040,31 +1042,24 @@ cSampleManager::Initialise(void)
for ( int32 i = 0; i < TOTAL_STREAMED_SOUNDS; i++ )
{
bool opened = false;
char filename[MAX_PATH];
sprintf(filename, "%s.VB", StreamedNameTable[i]);
aStream[0] = new CStream(filename, ALStreamSources[0], ALStreamBuffers[0], IsThisTrackAt16KHz(i) ? 16000 : 32000);
if ( aStream[0] && !aStream[0]->IsOpened() )
{
delete aStream[0];
aStream[0] = NULL;
}
if ( !aStream[0] )
if ( aStream[0] )
opened = aStream[0]->Open(filename, IsThisTrackAt16KHz(i) ? 16000 : 32000) )
if ( !opened )
{
sprintf(filename, "%s.MP3", StreamedNameTable[i]);
aStream[0] = new CStream(filename, ALStreamSources[0], ALStreamBuffers[0], IsThisTrackAt16KHz(i) ? 16000 : 32000);
if ( aStream[0] && !aStream[0]->IsOpened() )
if ( aStream[0] )
{
delete aStream[0];
aStream[0] = NULL;
opened = aStream[0]->Open(filename, IsThisTrackAt16KHz(i) ? 16000 : 32000)
}
}
if ( aStream[0] && aStream[0]->IsOpened() )
if ( opened )
{
uint32 tatalms = aStream[0]->GetLengthMS();
delete aStream[0];
aStream[0] = NULL;
aStream[0]->Close();
nStreamLength[i] = tatalms;
}
@ -1108,12 +1103,13 @@ cSampleManager::Initialise(void)
{
for ( int32 i = 0; i < MAX_STREAMS; i++ )
{
aStream[i] = NULL;
aStream[i]->Close();
nStreamVolume[i] = 100;
nStreamPan[i] = 63;
}
}
{
_bSampmanInitialised = TRUE;
@ -1195,14 +1191,7 @@ void
cSampleManager::Terminate(void)
{
for (int32 i = 0; i < MAX_STREAMS; i++)
{
CStream *stream = aStream[i];
if (stream)
{
delete stream;
aStream[i] = NULL;
}
}
aStream[i]->Close();
for ( int32 i = 0; i < NUM_CHANNELS; i++ )
aChannel[i].Term();
@ -1253,6 +1242,9 @@ cSampleManager::Terminate(void)
CStream::Terminate();
for(int32 i = 0; i < MAX_STREAMS; i++)
delete aStream[i];
if ( nSampleBankMemoryStartAddress[SFX_BANK_0] != 0 )
{
free((void *)nSampleBankMemoryStartAddress[SFX_BANK_0]);
@ -1778,40 +1770,20 @@ cSampleManager::PreloadStreamedFile(uint32 nFile, uint8 nStream)
if ( nFile < TOTAL_STREAMED_SOUNDS )
{
if ( aStream[nStream] )
{
delete aStream[nStream];
aStream[nStream] = NULL;
}
CStream *stream = aStream[nStream];
stream->Close();
sprintf(filename, "%s.VB", StreamedNameTable[nFile]);
CStream *stream = new CStream(filename, ALStreamSources[nStream], ALStreamBuffers[nStream], IsThisTrackAt16KHz(nFile) ? 16000 : 32000);
if ( stream && !stream->IsOpened() )
{
delete stream;
stream = NULL;
}
if (!stream)
bool opened = stream->Open(filename, IsThisTrackAt16KHz(nFile) ? 16000 : 32000);
if ( !opened )
{
sprintf(filename, "%s.MP3", StreamedNameTable[nFile]);
stream = new CStream(filename, ALStreamSources[nStream], ALStreamBuffers[nStream], IsThisTrackAt16KHz(nFile) ? 16000 : 32000);
if ( stream && !stream->IsOpened() )
{
delete stream;
stream = NULL;
}
opened = stream->Open(filename, IsThisTrackAt16KHz(nFile) ? 16000 : 32000);
}
ASSERT(stream != NULL);
aStream[nStream] = stream;
if ( !stream->Setup() )
if ( opened && !stream->Setup() )
{
delete stream;
aStream[nStream] = NULL;
stream->Close();
}
}
}
@ -1823,7 +1795,7 @@ cSampleManager::PauseStream(bool8 nPauseFlag, uint8 nStream)
CStream *stream = aStream[nStream];
if ( stream )
if ( stream->IsOpened() )
{
stream->SetPause(nPauseFlag != FALSE);
}
@ -1836,12 +1808,9 @@ cSampleManager::StartPreloadedStreamedFile(uint8 nStream)
CStream *stream = aStream[nStream];
if ( stream )
if ( stream->IsOpened() )
{
if ( stream->IsOpened() )
{
stream->Start();
}
stream->Start();
}
}
@ -1855,11 +1824,8 @@ cSampleManager::StartStreamedFile(uint32 nFile, uint32 nPos, uint8 nStream)
if ( nFile >= TOTAL_STREAMED_SOUNDS )
return FALSE;
if ( aStream[nStream] )
{
delete aStream[nStream];
aStream[nStream] = NULL;
}
aStream[nStream]->Close();
if ( nFile == STREAMED_SOUND_RADIO_MP3_PLAYER )
{
do
@ -1876,27 +1842,17 @@ cSampleManager::StartStreamedFile(uint32 nFile, uint32 nPos, uint8 nStream)
nFile = 0;
sprintf(filename, "%s.VB", StreamedNameTable[nFile]);
CStream* stream = new CStream(filename, ALStreamSources[nStream], ALStreamBuffers[nStream], IsThisTrackAt16KHz(nFile) ? 16000 : 32000);
if ( stream && !stream->IsOpened() )
{
delete stream;
stream = NULL;
}
if (!stream)
CStream *stream = aStream[nStream];
bool opened = stream->Open(filename, IsThisTrackAt16KHz(nFile) ? 16000 : 32000);
if ( !opened )
{
sprintf(filename, "%s.MP3", StreamedNameTable[nFile]);
stream = new CStream(filename, ALStreamSources[nStream], ALStreamBuffers[nStream], IsThisTrackAt16KHz(nFile) ? 16000 : 32000);
if ( stream && !stream->IsOpened() )
{
delete stream;
stream = NULL;
}
opened = stream->Open(filename, IsThisTrackAt16KHz(nFile) ? 16000 : 32000);
}
aStream[nStream] = stream;
if (stream->Setup()) {
if ( opened && stream->Setup() ) {
stream->SetLoopCount(nStreamLoopedFlag[nStream] ? 0 : 1);
nStreamLoopedFlag[nStream] = TRUE;
if (position != 0)
@ -1906,20 +1862,19 @@ cSampleManager::StartStreamedFile(uint32 nFile, uint32 nPos, uint8 nStream)
return TRUE;
} else {
delete stream;
aStream[nStream] = NULL;
stream->Close();
}
return FALSE;
} else {
if (e->pLinkPath != NULL)
aStream[nStream] = new CStream(e->pLinkPath, ALStreamSources[nStream], ALStreamBuffers[nStream], IsThisTrackAt16KHz(nFile) ? 16000 : 32000);
aStream[nStream]->Open(e->pLinkPath, IsThisTrackAt16KHz(nFile) ? 16000 : 32000);
else {
strcpy(filename, _mp3DirectoryPath);
strcat(filename, e->aFilename);
aStream[nStream] = new CStream(filename, ALStreamSources[nStream], ALStreamBuffers[nStream]);
aStream[nStream]->Open(filename);
}
if (aStream[nStream]->Setup()) {
@ -1931,8 +1886,7 @@ cSampleManager::StartStreamedFile(uint32 nFile, uint32 nPos, uint8 nStream)
_bIsMp3Active = TRUE;
return TRUE;
} else {
delete aStream[nStream];
aStream[nStream] = NULL;
aStream[nStream]->Close();
}
// fall through, start playing from another song
}
@ -1951,28 +1905,16 @@ cSampleManager::StartStreamedFile(uint32 nFile, uint32 nPos, uint8 nStream)
_bIsMp3Active = 0;
sprintf(filename, "%s.VB", StreamedNameTable[nFile]);
CStream* stream = new CStream(filename, ALStreamSources[nStream], ALStreamBuffers[nStream], IsThisTrackAt16KHz(nFile) ? 16000 : 32000);
if ( stream && !stream->IsOpened() )
{
delete stream;
stream = NULL;
}
CStream* stream = aStream[nStream];
bool opened = stream->Open(filename, IsThisTrackAt16KHz(nFile) ? 16000 : 32000);
if (!stream)
if ( !opened )
{
sprintf(filename, "%s.MP3", StreamedNameTable[nFile]);
stream = new CStream(filename, ALStreamSources[nStream], ALStreamBuffers[nStream], IsThisTrackAt16KHz(nFile) ? 16000 : 32000);
if ( stream && !stream->IsOpened() )
{
delete stream;
stream = NULL;
}
opened = stream->Open(filename, IsThisTrackAt16KHz(nFile) ? 16000 : 32000);
}
aStream[nStream] = stream;
if (stream->Setup()) {
if (opened && stream->Setup()) {
stream->SetLoopCount(nStreamLoopedFlag[nStream] ? 0 : 1);
nStreamLoopedFlag[nStream] = TRUE;
if (position != 0)
@ -1982,19 +1924,18 @@ cSampleManager::StartStreamedFile(uint32 nFile, uint32 nPos, uint8 nStream)
return TRUE;
} else {
delete stream;
aStream[nStream] = NULL;
stream->Close();
}
return FALSE;
}
}
if (mp3->pLinkPath != NULL)
aStream[nStream] = new CStream(mp3->pLinkPath, ALStreamSources[nStream], ALStreamBuffers[nStream], IsThisTrackAt16KHz(nFile) ? 16000 : 32000);
aStream[nStream]->Open(mp3->pLinkPath, IsThisTrackAt16KHz(nFile) ? 16000 : 32000);
else {
strcpy(filename, _mp3DirectoryPath);
strcat(filename, mp3->aFilename);
aStream[nStream] = new CStream(filename, ALStreamSources[nStream], ALStreamBuffers[nStream], IsThisTrackAt16KHz(nFile) ? 16000 : 32000);
aStream[nStream]->Open(filename, IsThisTrackAt16KHz(nFile) ? 16000 : 32000);
}
if (aStream[nStream]->Setup()) {
@ -2004,8 +1945,7 @@ cSampleManager::StartStreamedFile(uint32 nFile, uint32 nPos, uint8 nStream)
#endif
return TRUE;
} else {
delete aStream[nStream];
aStream[nStream] = NULL;
aStream[nStream]->Close();
}
}
@ -2017,7 +1957,7 @@ cSampleManager::StartStreamedFile(uint32 nFile, uint32 nPos, uint8 nStream)
}
sprintf(filename, "%s.VB", StreamedNameTable[nFile]);
CStream *stream = new CStream(filename, ALStreamSources[nStream], ALStreamBuffers[nStream], IsThisTrackAt16KHz(nFile) ? 16000 : 32000);
CStream *stream = aStream[nStream];
if ( stream && !stream->IsOpened() )
{
@ -2036,7 +1976,7 @@ cSampleManager::StartStreamedFile(uint32 nFile, uint32 nPos, uint8 nStream)
}
}
aStream[nStream] = stream;
aStream[nStream]->Open(filename, IsThisTrackAt16KHz(nFile) ? 16000 : 32000);
if ( stream->Setup() ) {
stream->SetLoopCount(nStreamLoopedFlag[nStream] ? 0 : 1);
@ -2048,8 +1988,7 @@ cSampleManager::StartStreamedFile(uint32 nFile, uint32 nPos, uint8 nStream)
return TRUE;
} else {
delete stream;
aStream[nStream] = NULL;
stream->Close();
}
return FALSE;
}
@ -2061,14 +2000,10 @@ cSampleManager::StopStreamedFile(uint8 nStream)
CStream *stream = aStream[nStream];
if ( stream )
{
delete stream;
aStream[nStream] = NULL;
stream->Close();
if ( nStream == 0 )
_bIsMp3Active = FALSE;
}
if ( nStream == 0 )
_bIsMp3Active = FALSE;
}
int32
@ -2078,7 +2013,7 @@ cSampleManager::GetStreamedFilePosition(uint8 nStream)
CStream *stream = aStream[nStream];
if ( stream )
if ( stream->IsOpened() )
{
if ( _bIsMp3Active )
{
@ -2121,7 +2056,7 @@ cSampleManager::SetStreamedVolumeAndPan(uint8 nVolume, uint8 nPan, bool8 nEffect
CStream *stream = aStream[nStream];
if ( stream )
if ( stream->IsOpened() )
{
if ( nEffectFlag ) {
if ( nStream == 1 || nStream == 2 )
@ -2151,7 +2086,7 @@ cSampleManager::IsStreamPlaying(uint8 nStream)
CStream *stream = aStream[nStream];
if ( stream )
if ( stream->IsOpened() )
{
if ( stream->IsPlaying() )
return TRUE;
@ -2167,7 +2102,7 @@ cSampleManager::Service(void)
{
CStream *stream = aStream[i];
if ( stream )
if ( stream->IsOpened() )
stream->Update();
}
int refCount = CChannel::channelsThatNeedService;

View File

@ -403,6 +403,7 @@ static_assert(false, "SUPPORT_XBOX_SCRIPT and SUPPORT_MOBILE_SCRIPT are mutually
//#define PS2_AUDIO_PATHS // changes audio paths for cutscenes and radio to PS2 paths (needs vbdec on MSS builds)
//#define AUDIO_OAL_USE_SNDFILE // use libsndfile to decode WAVs instead of our internal decoder
#define AUDIO_OAL_USE_MPG123 // use mpg123 to support mp3 files
#define MULTITHREADED_AUDIO // for streams. requires C++11 or later
#ifdef AUDIO_OPUS
#define AUDIO_OAL_USE_OPUS // enable support of opus files
@ -516,5 +517,6 @@ static_assert(false, "SUPPORT_XBOX_SCRIPT and SUPPORT_MOBILE_SCRIPT are mutually
#undef FREE_CAM
#undef BIG_IMG
#undef PS2_AUDIO_CHANNELS
#undef MULTITHREADED_AUDIO
#undef RADIO_SCROLL_TO_PREV_STATION
#endif