OK, this is the first pass at adding the new TIA sound emulation

core to Stella, as outlined in a thread on AtariAge.  It's currently
not working 100% due to timing issues with the TIA class.

Expect breakage over the next few months, as this code and the TIA
code in general will be borked from time to time.


git-svn-id: svn://svn.code.sf.net/p/stella/code/trunk@3306 8b62c5a3-ac7e-4cc8-8f21-d9a121418aba
This commit is contained in:
stephena 2016-04-03 17:15:19 +00:00
parent c0095ca59a
commit 81e9480100
9 changed files with 674 additions and 989 deletions

View File

@ -37,8 +37,7 @@ class SoundNull : public Sound
{
public:
/**
Create a new sound object. The init method must be invoked before
using the object.
Create a new sound object with no functionality.
*/
SoundNull(OSystem& osystem) : Sound(osystem)
{
@ -49,40 +48,14 @@ class SoundNull : public Sound
public:
/**
Enables/disables the sound subsystem.
@param enable Either true or false, to enable or disable the sound system
@return Whether the sound system was enabled or disabled
*/
void setEnabled(bool enable) override { }
/**
The system cycle counter is being adjusting by the specified amount. Any
members using the system cycle counter should be adjusted as needed.
@param amount The amount the cycle counter is being adjusted by
*/
void adjustCycleCounter(Int32 amount) override { }
/**
Sets the number of channels (mono or stereo sound).
@param channels The number of channels
*/
void setChannels(uInt32 channels) override { }
/**
Sets the display framerate. Sound generation for NTSC and PAL games
depends on the framerate, so we need to set it here.
@param framerate The base framerate depending on NTSC or PAL ROM
*/
void setFrameRate(float framerate) override { }
void setEnabled(bool) override { }
/**
Initializes the sound device. This must be called before any
calls are made to derived methods.
*/
void open() override { }
void open(bool) override { }
/**
Should be called to close the sound device. Once called the sound
@ -92,89 +65,25 @@ class SoundNull : public Sound
/**
Set the mute state of the sound object. While muted no sound is played.
@param state Mutes sound if true, unmute if false
*/
void mute(bool state) override { }
void mute(bool) override { }
/**
Reset the sound device.
*/
void reset() { }
/**
Sets the sound register to a given value.
@param addr The register address
@param value The value to save into the register
@param cycle The system cycle at which the register is being updated
*/
void set(uInt16 addr, uInt8 value, Int32 cycle) override { }
/**
Sets the volume of the sound device to the specified level. The
volume is given as a percentage from 0 to 100. Values outside
this range indicate that the volume shouldn't be changed at all.
@param percent The new volume percentage level for the sound device
*/
void setVolume(Int32 percent) override { }
void setVolume(uInt32) override { }
/**
Adjusts the volume of the sound device based on the given direction.
@param direction Increase or decrease the current volume by a predefined
amount based on the direction (1 = increase, -1 =decrease)
*/
void adjustVolume(Int8 direction) override { }
public:
/**
Saves the current state of this device to the given Serializer.
@param out The serializer device to save to.
@return The result of the save. True on success, false on failure.
*/
bool save(Serializer& out) const
{
out.putString("TIASound");
for(int i = 0; i < 6; ++i)
out.putByte(0);
// myLastRegisterSetCycle
out.putInt(0);
return true;
}
/**
Loads the current state of this device from the given Serializer.
@param in The Serializer device to load from.
@return The result of the load. True on success, false on failure.
*/
bool load(Serializer& in)
{
if(in.getString() != "TIASound")
return false;
// Read sound registers and discard
for(int i = 0; i < 6; ++i)
in.getByte();
// myLastRegisterSetCycle
in.getInt();
return true;
}
/**
Get a descriptor for this console class (used in error checking).
@return The name of the object
*/
string name() const { return "TIASound"; }
void adjustVolume(Int8) override { }
private:
// Following constructors and assignment operators not supported

View File

@ -24,8 +24,7 @@
#include <cmath>
#include <SDL.h>
#include "TIASnd.hxx"
#include "TIATables.hxx"
#include "TIA.hxx"
#include "FrameBuffer.hxx"
#include "Settings.hxx"
#include "System.hxx"
@ -38,11 +37,6 @@ SoundSDL2::SoundSDL2(OSystem& osystem)
: Sound(osystem),
myIsEnabled(false),
myIsInitializedFlag(false),
myLastRegisterSetCycle(0),
myNumChannels(0),
myFragmentSizeLogBase2(0),
myFragmentSizeLogDiv1(0),
myFragmentSizeLogDiv2(0),
myIsMuted(true),
myVolume(100)
{
@ -53,11 +47,12 @@ SoundSDL2::SoundSDL2(OSystem& osystem)
// This fixes a bug most prevalent with ATI video cards in Windows,
// whereby sound stopped working after the first video change
SDL_AudioSpec desired;
SDL_memset(&desired, 0, sizeof(desired));
desired.freq = myOSystem.settings().getInt("freq");
desired.format = AUDIO_S16SYS;
desired.channels = 2;
desired.samples = myOSystem.settings().getInt("fragsize");
desired.callback = callback;
desired.callback = getSamples;
desired.userdata = static_cast<void*>(this);
ostringstream buf;
@ -69,24 +64,6 @@ SoundSDL2::SoundSDL2(OSystem& osystem)
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)
{
buf << "WARNING: Sound device doesn't support realtime audio! Make "
<< "sure a sound" << endl
<< " server isn't running. Audio is disabled." << endl;
myOSystem.logMessage(buf.str(), 0);
SDL_CloseAudio();
return;
}
// Pre-compute fragment-related variables as much as possible
myFragmentSizeLogBase2 = log(myHardwareSpec.samples) / log(2.0);
myFragmentSizeLogDiv1 = myFragmentSizeLogBase2 / 60.0;
myFragmentSizeLogDiv2 = (myFragmentSizeLogBase2 - 1) / 60.0;
myIsInitializedFlag = true;
SDL_PauseAudio(1);
@ -99,6 +76,7 @@ SoundSDL2::~SoundSDL2()
// Close the SDL audio system if it's initialized
if(myIsInitializedFlag)
{
//FIXME SDL_PauseAudio(1);
SDL_CloseAudio();
myIsEnabled = myIsInitializedFlag = false;
}
@ -114,7 +92,7 @@ void SoundSDL2::setEnabled(bool state)
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL2::open()
void SoundSDL2::open(bool stereo)
{
myOSystem.logMessage("SoundSDL2::open started ...", 2);
myIsEnabled = false;
@ -126,9 +104,8 @@ void SoundSDL2::open()
}
// Now initialize the TIASound object which will actually generate sound
myTIASound.outputFrequency(myHardwareSpec.freq);
const string& chanResult =
myTIASound.channels(myHardwareSpec.channels, myNumChannels == 2);
myOSystem.console().tia().sound().channels(myHardwareSpec.channels, stereo);
// Adjust volume to that defined in settings
myVolume = myOSystem.settings().getInt("volume");
@ -147,7 +124,7 @@ void SoundSDL2::open()
// And start the SDL sound subsystem ...
myIsEnabled = true;
mute(false);
SDL_PauseAudio(0);//myIsMuted ? 1 : 0);
myOSystem.logMessage("SoundSDL2::open finished", 2);
}
@ -159,9 +136,8 @@ void SoundSDL2::close()
{
myIsEnabled = false;
SDL_PauseAudio(1);
myLastRegisterSetCycle = 0;
myTIASound.reset();
myRegWriteQueue.clear();
if(myOSystem.hasConsole())
myOSystem.console().tia().sound().reset();
myOSystem.logMessage("SoundSDL2::close", 2);
}
}
@ -172,7 +148,10 @@ void SoundSDL2::mute(bool state)
if(myIsInitializedFlag)
{
myIsMuted = state;
SDL_PauseAudio(myIsMuted ? 1 : 0);
if(myOSystem.hasConsole())
myOSystem.console().tia().sound().volume(myIsMuted ? 0 : myVolume);
// SDL_PauseAudio(myIsMuted ? 1 : 0);
}
}
@ -182,23 +161,21 @@ void SoundSDL2::reset()
if(myIsInitializedFlag)
{
SDL_PauseAudio(1);
myLastRegisterSetCycle = 0;
myTIASound.reset();
myRegWriteQueue.clear();
mute(myIsMuted);
if(myOSystem.hasConsole())
myOSystem.console().tia().sound().reset();
// mute(myIsMuted);
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL2::setVolume(Int32 percent)
void SoundSDL2::setVolume(uInt32 volume)
{
if(myIsInitializedFlag && (percent >= 0) && (percent <= 100))
if(myIsInitializedFlag && (volume <= 100))
{
myOSystem.settings().setValue("volume", percent);
SDL_LockAudio();
myVolume = percent;
myTIASound.volume(percent);
SDL_UnlockAudio();
myOSystem.settings().setValue("volume", volume);
myVolume = volume;
if(myOSystem.hasConsole())
myOSystem.console().tia().sound().volume(volume);
}
}
@ -229,52 +206,25 @@ void SoundSDL2::adjustVolume(Int8 direction)
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL2::adjustCycleCounter(Int32 amount)
void SoundSDL2::getSamples(void* udata, uInt8* stream, int len)
{
myLastRegisterSetCycle += amount;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL2::setChannels(uInt32 channels)
{
if(channels == 1 || channels == 2)
myNumChannels = channels;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL2::setFrameRate(float framerate)
{
// Recalculate since frame rate has changed
// FIXME - should we clear out the queue or adjust the values in it?
myFragmentSizeLogDiv1 = myFragmentSizeLogBase2 / framerate;
myFragmentSizeLogDiv2 = (myFragmentSizeLogBase2 - 1) / framerate;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL2::set(uInt16 addr, uInt8 value, Int32 cycle)
{
SDL_LockAudio();
// First, calculate 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.
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();
SoundSDL2* sound = static_cast<SoundSDL2*>(udata);
if(sound->myIsEnabled)
{
// The callback is requesting 8-bit data, but the TIA sound emulator
// deals in 16-bit data
// So, we need to convert the pointer and half the length
uInt16* buffer = reinterpret_cast<uInt16*>(stream);
Int32 left =
sound->myOSystem.console().tia().sound().getSamples(buffer, uInt32(len) >> 1);
if(left > 0) // Is silence required?
SDL_memset(buffer, 0, uInt32(left) << 1);
}
else
SDL_memset(stream, 0, len); // Write 'silence'
}
#if 0
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL2::processFragment(Int16* stream, uInt32 length)
{
@ -355,166 +305,6 @@ void SoundSDL2::processFragment(Int16* stream, uInt32 length)
}
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL2::callback(void* udata, uInt8* stream, int len)
{
SoundSDL2* sound = static_cast<SoundSDL2*>(udata);
if(sound->myIsEnabled)
{
// The callback is requesting 8-bit (unsigned) data, but the TIA sound
// emulator deals in 16-bit (signed) data
// So, we need to convert the pointer and half the length
sound->processFragment(reinterpret_cast<Int16*>(stream), uInt32(len) >> 1);
}
else
SDL_memset(stream, 0, len); // Write 'silence'
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool SoundSDL2::save(Serializer& out) const
{
try
{
out.putString(name());
// Only get the TIA sound registers if sound is enabled
if(myIsInitializedFlag)
{
out.putByte(myTIASound.get(TIARegister::AUDC0));
out.putByte(myTIASound.get(TIARegister::AUDC1));
out.putByte(myTIASound.get(TIARegister::AUDF0));
out.putByte(myTIASound.get(TIARegister::AUDF1));
out.putByte(myTIASound.get(TIARegister::AUDV0));
out.putByte(myTIASound.get(TIARegister::AUDV1));
}
else
for(int i = 0; i < 6; ++i)
out.putByte(0);
out.putInt(myLastRegisterSetCycle);
}
catch(...)
{
myOSystem.logMessage("ERROR: SoundSDL2::save", 0);
return false;
}
return true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool SoundSDL2::load(Serializer& in)
{
try
{
if(in.getString() != name())
return false;
// 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(TIARegister::AUDC0, in.getByte());
myTIASound.set(TIARegister::AUDC1, in.getByte());
myTIASound.set(TIARegister::AUDF0, in.getByte());
myTIASound.set(TIARegister::AUDF1, in.getByte());
myTIASound.set(TIARegister::AUDV0, in.getByte());
myTIASound.set(TIARegister::AUDV1, in.getByte());
if(!myIsMuted) SDL_PauseAudio(0);
}
else
for(int i = 0; i < 6; ++i)
in.getByte();
myLastRegisterSetCycle = in.getInt();
}
catch(...)
{
myOSystem.logMessage("ERROR: SoundSDL2::load", 0);
return false;
}
return true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SoundSDL2::RegWriteQueue::RegWriteQueue(uInt32 capacity)
: myBuffer(make_ptr<RegWrite[]>(capacity)),
myCapacity(capacity),
mySize(0),
myHead(0),
myTail(0)
{
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL2::RegWriteQueue::clear()
{
myHead = myTail = mySize = 0;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL2::RegWriteQueue::dequeue()
{
if(mySize > 0)
{
myHead = (myHead + 1) % myCapacity;
--mySize;
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
double SoundSDL2::RegWriteQueue::duration() const
{
double duration = 0.0;
for(uInt32 i = 0; i < mySize; ++i)
{
duration += myBuffer[(myHead + i) % myCapacity].delta;
}
return duration;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL2::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;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SoundSDL2::RegWrite& SoundSDL2::RegWriteQueue::front() const
{
assert(mySize != 0);
return myBuffer[myHead];
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt32 SoundSDL2::RegWriteQueue::size() const
{
return mySize;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL2::RegWriteQueue::grow()
{
unique_ptr<RegWrite[]> buffer = make_ptr<RegWrite[]>(myCapacity*2);
for(uInt32 i = 0; i < mySize; ++i)
buffer[i] = myBuffer[(myHead + i) % myCapacity];
myHead = 0;
myTail = mySize;
myCapacity *= 2;
myBuffer = std::move(buffer);
}
#endif
#endif // SOUND_SUPPORT

View File

@ -27,27 +27,22 @@ class OSystem;
#include <SDL.h>
#include "bspf.hxx"
#include "TIASnd.hxx"
#include "Sound.hxx"
/**
This class implements the sound API for SDL.
This class implements the sound API for SDL2.
@author Stephen Anthony and Bradford W. Mott
@author Stephen Anthony
@version $Id$
*/
class SoundSDL2 : public Sound
{
public:
/**
Create a new sound object. The init method must be invoked before
using the object.
Create a new sound object. The open() method must be invoked
before using the object.
*/
SoundSDL2(OSystem& osystem);
/**
Destructor
*/
virtual ~SoundSDL2();
public:
@ -58,38 +53,13 @@ class SoundSDL2 : public Sound
*/
void setEnabled(bool state) override;
/**
The system cycle counter is being adjusting by the specified amount. Any
members using the system cycle counter should be adjusted as needed.
@param amount The amount the cycle counter is being adjusted by
*/
void adjustCycleCounter(Int32 amount) override;
/**
Sets the number of channels (mono or stereo sound). Note that this
determines how the emulation should 'mix' the channels of the TIA sound
system (of which there are always two). It does not specify the actual
number of hardware channels that SDL should use; it will always attempt
to use two channels in hardware.
@param channels The number of channels
*/
void setChannels(uInt32 channels) override;
/**
Sets the display framerate. Sound generation for NTSC and PAL games
depends on the framerate, so we need to set it here.
@param framerate The base framerate depending on NTSC or PAL ROM
*/
void setFrameRate(float framerate) override;
/**
Initializes the sound device. This must be called before any
calls are made to derived methods.
@param stereo The number of channels (mono -> 1, stereo -> 2)
*/
void open() override;
void open(bool stereo) override;
/**
Should be called to close the sound device. Once called the sound
@ -109,23 +79,14 @@ class SoundSDL2 : public Sound
*/
void reset() override;
/**
Sets the sound register to a given value.
@param addr The register address
@param value The value to save into the register
@param cycle The system cycle at which the register is being updated
*/
void set(uInt16 addr, uInt8 value, Int32 cycle) override;
/**
Sets the volume of the sound device to the specified level. The
volume is given as a percentage from 0 to 100. Values outside
this range indicate that the volume shouldn't be changed at all.
@param percent The new volume percentage level for the sound device
@param volume The new volume percentage level for the sound device
*/
void setVolume(Int32 percent) override;
void setVolume(uInt32 volume) override;
/**
Adjusts the volume of the sound device based on the given direction.
@ -135,143 +96,13 @@ class SoundSDL2 : public Sound
*/
void adjustVolume(Int8 direction) override;
public:
/**
Saves the current state of this device to the given Serializer.
@param out The serializer device to save to.
@return The result of the save. True on success, false on failure.
*/
bool save(Serializer& out) const override;
/**
Loads the current state of this device from the given Serializer.
@param in The Serializer device to load from.
@return The result of the load. True on success, false on failure.
*/
bool load(Serializer& in) override;
/**
Get a descriptor for this console class (used in error checking).
@return The name of the object
*/
string name() const override { return "TIASound"; }
protected:
/**
Invoked by the sound callback to process the next sound fragment.
The stream is 16-bits (even though the callback is 8-bits), since
the TIASnd class always generates signed 16-bit stereo samples.
@param stream Pointer to the start of the fragment
@param length Length of the fragment
*/
void processFragment(Int16* stream, uInt32 length);
protected:
// Struct to hold information regarding a TIA sound register write
struct RegWrite
{
uInt16 addr;
uInt8 value;
double delta;
};
/**
A queue class used to hold TIA sound register writes before being
processed while creating a sound fragment.
*/
class RegWriteQueue
{
public:
/**
Create a new queue instance with the specified initial
capacity. If the queue ever reaches its capacity then it will
automatically increase its size.
*/
RegWriteQueue(uInt32 capacity = 512);
public:
/**
Clear any items stored in the queue.
*/
void clear();
/**
Dequeue the first object in the queue.
*/
void dequeue();
/**
Return the duration of all the items in the queue.
*/
double duration() const;
/**
Enqueue the specified object.
*/
void enqueue(const RegWrite& info);
/**
Return the item at the front on the queue.
@return The item at the front of the queue.
*/
RegWrite& front() const;
/**
Answers the number of items currently in the queue.
@return The number of items in the queue.
*/
uInt32 size() const;
private:
// Increase the size of the queue
void grow();
private:
unique_ptr<RegWrite[]> myBuffer;
uInt32 myCapacity;
uInt32 mySize;
uInt32 myHead;
uInt32 myTail;
private:
// Following constructors and assignment operators not supported
RegWriteQueue(const RegWriteQueue&) = delete;
RegWriteQueue(RegWriteQueue&&) = delete;
RegWriteQueue& operator=(const RegWriteQueue&) = delete;
RegWriteQueue& operator=(RegWriteQueue&&) = delete;
};
private:
// TIASound emulation object
TIASound myTIASound;
// Indicates if the sound subsystem is to be initialized
bool myIsEnabled;
// Indicates if the sound device was successfully initialized
bool myIsInitializedFlag;
// Indicates the cycle when a sound register was last set
Int32 myLastRegisterSetCycle;
// Indicates the number of channels (mono or stereo)
uInt32 myNumChannels;
// Log base 2 of the selected fragment size
double myFragmentSizeLogBase2;
// The myFragmentSizeLogBase2 variable is used in only two places,
// both of which involve an expensive division in the sound
// processing callback
// These are pre-computed to speed up the callback as much as possible
double myFragmentSizeLogDiv1, myFragmentSizeLogDiv2;
// Indicates if the sound is currently muted
bool myIsMuted;
@ -281,12 +112,9 @@ class SoundSDL2 : public Sound
// Audio specification structure
SDL_AudioSpec myHardwareSpec;
// Queue of TIA register writes
RegWriteQueue myRegWriteQueue;
private:
// Callback function invoked by the SDL Audio library when it needs data
static void callback(void* udata, uInt8* stream, int len);
static void getSamples(void* udata, uInt8* stream, int len);
// Following constructors and assignment operators not supported
SoundSDL2() = delete;

View File

@ -430,9 +430,7 @@ void Console::initializeAudio()
const string& sound = myProperties.get(Cartridge_Sound);
myOSystem.sound().close();
myOSystem.sound().setChannels(sound == "STEREO" ? 2 : 1);
myOSystem.sound().setFrameRate(myFramerate);
myOSystem.sound().open();
myOSystem.sound().open(sound == "STEREO");
// Make sure auto-frame calculation is only enabled when necessary
myTIA->enableAutoFrame(framerate <= 0);
@ -838,7 +836,6 @@ void Console::setFramerate(float framerate)
{
myFramerate = framerate;
myOSystem.setFramerate(framerate);
myOSystem.sound().setFrameRate(framerate);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

View File

@ -22,7 +22,6 @@
class OSystem;
#include "Serializable.hxx"
#include "bspf.hxx"
/**
@ -32,11 +31,11 @@ class OSystem;
@author Stephen Anthony
@version $Id$
*/
class Sound : public Serializable
class Sound
{
public:
/**
Create a new sound object. The init method must be invoked before
Create a new sound object. The open method must be invoked before
using the object.
*/
Sound(OSystem& osystem) : myOSystem(osystem) { }
@ -50,34 +49,13 @@ class Sound : public Serializable
*/
virtual void setEnabled(bool enable) = 0;
/**
The system cycle counter is being adjusting by the specified amount. Any
members using the system cycle counter should be adjusted as needed.
@param amount The amount the cycle counter is being adjusted by
*/
virtual void adjustCycleCounter(Int32 amount) = 0;
/**
Sets the number of channels (mono or stereo sound).
@param channels The number of channels
*/
virtual void setChannels(uInt32 channels) = 0;
/**
Sets the display framerate. Sound generation for NTSC and PAL games
depends on the framerate, so we need to set it here.
@param framerate The base framerate depending on NTSC or PAL ROM
*/
virtual void setFrameRate(float framerate) = 0;
/**
Start the sound system, initializing it if necessary. This must be
called before any calls are made to derived methods.
@param stereo The number of channels (mono -> 1, stereo -> 2)
*/
virtual void open() = 0;
virtual void open(bool stereo) = 0;
/**
Should be called to stop the sound system. Once called the sound
@ -97,23 +75,14 @@ class Sound : public Serializable
*/
virtual void reset() = 0;
/**
Sets the sound register to a given value.
@param addr The register address
@param value The value to save into the register
@param cycle The system cycle at which the register is being updated
*/
virtual void set(uInt16 addr, uInt8 value, Int32 cycle) = 0;
/**
Sets the volume of the sound device to the specified level. The
volume is given as a percentage from 0 to 100. Values outside
this range indicate that the volume shouldn't be changed at all.
@param percent The new volume percentage level for the sound device
@param volume The new volume percentage level for the sound device
*/
virtual void setVolume(Int32 percent) = 0;
virtual void setVolume(uInt32 volume) = 0;
/**
Adjusts the volume of the sound device based on the given direction.

View File

@ -21,26 +21,23 @@
#include <cstdlib>
#include <cstring>
#include "bspf.hxx"
#ifdef DEBUGGER_SUPPORT
#include "CartDebug.hxx"
#endif
#include "Console.hxx"
#include "Control.hxx"
#include "Device.hxx"
#include "M6502.hxx"
#include "Settings.hxx"
#include "Sound.hxx"
#include "System.hxx"
#include "TIATables.hxx"
#include "TIA.hxx"
#define HBLANK 68
#define CLAMP_POS(reg) if(reg < 0) { reg += 160; } reg %= 160;
static Int32 START_SLINE = -1;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TIA::TIA(Console& console, Sound& sound, Settings& settings)
: myConsole(console),
@ -65,6 +62,7 @@ TIA::TIA(Console& console, Sound& sound, Settings& settings)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void TIA::initialize()
{
START_SLINE = -1;
myFramePointer = nullptr;
myFramePointerOffset = myFramePointerClocks = myStopDisplayOffset = 0;
@ -214,9 +212,6 @@ void TIA::systemCyclesReset()
// Get the current system cycle
uInt32 cycles = mySystem->cycles();
// Adjust the sound cycle indicator
mySound.adjustCycleCounter(-1 * cycles);
// Adjust the dump cycle
myDumpDisabledCycle -= cycles;
@ -344,7 +339,7 @@ bool TIA::save(Serializer& out) const
out.putInt(myPALFrameCounter);
// Save the sound sample stuff ...
mySound.save(out);
myTIASound.save(out);
}
catch(...)
{
@ -448,7 +443,7 @@ bool TIA::load(Serializer& in)
myPALFrameCounter = in.getInt();
// Load the sound sample stuff ...
mySound.load(in);
myTIASound.load(in);
// Reset TIA bits to be on
enableBits(true);
@ -540,7 +535,7 @@ inline void TIA::startFrame()
// so that we can adjust the frame's starting clock by this amount. This
// is necessary since some games position objects during VSYNC and the
// TIA's internal counters are not reset by VSYNC.
uInt32 clocks = ((mySystem->cycles() * 3) - myClockWhenFrameStarted) % 228;
uInt32 clocks = clocksThisLine();
// Ask the system to reset the cycle count so it doesn't overflow
mySystem->resetCycles();
@ -587,6 +582,8 @@ inline void TIA::startFrame()
myFrameCounter++;
if(myScanlineCountForLastFrame >= 287)
myPALFrameCounter++;
START_SLINE = -1;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -1230,6 +1227,16 @@ void TIA::updateFrame(Int32 clock)
myHMOVEBlankEnabled = false;
}
#if 0
if(START_SLINE != scanlines())
{
cerr << " => scanline: " << scanlines() << endl;
// cerr << "scanline: " << (line-1) << endl;
myTIASound.queueSamples();
START_SLINE = scanlines();
}
#endif
// TODO - this needs to be updated to actually do as the comment suggests
#if 1
// See if we're at the end of a scanline
@ -1239,8 +1246,17 @@ void TIA::updateFrame(Int32 clock)
// of the player has passed. However, for now we'll just reset at the
// end of the scanline since the other way would be too slow.
mySuppressP0 = mySuppressP1 = 0;
#if 1
if(START_SLINE != scanlines())
{
cerr << " => scanline: " << scanlines() << endl;
myTIASound.queueSamples();
}
#endif
}
#endif
}
}
@ -1252,6 +1268,14 @@ inline void TIA::waitHorizontalSync()
if(cyclesToEndOfLine < 76)
mySystem->incrementCycles(cyclesToEndOfLine);
#if 1
{
cerr << " => scanline(W): " << scanlines() << endl;
myTIASound.queueSamples();
START_SLINE = scanlines();
}
#endif
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -1865,42 +1889,48 @@ bool TIA::poke(uInt16 addr, uInt8 value)
case AUDC0: // Audio control 0
{
myAUDC0 = value & 0x0f;
mySound.set(addr, value, mySystem->cycles());
cerr << scanlines() << ": C0, " << hex << int(myAUDC0) << ", " << dec << ((clock - myClockWhenFrameStarted) % 228) << endl;
myTIASound.writeAudC0(value, (clock - myClockWhenFrameStarted) % 228);
break;
}
case AUDC1: // Audio control 1
{
myAUDC1 = value & 0x0f;
mySound.set(addr, value, mySystem->cycles());
cerr << scanlines() << ": C1, " << hex << int(myAUDC1) << ", " << dec << ((clock - myClockWhenFrameStarted) % 228) << endl;
myTIASound.writeAudC1(value, (clock - myClockWhenFrameStarted) % 228);
break;
}
case AUDF0: // Audio frequency 0
{
myAUDF0 = value & 0x1f;
mySound.set(addr, value, mySystem->cycles());
cerr << scanlines() << ": F0, " << hex << int(myAUDF0) << ", " << dec << ((clock - myClockWhenFrameStarted) % 228) << endl;
myTIASound.writeAudF0(value, (clock - myClockWhenFrameStarted) % 228);
break;
}
case AUDF1: // Audio frequency 1
{
myAUDF1 = value & 0x1f;
mySound.set(addr, value, mySystem->cycles());
cerr << scanlines() << ": F1, " << hex << int(myAUDF1) << ", " << dec << ((clock - myClockWhenFrameStarted) % 228) << endl;
myTIASound.writeAudF1(value, (clock - myClockWhenFrameStarted) % 228);
break;
}
case AUDV0: // Audio volume 0
{
myAUDV0 = value & 0x0f;
mySound.set(addr, value, mySystem->cycles());
cerr << scanlines() << ": V0, " << hex << int(myAUDV0) << ", " << dec << ((clock - myClockWhenFrameStarted) % 228) << endl;
myTIASound.writeAudV0(value, (clock - myClockWhenFrameStarted) % 228);
break;
}
case AUDV1: // Audio volume 1
{
myAUDV1 = value & 0x0f;
mySound.set(addr, value, mySystem->cycles());
cerr << scanlines() << ": V1, " << hex << int(myAUDV1) << ", " << dec << ((clock - myClockWhenFrameStarted) % 228) << endl;
myTIASound.writeAudV1(value, (clock - myClockWhenFrameStarted) % 228);
break;
}

View File

@ -28,6 +28,7 @@ class Sound;
#include "Device.hxx"
#include "System.hxx"
#include "TIATables.hxx"
#include "TIASnd.hxx"
/**
This class is a device that emulates the Television Interface Adaptor
@ -96,6 +97,14 @@ class TIA : public Device
*/
void install(System& system, Device& device);
/**
Get the TIA sound object associated with the TIA.
@param out The Serializer object to use
@return False on any errors, else true
*/
TIASound& sound() { return myTIASound; }
/**
Save the current state of this device to the given Serializer.
@ -407,6 +416,9 @@ class TIA : public Device
// Settings object the TIA is associated with
Settings& mySettings;
// TIASound emulation object
TIASound myTIASound;
// Pointer to the current frame buffer
unique_ptr<uInt8[]> myCurrentFrameBuffer;

View File

@ -22,45 +22,58 @@
#include "TIASnd.hxx"
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TIASound::TIASound(Int32 outputFrequency)
: myChannelMode(Hardware2Stereo),
myOutputFrequency(outputFrequency),
myOutputCounter(0),
myVolumePercentage(100)
TIASound::TIASound()
: myChannelMode(Hardware2Mono),
myHWVol(100)
{
// Build volume lookup table
constexpr double ra = 1.0 / 30.0;
constexpr double rb = 1.0 / 15.0;
constexpr double rc = 1.0 / 7.5;
constexpr double rd = 1.0 / 3.75;
memset(myVolLUT, 0, sizeof(uInt16)*256*101);
for(int i = 1; i < 256; ++i)
{
double r2 = 0.0;
if(i & 0x01) r2 += ra;
if(i & 0x02) r2 += rb;
if(i & 0x04) r2 += rc;
if(i & 0x08) r2 += rd;
if(i & 0x10) r2 += ra;
if(i & 0x20) r2 += rb;
if(i & 0x40) r2 += rc;
if(i & 0x80) r2 += rd;
r2 = 1.0 / r2;
uInt16 vol = uInt16(32768.0 * (1.0 - r2 / (1.0 + r2)) + 0.5);
// Pre-calculate all possible volume levels
for(int j = 0; j <= 100; ++j)
myVolLUT[i][j] = vol * j / 100;
}
reset();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void TIASound::reset()
{
// Fill the polynomials
polyInit(Bit4, 4, 4, 3);
polyInit(Bit5, 5, 5, 3);
polyInit(Bit9, 9, 9, 5);
myAud0State.reset();
myAud1State.reset();
while(!mySamples.empty()) mySamples.pop();
// Initialize instance variables
for(int chan = 0; chan <= 1; ++chan)
for(int i = 0; i < 2; ++i)
{
myVolume[chan] = 0;
myDivNCnt[chan] = 0;
myDivNMax[chan] = 0;
myDiv3Cnt[chan] = 3;
myAUDC[chan] = 0;
myAUDF[chan] = 0;
myAUDV[chan] = 0;
myP4[chan] = 0;
myP5[chan] = 0;
myP9[chan] = 0;
myAudC0[i][0] = myAudC0[i][1] = 0;
myAudC1[i][0] = myAudC1[i][1] = 0;
myAudF0[i] = myAudF1[i] = 0;
myAudV0[i] = myAudV1[i] = 0;
}
myOutputCounter = 0;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void TIASound::outputFrequency(Int32 freq)
{
myOutputFrequency = freq;
myDeferredC0 = myDeferredC1 = myDeferredF0 = myDeferredF1 =
myDeferredV0 = myDeferredV1 = 0xff;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -73,318 +86,397 @@ string TIASound::channels(uInt32 hardware, bool stereo)
switch(myChannelMode)
{
case Hardware1: return "Hardware1";
case Hardware2Mono: return "Hardware2Mono";
case Hardware2Stereo: return "Hardware2Stereo";
case Hardware1: return "Hardware1";
default: return EmptyString;
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void TIASound::set(uInt16 address, uInt8 value)
void TIASound::writeAudC0(uInt8 value, uInt32 clock)
{
int chan = ~address & 0x1;
switch(address)
{
case TIARegister::AUDC0:
case TIARegister::AUDC1:
myAUDC[chan] = value & 0x0f;
break;
case TIARegister::AUDF0:
case TIARegister::AUDF1:
myAUDF[chan] = value & 0x1f;
break;
case TIARegister::AUDV0:
case TIARegister::AUDV1:
myAUDV[chan] = (value & 0x0f) << AUDV_SHIFT;
break;
default:
return;
}
uInt16 newVal = 0;
// An AUDC value of 0 is a special case
if (myAUDC[chan] == SET_TO_1 || myAUDC[chan] == POLY5_POLY5)
{
// Indicate the clock is zero so no processing will occur,
// and set the output to the selected volume
newVal = 0;
myVolume[chan] = (myAUDV[chan] * myVolumePercentage) / 100;
}
else
{
// Otherwise calculate the 'divide by N' value
newVal = myAUDF[chan] + 1;
// If bits 2 & 3 are set, then multiply the 'div by n' count by 3
if((myAUDC[chan] & DIV3_MASK) == DIV3_MASK && myAUDC[chan] != POLY5_DIV3)
newVal *= 3;
}
// Only reset those channels that have changed
if(newVal != myDivNMax[chan])
{
// Reset the divide by n counters
myDivNMax[chan] = newVal;
// If the channel is now volume only or was volume only,
// reset the counter (otherwise let it complete the previous)
if ((myDivNCnt[chan] == 0) || (newVal == 0))
myDivNCnt[chan] = newVal;
}
value &= 0x0F;
if(clock <= Cycle1Phase1) myAudC0[0][0] = value;
if(clock <= Cycle1Phase2) myAudC0[0][1] = value;
if(clock <= Cycle2Phase1) myAudC0[1][0] = value;
if(clock <= Cycle2Phase2) myAudC0[1][1] = value;
else // We missed both cycles, so defer write until next line
myDeferredC0 = value;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt8 TIASound::get(uInt16 address) const
void TIASound::writeAudC1(uInt8 value, uInt32 clock)
{
switch(address)
value &= 0x0F;
if(clock <= Cycle1Phase1) myAudC1[0][0] = value;
if(clock <= Cycle1Phase2) myAudC1[0][1] = value;
if(clock <= Cycle2Phase1) myAudC1[1][0] = value;
if(clock <= Cycle2Phase2) myAudC1[1][1] = value;
else // We missed both cycles, so defer write until next line
myDeferredC1 = value;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void TIASound::writeAudF0(uInt8 value, uInt32 clock)
{
value &= 0x1F;
if(clock <= Cycle1Phase1) myAudF0[0] = value;
if(clock <= Cycle2Phase1) myAudF0[1] = value;
else // We missed both cycles, so defer write until next line
myDeferredF0 = value;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void TIASound::writeAudF1(uInt8 value, uInt32 clock)
{
value &= 0x1F;
if(clock <= Cycle1Phase1) myAudF1[0] = value;
if(clock <= Cycle2Phase1) myAudF1[1] = value;
else // We missed both cycles, so defer write until next line
myDeferredF1 = value;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void TIASound::writeAudV0(uInt8 value, uInt32 clock)
{
value &= 0x0F;
if(clock <= Cycle1Phase2) myAudV0[0] = value;
if(clock <= Cycle2Phase2) myAudV0[1] = value;
else // We missed both cycles, so defer write until next line
myDeferredV0 = value;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void TIASound::writeAudV1(uInt8 value, uInt32 clock)
{
value &= 0x0F;
if(clock <= Cycle1Phase2) myAudV1[0] = value;
if(clock <= Cycle2Phase2) myAudV1[1] = value;
else // We missed both cycles, so defer write until next line
myDeferredV1 = value;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void TIASound::queueSamples()
{
// Cycle 1
bool aud0_c1 = updateAudioState(myAud0State, myAudF0[0], myAudC0[0]);
bool aud1_c1 = updateAudioState(myAud1State, myAudF1[0], myAudC1[0]);
// Cycle 2
bool aud0_c2 = updateAudioState(myAud0State, myAudF0[1], myAudC0[1]);
bool aud1_c2 = updateAudioState(myAud1State, myAudF1[1], myAudC1[1]);
switch(myChannelMode)
{
case TIARegister::AUDC0: return myAUDC[0];
case TIARegister::AUDC1: return myAUDC[1];
case TIARegister::AUDF0: return myAUDF[0];
case TIARegister::AUDF1: return myAUDF[1];
case TIARegister::AUDV0: return myAUDV[0] >> AUDV_SHIFT;
case TIARegister::AUDV1: return myAUDV[1] >> AUDV_SHIFT;
default: return 0;
case Hardware2Mono: // mono sampling with 2 hardware channels
{
uInt32 idx1 = 0;
if(aud0_c1) idx1 |= myAudV0[0];
if(aud1_c1) idx1 |= (myAudV1[0] << 4);
uInt16 vol1 = myVolLUT[idx1][myHWVol];
mySamples.push(vol1);
mySamples.push(vol1);
uInt32 idx2 = 0;
if(aud0_c2) idx2 |= myAudV0[1];
if(aud1_c2) idx2 |= (myAudV1[1] << 4);
uInt16 vol2 = myVolLUT[idx2][myHWVol];
mySamples.push(vol2);
mySamples.push(vol2);
break;
}
case Hardware2Stereo: // stereo sampling with 2 hardware channels
{
mySamples.push(myVolLUT[aud0_c1 ? myAudV0[0] : 0][myHWVol]);
mySamples.push(myVolLUT[aud1_c1 ? myAudV1[0] : 0][myHWVol]);
mySamples.push(myVolLUT[aud0_c2 ? myAudV0[1] : 0][myHWVol]);
mySamples.push(myVolLUT[aud1_c2 ? myAudV1[1] : 0][myHWVol]);
break;
}
case Hardware1: // mono/stereo sampling with only 1 hardware channel
{
uInt32 idx1 = 0;
if(aud0_c1) idx1 |= myAudV0[0];
if(aud1_c1) idx1 |= (myAudV1[0] << 4);
mySamples.push(myVolLUT[idx1][myHWVol]);
uInt32 idx2 = 0;
if(aud0_c2) idx2 |= myAudV0[1];
if(aud1_c2) idx2 |= (myAudV1[1] << 4);
mySamples.push(myVolLUT[idx2][myHWVol]);
break;
}
}
/////////////////////////////////////////////////
// End of line, allow deferred updates
/////////////////////////////////////////////////
// AUDC0
if(myDeferredC0 != 0xff) // write occurred after cycle2:phase2
{
myAudC0[0][0] = myAudC0[0][1] = myAudC0[1][0] = myAudC0[1][1] = myDeferredC0;
myDeferredC0 = 0xff;
}
else // write occurred after cycle1:phase1
myAudC0[0][0] = myAudC0[1][1];
// AUDC1
if(myDeferredC1 != 0xff) // write occurred after cycle2:phase2
{
myAudC1[0][0] = myAudC1[0][1] = myAudC1[1][0] = myAudC1[1][1] = myDeferredC1;
myDeferredC1 = 0xff;
}
else // write occurred after cycle1:phase1
myAudC1[0][0] = myAudC1[1][1];
// AUDF0
if(myDeferredF0 != 0xff) // write occurred after cycle2:phase2
{
myAudF0[0] = myAudF0[1] = myDeferredF0;
myDeferredF0 = 0xff;
}
else // write occurred after cycle1:phase1
myAudF0[0] = myAudF0[1];
// AUDF1
if(myDeferredF1 != 0xff) // write occurred after cycle2:phase2
{
myAudF1[0] = myAudF1[1] = myDeferredF1;
myDeferredF1 = 0xff;
}
else // write occurred after cycle1:phase1
myAudF1[0] = myAudF1[1];
// AUDV0
if(myDeferredV0 != 0xff) // write occurred after cycle2:phase2
{
myAudV0[0] = myAudV0[1] = myDeferredV0;
myDeferredV0 = 0xff;
}
else // write occurred after cycle1:phase1
myAudV0[0] = myAudV0[1];
// AUDV1
if(myDeferredV1 != 0xff) // write occurred after cycle2:phase2
{
myAudV1[0] = myAudV1[1] = myDeferredV1;
myDeferredV1 = 0xff;
}
else // write occurred after cycle1:phase1
myAudV1[0] = myAudV1[1];
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void TIASound::volume(uInt32 percent)
{
if(percent <= 100)
myVolumePercentage = percent;
myHWVol = std::min(percent, 100u);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void TIASound::process(Int16* buffer, uInt32 samples)
bool TIASound::updateAudioState(AudioState& state, uInt32 audf, uInt32* audc)
{
// Make temporary local copy
uInt8 audc0 = myAUDC[0], audc1 = myAUDC[1];
uInt8 p5_0 = myP5[0], p5_1 = myP5[1];
uInt8 div_n_cnt0 = myDivNCnt[0], div_n_cnt1 = myDivNCnt[1];
Int16 v0 = myVolume[0], v1 = myVolume[1];
bool pulse_fb; // pulse counter LFSR feedback
// Take external volume into account
Int16 audv0 = (myAUDV[0] * myVolumePercentage) / 100,
audv1 = (myAUDV[1] * myVolumePercentage) / 100;
// Loop until the sample buffer is full
while(samples > 0)
// -- Logic updated on phase 1 of the audio clock --
if(state.clk_en)
{
// Process channel 0
if (div_n_cnt0 > 1)
// Latch bit 4 of noise counter
state.noise_cnt_4 = state.noise_cnt & 0x01;
// Latch pulse counter hold condition
switch(audc[0] & 0x03)
{
div_n_cnt0--;
}
else if (div_n_cnt0 == 1)
{
int prev_bit5 = Bit5[p5_0];
div_n_cnt0 = myDivNMax[0];
// The P5 counter has multiple uses, so we increment it here
p5_0++;
if (p5_0 == POLY5_SIZE)
p5_0 = 0;
// Check clock modifier for clock tick
if ((audc0 & 0x02) == 0 ||
((audc0 & 0x01) == 0 && Div31[p5_0]) ||
((audc0 & 0x01) == 1 && Bit5[p5_0]) ||
((audc0 & 0x0f) == POLY5_DIV3 && Bit5[p5_0] != prev_bit5))
{
if (audc0 & 0x04) // Pure modified clock selected
{
if ((audc0 & 0x0f) == POLY5_DIV3) // POLY5 -> DIV3 mode
{
if ( Bit5[p5_0] != prev_bit5 )
{
myDiv3Cnt[0]--;
if ( !myDiv3Cnt[0] )
{
myDiv3Cnt[0] = 3;
v0 = v0 ? 0 : audv0;
}
}
}
else
{
// If the output was set turn it off, else turn it on
v0 = v0 ? 0 : audv0;
}
}
else if (audc0 & 0x08) // Check for p5/p9
{
if (audc0 == POLY9) // Check for poly9
{
// Increase the poly9 counter
myP9[0]++;
if (myP9[0] == POLY9_SIZE)
myP9[0] = 0;
v0 = Bit9[myP9[0]] ? audv0 : 0;
}
else if ( audc0 & 0x02 )
{
v0 = (v0 || audc0 & 0x01) ? 0 : audv0;
}
else // Must be poly5
{
v0 = Bit5[p5_0] ? audv0 : 0;
}
}
else // Poly4 is the only remaining option
{
// Increase the poly4 counter
myP4[0]++;
if (myP4[0] == POLY4_SIZE)
myP4[0] = 0;
v0 = Bit4[myP4[0]] ? audv0 : 0;
}
}
}
// Process channel 1
if (div_n_cnt1 > 1)
{
div_n_cnt1--;
}
else if (div_n_cnt1 == 1)
{
int prev_bit5 = Bit5[p5_1];
div_n_cnt1 = myDivNMax[1];
// The P5 counter has multiple uses, so we increment it here
p5_1++;
if (p5_1 == POLY5_SIZE)
p5_1 = 0;
// Check clock modifier for clock tick
if ((audc1 & 0x02) == 0 ||
((audc1 & 0x01) == 0 && Div31[p5_1]) ||
((audc1 & 0x01) == 1 && Bit5[p5_1]) ||
((audc1 & 0x0f) == POLY5_DIV3 && Bit5[p5_1] != prev_bit5))
{
if (audc1 & 0x04) // Pure modified clock selected
{
if ((audc1 & 0x0f) == POLY5_DIV3) // POLY5 -> DIV3 mode
{
if ( Bit5[p5_1] != prev_bit5 )
{
myDiv3Cnt[1]--;
if ( ! myDiv3Cnt[1] )
{
myDiv3Cnt[1] = 3;
v1 = v1 ? 0 : audv1;
}
}
}
else
{
// If the output was set turn it off, else turn it on
v1 = v1 ? 0 : audv1;
}
}
else if (audc1 & 0x08) // Check for p5/p9
{
if (audc1 == POLY9) // Check for poly9
{
// Increase the poly9 counter
myP9[1]++;
if (myP9[1] == POLY9_SIZE)
myP9[1] = 0;
v1 = Bit9[myP9[1]] ? audv1 : 0;
}
else if ( audc1 & 0x02 )
{
v1 = (v1 || audc1 & 0x01) ? 0 : audv1;
}
else // Must be poly5
{
v1 = Bit5[p5_1] ? audv1 : 0;
}
}
else // Poly4 is the only remaining option
{
// Increase the poly4 counter
myP4[1]++;
if (myP4[1] == POLY4_SIZE)
myP4[1] = 0;
v1 = Bit4[myP4[1]] ? audv1 : 0;
}
}
}
myOutputCounter += myOutputFrequency;
switch(myChannelMode)
{
case Hardware2Mono: // mono sampling with 2 hardware channels
while((samples > 0) && (myOutputCounter >= 31400))
{
Int16 byte = v0 + v1;
*(buffer++) = byte;
*(buffer++) = byte;
myOutputCounter -= 31400;
samples--;
}
case 0x00:
state.pulse_cnt_hold = false;
break;
case Hardware2Stereo: // stereo sampling with 2 hardware channels
while((samples > 0) && (myOutputCounter >= 31400))
{
*(buffer++) = v0;
*(buffer++) = v1;
myOutputCounter -= 31400;
samples--;
}
case 0x01:
state.pulse_cnt_hold = false;
break;
case 0x02:
state.pulse_cnt_hold = ((state.noise_cnt & 0x1e) != 0x02);
break;
case 0x03:
state.pulse_cnt_hold = !state.noise_cnt_4;
break;
}
case Hardware1: // mono/stereo sampling with only 1 hardware channel
while((samples > 0) && (myOutputCounter >= 31400))
{
*(buffer++) = v0 + v1;
myOutputCounter -= 31400;
samples--;
}
// Latch noise counter LFSR feedback
switch(audc[0] & 0x03)
{
case 0x00:
state.noise_fb = ((state.pulse_cnt & 0x01) ^ (state.noise_cnt & 0x01)) |
!((state.noise_cnt ? 1 : 0) | (state.pulse_cnt != 0x0a)) |
!(audc[0] & 0x0c);
break;
default:
state.noise_fb = (((state.noise_cnt & 0x04) ? 1 : 0) ^ (state.noise_cnt & 0x01)) |
!state.noise_cnt;
break;
}
}
// Save for next round
myP5[0] = p5_0;
myP5[1] = p5_1;
myVolume[0] = v0;
myVolume[1] = v1;
myDivNCnt[0] = div_n_cnt0;
myDivNCnt[1] = div_n_cnt1;
}
// Set (or clear) audio clock enable
state.clk_en = (state.div_cnt == audf);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void TIASound::polyInit(uInt8* poly, int size, int f0, int f1)
{
int mask = (1 << size) - 1, x = mask;
// Increment clock divider counter
if((state.div_cnt == audf) || (state.div_cnt == 31))
state.div_cnt = 0;
else
state.div_cnt++;
for(int i = 0; i < mask; i++)
// -- Logic updated on phase 2 of the audio clock --
if(state.clk_en)
{
int bit0 = ( ( size - f0 ) ? ( x >> ( size - f0 ) ) : x ) & 0x01;
int bit1 = ( ( size - f1 ) ? ( x >> ( size - f1 ) ) : x ) & 0x01;
poly[i] = x & 1;
// calculate next bit
x = ( x >> 1 ) | ( ( bit0 ^ bit1 ) << ( size - 1) );
// Evaluate pulse counter combinatorial logic
switch(audc[1] >> 2)
{
case 0x00:
pulse_fb = (((state.pulse_cnt & 0x02) ? 1 : 0) ^ (state.pulse_cnt & 0x01)) &
(state.pulse_cnt != 0x0a) && (audc[1] & 0x03);
break;
case 0x01:
pulse_fb = !(state.pulse_cnt & 0x08);
break;
case 0x02:
pulse_fb = !state.noise_cnt_4;
break;
case 0x03:
pulse_fb = !((state.pulse_cnt & 0x02) || !(state.pulse_cnt & 0x0e));
break;
}
// Increment noise counter
state.noise_cnt >>= 1;
if(state.noise_fb)
state.noise_cnt |= 0x10;
// Increment pulse counter
if(!state.pulse_cnt_hold)
{
state.pulse_cnt = (~(state.pulse_cnt >> 1) & 0x07);
if(pulse_fb)
state.pulse_cnt |= 0x08;
}
}
// Pulse generator output
return (state.pulse_cnt & 0x01);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const uInt8 TIASound::Div31[POLY5_SIZE] = {
0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
bool TIASound::save(Serializer& out) const
{
#if 0
try
{
out.putString(name());
// Only get the TIA sound registers if sound is enabled
if(myIsInitializedFlag)
{
out.putByte(myTIASound.get(TIARegister::AUDC0));
out.putByte(myTIASound.get(TIARegister::AUDC1));
out.putByte(myTIASound.get(TIARegister::AUDF0));
out.putByte(myTIASound.get(TIARegister::AUDF1));
out.putByte(myTIASound.get(TIARegister::AUDV0));
out.putByte(myTIASound.get(TIARegister::AUDV1));
}
else
for(int i = 0; i < 6; ++i)
out.putByte(0);
out.putInt(myLastRegisterSetCycle);
}
catch(...)
{
myOSystem.logMessage("ERROR: SoundSDL2::save", 0);
return false;
}
#endif
return true; // TODO
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool TIASound::load(Serializer& in)
{
#if 0
try
{
if(in.getString() != name())
return false;
// 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(TIARegister::AUDC0, in.getByte());
myTIASound.set(TIARegister::AUDC1, in.getByte());
myTIASound.set(TIARegister::AUDF0, in.getByte());
myTIASound.set(TIARegister::AUDF1, in.getByte());
myTIASound.set(TIARegister::AUDV0, in.getByte());
myTIASound.set(TIARegister::AUDV1, in.getByte());
if(!myIsMuted) SDL_PauseAudio(0);
}
else
for(int i = 0; i < 6; ++i)
in.getByte();
myLastRegisterSetCycle = in.getInt();
}
catch(...)
{
myOSystem.logMessage("ERROR: SoundSDL2::load", 0);
return false;
}
#endif
return true; // TODO
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool TIASound::AudioState::save(Serializer& out) const
{
try
{
out.putBool(clk_en);
out.putBool(noise_fb);
out.putBool(noise_cnt_4);
out.putBool(pulse_cnt_hold);
out.putInt(div_cnt);
out.putInt(noise_cnt);
out.putInt(pulse_cnt);
}
catch(...)
{
// FIXME myOSystem.logMessage("ERROR: TIASnd_state::save", 0);
return false;
}
return true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool TIASound::AudioState::load(Serializer& in)
{
try
{
clk_en = in.getBool();
noise_fb = in.getBool();
noise_cnt_4 = in.getBool();
pulse_cnt_hold = in.getBool();
div_cnt = in.getInt();
noise_cnt = in.getInt();
pulse_cnt = in.getInt();
}
catch(...)
{
// FIXME myOSystem.logMessage("ERROR: TIASnd_state::load", 0);
return false;
}
return true;
}

View File

@ -17,40 +17,82 @@
// $Id$
//============================================================================
//-----------------------------------------------------------------------------
//
// Title: TIA Audio Generator
//
// Project - MMDC
// Version - _VERSION
// Author - Chris Brenner
// Description - This code is translated more or less verbatim from my Verilog
// code for my FPGA project. It provides core logic that can be used to achieve
// cycle accurate emulation of the Atari 2600 TIA audio blocks.
//
// The core logic is broken up into two functions. The updateAudioState()
// function contains the logic for the clock divider and pulse generator. It is
// used to update the state of the audio logic, and should be called once for
// every audio clock cycle for each channel.
//
// The queueSamples() function implements the volume control, and generates
// audio samples. It depends on the state of the pulse generator of both audio
// channels, so it internally calls the updateAudioState() function.
//
// The constructor is called on startup in order to initialize the audio logic
// and volume control LUT.
//
// The accuracy of the emulation is dependent upon the accuracy of the data
// contained in the AUDCx, AUDFx, and AUDVx registers. It is important that these
// registers contain the most recent value written prior to the current clock
// cycle and phase.
//
// The TIA audio clock is a 31.4 KHz two phase clock that occurs twice every
// horizontal scan line. The mapping of the cycles and phases are as follows.
// Cycle 1 - Phase 1: color clock 8
// Cycle 1 - Phase 2: color clock 36
// Cycle 2 - Phase 1: color clock 80
// Cycle 2 - Phase 2: color clock 148
//
// Since software can change the value of the registers in between clock cycles
// and phases, it's necessary to develop a mechanism for keeping these registers
// up to date when doing bulk processing. One method would be to time stamp the
// register writes, and queue them into a FIFO, but I leave this design decision
// to the emulator developer. The phase requirements are listed here.
// AUDC0, AUDC1: used by the pulse generator at both phases
// AUDF0, AUDF1: used by the clock divider at phase 1
// AUDV0, AUDV1: used by the volume control at phase 2
//
// In a real 2600, the volume control is analog, and is affected the instant when
// AUDVx is written. However, since we generate audio samples at phase 2 of the
// clock, the granularity of volume control updates is limited to our sample rate,
// and changes that occur in between clocks result in only the last change prior
// to phase 2 having an affect on the volume.
//
// @author Chris Brenner (original C implementation) and
// Stephen Anthony (C++ conversion, integration into Stella)
//-----------------------------------------------------------------------------
#ifndef TIASOUND_HXX
#define TIASOUND_HXX
#include <queue>
#include "bspf.hxx"
#include "Serializable.hxx"
/**
This class implements a fairly accurate emulation of the TIA sound
hardware. This class uses code/ideas from z26 and MESS.
Currently, the sound generation routines work at 31400Hz only.
Resampling can be done by passing in a different output frequency.
@author Bradford W. Mott, Stephen Anthony, z26 and MESS teams
@version $Id$
*/
class TIASound
class TIASound : public Serializable
{
public:
/**
Create a new TIA Sound object using the specified output frequency
Create a new TIA Sound object.
*/
TIASound(Int32 outputFrequency = 31400);
TIASound();
public:
/**
Reset the sound emulation to its power-on state
Reset the sound emulation to its power-on state.
*/
void reset();
/**
Set the frequency output samples should be generated at
*/
void outputFrequency(Int32 freq);
/**
Selects the number of audio channels per sample. There are two factors
to consider: hardware capability and desired mixing.
@ -63,117 +105,133 @@ class TIASound
*/
string channels(uInt32 hardware, bool stereo);
public:
/**
Sets the specified sound register to the given value
Sets the specified sound register to the given value.
@param address Register address
@param value Value to store in the register
@param value Value to store in the register
@param clock Colour clock at which the write occurred
*/
void set(uInt16 address, uInt8 value);
/**
Gets the specified sound register's value
@param address Register address
*/
uInt8 get(uInt16 address) const;
void writeAudC0(uInt8 value, uInt32 clock);
void writeAudC1(uInt8 value, uInt32 clock);
void writeAudF0(uInt8 value, uInt32 clock);
void writeAudF1(uInt8 value, uInt32 clock);
void writeAudV0(uInt8 value, uInt32 clock);
void writeAudV1(uInt8 value, uInt32 clock);
/**
Create sound samples based on the current sound register settings
in the specified buffer. NOTE: If channels is set to stereo then
the buffer will need to be twice as long as the number of samples.
The samples are stored in an internal queue, to be removed as
necessary by getSamples() (invoked by the sound hardware callback).
@param buffer The location to store generated samples
@param samples The number of samples to generate
This method must be called once per scanline from the TIA class.
*/
void process(Int16* buffer, uInt32 samples);
void queueSamples();
/**
Move specified number of samples from the internal queue into the
given buffer.
@param buffer The location to move generated samples
@param samples The number of samples to move
@return The number of samples left to fill the buffer
Should normally be 0, since we want to fill it completely
*/
Int32 getSamples(uInt16* buffer, uInt32 samples)
{
while(mySamples.size() > 0 && samples--)
{
*buffer++ = mySamples.front();
mySamples.pop();
}
return samples;
}
/**
Set the volume of the samples created (0-100)
*/
void volume(uInt32 percent);
private:
void polyInit(uInt8* poly, int size, int f0, int f1);
/**
Saves the current state of this device to the given Serializer.
@param out The serializer device to save to.
@return The result of the save. True on success, false on failure.
*/
bool save(Serializer& out) const override;
/**
Loads the current state of this device from the given Serializer.
@param in The Serializer device to load from.
@return The result of the load. True on success, false on failure.
*/
bool load(Serializer& in) override;
/**
Get a descriptor for this console class (used in error checking).
@return The name of the object
*/
string name() const override { return "TIASound"; }
private:
// Definitions for AUDCx (15, 16)
enum AUDCxRegister
struct AudioState : public Serializable
{
SET_TO_1 = 0x00, // 0000
POLY4 = 0x01, // 0001
DIV31_POLY4 = 0x02, // 0010
POLY5_POLY4 = 0x03, // 0011
PURE1 = 0x04, // 0100
PURE2 = 0x05, // 0101
DIV31_PURE = 0x06, // 0110
POLY5_2 = 0x07, // 0111
POLY9 = 0x08, // 1000
POLY5 = 0x09, // 1001
DIV31_POLY5 = 0x0a, // 1010
POLY5_POLY5 = 0x0b, // 1011
DIV3_PURE = 0x0c, // 1100
DIV3_PURE2 = 0x0d, // 1101
DIV93_PURE = 0x0e, // 1110
POLY5_DIV3 = 0x0f // 1111
};
AudioState() { reset(); }
void reset()
{
clk_en = noise_fb = noise_cnt_4 = pulse_cnt_hold = false;
div_cnt = 0; noise_cnt = pulse_cnt = 0;
}
bool save(Serializer& out) const override;
bool load(Serializer& in) override;
string name() const override { return "TIASound_AudioState"; }
enum {
POLY4_SIZE = 0x000f,
POLY5_SIZE = 0x001f,
POLY9_SIZE = 0x01ff,
DIV3_MASK = 0x0c,
AUDV_SHIFT = 10 // shift 2 positions for AUDV,
// then another 8 for 16-bit sound
bool clk_en, noise_fb, noise_cnt_4, pulse_cnt_hold;
uInt32 div_cnt, noise_cnt, pulse_cnt;
};
bool updateAudioState(AudioState& state, uInt32 audf, uInt32* audc);
enum ChannelMode {
Hardware2Mono, // mono sampling with 2 hardware channels
Hardware2Stereo, // stereo sampling with 2 hardware channels
Hardware1 // mono/stereo sampling with only 1 hardware channel
};
private:
// Structures to hold the 6 tia sound control bytes
uInt8 myAUDC[2]; // AUDCx (15, 16)
uInt8 myAUDF[2]; // AUDFx (17, 18)
Int16 myAUDV[2]; // AUDVx (19, 1A)
Int16 myVolume[2]; // Last output volume for each channel
uInt8 myP4[2]; // Position pointer for the 4-bit POLY array
uInt8 myP5[2]; // Position pointer for the 5-bit POLY array
uInt16 myP9[2]; // Position pointer for the 9-bit POLY array
uInt8 myDivNCnt[2]; // Divide by n counter. one for each channel
uInt8 myDivNMax[2]; // Divide by n maximum, one for each channel
uInt8 myDiv3Cnt[2]; // Div 3 counter, used for POLY5_DIV3 mode
ChannelMode myChannelMode;
Int32 myOutputFrequency;
Int32 myOutputCounter;
uInt32 myVolumePercentage;
/*
Initialize the bit patterns for the polynomials (at runtime).
AudioState myAud0State; // storage for AUD0 state
AudioState myAud1State; // storage for AUD1 state
The 4bit and 5bit patterns are the identical ones used in the tia chip.
Though the patterns could be packed with 8 bits per byte, using only a
single bit per byte keeps the math simple, which is important for
efficient processing.
*/
uInt8 Bit4[POLY4_SIZE];
uInt8 Bit5[POLY5_SIZE];
uInt8 Bit9[POLY9_SIZE];
uInt16 myVolLUT[256][101]; // storage for volume look-up table
uInt32 myHWVol; // actual output volume to use
/*
The 'Div by 31' counter is treated as another polynomial because of
the way it operates. It does not have a 50% duty cycle, but instead
has a 13:18 ratio (of course, 13+18 = 31). This could also be
implemented by using counters.
*/
static const uInt8 Div31[POLY5_SIZE];
uInt32 myAudF0[2]; // audfx[0]: value at clock1-phase1
uInt32 myAudF1[2]; // audfx[1]: value at clock2-phase1
uInt32 myAudV0[2]; // audvx[0]: value at clock1-phase2
uInt32 myAudV1[2]; // audvx[1]: value at clock2-phase2
uInt32 myAudC0[2][2]; // audcx[0][0]: value at clock1-phase1,
// audcx[0][1]: value at clock1:phase2
uInt32 myAudC1[2][2]; // audcx[1][0]: value at clock2-phase1,
// audcx[1][1]: value at clock2:phase2
// A value not equal to 0xff indicates that *both* cycles were missed
// on the previous line, and must be updated on the next line
uInt32 myDeferredC0, myDeferredC1;
uInt32 myDeferredF0, myDeferredF1;
uInt32 myDeferredV0, myDeferredV1;
// Contains the samples previously created by queueSamples()
// This will be periodically emptied by getSamples()
queue<uInt16> mySamples;
// The colour clock at which each cycle/phase ends
// Any writes to sound registers that occur after a respective
// cycle/phase are deferred until the next update interval
static constexpr uInt32
Cycle1Phase1 = 8, Cycle1Phase2 = 36, Cycle2Phase1 = 80, Cycle2Phase2 = 148;
private:
// Following constructors and assignment operators not supported