mirror of
https://github.com/libsdl-org/SDL.git
synced 2026-03-20 15:51:07 +01:00
369 lines
9.7 KiB
C++
369 lines
9.7 KiB
C++
|
|
/*
|
||
|
|
Simple DirectMedia Layer
|
||
|
|
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
|
||
|
|
|
||
|
|
This software is provided 'as-is', without any express or implied
|
||
|
|
warranty. In no event will the authors be held liable for any damages
|
||
|
|
arising from the use of this software.
|
||
|
|
|
||
|
|
Permission is granted to anyone to use this software for any purpose,
|
||
|
|
including commercial applications, and to alter it and redistribute it
|
||
|
|
freely, subject to the following restrictions:
|
||
|
|
|
||
|
|
1. The origin of this software must not be misrepresented; you must not
|
||
|
|
claim that you wrote the original software. If you use this software
|
||
|
|
in a product, an acknowledgment in the product documentation would be
|
||
|
|
appreciated but is not required.
|
||
|
|
2. Altered source versions must be plainly marked as such, and must not be
|
||
|
|
misrepresented as being the original software.
|
||
|
|
3. This notice may not be removed or altered from any source distribution.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#ifdef __cplusplus
|
||
|
|
extern "C" {
|
||
|
|
#endif
|
||
|
|
|
||
|
|
#include "SDL_ngageaudio.h"
|
||
|
|
#include "../SDL_sysaudio.h"
|
||
|
|
#include "SDL_internal.h"
|
||
|
|
|
||
|
|
#ifdef __cplusplus
|
||
|
|
}
|
||
|
|
#endif
|
||
|
|
|
||
|
|
#ifdef SDL_AUDIO_DRIVER_NGAGE
|
||
|
|
|
||
|
|
#include "SDL_ngageaudio.hpp"
|
||
|
|
|
||
|
|
CAudio::CAudio() : CActive(EPriorityStandard), iBufDes(NULL, 0) {}
|
||
|
|
|
||
|
|
CAudio *CAudio::NewL(TInt aLatency)
|
||
|
|
{
|
||
|
|
CAudio *self = new (ELeave) CAudio();
|
||
|
|
CleanupStack::PushL(self);
|
||
|
|
self->ConstructL(aLatency);
|
||
|
|
CleanupStack::Pop(self);
|
||
|
|
return self;
|
||
|
|
}
|
||
|
|
|
||
|
|
void CAudio::ConstructL(TInt aLatency)
|
||
|
|
{
|
||
|
|
CActiveScheduler::Add(this);
|
||
|
|
User::LeaveIfError(iTimer.CreateLocal());
|
||
|
|
iTimerCreated = ETrue;
|
||
|
|
|
||
|
|
iStream = CMdaAudioOutputStream::NewL(*this);
|
||
|
|
if (!iStream) {
|
||
|
|
SDL_Log("Error: Failed to create audio stream");
|
||
|
|
User::Leave(KErrNoMemory);
|
||
|
|
}
|
||
|
|
|
||
|
|
iLatency = aLatency;
|
||
|
|
iLatencySamples = aLatency * 8; // 8kHz.
|
||
|
|
|
||
|
|
// Determine minimum and maximum number of samples to write with one
|
||
|
|
// WriteL request.
|
||
|
|
iMinWrite = iLatencySamples / 8;
|
||
|
|
iMaxWrite = iLatencySamples / 2;
|
||
|
|
|
||
|
|
// Set defaults.
|
||
|
|
iState = EStateNone;
|
||
|
|
iTimerCreated = EFalse;
|
||
|
|
iTimerActive = EFalse;
|
||
|
|
}
|
||
|
|
|
||
|
|
CAudio::~CAudio()
|
||
|
|
{
|
||
|
|
if (iStream) {
|
||
|
|
iStream->Stop();
|
||
|
|
|
||
|
|
while (iState != EStateDone) {
|
||
|
|
User::After(100000); // 100ms.
|
||
|
|
}
|
||
|
|
|
||
|
|
delete iStream;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void CAudio::Start()
|
||
|
|
{
|
||
|
|
if (iStream) {
|
||
|
|
// Set to 8kHz mono audio.
|
||
|
|
iStreamSettings.iChannels = TMdaAudioDataSettings::EChannelsMono;
|
||
|
|
iStreamSettings.iSampleRate = TMdaAudioDataSettings::ESampleRate8000Hz;
|
||
|
|
iStream->Open(&iStreamSettings);
|
||
|
|
iState = EStateOpening;
|
||
|
|
} else {
|
||
|
|
SDL_Log("Error: Failed to open audio stream");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Feeds more processed data to the audio stream.
|
||
|
|
void CAudio::Feed()
|
||
|
|
{
|
||
|
|
// If a WriteL is already in progress, or we aren't even playing;
|
||
|
|
// do nothing!
|
||
|
|
if ((iState != EStateWriting) && (iState != EStatePlaying)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Figure out the number of samples that really have been played
|
||
|
|
// through the output.
|
||
|
|
TTimeIntervalMicroSeconds pos = iStream->Position();
|
||
|
|
|
||
|
|
TInt played = 8 * (pos.Int64() / TInt64(1000)).GetTInt(); // 8kHz.
|
||
|
|
|
||
|
|
played += iBaseSamplesPlayed;
|
||
|
|
|
||
|
|
// Determine the difference between the number of samples written to
|
||
|
|
// CMdaAudioOutputStream and the number of samples it has played.
|
||
|
|
// The difference is the amount of data in the buffers.
|
||
|
|
if (played < 0) {
|
||
|
|
played = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
TInt buffered = iSamplesWritten - played;
|
||
|
|
if (buffered < 0) {
|
||
|
|
buffered = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (iState == EStateWriting) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// The trick for low latency: Do not let the buffers fill up beyond the
|
||
|
|
// latency desired! We write as many samples as the difference between
|
||
|
|
// the latency target (in samples) and the amount of data buffered.
|
||
|
|
TInt samplesToWrite = iLatencySamples - buffered;
|
||
|
|
|
||
|
|
// Do not write very small blocks. This should improve efficiency, since
|
||
|
|
// writes to the streaming API are likely to be expensive.
|
||
|
|
if (samplesToWrite < iMinWrite) {
|
||
|
|
// Not enough data to write, set up a timer to fire after a while.
|
||
|
|
// Try againwhen it expired.
|
||
|
|
if (iTimerActive) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
iTimerActive = ETrue;
|
||
|
|
SetActive();
|
||
|
|
iTimer.After(iStatus, (1000 * iLatency) / 8);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Do not write more than the set number of samples at once.
|
||
|
|
int numSamples = samplesToWrite;
|
||
|
|
if (numSamples > iMaxWrite) {
|
||
|
|
numSamples = iMaxWrite;
|
||
|
|
}
|
||
|
|
|
||
|
|
SDL_AudioDevice *device = NGAGE_GetAudioDeviceAddr();
|
||
|
|
if (device) {
|
||
|
|
SDL_PrivateAudioData *phdata = (SDL_PrivateAudioData *)device->hidden;
|
||
|
|
|
||
|
|
iBufDes.Set(phdata->buffer, 2 * numSamples, 2 * numSamples);
|
||
|
|
iStream->WriteL(iBufDes);
|
||
|
|
iState = EStateWriting;
|
||
|
|
|
||
|
|
// Keep track of the number of samples written (for latency calculations).
|
||
|
|
iSamplesWritten += numSamples;
|
||
|
|
} else {
|
||
|
|
// Output device not ready yet. Let's go for another round.
|
||
|
|
if (iTimerActive) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
iTimerActive = ETrue;
|
||
|
|
SetActive();
|
||
|
|
iTimer.After(iStatus, (1000 * iLatency) / 8);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void CAudio::RunL()
|
||
|
|
{
|
||
|
|
iTimerActive = EFalse;
|
||
|
|
Feed();
|
||
|
|
}
|
||
|
|
|
||
|
|
void CAudio::DoCancel()
|
||
|
|
{
|
||
|
|
iTimerActive = EFalse;
|
||
|
|
iTimer.Cancel();
|
||
|
|
}
|
||
|
|
|
||
|
|
void CAudio::StartThread()
|
||
|
|
{
|
||
|
|
TInt heapMinSize = 8192; // 8 KB initial heap size.
|
||
|
|
TInt heapMaxSize = 1024 * 1024; // 1 MB maximum heap size.
|
||
|
|
|
||
|
|
TInt err = iProcess.Create(_L("ProcessThread"), ProcessThreadCB, KDefaultStackSize * 2, heapMinSize, heapMaxSize, this);
|
||
|
|
if (err == KErrNone) {
|
||
|
|
iProcess.SetPriority(EPriorityLess);
|
||
|
|
iProcess.Resume();
|
||
|
|
} else {
|
||
|
|
SDL_Log("Error: Failed to create audio processing thread: %d", err);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void CAudio::StopThread()
|
||
|
|
{
|
||
|
|
if (iStreamStarted) {
|
||
|
|
iProcess.Kill(KErrNone);
|
||
|
|
iProcess.Close();
|
||
|
|
iStreamStarted = EFalse;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
TInt CAudio::ProcessThreadCB(TAny *aPtr)
|
||
|
|
{
|
||
|
|
CAudio *self = static_cast<CAudio *>(aPtr);
|
||
|
|
SDL_AudioDevice *device = NGAGE_GetAudioDeviceAddr();
|
||
|
|
|
||
|
|
while (self->iStreamStarted) {
|
||
|
|
if (device) {
|
||
|
|
SDL_PlaybackAudioThreadIterate(device);
|
||
|
|
} else {
|
||
|
|
device = NGAGE_GetAudioDeviceAddr();
|
||
|
|
}
|
||
|
|
User::After(100000); // 100ms.
|
||
|
|
}
|
||
|
|
return KErrNone;
|
||
|
|
}
|
||
|
|
|
||
|
|
void CAudio::MaoscOpenComplete(TInt aError)
|
||
|
|
{
|
||
|
|
if (aError == KErrNone) {
|
||
|
|
iStream->SetVolume(1);
|
||
|
|
iStreamStarted = ETrue;
|
||
|
|
StartThread();
|
||
|
|
|
||
|
|
} else {
|
||
|
|
SDL_Log("Error: Failed to open audio stream: %d", aError);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void CAudio::MaoscBufferCopied(TInt aError, const TDesC8 & /*aBuffer*/)
|
||
|
|
{
|
||
|
|
if (aError == KErrNone) {
|
||
|
|
iState = EStatePlaying;
|
||
|
|
Feed();
|
||
|
|
} else if (aError == KErrAbort) {
|
||
|
|
// The stream has been stopped.
|
||
|
|
iState = EStateDone;
|
||
|
|
} else {
|
||
|
|
SDL_Log("Error: Failed to copy audio buffer: %d", aError);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void CAudio::MaoscPlayComplete(TInt aError)
|
||
|
|
{
|
||
|
|
// If we finish due to an underflow, we'll need to restart playback.
|
||
|
|
// Normally KErrUnderlow is raised at stream end, but in our case the API
|
||
|
|
// should never see the stream end -- we are continuously feeding it more
|
||
|
|
// data! Many underflow errors mean that the latency target is too low.
|
||
|
|
if (aError == KErrUnderflow) {
|
||
|
|
// The number of samples played gets resetted to zero when we restart
|
||
|
|
// playback after underflow.
|
||
|
|
iBaseSamplesPlayed = iSamplesWritten;
|
||
|
|
|
||
|
|
iStream->Stop();
|
||
|
|
Cancel();
|
||
|
|
|
||
|
|
iStream->SetAudioPropertiesL(TMdaAudioDataSettings::ESampleRate8000Hz, TMdaAudioDataSettings::EChannelsMono);
|
||
|
|
|
||
|
|
iState = EStatePlaying;
|
||
|
|
Feed();
|
||
|
|
return;
|
||
|
|
|
||
|
|
} else if (aError != KErrNone) {
|
||
|
|
// Handle error.
|
||
|
|
}
|
||
|
|
|
||
|
|
// We shouldn't get here.
|
||
|
|
SDL_Log("%s: %d", __FUNCTION__, aError);
|
||
|
|
}
|
||
|
|
|
||
|
|
static TBool gAudioRunning;
|
||
|
|
|
||
|
|
TBool AudioIsReady()
|
||
|
|
{
|
||
|
|
return gAudioRunning;
|
||
|
|
}
|
||
|
|
|
||
|
|
TInt AudioThreadCB(TAny *aParams)
|
||
|
|
{
|
||
|
|
CTrapCleanup *cleanup = CTrapCleanup::New();
|
||
|
|
if (!cleanup) {
|
||
|
|
return KErrNoMemory;
|
||
|
|
}
|
||
|
|
|
||
|
|
CActiveScheduler *scheduler = new CActiveScheduler();
|
||
|
|
if (!scheduler) {
|
||
|
|
delete cleanup;
|
||
|
|
return KErrNoMemory;
|
||
|
|
}
|
||
|
|
|
||
|
|
CActiveScheduler::Install(scheduler);
|
||
|
|
|
||
|
|
TRAPD(err,
|
||
|
|
{
|
||
|
|
TInt latency = *(TInt *)aParams;
|
||
|
|
CAudio *audio = CAudio::NewL(latency);
|
||
|
|
CleanupStack::PushL(audio);
|
||
|
|
|
||
|
|
gAudioRunning = ETrue;
|
||
|
|
audio->Start();
|
||
|
|
TBool once = EFalse;
|
||
|
|
|
||
|
|
while (gAudioRunning) {
|
||
|
|
// Allow active scheduler to process any events.
|
||
|
|
TInt error;
|
||
|
|
CActiveScheduler::RunIfReady(error, CActive::EPriorityIdle);
|
||
|
|
|
||
|
|
if (!once) {
|
||
|
|
SDL_AudioDevice *device = NGAGE_GetAudioDeviceAddr();
|
||
|
|
if (device) {
|
||
|
|
// Stream ready; start feeding audio data.
|
||
|
|
// After feeding it once, the callbacks will take over.
|
||
|
|
audio->iState = CAudio::EStatePlaying;
|
||
|
|
audio->Feed();
|
||
|
|
once = ETrue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
User::After(100000); // 100ms.
|
||
|
|
}
|
||
|
|
|
||
|
|
CleanupStack::PopAndDestroy(audio);
|
||
|
|
});
|
||
|
|
|
||
|
|
delete scheduler;
|
||
|
|
delete cleanup;
|
||
|
|
return err;
|
||
|
|
}
|
||
|
|
|
||
|
|
RThread audioThread;
|
||
|
|
|
||
|
|
void InitAudio(TInt *aLatency)
|
||
|
|
{
|
||
|
|
_LIT(KAudioThreadName, "AudioThread");
|
||
|
|
|
||
|
|
TInt err = audioThread.Create(KAudioThreadName, AudioThreadCB, KDefaultStackSize, 0, aLatency);
|
||
|
|
if (err != KErrNone) {
|
||
|
|
User::Leave(err);
|
||
|
|
}
|
||
|
|
|
||
|
|
audioThread.Resume();
|
||
|
|
}
|
||
|
|
|
||
|
|
void DeinitAudio()
|
||
|
|
{
|
||
|
|
gAudioRunning = EFalse;
|
||
|
|
|
||
|
|
TRequestStatus status;
|
||
|
|
audioThread.Logon(status);
|
||
|
|
User::WaitForRequest(status);
|
||
|
|
|
||
|
|
audioThread.Close();
|
||
|
|
}
|
||
|
|
|
||
|
|
#endif // SDL_AUDIO_DRIVER_NGAGE
|