diff --git a/src/common/SoundNull.hxx b/src/common/SoundNull.hxx index c44c9e357..783b8c87a 100644 --- a/src/common/SoundNull.hxx +++ b/src/common/SoundNull.hxx @@ -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 diff --git a/src/common/SoundSDL2.cxx b/src/common/SoundSDL2.cxx index 591e8a2d7..09f2cf968 100644 --- a/src/common/SoundSDL2.cxx +++ b/src/common/SoundSDL2.cxx @@ -24,8 +24,7 @@ #include #include -#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(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(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' } +#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(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 #endif // SOUND_SUPPORT diff --git a/src/common/SoundSDL2.hxx b/src/common/SoundSDL2.hxx index f25811a98..38f3403c8 100644 --- a/src/common/SoundSDL2.hxx +++ b/src/common/SoundSDL2.hxx @@ -27,27 +27,22 @@ class OSystem; #include #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 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; diff --git a/src/emucore/Console.cxx b/src/emucore/Console.cxx index cdb6fff54..085f91730 100644 --- a/src/emucore/Console.cxx +++ b/src/emucore/Console.cxx @@ -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); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/emucore/Sound.hxx b/src/emucore/Sound.hxx index f020433bb..96046b4d0 100644 --- a/src/emucore/Sound.hxx +++ b/src/emucore/Sound.hxx @@ -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. diff --git a/src/emucore/TIA.cxx b/src/emucore/TIA.cxx index f8b5165fc..1c49f091a 100644 --- a/src/emucore/TIA.cxx +++ b/src/emucore/TIA.cxx @@ -21,26 +21,23 @@ #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), @@ -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; } diff --git a/src/emucore/TIA.hxx b/src/emucore/TIA.hxx index db5464d32..2efd8fd73 100644 --- a/src/emucore/TIA.hxx +++ b/src/emucore/TIA.hxx @@ -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 myCurrentFrameBuffer; diff --git a/src/emucore/TIASnd.cxx b/src/emucore/TIASnd.cxx index c79336630..9080bfe94 100644 --- a/src/emucore/TIASnd.cxx +++ b/src/emucore/TIASnd.cxx @@ -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; +} diff --git a/src/emucore/TIASnd.hxx b/src/emucore/TIASnd.hxx index 2730d544a..27bdb998a 100644 --- a/src/emucore/TIASnd.hxx +++ b/src/emucore/TIASnd.hxx @@ -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 + #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 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