From 1ecfee71e7161e8856f3073c9eb01a203b2f34cb Mon Sep 17 00:00:00 2001 From: stephena Date: Thu, 18 Aug 2016 18:44:57 +0000 Subject: [PATCH] Reverted new TIA sound code until it is more mature. There are other improvements in the pipeline waiting to be done, and are being delayed by the sound stuff. git-svn-id: svn://svn.code.sf.net/p/stella/code/trunk@3310 8b62c5a3-ac7e-4cc8-8f21-d9a121418aba --- src/common/SoundNull.hxx | 107 ++++++- src/common/SoundSDL2.cxx | 288 ++++++++++++++--- src/common/SoundSDL2.hxx | 192 +++++++++++- src/common/Variant.hxx | 2 +- src/emucore/Console.cxx | 5 +- src/emucore/Console.hxx | 4 + src/emucore/Sound.hxx | 45 ++- src/emucore/TIA.cxx | 70 ++--- src/emucore/TIA.hxx | 12 - src/emucore/TIASnd.cxx | 660 +++++++++++++++++---------------------- src/emucore/TIASnd.hxx | 258 ++++++--------- 11 files changed, 981 insertions(+), 662 deletions(-) diff --git a/src/common/SoundNull.hxx b/src/common/SoundNull.hxx index 783b8c87a..c9e498081 100644 --- a/src/common/SoundNull.hxx +++ b/src/common/SoundNull.hxx @@ -37,25 +37,56 @@ class SoundNull : public Sound { public: /** - Create a new sound object with no functionality. + Create a new sound object. The init method must be invoked before + using the object. */ SoundNull(OSystem& osystem) : Sound(osystem) { myOSystem->logMessage("Sound disabled.\n", 1); } + + /** + Destructor + */ virtual ~SoundNull() = default; 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) override { } + 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 { } /** Initializes the sound device. This must be called before any calls are made to derived methods. */ - void open(bool) override { } + void open() override { } /** Should be called to close the sound device. Once called the sound @@ -65,25 +96,89 @@ 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) override { } + void mute(bool state) 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(uInt32) override { } + void setVolume(Int32 percent) 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) override { } + 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"; } private: // Following constructors and assignment operators not supported diff --git a/src/common/SoundSDL2.cxx b/src/common/SoundSDL2.cxx index 09f2cf968..591e8a2d7 100644 --- a/src/common/SoundSDL2.cxx +++ b/src/common/SoundSDL2.cxx @@ -24,7 +24,8 @@ #include #include -#include "TIA.hxx" +#include "TIASnd.hxx" +#include "TIATables.hxx" #include "FrameBuffer.hxx" #include "Settings.hxx" #include "System.hxx" @@ -37,6 +38,11 @@ SoundSDL2::SoundSDL2(OSystem& osystem) : Sound(osystem), myIsEnabled(false), myIsInitializedFlag(false), + myLastRegisterSetCycle(0), + myNumChannels(0), + myFragmentSizeLogBase2(0), + myFragmentSizeLogDiv1(0), + myFragmentSizeLogDiv2(0), myIsMuted(true), myVolume(100) { @@ -47,12 +53,11 @@ 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 = getSamples; + desired.callback = callback; desired.userdata = static_cast(this); ostringstream buf; @@ -64,6 +69,24 @@ 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); @@ -76,7 +99,6 @@ SoundSDL2::~SoundSDL2() // Close the SDL audio system if it's initialized if(myIsInitializedFlag) { -//FIXME SDL_PauseAudio(1); SDL_CloseAudio(); myIsEnabled = myIsInitializedFlag = false; } @@ -92,7 +114,7 @@ void SoundSDL2::setEnabled(bool state) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void SoundSDL2::open(bool stereo) +void SoundSDL2::open() { myOSystem.logMessage("SoundSDL2::open started ...", 2); myIsEnabled = false; @@ -104,8 +126,9 @@ void SoundSDL2::open(bool stereo) } // Now initialize the TIASound object which will actually generate sound + myTIASound.outputFrequency(myHardwareSpec.freq); const string& chanResult = - myOSystem.console().tia().sound().channels(myHardwareSpec.channels, stereo); + myTIASound.channels(myHardwareSpec.channels, myNumChannels == 2); // Adjust volume to that defined in settings myVolume = myOSystem.settings().getInt("volume"); @@ -124,7 +147,7 @@ void SoundSDL2::open(bool stereo) // And start the SDL sound subsystem ... myIsEnabled = true; - SDL_PauseAudio(0);//myIsMuted ? 1 : 0); + mute(false); myOSystem.logMessage("SoundSDL2::open finished", 2); } @@ -136,8 +159,9 @@ void SoundSDL2::close() { myIsEnabled = false; SDL_PauseAudio(1); - if(myOSystem.hasConsole()) - myOSystem.console().tia().sound().reset(); + myLastRegisterSetCycle = 0; + myTIASound.reset(); + myRegWriteQueue.clear(); myOSystem.logMessage("SoundSDL2::close", 2); } } @@ -148,10 +172,7 @@ void SoundSDL2::mute(bool state) if(myIsInitializedFlag) { myIsMuted = state; - if(myOSystem.hasConsole()) - myOSystem.console().tia().sound().volume(myIsMuted ? 0 : myVolume); - - // SDL_PauseAudio(myIsMuted ? 1 : 0); + SDL_PauseAudio(myIsMuted ? 1 : 0); } } @@ -161,21 +182,23 @@ void SoundSDL2::reset() if(myIsInitializedFlag) { SDL_PauseAudio(1); - if(myOSystem.hasConsole()) - myOSystem.console().tia().sound().reset(); -// mute(myIsMuted); + myLastRegisterSetCycle = 0; + myTIASound.reset(); + myRegWriteQueue.clear(); + mute(myIsMuted); } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void SoundSDL2::setVolume(uInt32 volume) +void SoundSDL2::setVolume(Int32 percent) { - if(myIsInitializedFlag && (volume <= 100)) + if(myIsInitializedFlag && (percent >= 0) && (percent <= 100)) { - myOSystem.settings().setValue("volume", volume); - myVolume = volume; - if(myOSystem.hasConsole()) - myOSystem.console().tia().sound().volume(volume); + myOSystem.settings().setValue("volume", percent); + SDL_LockAudio(); + myVolume = percent; + myTIASound.volume(percent); + SDL_UnlockAudio(); } } @@ -206,25 +229,52 @@ void SoundSDL2::adjustVolume(Int8 direction) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void SoundSDL2::getSamples(void* udata, uInt8* stream, int len) +void SoundSDL2::adjustCycleCounter(Int32 amount) { - SoundSDL2* sound = static_cast(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(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' + 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(); } -#if 0 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void SoundSDL2::processFragment(Int16* stream, uInt32 length) { @@ -305,6 +355,166 @@ void SoundSDL2::processFragment(Int16* stream, uInt32 length) } } } -#endif + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void SoundSDL2::callback(void* udata, uInt8* stream, int len) +{ + SoundSDL2* sound = static_cast(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(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(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 buffer = make_ptr(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 // SOUND_SUPPORT diff --git a/src/common/SoundSDL2.hxx b/src/common/SoundSDL2.hxx index 38f3403c8..f25811a98 100644 --- a/src/common/SoundSDL2.hxx +++ b/src/common/SoundSDL2.hxx @@ -27,22 +27,27 @@ class OSystem; #include #include "bspf.hxx" +#include "TIASnd.hxx" #include "Sound.hxx" /** - This class implements the sound API for SDL2. + This class implements the sound API for SDL. - @author Stephen Anthony + @author Stephen Anthony and Bradford W. Mott @version $Id$ */ class SoundSDL2 : public Sound { public: /** - Create a new sound object. The open() method must be invoked - before using the object. + Create a new sound object. The init method must be invoked before + using the object. */ SoundSDL2(OSystem& osystem); + + /** + Destructor + */ virtual ~SoundSDL2(); public: @@ -53,13 +58,38 @@ 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(bool stereo) override; + void open() override; /** Should be called to close the sound device. Once called the sound @@ -79,14 +109,23 @@ 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 volume The new volume percentage level for the sound device + @param percent The new volume percentage level for the sound device */ - void setVolume(uInt32 volume) override; + void setVolume(Int32 percent) override; /** Adjusts the volume of the sound device based on the given direction. @@ -96,13 +135,143 @@ 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 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; @@ -112,9 +281,12 @@ 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 getSamples(void* udata, uInt8* stream, int len); + static void callback(void* udata, uInt8* stream, int len); // Following constructors and assignment operators not supported SoundSDL2() = delete; diff --git a/src/common/Variant.hxx b/src/common/Variant.hxx index 10dfc2a84..9abc04ca7 100644 --- a/src/common/Variant.hxx +++ b/src/common/Variant.hxx @@ -44,7 +44,7 @@ class Variant } public: - Variant() : data("") { } + Variant() { } Variant(const string& s) : data(s) { } Variant(const char* s) : data(s) { } diff --git a/src/emucore/Console.cxx b/src/emucore/Console.cxx index 1903f41b1..1dcf8d0aa 100644 --- a/src/emucore/Console.cxx +++ b/src/emucore/Console.cxx @@ -430,7 +430,9 @@ void Console::initializeAudio() const string& sound = myProperties.get(Cartridge_Sound); myOSystem.sound().close(); - myOSystem.sound().open(sound == "STEREO"); + myOSystem.sound().setChannels(sound == "STEREO" ? 2 : 1); + myOSystem.sound().setFrameRate(myFramerate); + myOSystem.sound().open(); // Make sure auto-frame calculation is only enabled when necessary myTIA->enableAutoFrame(framerate <= 0); @@ -836,6 +838,7 @@ void Console::setFramerate(float framerate) { myFramerate = framerate; myOSystem.setFramerate(framerate); + myOSystem.sound().setFrameRate(framerate); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/emucore/Console.hxx b/src/emucore/Console.hxx index e165abf60..bd97fd97a 100644 --- a/src/emucore/Console.hxx +++ b/src/emucore/Console.hxx @@ -71,6 +71,10 @@ class Console : public Serializable */ Console(OSystem& osystem, unique_ptr& cart, const Properties& props); + + /** + Destructor + */ virtual ~Console(); public: diff --git a/src/emucore/Sound.hxx b/src/emucore/Sound.hxx index 96046b4d0..f020433bb 100644 --- a/src/emucore/Sound.hxx +++ b/src/emucore/Sound.hxx @@ -22,6 +22,7 @@ class OSystem; +#include "Serializable.hxx" #include "bspf.hxx" /** @@ -31,11 +32,11 @@ class OSystem; @author Stephen Anthony @version $Id$ */ -class Sound +class Sound : public Serializable { public: /** - Create a new sound object. The open method must be invoked before + Create a new sound object. The init method must be invoked before using the object. */ Sound(OSystem& osystem) : myOSystem(osystem) { } @@ -49,13 +50,34 @@ class Sound */ 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(bool stereo) = 0; + virtual void open() = 0; /** Should be called to stop the sound system. Once called the sound @@ -75,14 +97,23 @@ class Sound */ 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 volume The new volume percentage level for the sound device + @param percent The new volume percentage level for the sound device */ - virtual void setVolume(uInt32 volume) = 0; + virtual void setVolume(Int32 percent) = 0; /** Adjusts the volume of the sound device based on the given direction. diff --git a/src/emucore/TIA.cxx b/src/emucore/TIA.cxx index f61868be2..f8b5165fc 100644 --- a/src/emucore/TIA.cxx +++ b/src/emucore/TIA.cxx @@ -21,23 +21,26 @@ #include #include +#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), @@ -62,7 +65,6 @@ TIA::TIA(Console& console, Sound& sound, Settings& settings) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void TIA::initialize() { - START_SLINE = -1; myFramePointer = nullptr; myFramePointerOffset = myFramePointerClocks = myStopDisplayOffset = 0; @@ -212,6 +214,9 @@ 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; @@ -339,7 +344,7 @@ bool TIA::save(Serializer& out) const out.putInt(myPALFrameCounter); // Save the sound sample stuff ... - myTIASound.save(out); + mySound.save(out); } catch(...) { @@ -443,7 +448,7 @@ bool TIA::load(Serializer& in) myPALFrameCounter = in.getInt(); // Load the sound sample stuff ... - myTIASound.load(in); + mySound.load(in); // Reset TIA bits to be on enableBits(true); @@ -535,7 +540,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 = clocksThisLine(); + uInt32 clocks = ((mySystem->cycles() * 3) - myClockWhenFrameStarted) % 228; // Ask the system to reset the cycle count so it doesn't overflow mySystem->resetCycles(); @@ -582,8 +587,6 @@ inline void TIA::startFrame() myFrameCounter++; if(myScanlineCountForLastFrame >= 287) myPALFrameCounter++; - - START_SLINE = -1; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1227,16 +1230,6 @@ 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 @@ -1246,17 +1239,8 @@ 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 - } } @@ -1268,14 +1252,6 @@ inline void TIA::waitHorizontalSync() if(cyclesToEndOfLine < 76) mySystem->incrementCycles(cyclesToEndOfLine); - -#if 1 -{ - cerr << " => scanline(W): " << scanlines() << endl; - myTIASound.queueSamples(); - START_SLINE = scanlines(); -} -#endif } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1765,7 +1741,6 @@ bool TIA::poke(uInt16 addr, uInt8 value) break; } myPOSP0 = newx; - CLAMP_POS(myPOSP0); } break; } @@ -1816,7 +1791,6 @@ bool TIA::poke(uInt16 addr, uInt8 value) break; } myPOSP1 = newx; - CLAMP_POS(myPOSP1); } break; } @@ -1841,7 +1815,6 @@ bool TIA::poke(uInt16 addr, uInt8 value) if(newx != myPOSM0) { myPOSM0 = newx; - CLAMP_POS(myPOSM0); } break; } @@ -1866,7 +1839,6 @@ bool TIA::poke(uInt16 addr, uInt8 value) if(newx != myPOSM1) { myPOSM1 = newx; - CLAMP_POS(myPOSM1); } break; } @@ -1893,48 +1865,42 @@ bool TIA::poke(uInt16 addr, uInt8 value) case AUDC0: // Audio control 0 { myAUDC0 = value & 0x0f; -cerr << scanlines() << ": C0, " << std::hex << int(myAUDC0) << ", " << std::dec << ((clock - myClockWhenFrameStarted) % 228) << endl; - myTIASound.writeAudC0(value, (clock - myClockWhenFrameStarted) % 228); + mySound.set(addr, value, mySystem->cycles()); break; } case AUDC1: // Audio control 1 { myAUDC1 = value & 0x0f; -cerr << scanlines() << ": C1, " << std::hex << int(myAUDC1) << ", " << std::dec << ((clock - myClockWhenFrameStarted) % 228) << endl; - myTIASound.writeAudC1(value, (clock - myClockWhenFrameStarted) % 228); + mySound.set(addr, value, mySystem->cycles()); break; } case AUDF0: // Audio frequency 0 { myAUDF0 = value & 0x1f; -cerr << scanlines() << ": F0, " << std::hex << int(myAUDF0) << ", " << std::dec << ((clock - myClockWhenFrameStarted) % 228) << endl; - myTIASound.writeAudF0(value, (clock - myClockWhenFrameStarted) % 228); + mySound.set(addr, value, mySystem->cycles()); break; } case AUDF1: // Audio frequency 1 { myAUDF1 = value & 0x1f; -cerr << scanlines() << ": F1, " << std::hex << int(myAUDF1) << ", " << std::dec << ((clock - myClockWhenFrameStarted) % 228) << endl; - myTIASound.writeAudF1(value, (clock - myClockWhenFrameStarted) % 228); + mySound.set(addr, value, mySystem->cycles()); break; } case AUDV0: // Audio volume 0 { myAUDV0 = value & 0x0f; -cerr << scanlines() << ": V0, " << std::hex << int(myAUDV0) << ", " << std::dec << ((clock - myClockWhenFrameStarted) % 228) << endl; - myTIASound.writeAudV0(value, (clock - myClockWhenFrameStarted) % 228); + mySound.set(addr, value, mySystem->cycles()); break; } case AUDV1: // Audio volume 1 { myAUDV1 = value & 0x0f; -cerr << scanlines() << ": V1, " << std::hex << int(myAUDV1) << ", " << std::dec << ((clock - myClockWhenFrameStarted) % 228) << endl; - myTIASound.writeAudV1(value, (clock - myClockWhenFrameStarted) % 228); + mySound.set(addr, value, mySystem->cycles()); break; } @@ -2284,7 +2250,7 @@ cerr << scanlines() << ": V1, " << std::hex << int(myAUDV1) << ", " << std::dec default: { #ifdef DEBUG_ACCESSES - cerr << "BAD TIA Poke: " << std::hex << addr << endl; + cerr << "BAD TIA Poke: " << hex << addr << endl; #endif break; } diff --git a/src/emucore/TIA.hxx b/src/emucore/TIA.hxx index 2efd8fd73..db5464d32 100644 --- a/src/emucore/TIA.hxx +++ b/src/emucore/TIA.hxx @@ -28,7 +28,6 @@ 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 @@ -97,14 +96,6 @@ 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. @@ -416,9 +407,6 @@ 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 myCurrentFrameBuffer; diff --git a/src/emucore/TIASnd.cxx b/src/emucore/TIASnd.cxx index 35f915b86..c79336630 100644 --- a/src/emucore/TIASnd.cxx +++ b/src/emucore/TIASnd.cxx @@ -22,58 +22,45 @@ #include "TIASnd.hxx" // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -TIASound::TIASound() - : myChannelMode(Hardware2Mono), - myHWVol(100) +TIASound::TIASound(Int32 outputFrequency) + : myChannelMode(Hardware2Stereo), + myOutputFrequency(outputFrequency), + myOutputCounter(0), + myVolumePercentage(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() { - myAud0State.reset(); - myAud1State.reset(); - while(!mySamples.empty()) mySamples.pop(); + // Fill the polynomials + polyInit(Bit4, 4, 4, 3); + polyInit(Bit5, 5, 5, 3); + polyInit(Bit9, 9, 9, 5); - for(int i = 0; i < 2; ++i) + // Initialize instance variables + for(int chan = 0; chan <= 1; ++chan) { - myAudC0[i][0] = myAudC0[i][1] = 0; - myAudC1[i][0] = myAudC1[i][1] = 0; - myAudF0[i] = myAudF1[i] = 0; - myAudV0[i] = myAudV1[i] = 0; + 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; } - myDeferredC0 = myDeferredC1 = myDeferredF0 = myDeferredF1 = - myDeferredV0 = myDeferredV1 = 0xff; + myOutputCounter = 0; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void TIASound::outputFrequency(Int32 freq) +{ + myOutputFrequency = freq; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -86,397 +73,318 @@ 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::writeAudC0(uInt8 value, uInt32 clock) +void TIASound::set(uInt16 address, uInt8 value) { - 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; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void TIASound::writeAudC1(uInt8 value, uInt32 clock) -{ - 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) + int chan = ~address & 0x1; + switch(address) { - 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); + case TIARegister::AUDC0: + case TIARegister::AUDC1: + myAUDC[chan] = value & 0x0f; 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]); + case TIARegister::AUDF0: + case TIARegister::AUDF1: + myAUDF[chan] = value & 0x1f; 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]); + case TIARegister::AUDV0: + case TIARegister::AUDV1: + myAUDV[chan] = (value & 0x0f) << AUDV_SHIFT; break; - } + + default: + return; } - ///////////////////////////////////////////////// - // End of line, allow deferred updates - ///////////////////////////////////////////////// + uInt16 newVal = 0; - // AUDC0 - if(myDeferredC0 != 0xff) // write occurred after cycle2:phase2 + // An AUDC value of 0 is a special case + if (myAUDC[chan] == SET_TO_1 || myAUDC[chan] == POLY5_POLY5) { - myAudC0[0][0] = myAudC0[0][1] = myAudC0[1][0] = myAudC0[1][1] = myDeferredC0; - myDeferredC0 = 0xff; + // 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 // write occurred after cycle1:phase1 - myAudC0[0][0] = myAudC0[1][1]; - - // AUDC1 - if(myDeferredC1 != 0xff) // write occurred after cycle2:phase2 + else { - 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]; + // Otherwise calculate the 'divide by N' value + newVal = myAUDF[chan] + 1; - // AUDF0 - if(myDeferredF0 != 0xff) // write occurred after cycle2:phase2 - { - myAudF0[0] = myAudF0[1] = myDeferredF0; - myDeferredF0 = 0xff; + // 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; } - else // write occurred after cycle1:phase1 - myAudF0[0] = myAudF0[1]; - // AUDF1 - if(myDeferredF1 != 0xff) // write occurred after cycle2:phase2 + // Only reset those channels that have changed + if(newVal != myDivNMax[chan]) { - myAudF1[0] = myAudF1[1] = myDeferredF1; - myDeferredF1 = 0xff; - } - else // write occurred after cycle1:phase1 - myAudF1[0] = myAudF1[1]; + // Reset the divide by n counters + myDivNMax[chan] = newVal; - // AUDV0 - if(myDeferredV0 != 0xff) // write occurred after cycle2:phase2 - { - myAudV0[0] = myAudV0[1] = myDeferredV0; - myDeferredV0 = 0xff; + // 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; } - else // write occurred after cycle1:phase1 - myAudV0[0] = myAudV0[1]; +} - // AUDV1 - if(myDeferredV1 != 0xff) // write occurred after cycle2:phase2 +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt8 TIASound::get(uInt16 address) const +{ + switch(address) { - myAudV1[0] = myAudV1[1] = myDeferredV1; - myDeferredV1 = 0xff; + 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; } - else // write occurred after cycle1:phase1 - myAudV1[0] = myAudV1[1]; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void TIASound::volume(uInt32 percent) { - myHWVol = std::min(percent, 100u); + if(percent <= 100) + myVolumePercentage = percent; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool TIASound::updateAudioState(AudioState& state, uInt32 audf, uInt32* audc) +void TIASound::process(Int16* buffer, uInt32 samples) { - bool pulse_fb = false; // pulse counter LFSR feedback + // 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]; - // -- Logic updated on phase 1 of the audio clock -- - if(state.clk_en) + // 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) { - // Latch bit 4 of noise counter - state.noise_cnt_4 = state.noise_cnt & 0x01; - - // Latch pulse counter hold condition - switch(audc[0] & 0x03) + // Process channel 0 + if (div_n_cnt0 > 1) { - case 0x00: - state.pulse_cnt_hold = false; - break; - 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; + 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; + } + } } - // Latch noise counter LFSR feedback - switch(audc[0] & 0x03) + // Process channel 1 + if (div_n_cnt1 > 1) { - 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); + 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--; + } break; - default: - state.noise_fb = (((state.noise_cnt & 0x04) ? 1 : 0) ^ (state.noise_cnt & 0x01)) | - !state.noise_cnt; + + case Hardware2Stereo: // stereo sampling with 2 hardware channels + while((samples > 0) && (myOutputCounter >= 31400)) + { + *(buffer++) = v0; + *(buffer++) = v1; + myOutputCounter -= 31400; + samples--; + } + break; + + case Hardware1: // mono/stereo sampling with only 1 hardware channel + while((samples > 0) && (myOutputCounter >= 31400)) + { + *(buffer++) = v0 + v1; + myOutputCounter -= 31400; + samples--; + } break; } } - // Set (or clear) audio clock enable - state.clk_en = (state.div_cnt == audf); - - // Increment clock divider counter - if((state.div_cnt == audf) || (state.div_cnt == 31)) - state.div_cnt = 0; - else - state.div_cnt++; - - // -- Logic updated on phase 2 of the audio clock -- - if(state.clk_en) - { - // 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); + // 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; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool TIASound::save(Serializer& out) const +void TIASound::polyInit(uInt8* poly, int size, int f0, int f1) { -#if 0 - try - { - out.putString(name()); + int mask = (1 << size) - 1, x = mask; - // 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(...) + for(int i = 0; i < mask; i++) { - myOSystem.logMessage("ERROR: SoundSDL2::save", 0); - return false; + 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) ); } -#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; -} +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 +}; diff --git a/src/emucore/TIASnd.hxx b/src/emucore/TIASnd.hxx index 0e508839b..2730d544a 100644 --- a/src/emucore/TIASnd.hxx +++ b/src/emucore/TIASnd.hxx @@ -17,82 +17,40 @@ // $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 - #include "bspf.hxx" -#include "Serializable.hxx" -class TIASound : public Serializable +/** + 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 { public: /** - Create a new TIA Sound object. + Create a new TIA Sound object using the specified output frequency */ - TIASound(); + TIASound(Int32 outputFrequency = 31400); 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. @@ -105,133 +63,117 @@ class TIASound : public Serializable */ 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 value Value to store in the register - @param clock Colour clock at which the write occurred + @param address Register address + @param value Value to store in the register */ - 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); + void set(uInt16 address, uInt8 value); + + /** + Gets the specified sound register's value + + @param address Register address + */ + uInt8 get(uInt16 address) const; /** 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). - This method must be called once per scanline from the TIA class. + @param buffer The location to store generated samples + @param samples The number of samples to generate */ - 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; - } + void process(Int16* buffer, uInt32 samples); /** Set the volume of the samples created (0-100) */ void volume(uInt32 percent); - /** - 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: + void polyInit(uInt8* poly, int size, int f0, int f1); private: - struct AudioState : public Serializable + // Definitions for AUDCx (15, 16) + enum AUDCxRegister { - 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"; } - - bool clk_en, noise_fb, noise_cnt_4, pulse_cnt_hold; - uInt32 div_cnt, noise_cnt, pulse_cnt; + 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 + }; + + 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 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; - AudioState myAud0State; // storage for AUD0 state - AudioState myAud1State; // storage for AUD1 state + /* + Initialize the bit patterns for the polynomials (at runtime). - uInt16 myVolLUT[256][101]; // storage for volume look-up table - uInt32 myHWVol; // actual output volume to use + 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]; - 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() - std::queue 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; + /* + 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]; private: // Following constructors and assignment operators not supported