Packages

ALEx lets you run the Arcade Learning Environment from Elixir.

Current section

Files

Jump to
alex csrc ale src common SoundSDL.cxx
Raw

csrc/ale/src/common/SoundSDL.cxx

//============================================================================
//
// SSSS tt lll lll
// SS SS tt ll ll
// SS tttttt eeee ll ll aaaa
// SSSS tt ee ee ll ll aa
// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator"
// SS SS tt ee ll ll aa aa
// SSSS ttt eeeee llll llll aaaaa
//
// Copyright (c) 1995-2007 by Bradford W. Mott and the Stella team
//
// See the file "license" for information on usage and redistribution of
// this file, and for a DISCLAIMER OF ALL WARRANTIES.
//
// $Id: SoundSDL.cxx,v 1.38 2007/07/27 13:49:16 stephena Exp $
//============================================================================
#ifdef SOUND_SUPPORT
#include <sstream>
#include <cassert>
#include <cmath>
#include "SDL.h"
#include "TIASnd.hxx"
// #include "FrameBuffer.hxx"
#include "Serializer.hxx"
#include "Deserializer.hxx"
#include "Settings.hxx"
#include "System.hxx"
#include "OSystem.hxx"
#include "SoundSDL.hxx"
#include "Log.hpp"
using namespace std;
using namespace ale;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SoundSDL::SoundSDL(OSystem* osystem)
: Sound(osystem),
myIsEnabled(osystem->settings().getBool("sound")),
myIsInitializedFlag(false),
myLastRegisterSetCycle(0),
myDisplayFrameRate(60),
myNumChannels(1),
myFragmentSizeLogBase2(0),
myIsMuted(false),
myVolume(100),
myNumRecordSamplesNeeded(0)
{
if (osystem->settings().getString("record_sound_filename").size() > 0) {
std::string filename = osystem->settings().getString("record_sound_filename");
mySoundExporter.reset(new ale::sound::SoundExporter(filename, myNumChannels));
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SoundSDL::~SoundSDL()
{
// Close the SDL audio system if it's initialized
close();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL::setEnabled(bool state)
{
myIsEnabled = state;
myOSystem->settings().setBool("sound", state);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL::initialize()
{
// Check whether to start the sound subsystem
if(!myIsEnabled)
{
close();
if(myOSystem->settings().getBool("showinfo"))
cerr << "Sound disabled." << endl << endl;
return;
}
// Make sure the sound queue is clear
myRegWriteQueue.clear();
myTIASound.reset();
if(!((SDL_WasInit(SDL_INIT_AUDIO) & SDL_INIT_AUDIO) > 0))
{
myIsInitializedFlag = false;
myIsMuted = false;
myLastRegisterSetCycle = 0;
if(SDL_InitSubSystem(SDL_INIT_AUDIO) < 0)
{
Logger::Warning << "WARNING: Couldn't initialize SDL audio system! " << endl;
Logger::Warning << " " << SDL_GetError() << endl;
return;
}
else
{
uInt32 fragsize = myOSystem->settings().getInt("fragsize");
Int32 frequency = myOSystem->settings().getInt("freq");
Int32 tiafreq = myOSystem->settings().getInt("tiafreq");
SDL_AudioSpec desired;
desired.freq = frequency;
#ifndef GP2X
desired.format = AUDIO_U8;
#else
desired.format = AUDIO_U16;
#endif
desired.channels = myNumChannels;
desired.samples = fragsize;
desired.callback = callback;
desired.userdata = (void*)this;
if(SDL_OpenAudio(&desired, &myHardwareSpec) < 0)
{
Logger::Warning << "WARNING: Couldn't open SDL audio system! " << endl;
Logger::Warning << " " << SDL_GetError() << endl;
return;
}
// Make sure the sample buffer isn't to big (if it is the sound code
// will not work so we'll need to disable the audio support)
if(((float)myHardwareSpec.samples / (float)myHardwareSpec.freq) >= 0.25)
{
Logger::Warning << "WARNING: Sound device doesn't support realtime audio! Make ";
Logger::Warning << "sure a sound" << endl;
Logger::Warning << " server isn't running. Audio is disabled." << endl;
SDL_CloseAudio();
return;
}
myIsInitializedFlag = true;
myIsMuted = false;
myFragmentSizeLogBase2 = log((double)myHardwareSpec.samples) / log(2.0);
// Now initialize the TIASound object which will actually generate sound
myTIASound.outputFrequency(myHardwareSpec.freq);
myTIASound.tiaFrequency(tiafreq);
myTIASound.channels(myHardwareSpec.channels);
bool clipvol = myOSystem->settings().getBool("clipvol");
myTIASound.clipVolume(clipvol);
// Adjust volume to that defined in settings
myVolume = myOSystem->settings().getInt("volume");
setVolume(myVolume);
// Show some info
if(myOSystem->settings().getBool("showinfo"))
cerr << "Sound enabled:" << endl
<< " Volume : " << myVolume << endl
<< " Frag size : " << fragsize << endl
<< " Frequency : " << myHardwareSpec.freq << endl
<< " Format : " << myHardwareSpec.format << endl
<< " TIA Freq. : " << tiafreq << endl
<< " Channels : " << myNumChannels << endl
<< " Clip volume: " << (int)clipvol << endl << endl;
}
}
// And start the SDL sound subsystem ...
if(myIsInitializedFlag)
{
SDL_PauseAudio(0);
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL::close()
{
if(myIsInitializedFlag)
{
SDL_CloseAudio();
myIsInitializedFlag = false;
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool SoundSDL::isSuccessfullyInitialized() const
{
return myIsInitializedFlag;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL::mute(bool state)
{
if(myIsInitializedFlag)
{
// Ignore multiple calls to do the same thing
if(myIsMuted == state)
{
return;
}
myIsMuted = state;
SDL_PauseAudio(myIsMuted ? 1 : 0);
myRegWriteQueue.clear();
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL::reset()
{
if(myIsInitializedFlag)
{
SDL_PauseAudio(1);
myIsMuted = false;
myLastRegisterSetCycle = 0;
myTIASound.reset();
myRegWriteQueue.clear();
SDL_PauseAudio(0);
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL::setVolume(Int32 percent)
{
if(myIsInitializedFlag)
{
if((percent >= 0) && (percent <= 100))
{
myOSystem->settings().setInt("volume", percent);
SDL_LockAudio();
myVolume = percent;
myTIASound.volume(percent);
SDL_UnlockAudio();
}
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL::adjustVolume(Int8 direction)
{
ostringstream strval;
string message;
Int32 percent = myVolume;
if(direction == -1)
percent -= 2;
else if(direction == 1)
percent += 2;
if((percent < 0) || (percent > 100))
return;
setVolume(percent);
// Now show an onscreen message
// strval << percent;
// message = "Volume set to ";
// message += strval.str();
// myOSystem->frameBuffer().showMessage(message);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL::adjustCycleCounter(Int32 amount)
{
myLastRegisterSetCycle += amount;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL::setChannels(uInt32 channels)
{
if(channels == 1 || channels == 2)
myNumChannels = channels;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL::setFrameRate(uInt32 framerate)
{
// FIXME, we should clear out the queue or adjust the values in it
myDisplayFrameRate = framerate;
myLastRegisterSetCycle = 0;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL::set(uInt16 addr, uInt8 value, Int32 cycle)
{
SDL_LockAudio();
// First, calulate how many seconds would have past since the last
// register write on a real 2600
double delta = (((double)(cycle - myLastRegisterSetCycle)) /
(1193191.66666667));
// Now, adjust the time based on the frame rate the user has selected. For
// the sound to "scale" correctly, we have to know the games real frame
// rate (e.g., 50 or 60) and the currently emulated frame rate. We use these
// values to "scale" the time before the register change occurs.
delta = delta * (myDisplayFrameRate / (double)myOSystem->frameRate());
RegWrite info;
info.addr = addr;
info.value = value;
info.delta = delta;
myRegWriteQueue.enqueue(info);
// Update last cycle counter to the current cycle
myLastRegisterSetCycle = cycle;
SDL_UnlockAudio();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL::processFragment(uInt8* stream, Int32 length)
{
if(!myIsInitializedFlag)
return;
uInt32 channels = myHardwareSpec.channels;
length = length / channels;
// If there are excessive items on the queue then we'll remove some
if(myRegWriteQueue.duration() >
(myFragmentSizeLogBase2 / myDisplayFrameRate))
{
double removed = 0.0;
while(removed < ((myFragmentSizeLogBase2 - 1) / myDisplayFrameRate))
{
RegWrite& info = myRegWriteQueue.front();
removed += info.delta;
myTIASound.set(info.addr, info.value);
myRegWriteQueue.dequeue();
}
// cerr << "Removed Items from RegWriteQueue!" << endl;
}
double position = 0.0;
double remaining = length;
while(remaining > 0.0)
{
if(myRegWriteQueue.size() == 0)
{
// There are no more pending TIA sound register updates so we'll
// use the current settings to finish filling the sound fragment
// myTIASound.process(stream + (uInt32)position, length - (uInt32)position);
myTIASound.process(stream + ((uInt32)position * channels),
length - (uInt32)position);
// Since we had to fill the fragment we'll reset the cycle counter
// to zero. NOTE: This isn't 100% correct, however, it'll do for
// now. We should really remember the overrun and remove it from
// the delta of the next write.
myLastRegisterSetCycle = 0;
break;
}
else
{
// There are pending TIA sound register updates so we need to
// update the sound buffer to the point of the next register update
RegWrite& info = myRegWriteQueue.front();
// How long will the remaining samples in the fragment take to play
double duration = remaining / (double)myHardwareSpec.freq;
// Does the register update occur before the end of the fragment?
if(info.delta <= duration)
{
// If the register update time hasn't already passed then
// process samples upto the point where it should occur
if(info.delta > 0.0)
{
// Process the fragment upto the next TIA register write. We
// round the count passed to process up if needed.
double samples = (myHardwareSpec.freq * info.delta);
// myTIASound.process(stream + (uInt32)position, (uInt32)samples +
// (uInt32)(position + samples) -
// ((uInt32)position + (uInt32)samples));
myTIASound.process(stream + ((uInt32)position * channels),
(uInt32)samples + (uInt32)(position + samples) -
((uInt32)position + (uInt32)samples));
position += samples;
remaining -= samples;
}
myTIASound.set(info.addr, info.value);
myRegWriteQueue.dequeue();
}
else
{
// The next register update occurs in the next fragment so finish
// this fragment with the current TIA settings and reduce the register
// update delay by the corresponding amount of time
// myTIASound.process(stream + (uInt32)position, length - (uInt32)position);
myTIASound.process(stream + ((uInt32)position * channels),
length - (uInt32)position);
info.delta -= duration;
break;
}
}
}
// If recording sound, do so now
if (mySoundExporter.get() != NULL && myNumRecordSamplesNeeded > 0) {
mySoundExporter->addSamples(stream, length);
// Consume this many samples
myNumRecordSamplesNeeded -= length;
}
}
void SoundSDL::recordNextFrame() {
// Grow the required samples by a frame's worth
if (mySoundExporter.get() != NULL)
myNumRecordSamplesNeeded += ale::sound::SoundExporter::SamplesPerFrame;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL::callback(void* udata, uInt8* stream, int len)
{
SoundSDL* sound = (SoundSDL*)udata;
sound->processFragment(stream, (Int32)len);
#ifdef ATARIVOX_SUPPORT
Logger::Info << "SoundSDL::callback(): len==" << len << endl;
// See if we need sound from the AtariVox
AtariVox *vox = sound->myOSystem->console().atariVox();
if(vox)
{
// If so, mix 'em together (this is a crappy way to mix audio streams...)
uInt8 *s = stream;
for(int i=0; i<len/OUTPUT_BUFFER_SIZE; i++)
{
int count;
uInt8 *voxSamples = vox->getSpeakJet()->getSamples(&count);
if(!count)
break;
SDL_MixAudio(s, voxSamples, OUTPUT_BUFFER_SIZE, SDL_MIX_MAXVOLUME);
s += OUTPUT_BUFFER_SIZE;
}
}
#endif
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool SoundSDL::load(Deserializer& in)
{
string device = "TIASound";
try
{
if(in.getString() != device)
return false;
uInt8 reg1 = 0, reg2 = 0, reg3 = 0, reg4 = 0, reg5 = 0, reg6 = 0;
reg1 = (uInt8) in.getInt();
reg2 = (uInt8) in.getInt();
reg3 = (uInt8) in.getInt();
reg4 = (uInt8) in.getInt();
reg5 = (uInt8) in.getInt();
reg6 = (uInt8) in.getInt();
myLastRegisterSetCycle = (Int32) in.getInt();
// Only update the TIA sound registers if sound is enabled
// Make sure to empty the queue of previous sound fragments
if(myIsInitializedFlag)
{
SDL_PauseAudio(1);
myRegWriteQueue.clear();
myTIASound.set(0x15, reg1);
myTIASound.set(0x16, reg2);
myTIASound.set(0x17, reg3);
myTIASound.set(0x18, reg4);
myTIASound.set(0x19, reg5);
myTIASound.set(0x1a, reg6);
SDL_PauseAudio(0);
}
}
catch(char *msg)
{
Logger::Error << msg << endl;
return false;
}
catch(...)
{
Logger::Error << "Unknown error in load state for " << device << endl;
return false;
}
return true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool SoundSDL::save(Serializer& out)
{
string device = "TIASound";
try
{
out.putString(device);
uInt8 reg1 = 0, reg2 = 0, reg3 = 0, reg4 = 0, reg5 = 0, reg6 = 0;
// Only get the TIA sound registers if sound is enabled
if(myIsInitializedFlag)
{
reg1 = myTIASound.get(0x15);
reg2 = myTIASound.get(0x16);
reg3 = myTIASound.get(0x17);
reg4 = myTIASound.get(0x18);
reg5 = myTIASound.get(0x19);
reg6 = myTIASound.get(0x1a);
}
out.putInt(reg1);
out.putInt(reg2);
out.putInt(reg3);
out.putInt(reg4);
out.putInt(reg5);
out.putInt(reg6);
out.putInt(myLastRegisterSetCycle);
}
catch(char *msg)
{
Logger::Error << msg << endl;
return false;
}
catch(...)
{
Logger::Error << "Unknown error in save state for " << device << endl;
return false;
}
return true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SoundSDL::RegWriteQueue::RegWriteQueue(uInt32 capacity)
: myCapacity(capacity),
myBuffer(0),
mySize(0),
myHead(0),
myTail(0)
{
myBuffer = new RegWrite[myCapacity];
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SoundSDL::RegWriteQueue::~RegWriteQueue()
{
delete[] myBuffer;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL::RegWriteQueue::clear()
{
myHead = myTail = mySize = 0;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL::RegWriteQueue::dequeue()
{
if(mySize > 0)
{
myHead = (myHead + 1) % myCapacity;
--mySize;
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
double SoundSDL::RegWriteQueue::duration()
{
double duration = 0.0;
for(uInt32 i = 0; i < mySize; ++i)
{
duration += myBuffer[(myHead + i) % myCapacity].delta;
}
return duration;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL::RegWriteQueue::enqueue(const RegWrite& info)
{
// If an attempt is made to enqueue more than the queue can hold then
// we'll enlarge the queue's capacity.
if(mySize == myCapacity)
{
grow();
}
myBuffer[myTail] = info;
myTail = (myTail + 1) % myCapacity;
++mySize;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SoundSDL::RegWrite& SoundSDL::RegWriteQueue::front()
{
assert(mySize != 0);
return myBuffer[myHead];
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt32 SoundSDL::RegWriteQueue::size() const
{
return mySize;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL::RegWriteQueue::grow()
{
RegWrite* buffer = new RegWrite[myCapacity * 2];
for(uInt32 i = 0; i < mySize; ++i)
{
buffer[i] = myBuffer[(myHead + i) % myCapacity];
}
myHead = 0;
myTail = mySize;
myCapacity = myCapacity * 2;
delete[] myBuffer;
myBuffer = buffer;
}
#endif // SOUND_SUPPORT