diff --git a/.vscode/settings.json b/.vscode/settings.json index b3bbb7fc3..7d0543e6c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -26,6 +26,39 @@ "string_view": "cpp", "system_error": "cpp", "vector": "cpp", - "stdexcept": "cpp" + "sstream": "cpp", + "__bit_reference": "cpp", + "__functional_base": "cpp", + "algorithm": "cpp", + "bitset": "cpp", + "chrono": "cpp", + "functional": "cpp", + "iterator": "cpp", + "limits": "cpp", + "locale": "cpp", + "memory": "cpp", + "ratio": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "stdexcept": "cpp", + "fstream": "cpp", + "__locale": "cpp", + "__string": "cpp", + "__config": "cpp", + "__nullptr": "cpp", + "cstddef": "cpp", + "exception": "cpp", + "initializer_list": "cpp", + "new": "cpp", + "typeinfo": "cpp", + "__mutex_base": "cpp", + "mutex": "cpp", + "condition_variable": "cpp", + "*.ins": "cpp", + "cstring": "cpp", + "iostream": "cpp", + "cstdint": "cpp", + "ostream": "cpp", + "__memory": "cpp" } } diff --git a/Makefile b/Makefile index 6ab2430d9..c3a70ddad 100644 --- a/Makefile +++ b/Makefile @@ -101,7 +101,8 @@ MODULES += \ src/emucore/tia/frame-manager \ src/gui \ src/common \ - src/common/tv_filters + src/common/tv_filters \ + src/common/audio ###################################################################### # The build rules follow - normally you should have no need to diff --git a/TODO_audio.md b/TODO_audio.md new file mode 100644 index 000000000..48179f2a0 --- /dev/null +++ b/TODO_audio.md @@ -0,0 +1,9 @@ +# Missing features + + * Reimplement target FPS mode + * Add GUI for new audio parameters (prebuffer fragment count, resampling quality) + +# Cleanup + + * Remove or turn sterr output into log messages + * Document EmulationTiming diff --git a/src/common/AudioQueue.cxx b/src/common/AudioQueue.cxx new file mode 100644 index 000000000..251796a9c --- /dev/null +++ b/src/common/AudioQueue.cxx @@ -0,0 +1,142 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#include "AudioQueue.hxx" + +using std::mutex; +using std::lock_guard; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +AudioQueue::AudioQueue(uInt32 fragmentSize, uInt32 capacity, bool isStereo) + : myFragmentSize(fragmentSize), + myIsStereo(isStereo), + myFragmentQueue(capacity), + myAllFragments(capacity + 2), + mySize(0), + myNextFragment(0) +{ + const uInt8 sampleSize = myIsStereo ? 2 : 1; + + myFragmentBuffer = make_unique(myFragmentSize * sampleSize * (capacity + 2)); + + for (uInt32 i = 0; i < capacity; i++) + myFragmentQueue[i] = myAllFragments[i] = myFragmentBuffer.get() + i * sampleSize * myFragmentSize; + + myAllFragments[capacity] = myFirstFragmentForEnqueue = + myFragmentBuffer.get() + capacity * sampleSize * myFragmentSize; + + myAllFragments[capacity + 1] = myFirstFragmentForDequeue = + myFragmentBuffer.get() + (capacity + 1) * sampleSize * myFragmentSize; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt32 AudioQueue::capacity() const +{ + return uInt32(myFragmentQueue.size()); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt32 AudioQueue::size() +{ + lock_guard guard(myMutex); + + return mySize; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool AudioQueue::isStereo() const +{ + return myIsStereo; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt32 AudioQueue::fragmentSize() const +{ + return myFragmentSize; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Int16* AudioQueue::enqueue(Int16* fragment) +{ + lock_guard guard(myMutex); + + Int16* newFragment; + + if (!fragment) { + if (!myFirstFragmentForEnqueue) throw runtime_error("enqueue called empty"); + + newFragment = myFirstFragmentForEnqueue; + myFirstFragmentForEnqueue = nullptr; + + return newFragment; + } + + const uInt8 capacity = myFragmentQueue.size(); + const uInt8 fragmentIndex = (myNextFragment + mySize) % capacity; + + newFragment = myFragmentQueue.at(fragmentIndex); + myFragmentQueue.at(fragmentIndex) = fragment; + + if (mySize < capacity) mySize++; + else { + myNextFragment = (myNextFragment + 1) % capacity; + if (!myIgnoreOverflows) (cerr << "audio buffer overflow\n").flush(); + } + + return newFragment; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Int16* AudioQueue::dequeue(Int16* fragment) +{ + lock_guard guard(myMutex); + + if (mySize == 0) return nullptr; + + if (!fragment) { + if (!myFirstFragmentForDequeue) throw runtime_error("dequeue called empty"); + + fragment = myFirstFragmentForDequeue; + myFirstFragmentForDequeue = nullptr; + } + + Int16* nextFragment = myFragmentQueue.at(myNextFragment); + myFragmentQueue.at(myNextFragment) = fragment; + + mySize--; + myNextFragment = (myNextFragment + 1) % myFragmentQueue.size(); + + return nextFragment; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioQueue::closeSink(Int16* fragment) +{ + lock_guard guard(myMutex); + + if (myFirstFragmentForDequeue && fragment) + throw new runtime_error("attempt to return unknown buffer on closeSink"); + + if (!myFirstFragmentForDequeue) + myFirstFragmentForDequeue = fragment; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioQueue::ignoreOverflows(bool shouldIgnoreOverflows) +{ + myIgnoreOverflows = shouldIgnoreOverflows; +} diff --git a/src/common/AudioQueue.hxx b/src/common/AudioQueue.hxx new file mode 100644 index 000000000..af714a925 --- /dev/null +++ b/src/common/AudioQueue.hxx @@ -0,0 +1,142 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#ifndef AUDIO_QUEUE_HXX +#define AUDIO_QUEUE_HXX + +#include + +#include "bspf.hxx" + +/** + This class implements a an audio queue that acts both like a ring buffer + and a pool of audio fragments. The TIA emulation core fills a fragment + with samples and then returns it to the queue, receiving a new fragment + in return. The sound driver removes fragments for playback from the + queue and returns the used fragment in this process. + + The queue needs to be threadsafe as the (SDL) audio driver runs on a + separate thread. Samples are stored as signed 16 bit integers + (platform endian). +*/ +class AudioQueue +{ + public: + + /** + Create a new AudioQueue. + + @param fragmentSize The size (in stereo / mono samples) of each fragment + @param capacity The number of fragments that can be queued before wrapping. + @param isStereo Whether samples are stereo or mono. + @param sampleRate The sample rate. This is not used, but can be queried. + */ + AudioQueue(uInt32 fragmentSize, uInt32 capacity, bool isStereo); + + /** + Capacity getter. + */ + uInt32 capacity() const; + + /** + Size getter. + */ + uInt32 size(); + + /** + Stereo / mono getter. + */ + bool isStereo() const; + + /** + Fragment size getter. + */ + uInt32 fragmentSize() const; + + /** + Enqueue a new fragment and get a new fragmen to fill. + + @param fragment The returned fragment. This must be empty on the first call (when + there is nothing to return) + */ + Int16* enqueue(Int16* fragment = nullptr); + + /** + Dequeue a fragment for playback and return the played fragment. This may + return 0 if there is no queued fragment to return (in this case, the returned + fragment is not enqueued and must be passed in the next invocation). + + @param fragment The returned fragment. This must be empty on the first call (when + there is nothing to return). + */ + Int16* dequeue(Int16* fragment = nullptr); + + /** + Return the currently playing fragment without drawing a new one. This is called + if the sink is closed and prepares the queue to be reopened. + */ + void closeSink(Int16* fragment); + + /** + Should we ignore overflows? + */ + void ignoreOverflows(bool shouldIgnoreOverflows); + + private: + + // The size of an individual fragment (in stereo / mono samples) + uInt32 myFragmentSize; + + // Are we using stereo samples? + bool myIsStereo; + + // The fragment queue + vector myFragmentQueue; + + // All fragments, including the two fragments that are in circulation. + vector myAllFragments; + + // We allocate a consecutive slice of memory for the fragments. + unique_ptr myFragmentBuffer; + + // The nubmer if queued fragments + uInt32 mySize; + + // The next fragment. + uInt32 myNextFragment; + + // We need a mutex for thread safety. + std::mutex myMutex; + + // The first (empty) enqueue call returns this fragment. + Int16* myFirstFragmentForEnqueue; + // The first (empty) dequeue call replaces the returned fragment with this fragment. + Int16* myFirstFragmentForDequeue; + + // Log overflows? + bool myIgnoreOverflows; + + private: + + AudioQueue() = delete; + AudioQueue(const AudioQueue&) = delete; + AudioQueue(AudioQueue&&) = delete; + AudioQueue& operator=(const AudioQueue&) = delete; + AudioQueue& operator=(AudioQueue&&) = delete; +}; + +#endif // AUDIO_QUEUE_HXX diff --git a/src/common/AudioSettings.cxx b/src/common/AudioSettings.cxx new file mode 100644 index 000000000..f1b87a0cc --- /dev/null +++ b/src/common/AudioSettings.cxx @@ -0,0 +1,291 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#include "AudioSettings.hxx" +#include "Settings.hxx" + +namespace { + uInt32 convertInt(int x, int defaultValue) + { + return x <= defaultValue ? defaultValue : x; + } + + AudioSettings::Preset normalizedPreset(int numericPreset) + { + return ( + numericPreset >= static_cast(AudioSettings::Preset::custom) && + numericPreset <= static_cast(AudioSettings::Preset::veryHighQualityVeryLowLag) + ) ? static_cast(numericPreset) : AudioSettings::DEFAULT_PRESET; + } + + AudioSettings::ResamplingQuality normalizeResamplingQuality(int numericResamplingQuality) + { + return ( + numericResamplingQuality >= static_cast(AudioSettings::ResamplingQuality::nearestNeightbour) && + numericResamplingQuality <= static_cast(AudioSettings::ResamplingQuality::lanczos_3) + ) ? static_cast(numericResamplingQuality) : AudioSettings::DEFAULT_RESAMPLING_QUALITY; + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +AudioSettings::AudioSettings() + : mySettings(), + myIsPersistent(false) +{} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +AudioSettings::AudioSettings(Settings* settings) + : mySettings(settings), + myIsPersistent(true) +{ + setPreset(normalizedPreset(mySettings->getInt(SETTING_PRESET))); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioSettings::normalize(Settings& settings) +{ + int settingPreset = settings.getInt(SETTING_PRESET); + Preset preset = normalizedPreset(settingPreset); + if (static_cast(preset) != settingPreset) settings.setValue(SETTING_PRESET, static_cast(DEFAULT_PRESET)); + + switch (settings.getInt(SETTING_SAMPLE_RATE)) { + case 44100: + case 48000: + case 96000: + break; + + default: + settings.setValue(SETTING_SAMPLE_RATE, DEFAULT_SAMPLE_RATE); + break; + } + + switch (settings.getInt(SETTING_FRAGMENT_SIZE)) { + case 128: + case 256: + case 512: + case 1024: + case 2048: + case 4096: + break; + + default: + settings.setValue(SETTING_FRAGMENT_SIZE, DEFAULT_FRAGMENT_SIZE); + break; + } + + int settingBufferSize = settings.getInt(SETTING_BUFFER_SIZE); + if (settingBufferSize < 0 || settingBufferSize > MAX_BUFFER_SIZE) settings.setValue(SETTING_BUFFER_SIZE, DEFAULT_BUFFER_SIZE); + + int settingHeadroom = settings.getInt(SETTING_HEADROOM); + if (settingHeadroom < 0 || settingHeadroom > MAX_HEADROOM) settings.setValue(SETTING_HEADROOM, DEFAULT_HEADROOM); + + int settingResamplingQuality = settings.getInt(SETTING_RESAMPLING_QUALITY); + ResamplingQuality resamplingQuality = normalizeResamplingQuality(settingResamplingQuality); + if (static_cast(resamplingQuality) != settingResamplingQuality) + settings.setValue(SETTING_RESAMPLING_QUALITY, static_cast(DEFAULT_RESAMPLING_QUALITY)); + + int settingVolume = settings.getInt(SETTING_VOLUME); + if (settingVolume < 0 || settingVolume > 100) settings.setValue(SETTING_VOLUME, DEFAULT_VOLUME); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +AudioSettings::Preset AudioSettings::preset() +{ + updatePresetFromSettings(); + return myPreset; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt32 AudioSettings::sampleRate() +{ + updatePresetFromSettings(); + return customSettings() ? convertInt(mySettings->getInt(SETTING_SAMPLE_RATE), DEFAULT_SAMPLE_RATE) : myPresetSampleRate; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt32 AudioSettings::fragmentSize() +{ + updatePresetFromSettings(); + return customSettings() ? convertInt(mySettings->getInt(SETTING_FRAGMENT_SIZE), DEFAULT_FRAGMENT_SIZE) : myPresetFragmentSize; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt32 AudioSettings::bufferSize() +{ + updatePresetFromSettings(); + // 0 is a valid value -> keep it + return customSettings() ? convertInt(mySettings->getInt(SETTING_BUFFER_SIZE), 0) : myPresetBufferSize; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt32 AudioSettings::headroom() +{ + updatePresetFromSettings(); + // 0 is a valid value -> keep it + return customSettings() ? convertInt(mySettings->getInt(SETTING_HEADROOM), 0) : myPresetHeadroom; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +AudioSettings::ResamplingQuality AudioSettings::resamplingQuality() +{ + updatePresetFromSettings(); + return customSettings() ? normalizeResamplingQuality(mySettings->getInt(SETTING_RESAMPLING_QUALITY)) : myPresetResamplingQuality; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt32 AudioSettings::volume() const +{ + // 0 is a valid value -> keep it + return convertInt(mySettings->getInt(SETTING_VOLUME), 0); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool AudioSettings::enabled() const +{ + return mySettings->getBool(SETTING_ENABLED); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioSettings::setPreset(AudioSettings::Preset preset) +{ + if (preset == myPreset) return; + myPreset = preset; + + switch (myPreset) { + case Preset::custom: + break; + + case Preset::lowQualityMediumLag: + myPresetSampleRate = 44100; + myPresetFragmentSize = 1024; + myPresetBufferSize = 6; + myPresetHeadroom = 5; + myPresetResamplingQuality = ResamplingQuality::nearestNeightbour; + break; + + case Preset::highQualityMediumLag: + myPresetSampleRate = 44100; + myPresetFragmentSize = 1024; + myPresetBufferSize = 6; + myPresetHeadroom = 5; + myPresetResamplingQuality = ResamplingQuality::lanczos_2; + break; + + case Preset::highQualityLowLag: + myPresetSampleRate = 48000; + myPresetFragmentSize = 512; + myPresetBufferSize = 3; + myPresetHeadroom = 2; + myPresetResamplingQuality = ResamplingQuality::lanczos_2; + break; + + case Preset::veryHighQualityVeryLowLag: + myPresetSampleRate = 96000; + myPresetFragmentSize = 128; + myPresetBufferSize = 0; + myPresetHeadroom = 0; + myPresetResamplingQuality = ResamplingQuality::lanczos_3; + break; + + default: + throw runtime_error("invalid preset"); + } + + if (myIsPersistent) mySettings->setValue(SETTING_PRESET, static_cast(myPreset)); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioSettings::setSampleRate(uInt32 sampleRate) +{ + if (!myIsPersistent) return; + + mySettings->setValue(SETTING_SAMPLE_RATE, sampleRate); + normalize(*mySettings); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioSettings::setFragmentSize(uInt32 fragmentSize) +{ + if (!myIsPersistent) return; + + mySettings->setValue(SETTING_FRAGMENT_SIZE, fragmentSize); + normalize(*mySettings); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioSettings::setBufferSize(uInt32 bufferSize) +{ + if (!myIsPersistent) return; + + mySettings->setValue(SETTING_BUFFER_SIZE, bufferSize); + normalize(*mySettings); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioSettings::setHeadroom(uInt32 headroom) +{ + if (!myIsPersistent) return; + + mySettings->setValue(SETTING_HEADROOM, headroom); + normalize(*mySettings); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioSettings::setResamplingQuality(AudioSettings::ResamplingQuality resamplingQuality) +{ + if (!myIsPersistent) return; + + mySettings->setValue(SETTING_RESAMPLING_QUALITY, static_cast(resamplingQuality)); + normalize(*mySettings); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioSettings::setVolume(uInt32 volume) +{ + if (!myIsPersistent) return; + + mySettings->setValue(SETTING_VOLUME, volume); + normalize(*mySettings); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioSettings::setEnabled(bool isEnabled) +{ + if (!myIsPersistent) return; + + mySettings->setValue(SETTING_ENABLED, isEnabled); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioSettings::setPersistent(bool isPersistent) +{ + myIsPersistent = isPersistent; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool AudioSettings::customSettings() const +{ + return myPreset == Preset::custom; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioSettings::updatePresetFromSettings() +{ + if (!myIsPersistent) return; + + setPreset(normalizedPreset(mySettings->getInt(SETTING_PRESET))); +} diff --git a/src/common/AudioSettings.hxx b/src/common/AudioSettings.hxx new file mode 100644 index 000000000..61b66ffb2 --- /dev/null +++ b/src/common/AudioSettings.hxx @@ -0,0 +1,129 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#ifndef AUDIO_PARAMTERS_HXX +#define AUDIO_PARAMTERS_HXX + +#include "bspf.hxx" + +class Settings; + +class AudioSettings +{ + public: + + enum class Preset { + custom = 1, + lowQualityMediumLag = 2, + highQualityMediumLag = 3, + highQualityLowLag = 4, + veryHighQualityVeryLowLag = 5 + }; + + enum class ResamplingQuality { + nearestNeightbour = 1, + lanczos_2 = 2, + lanczos_3 = 3 + }; + + static constexpr const char* SETTING_PRESET = "audio.preset"; + static constexpr const char* SETTING_SAMPLE_RATE = "audio.sample_rate"; + static constexpr const char* SETTING_FRAGMENT_SIZE = "audio.fragment_size"; + static constexpr const char* SETTING_BUFFER_SIZE = "audio.buffer_size"; + static constexpr const char* SETTING_HEADROOM = "audio.headroom"; + static constexpr const char* SETTING_RESAMPLING_QUALITY = "audio.resampling_quality"; + static constexpr const char* SETTING_VOLUME = "audio.volume"; + static constexpr const char* SETTING_ENABLED = "audio.enabled"; + + static constexpr Preset DEFAULT_PRESET = Preset::highQualityMediumLag; + static constexpr uInt32 DEFAULT_SAMPLE_RATE = 44100; + static constexpr uInt32 DEFAULT_FRAGMENT_SIZE = 512; + static constexpr uInt32 DEFAULT_BUFFER_SIZE = 3; + static constexpr uInt32 DEFAULT_HEADROOM = 2; + static constexpr ResamplingQuality DEFAULT_RESAMPLING_QUALITY = ResamplingQuality::lanczos_2; + static constexpr uInt32 DEFAULT_VOLUME = 80; + static constexpr bool DEFAULT_ENABLED = true; + + static constexpr int MAX_BUFFER_SIZE = 10; + static constexpr int MAX_HEADROOM = 10; + + public: + + AudioSettings(); + + AudioSettings(Settings* mySettings); + + static void initialize(Settings& settings); + + static void normalize(Settings& settings); + + Preset preset(); + + uInt32 sampleRate(); + + uInt32 fragmentSize(); + + uInt32 bufferSize(); + + uInt32 headroom(); + + ResamplingQuality resamplingQuality(); + + uInt32 volume() const; + + bool enabled() const; + + void setPreset(Preset preset); + + void setSampleRate(uInt32 sampleRate); + + void setFragmentSize(uInt32 fragmentSize); + + void setBufferSize(uInt32 bufferSize); + + void setHeadroom(uInt32 headroom); + + void setResamplingQuality(ResamplingQuality resamplingQuality); + + void setVolume(uInt32 volume); + + void setEnabled(bool isEnabled); + + void setPersistent(bool isPersistent); + + private: + + bool customSettings() const; + + void updatePresetFromSettings(); + + private: + + Settings* mySettings; + + Preset myPreset; + + uInt32 myPresetSampleRate; + uInt32 myPresetFragmentSize; + uInt32 myPresetBufferSize; + uInt32 myPresetHeadroom; + ResamplingQuality myPresetResamplingQuality; + + bool myIsPersistent; +}; + +#endif // AUDIO_PARAMTERS_HXX diff --git a/src/common/FrameBufferSDL2.cxx b/src/common/FrameBufferSDL2.cxx index 9ac36fcfa..ab1ee1f14 100644 --- a/src/common/FrameBufferSDL2.cxx +++ b/src/common/FrameBufferSDL2.cxx @@ -144,10 +144,6 @@ bool FrameBufferSDL2::setVideoMode(const string& title, const VideoMode& mode) // Always recreate renderer (some systems need this) if(myRenderer) { - // Always clear the (double-buffered) renderer surface - SDL_RenderClear(myRenderer); - SDL_RenderPresent(myRenderer); - SDL_RenderClear(myRenderer); SDL_DestroyRenderer(myRenderer); myRenderer = nullptr; } @@ -234,6 +230,12 @@ bool FrameBufferSDL2::setVideoMode(const string& title, const VideoMode& mode) myOSystem.logMessage(msg, 0); return false; } + + // Always clear the (double-buffered) renderer surface + SDL_RenderClear(myRenderer); + SDL_RenderPresent(myRenderer); + SDL_RenderClear(myRenderer); + SDL_RendererInfo renderinfo; if(SDL_GetRendererInfo(myRenderer, &renderinfo) >= 0) myOSystem.settings().setValue("video", renderinfo.name); @@ -241,6 +243,7 @@ bool FrameBufferSDL2::setVideoMode(const string& title, const VideoMode& mode) return true; } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void FrameBufferSDL2::setTitle(const string& title) { myScreenTitle = title; diff --git a/src/common/MediaFactory.hxx b/src/common/MediaFactory.hxx index 1338b1ccd..708413967 100644 --- a/src/common/MediaFactory.hxx +++ b/src/common/MediaFactory.hxx @@ -50,6 +50,8 @@ #include "SoundNull.hxx" #endif +class AudioSettings; + /** This class deals with the different framebuffer/sound/event implementations for the various ports of Stella, and always returns a @@ -108,10 +110,10 @@ class MediaFactory return make_unique(osystem); } - static unique_ptr createAudio(OSystem& osystem) + static unique_ptr createAudio(OSystem& osystem, AudioSettings& audioSettings) { #ifdef SOUND_SUPPORT - return make_unique(osystem); + return make_unique(osystem, audioSettings); #else return make_unique(osystem); #endif diff --git a/src/common/SoundNull.hxx b/src/common/SoundNull.hxx index 198dc26d7..41bfbaef4 100644 --- a/src/common/SoundNull.hxx +++ b/src/common/SoundNull.hxx @@ -21,6 +21,8 @@ #include "bspf.hxx" #include "Sound.hxx" #include "OSystem.hxx" +#include "AudioQueue.hxx" +#include "EmulationTiming.hxx" /** This class implements a Null sound object, where-by sound generation @@ -53,26 +55,11 @@ class SoundNull : public Sound */ void setEnabled(bool state) 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() override { } + void open(shared_ptr, EmulationTiming*) override { } /** Should be called to close the sound device. Once called the sound @@ -92,15 +79,6 @@ class SoundNull : 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, uInt64 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 @@ -108,7 +86,7 @@ class SoundNull : public Sound @param percent The new volume percentage level for the sound device */ - void setVolume(Int32 percent) override { } + void setVolume(uInt32 percent) override { } /** Adjusts the volume of the sound device based on the given direction. @@ -118,54 +96,6 @@ class SoundNull : 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 - { - 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) override - { - 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 override { return "TIASound"; } - private: // Following constructors and assignment operators not supported SoundNull() = delete; diff --git a/src/common/SoundSDL2.cxx b/src/common/SoundSDL2.cxx index 0fb5f2cb1..298d31a12 100644 --- a/src/common/SoundSDL2.cxx +++ b/src/common/SoundSDL2.cxx @@ -22,46 +22,38 @@ #include #include "SDL_lib.hxx" -#include "TIASnd.hxx" -#include "TIAConstants.hxx" #include "FrameBuffer.hxx" #include "Settings.hxx" #include "System.hxx" #include "OSystem.hxx" #include "Console.hxx" #include "SoundSDL2.hxx" +#include "AudioQueue.hxx" +#include "EmulationTiming.hxx" +#include "AudioSettings.hxx" +#include "audio/SimpleResampler.hxx" +#include "audio/LanczosResampler.hxx" // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -SoundSDL2::SoundSDL2(OSystem& osystem) +SoundSDL2::SoundSDL2(OSystem& osystem, AudioSettings& audioSettings) : Sound(osystem), - myIsEnabled(false), myIsInitializedFlag(false), - myLastRegisterSetCycle(0), - myNumChannels(0), - myFragmentSizeLogBase2(0), - myFragmentSizeLogDiv1(0), - myFragmentSizeLogDiv2(0), - myIsMuted(true), - myVolume(100) + myVolume(100), + myVolumeFactor(0xffff), + myCurrentFragment(nullptr), + myAudioSettings(audioSettings) { myOSystem.logMessage("SoundSDL2::SoundSDL2 started ...", 2); -#ifdef BSPF_WINDOWS - // TODO - remove the following code once we convert to the new sound - // core, and use 32-bit floating point samples and do - // our own resampling - SDL_setenv("SDL_AUDIODRIVER", "directsound", true); -#endif - // The sound system is opened only once per program run, to eliminate // issues with opening and closing it multiple times // This fixes a bug most prevalent with ATI video cards in Windows, // whereby sound stopped working after the first video change SDL_AudioSpec desired; - desired.freq = myOSystem.settings().getInt("freq"); - desired.format = AUDIO_S16SYS; + desired.freq = myAudioSettings.sampleRate(); + desired.format = AUDIO_F32SYS; desired.channels = 2; - desired.samples = myOSystem.settings().getInt("fragsize"); + desired.samples = myAudioSettings.fragmentSize(); desired.callback = callback; desired.userdata = static_cast(this); @@ -87,13 +79,9 @@ SoundSDL2::SoundSDL2(OSystem& osystem) 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); + + mute(true); myOSystem.logMessage("SoundSDL2::SoundSDL2 initialized", 2); } @@ -101,43 +89,43 @@ SoundSDL2::SoundSDL2(OSystem& osystem) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - SoundSDL2::~SoundSDL2() { - // Close the SDL audio system if it's initialized - if(myIsInitializedFlag) - { - SDL_CloseAudio(); - myIsEnabled = myIsInitializedFlag = false; - } + if (!myIsInitializedFlag) return; + + SDL_CloseAudio(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void SoundSDL2::setEnabled(bool state) { - myOSystem.settings().setValue("sound", state); + myAudioSettings.setEnabled(state); + if (myAudioQueue) myAudioQueue->ignoreOverflows(!state); myOSystem.logMessage(state ? "SoundSDL2::setEnabled(true)" : - "SoundSDL2::setEnabled(false)", 2); + "SoundSDL2::setEnabled(false)", 2); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void SoundSDL2::open() +void SoundSDL2::open(shared_ptr audioQueue, + EmulationTiming* emulationTiming) { + myEmulationTiming = emulationTiming; + myOSystem.logMessage("SoundSDL2::open started ...", 2); - myIsEnabled = false; mute(true); - if(!myIsInitializedFlag || !myOSystem.settings().getBool("sound")) + + audioQueue->ignoreOverflows(!myAudioSettings.enabled()); + if(!myAudioSettings.enabled()) { myOSystem.logMessage("Sound disabled\n", 1); return; } - // Now initialize the TIASound object which will actually generate sound - myTIASound.outputFrequency(myHardwareSpec.freq); - const string& chanResult = - myTIASound.channels(myHardwareSpec.channels, myNumChannels == 2); + myAudioQueue = audioQueue; + myUnderrun = true; + myCurrentFragment = nullptr; // Adjust volume to that defined in settings - myVolume = myOSystem.settings().getInt("volume"); - setVolume(myVolume); + setVolume(myAudioSettings.volume()); // Show some info ostringstream buf; @@ -146,12 +134,12 @@ void SoundSDL2::open() << " Frag size: " << uInt32(myHardwareSpec.samples) << endl << " Frequency: " << uInt32(myHardwareSpec.freq) << endl << " Channels: " << uInt32(myHardwareSpec.channels) - << " (" << chanResult << ")" << endl << endl; myOSystem.logMessage(buf.str(), 1); + initResampler(); + // And start the SDL sound subsystem ... - myIsEnabled = true; mute(false); myOSystem.logMessage("SoundSDL2::open finished", 2); @@ -160,15 +148,16 @@ void SoundSDL2::open() // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void SoundSDL2::close() { - if(myIsInitializedFlag) - { - myIsEnabled = false; - SDL_PauseAudio(1); - myLastRegisterSetCycle = 0; - myTIASound.reset(); - myRegWriteQueue.clear(); - myOSystem.logMessage("SoundSDL2::close", 2); - } + if(!myIsInitializedFlag) return; + + mute(true); + + if (myAudioQueue) myAudioQueue->closeSink(myCurrentFragment); + myAudioQueue.reset(); + myCurrentFragment = nullptr; + + myOSystem.logMessage("SoundSDL2::close", 2); + } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -176,33 +165,25 @@ void SoundSDL2::mute(bool state) { if(myIsInitializedFlag) { - myIsMuted = state; - SDL_PauseAudio(myIsMuted ? 1 : 0); + SDL_PauseAudio(state ? 1 : 0); } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void SoundSDL2::reset() { - if(myIsInitializedFlag) - { - SDL_PauseAudio(1); - myLastRegisterSetCycle = 0; - myTIASound.reset(); - myRegWriteQueue.clear(); - mute(myIsMuted); - } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void SoundSDL2::setVolume(Int32 percent) +void SoundSDL2::setVolume(uInt32 percent) { - if(myIsInitializedFlag && (percent >= 0) && (percent <= 100)) + if(myIsInitializedFlag && (percent <= 100)) { - myOSystem.settings().setValue("volume", percent); - SDL_LockAudio(); + myAudioSettings.setVolume(percent); myVolume = percent; - myTIASound.volume(percent); + + SDL_LockAudio(); + myVolumeFactor = std::pow(static_cast(percent) / 100.f, 2.f); SDL_UnlockAudio(); } } @@ -234,285 +215,80 @@ void SoundSDL2::adjustVolume(Int8 direction) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void SoundSDL2::setChannels(uInt32 channels) +uInt32 SoundSDL2::getFragmentSize() const { - if(channels == 1 || channels == 2) - myNumChannels = channels; + return myHardwareSpec.samples; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void SoundSDL2::setFrameRate(float framerate) +uInt32 SoundSDL2::getSampleRate() const { - // 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; + return myHardwareSpec.freq; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void SoundSDL2::set(uInt16 addr, uInt8 value, uInt64 cycle) +void SoundSDL2::processFragment(float* stream, uInt32 length) { - SDL_LockAudio(); + myResampler->fillFragment(stream, length); - // 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. - myRegWriteQueue.enqueue(addr, value, delta); - - // Update last cycle counter to the current cycle - myLastRegisterSetCycle = cycle; - - SDL_UnlockAudio(); + for (uInt32 i = 0; i < length; i++) stream[i] = stream[i] * myVolumeFactor; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void SoundSDL2::processFragment(Int16* stream, uInt32 length) +void SoundSDL2::initResampler() { - uInt32 channels = myHardwareSpec.channels; - length = length / channels; + Resampler::NextFragmentCallback nextFragmentCallback = [this] () -> Int16* { + Int16* nextFragment = nullptr; - // If there are excessive items on the queue then we'll remove some - if(myRegWriteQueue.duration() > myFragmentSizeLogDiv1) - { - double removed = 0.0; - while(removed < myFragmentSizeLogDiv2) - { - RegWrite& info = myRegWriteQueue.front(); - removed += info.delta; - myTIASound.set(info.addr, info.value); - myRegWriteQueue.dequeue(); - } - } - - double position = 0.0; - double remaining = length; - - while(remaining > 0.0) - { - if(myRegWriteQueue.size() == 0) - { - // There are no more pending TIA sound register updates so we'll - // use the current settings to finish filling the sound fragment - myTIASound.process(stream + (uInt32(position) * channels), - length - uInt32(position)); - - // Since we had to fill the fragment we'll reset the cycle counter - // to zero. NOTE: This isn't 100% correct, however, it'll do for - // now. We should really remember the overrun and remove it from - // the delta of the next write. - myLastRegisterSetCycle = 0; - break; - } + if (myUnderrun) + nextFragment = myAudioQueue->size() > myEmulationTiming->prebufferFragmentCount() ? + myAudioQueue->dequeue(myCurrentFragment) : nullptr; else - { - // There are pending TIA sound register updates so we need to - // update the sound buffer to the point of the next register update - RegWrite& info = myRegWriteQueue.front(); + nextFragment = myAudioQueue->dequeue(myCurrentFragment); - // How long will the remaining samples in the fragment take to play - double duration = remaining / myHardwareSpec.freq; + myUnderrun = nextFragment == nullptr; + if (nextFragment) myCurrentFragment = nextFragment; - // Does the register update occur before the end of the fragment? - if(info.delta <= duration) - { - // If the register update time hasn't already passed then - // process samples upto the point where it should occur - if(info.delta > 0.0) - { - // Process the fragment upto the next TIA register write. We - // round the count passed to process up if needed. - double samples = (myHardwareSpec.freq * info.delta); - myTIASound.process(stream + (uInt32(position) * channels), - uInt32(samples) + uInt32(position + samples) - - (uInt32(position) + uInt32(samples))); + return nextFragment; + }; - position += samples; - remaining -= samples; - } - myTIASound.set(info.addr, info.value); - myRegWriteQueue.dequeue(); - } - else - { - // The next register update occurs in the next fragment so finish - // this fragment with the current TIA settings and reduce the register - // update delay by the corresponding amount of time - myTIASound.process(stream + (uInt32(position) * channels), - length - uInt32(position)); - info.delta -= duration; - break; - } - } + Resampler::Format formatFrom = + Resampler::Format(myEmulationTiming->audioSampleRate(), myAudioQueue->fragmentSize(), myAudioQueue->isStereo()); + Resampler::Format formatTo = + Resampler::Format(myHardwareSpec.freq, myHardwareSpec.samples, myHardwareSpec.channels > 1); + + + switch (myAudioSettings.resamplingQuality()) { + case AudioSettings::ResamplingQuality::nearestNeightbour: + myResampler = make_unique(formatFrom, formatTo, nextFragmentCallback); + (cerr << "resampling quality 1: using nearest neighbor resampling\n").flush(); + break; + + case AudioSettings::ResamplingQuality::lanczos_2: + (cerr << "resampling quality 2: using nearest Lanczos resampling, a = 2\n").flush(); + myResampler = make_unique(formatFrom, formatTo, nextFragmentCallback, 2); + break; + + case AudioSettings::ResamplingQuality::lanczos_3: + (cerr << "resampling quality 3: using nearest Lanczos resampling, a = 3\n").flush(); + myResampler = make_unique(formatFrom, formatTo, nextFragmentCallback, 3); + break; + + default: + throw runtime_error("invalid resampling quality"); + break; } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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); - } + SoundSDL2* self = static_cast(udata); + + if (self->myAudioQueue) + self->processFragment(reinterpret_cast(stream), len >> 2); 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.putLong(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.getLong(); - } - catch(...) - { - myOSystem.logMessage("ERROR: SoundSDL2::load", 0); - return false; - } - - return true; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -SoundSDL2::RegWriteQueue::RegWriteQueue(uInt32 capacity) - : myBuffer(make_unique(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(uInt16 addr, uInt8 value, double delta) -{ - // 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(); - - RegWrite& reg = myBuffer[myTail]; - reg.addr = addr; - reg.value = value; - reg.delta = delta; - 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_unique(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); + SDL_memset(stream, 0, len); } #endif // SOUND_SUPPORT diff --git a/src/common/SoundSDL2.hxx b/src/common/SoundSDL2.hxx index f8e811032..5ffdd63ef 100644 --- a/src/common/SoundSDL2.hxx +++ b/src/common/SoundSDL2.hxx @@ -21,17 +21,20 @@ #define SOUND_SDL2_HXX class OSystem; +class AudioQueue; +class EmulationTiming; +class AudioSettings; #include "SDL_lib.hxx" #include "bspf.hxx" -#include "TIASnd.hxx" #include "Sound.hxx" +#include "audio/Resampler.hxx" /** This class implements the sound API for SDL. - @author Stephen Anthony and Bradford W. Mott + @author Stephen Anthony and Christian Speckner (DirtyHairy) */ class SoundSDL2 : public Sound { @@ -40,7 +43,7 @@ class SoundSDL2 : public Sound Create a new sound object. The init method must be invoked before using the object. */ - SoundSDL2(OSystem& osystem); + SoundSDL2(OSystem& osystem, AudioSettings& audioSettings); /** Destructor @@ -51,34 +54,15 @@ class SoundSDL2 : public Sound /** Enables/disables the sound subsystem. - @param state True or false, to enable or disable the sound system + @param enable Either true or false, to enable or disable the sound system */ - void setEnabled(bool state) 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; + void setEnabled(bool enable) override; /** Initializes the sound device. This must be called before any calls are made to derived methods. */ - void open() override; + void open(shared_ptr audioQueue, EmulationTiming* emulationTiming) override; /** Should be called to close the sound device. Once called the sound @@ -98,15 +82,6 @@ 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, uInt64 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 @@ -114,7 +89,7 @@ class SoundSDL2 : public Sound @param percent The new volume percentage level for the sound device */ - void setVolume(Int32 percent) override; + void setVolume(uInt32 percent) override; /** Adjusts the volume of the sound device based on the given direction. @@ -124,29 +99,9 @@ class SoundSDL2 : public Sound */ void adjustVolume(Int8 direction) override; - public: - /** - Saves the current state of this device to the given Serializer. + uInt32 getFragmentSize() const override; - @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"; } + uInt32 getSampleRate() const override; protected: /** @@ -157,124 +112,33 @@ class SoundSDL2 : public Sound @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; - - RegWrite(uInt16 a = 0, uInt8 v = 0, double d = 0.0) - : addr(a), value(v), delta(d) { } - }; - - /** - 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(uInt16 addr, uInt8 value, double delta); - - /** - 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; - }; + void processFragment(float* stream, uInt32 length); private: - // TIASound emulation object - TIASound myTIASound; - // Indicates if the sound subsystem is to be initialized - bool myIsEnabled; + void initResampler(); + private: // Indicates if the sound device was successfully initialized bool myIsInitializedFlag; - // Indicates the cycle when a sound register was last set - uInt64 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; - // Current volume as a percentage (0 - 100) uInt32 myVolume; + float myVolumeFactor; // Audio specification structure SDL_AudioSpec myHardwareSpec; - // Queue of TIA register writes - RegWriteQueue myRegWriteQueue; + shared_ptr myAudioQueue; + + EmulationTiming* myEmulationTiming; + + Int16* myCurrentFragment; + bool myUnderrun; + + unique_ptr myResampler; + + AudioSettings& myAudioSettings; private: // Callback function invoked by the SDL Audio library when it needs data diff --git a/src/common/Version.hxx b/src/common/Version.hxx index ac15e5f7b..ef52325c7 100644 --- a/src/common/Version.hxx +++ b/src/common/Version.hxx @@ -18,7 +18,7 @@ #ifndef VERSION_HXX #define VERSION_HXX -#define STELLA_VERSION "5.2_pre" +#define STELLA_VERSION "5.2_soundtest-1" #define STELLA_BUILD "4138" #endif diff --git a/src/common/audio/ConvolutionBuffer.cxx b/src/common/audio/ConvolutionBuffer.cxx new file mode 100644 index 000000000..983b06209 --- /dev/null +++ b/src/common/audio/ConvolutionBuffer.cxx @@ -0,0 +1,46 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#include "ConvolutionBuffer.hxx" + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +ConvolutionBuffer::ConvolutionBuffer(uInt32 size) + : myFirstIndex(0), + mySize(size) +{ + myData = make_unique(mySize); + memset(myData.get(), 0, mySize * sizeof(float)); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void ConvolutionBuffer::shift(float nextValue) +{ + myData[myFirstIndex] = nextValue; + myFirstIndex = (myFirstIndex + 1) % mySize; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +float ConvolutionBuffer::convoluteWith(float* kernel) const +{ + float result = 0.; + + for (uInt32 i = 0; i < mySize; i++) { + result += kernel[i] * myData[(myFirstIndex + i) % mySize]; + } + + return result; +} diff --git a/src/common/audio/ConvolutionBuffer.hxx b/src/common/audio/ConvolutionBuffer.hxx new file mode 100644 index 000000000..d0a71ad3b --- /dev/null +++ b/src/common/audio/ConvolutionBuffer.hxx @@ -0,0 +1,51 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#ifndef CONVOLUTION_BUFFER_HXX +#define CONVOLUTION_BUFFER_HXX + +#include "bspf.hxx" + +class ConvolutionBuffer +{ + public: + + ConvolutionBuffer(uInt32 size); + + void shift(float nextValue); + + float convoluteWith(float* kernel) const; + + private: + + unique_ptr myData; + + uInt32 myFirstIndex; + + uInt32 mySize; + + private: + + ConvolutionBuffer() = delete; + ConvolutionBuffer(const ConvolutionBuffer&) = delete; + ConvolutionBuffer(ConvolutionBuffer&&) = delete; + ConvolutionBuffer& operator=(const ConvolutionBuffer&) = delete; + ConvolutionBuffer& operator=(ConvolutionBuffer&&) = delete; + +}; + +#endif // CONVOLUTION_BUFFER_HXX diff --git a/src/common/audio/LanczosResampler.cxx b/src/common/audio/LanczosResampler.cxx new file mode 100644 index 000000000..25ecd82ec --- /dev/null +++ b/src/common/audio/LanczosResampler.cxx @@ -0,0 +1,208 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#include +#ifndef M_PI + #define M_PI 3.14159265358979323846f +#endif + +#include "LanczosResampler.hxx" + +namespace { + + constexpr float CLIPPING_FACTOR = 0.75; + + uInt32 reducedDenominator(uInt32 n, uInt32 d) + { + for (uInt32 i = std::min(n ,d); i > 1; i--) { + if ((n % i == 0) && (d % i == 0)) { + n /= i; + d /= i; + i = std::min(n ,d); + } + } + + return d; + } + + float sinc(float x) + { + // We calculate the sinc with double precision in order to compensate for precision loss + // around zero + return x == 0.f ? 1 : static_cast( + sin(M_PI * static_cast(x)) / M_PI / static_cast(x) + ); + } + + double lanczosKernel(float x, uInt32 a) { + return sinc(x) * sinc(x / static_cast(a)); + } + +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +LanczosResampler::LanczosResampler( + Resampler::Format formatFrom, + Resampler::Format formatTo, + Resampler::NextFragmentCallback nextFragmentCallback, + uInt32 kernelParameter) +: + Resampler(formatFrom, formatTo, nextFragmentCallback), + // In order to find the number of kernels we need to precompute, we need to find N minimal such that + // + // N / formatTo.sampleRate = M / formatFrom.sampleRate + // + // with integral N and M. Equivalently, we have + // + // formatFrom.sampleRate / formatTo.sampleRate = M / N + // + // -> we find N from fully reducing the fraction. + myPrecomputedKernelCount(reducedDenominator(formatFrom.sampleRate, formatTo.sampleRate)), + myKernelSize(2 * kernelParameter), + myCurrentKernelIndex(0), + myKernelParameter(kernelParameter), + myCurrentFragment(nullptr), + myFragmentIndex(0), + myIsUnderrun(true), + myTimeIndex(0) +{ + myPrecomputedKernels = make_unique(myPrecomputedKernelCount * myKernelSize); + + if (myFormatFrom.stereo) + { + myBufferL = make_unique(myKernelSize); + myBufferR = make_unique(myKernelSize); + } + else + myBuffer = make_unique(myKernelSize); + + precomputeKernels(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void LanczosResampler::precomputeKernels() +{ + // timeIndex = time * formatFrom.sampleRate * formatTo.sampleRAte + uInt32 timeIndex = 0; + + for (uInt32 i = 0; i < myPrecomputedKernelCount; i++) { + float* kernel = myPrecomputedKernels.get() + myKernelSize * i; + // The kernel is normalized such to be evaluate on time * formatFrom.sampleRate + float center = + static_cast(timeIndex) / static_cast(myFormatTo.sampleRate); + + for (uInt32 j = 0; j < 2 * myKernelParameter; j++) { + kernel[j] = lanczosKernel( + center - static_cast(j) + static_cast(myKernelParameter) - 1.f, myKernelParameter + ) * CLIPPING_FACTOR; + } + + // Next step: time += 1 / formatTo.sampleRate + // + // By construction, we limit the argument during kernel evaluation to 0 .. 1, which + // corresponds to 0 .. 1 / formatFrom.sampleRate for time. To implement this, we decompose + // time as follows: + // + // time = N / formatFrom.sampleRate + delta + // timeIndex = N * formatTo.sampleRate + delta * formatTo.sampleRate * formatFrom.sampleRate + // + // with N integral and delta < 0. From this, it follows that we replace + // time with delta, i.e. take the modulus of timeIndex. + timeIndex = (timeIndex + myFormatFrom.sampleRate) % myFormatTo.sampleRate; + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void LanczosResampler::fillFragment(float* fragment, uInt32 length) +{ + if (myIsUnderrun) { + Int16* nextFragment = myNextFragmentCallback(); + + if (nextFragment) { + myCurrentFragment = nextFragment; + myFragmentIndex = 0; + myIsUnderrun = false; + } + } + + if (!myCurrentFragment) { + memset(fragment, 0, sizeof(float) * length); + return; + } + + const uInt32 outputSamples = myFormatTo.stereo ? (length >> 1) : length; + + for (uInt32 i = 0; i < outputSamples; i++) { + float* kernel = myPrecomputedKernels.get() + (myCurrentKernelIndex * myKernelSize); + myCurrentKernelIndex = (myCurrentKernelIndex + 1) % myPrecomputedKernelCount; + + if (myFormatFrom.stereo) { + float sampleL = myBufferL->convoluteWith(kernel); + float sampleR = myBufferR->convoluteWith(kernel); + + if (myFormatTo.stereo) { + fragment[2*i] = sampleL; + fragment[2*i + 1] = sampleR; + } + else + fragment[i] = (sampleL + sampleR) / 2.f; + } else { + float sample = myBuffer->convoluteWith(kernel); + + if (myFormatTo.stereo) + fragment[2*i] = fragment[2*i + 1] = sample; + else + fragment[i] = sample; + } + + myTimeIndex += myFormatFrom.sampleRate; + + uInt32 samplesToShift = myTimeIndex / myFormatTo.sampleRate; + if (samplesToShift == 0) continue; + + myTimeIndex %= myFormatTo.sampleRate; + shiftSamples(samplesToShift); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +inline void LanczosResampler::shiftSamples(uInt32 samplesToShift) +{ + while (samplesToShift-- > 0) { + if (myFormatFrom.stereo) { + myBufferL->shift(myCurrentFragment[2*myFragmentIndex] / static_cast(0x7fff)); + myBufferR->shift(myCurrentFragment[2*myFragmentIndex + 1] / static_cast(0x7fff)); + } + else + myBuffer->shift(myCurrentFragment[myFragmentIndex] / static_cast(0x7fff)); + + myFragmentIndex++; + + if (myFragmentIndex >= myFormatFrom.fragmentSize) { + myFragmentIndex %= myFormatFrom.fragmentSize; + + Int16* nextFragment = myNextFragmentCallback(); + if (nextFragment) { + myCurrentFragment = nextFragment; + myIsUnderrun = false; + } else { + (cerr << "audio buffer underrun\n").flush(); + myIsUnderrun = true; + } + } + } +} diff --git a/src/common/audio/LanczosResampler.hxx b/src/common/audio/LanczosResampler.hxx new file mode 100644 index 000000000..0f92f48d5 --- /dev/null +++ b/src/common/audio/LanczosResampler.hxx @@ -0,0 +1,65 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#ifndef LANCZOS_RESAMPLER_HXX +#define LANCZOS_RESAMPLER_HXX + +#include "bspf.hxx" +#include "Resampler.hxx" +#include "ConvolutionBuffer.hxx" + +class LanczosResampler : public Resampler +{ + public: + LanczosResampler( + Resampler::Format formatFrom, + Resampler::Format formatTo, + Resampler::NextFragmentCallback nextFragmentCallback, + uInt32 kernelParameter + ); + + virtual void fillFragment(float* fragment, uInt32 length); + + virtual ~LanczosResampler() = default; + + private: + + void precomputeKernels(); + + void shiftSamples(uInt32 samplesToShift); + + private: + + uInt32 myPrecomputedKernelCount; + uInt32 myKernelSize; + uInt32 myCurrentKernelIndex; + unique_ptr myPrecomputedKernels; + + uInt32 myKernelParameter; + + unique_ptr myBuffer; + unique_ptr myBufferL; + unique_ptr myBufferR; + + Int16* myCurrentFragment; + uInt32 myFragmentIndex; + bool myIsUnderrun; + + uInt32 myTimeIndex; +}; + +#endif // LANCZOS_RESAMPLER_HXX diff --git a/src/common/audio/Resampler.hxx b/src/common/audio/Resampler.hxx new file mode 100644 index 000000000..cd39818e4 --- /dev/null +++ b/src/common/audio/Resampler.hxx @@ -0,0 +1,79 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#ifndef RESAMPLER_HXX +#define RESAMPLER_HXX + +#include + +#include "bspf.hxx" + +class Resampler { + public: + + using NextFragmentCallback = std::function; + + class Format { + public: + + Format(uInt32 f_sampleRate, uInt32 f_fragmentSize, bool f_stereo) : + sampleRate(f_sampleRate), + fragmentSize(f_fragmentSize), + stereo(f_stereo) + {} + + public: + + uInt32 sampleRate; + uInt32 fragmentSize; + bool stereo; + + private: + + Format() = delete; + }; + + public: + + Resampler(Format formatFrom, Format formatTo, NextFragmentCallback nextFragmentCallback) : + myFormatFrom(formatFrom), + myFormatTo(formatTo), + myNextFragmentCallback(nextFragmentCallback) + {} + + virtual void fillFragment(float* fragment, uInt32 length) = 0; + + virtual ~Resampler() {} + + protected: + + Format myFormatFrom; + Format myFormatTo; + + NextFragmentCallback myNextFragmentCallback; + + private: + + Resampler() = delete; + Resampler(const Resampler&) = delete; + Resampler(Resampler&&) = delete; + Resampler& operator=(const Resampler&) = delete; + Resampler& operator=(Resampler&&) = delete; + +}; + +#endif // RESAMPLER_HXX diff --git a/src/common/audio/SimpleResampler.cxx b/src/common/audio/SimpleResampler.cxx new file mode 100644 index 000000000..5e7b4ce65 --- /dev/null +++ b/src/common/audio/SimpleResampler.cxx @@ -0,0 +1,96 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#include "SimpleResampler.hxx" + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +SimpleResampler::SimpleResampler( + Resampler::Format formatFrom, + Resampler::Format formatTo, + Resampler::NextFragmentCallback nextFragmentCallback) + : Resampler(formatFrom, formatTo, nextFragmentCallback), + myCurrentFragment(nullptr), + myTimeIndex(0), + myFragmentIndex(0), + myIsUnderrun(true) +{ +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void SimpleResampler::fillFragment(float* fragment, uInt32 length) +{ + if (myIsUnderrun) { + Int16* nextFragment = myNextFragmentCallback(); + + if (nextFragment) { + myCurrentFragment = nextFragment; + myFragmentIndex = 0; + myIsUnderrun = false; + } + } + + if (!myCurrentFragment) { + memset(fragment, 0, sizeof(float) * length); + return; + } + + const uInt32 outputSamples = myFormatTo.stereo ? (length >> 1) : length; + + // For the following math, remember that myTimeIndex = time * myFormatFrom.sampleRate * myFormatTo.sampleRate + for (uInt32 i = 0; i < outputSamples; i++) { + if (myFormatFrom.stereo) { + float sampleL = static_cast(myCurrentFragment[2*myFragmentIndex]) / static_cast(0x7fff); + float sampleR = static_cast(myCurrentFragment[2*myFragmentIndex + 1]) / static_cast(0x7fff); + + if (myFormatTo.stereo) { + fragment[2*i] = sampleL; + fragment[2*i + 1] = sampleR; + } + else + fragment[i] = (sampleL + sampleR) / 2.f; + } else { + float sample = static_cast(myCurrentFragment[myFragmentIndex] / static_cast(0x7fff)); + + if (myFormatTo.stereo) + fragment[2*i] = fragment[2*i + 1] = sample; + else + fragment[i] = sample; + } + + // time += 1 / myFormatTo.sampleRate + myTimeIndex += myFormatFrom.sampleRate; + + // time >= 1 / myFormatFrom.sampleRate + if (myTimeIndex >= myFormatTo.sampleRate) { + // myFragmentIndex += time * myFormatFrom.sampleRate + myFragmentIndex += myTimeIndex / myFormatTo.sampleRate; + myTimeIndex %= myFormatTo.sampleRate; + } + + if (myFragmentIndex >= myFormatFrom.fragmentSize) { + myFragmentIndex %= myFormatFrom.fragmentSize; + + Int16* nextFragment = myNextFragmentCallback(); + if (nextFragment) + myCurrentFragment = nextFragment; + else { + (cerr << "audio buffer underrun\n").flush(); + myIsUnderrun = true; + } + } + } +} diff --git a/src/common/audio/SimpleResampler.hxx b/src/common/audio/SimpleResampler.hxx new file mode 100644 index 000000000..92de9e05f --- /dev/null +++ b/src/common/audio/SimpleResampler.hxx @@ -0,0 +1,52 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#ifndef SIMPLE_RESAMPLER_HXX +#define SIMPLE_RESAMPLER_HXX + +#include "bspf.hxx" +#include "Resampler.hxx" + +class SimpleResampler : public Resampler +{ + public: + SimpleResampler( + Resampler::Format formatFrom, + Resampler::Format formatTo, + Resampler::NextFragmentCallback NextFragmentCallback + ); + + virtual void fillFragment(float* fragment, uInt32 length); + + private: + + Int16* myCurrentFragment; + uInt32 myTimeIndex; + uInt32 myFragmentIndex; + bool myIsUnderrun; + + private: + + SimpleResampler() = delete; + SimpleResampler(const SimpleResampler&) = delete; + SimpleResampler(SimpleResampler&&) = delete; + SimpleResampler& operator=(const SimpleResampler&) = delete; + SimpleResampler& operator=(const SimpleResampler&&) = delete; + +}; + +#endif // SIMPLE_RESAMPLER_HXX diff --git a/src/common/audio/module.mk b/src/common/audio/module.mk new file mode 100644 index 000000000..67ae01063 --- /dev/null +++ b/src/common/audio/module.mk @@ -0,0 +1,12 @@ +MODULE := src/common/audio + +MODULE_OBJS := \ + src/common/audio/SimpleResampler.o \ + src/common/audio/ConvolutionBuffer.o \ + src/common/audio/LanczosResampler.o + +MODULE_DIRS += \ + src/emucore/tia + +# Include common rules +include $(srcdir)/common.rules diff --git a/src/common/module.mk b/src/common/module.mk index bb06a15c4..4be025053 100644 --- a/src/common/module.mk +++ b/src/common/module.mk @@ -15,7 +15,9 @@ MODULE_OBJS := \ src/common/RewindManager.o \ src/common/SoundSDL2.o \ src/common/StateManager.o \ - src/common/ZipHandler.o + src/common/ZipHandler.o \ + src/common/AudioQueue.o \ + src/common/AudioSettings.o MODULE_DIRS += \ src/common diff --git a/src/debugger/Debugger.cxx b/src/debugger/Debugger.cxx index 65b440469..731cebb82 100644 --- a/src/debugger/Debugger.cxx +++ b/src/debugger/Debugger.cxx @@ -513,7 +513,7 @@ void Debugger::nextFrame(int frames) unlockSystem(); while(frames) { - myOSystem.console().tia().update(); + myOSystem.console().tia().update(myOSystem.console().emulationTiming().maxCyclesPerTimeslice()); --frames; } lockSystem(); diff --git a/src/debugger/gui/TiaOutputWidget.cxx b/src/debugger/gui/TiaOutputWidget.cxx index 384fa8d04..033c7caa7 100644 --- a/src/debugger/gui/TiaOutputWidget.cxx +++ b/src/debugger/gui/TiaOutputWidget.cxx @@ -168,6 +168,8 @@ void TiaOutputWidget::drawWidget(bool hilite) uInt32 scanx, scany, scanoffset; bool visible = instance().console().tia().electronBeamPos(scanx, scany); scanoffset = width * scany + scanx; + uInt8* tiaOutputBuffer = instance().console().tia().outputBuffer(); + TIASurface& tiaSurface(instance().frameBuffer().tiaSurface()); for(uInt32 y = 0, i = 0; y < height; ++y) { @@ -175,7 +177,7 @@ void TiaOutputWidget::drawWidget(bool hilite) for(uInt32 x = 0; x < width; ++x, ++i) { uInt8 shift = i >= scanoffset ? 1 : 0; - uInt32 pixel = instance().frameBuffer().tiaSurface().pixel(i, shift); + uInt32 pixel = tiaSurface.mapIndexedPixel(tiaOutputBuffer[i], shift); *line_ptr++ = pixel; *line_ptr++ = pixel; } diff --git a/src/emucore/Console.cxx b/src/emucore/Console.cxx index b96cc6df0..9b2d6ad20 100644 --- a/src/emucore/Console.cxx +++ b/src/emucore/Console.cxx @@ -55,6 +55,8 @@ #include "Version.hxx" #include "TIAConstants.hxx" #include "FrameLayout.hxx" +#include "AudioQueue.hxx" +#include "AudioSettings.hxx" #include "frame-manager/FrameManager.hxx" #include "frame-manager/FrameLayoutDetector.hxx" #include "frame-manager/YStartDetector.hxx" @@ -75,17 +77,17 @@ namespace { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Console::Console(OSystem& osystem, unique_ptr& cart, - const Properties& props) + const Properties& props, AudioSettings& audioSettings) : myOSystem(osystem), myEvent(osystem.eventHandler().event()), myProperties(props), myCart(std::move(cart)), myDisplayFormat(""), // Unknown TV format @ start - myFramerate(0.0), // Unknown framerate @ start myCurrentFormat(0), // Unknown format @ start, myAutodetectedYstart(0), myUserPaletteDefined(false), - myConsoleTiming(ConsoleTiming::ntsc) + myConsoleTiming(ConsoleTiming::ntsc), + myAudioSettings(audioSettings) { // Load user-defined palette for this ROM loadUserPalette(); @@ -93,7 +95,7 @@ Console::Console(OSystem& osystem, unique_ptr& cart, // Create subsystems for the console my6502 = make_unique(myOSystem.settings()); myRiot = make_unique(*this, myOSystem.settings()); - myTIA = make_unique(*this, myOSystem.sound(), myOSystem.settings()); + myTIA = make_unique(*this, myOSystem.settings()); myFrameManager = make_unique(); mySwitches = make_unique(myEvent, myProperties, myOSystem.settings()); @@ -146,7 +148,6 @@ Console::Console(OSystem& osystem, unique_ptr& cart, // Note that this can be overridden if a format is forced // For example, if a PAL ROM is forced to be NTSC, it will use NTSC-like // properties (60Hz, 262 scanlines, etc), but likely result in flicker - // The TIA will self-adjust the framerate if necessary setTIAProperties(); if(myDisplayFormat == "NTSC") { @@ -205,6 +206,10 @@ Console::~Console() // Some smart controllers need to be informed that the console is going away myLeftControl->close(); myRightControl->close(); + + // Close audio to prevent invalid access to myConsoleTiming from the audio + // callback + myOSystem.sound().close(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -381,6 +386,7 @@ void Console::setFormat(int format) setTIAProperties(); myTIA->frameReset(); initializeVideo(); // takes care of refreshing the screen + initializeAudio(); // ensure that audio synthesis is set up to match emulation speed } myOSystem.frameBuffer().showMessage(message); @@ -569,37 +575,29 @@ FBInitStatus Console::initializeVideo(bool full) } setPalette(myOSystem.settings().getString("palette")); - // Set the correct framerate based on the format of the ROM - // This can be overridden by changing the framerate in the - // VideoDialog box or on the commandline, but it can't be saved - // (ie, framerate is now determined based on number of scanlines). - int framerate = myOSystem.settings().getInt("framerate"); - if(framerate > 0) myFramerate = float(framerate); - myOSystem.setFramerate(myFramerate); - - // Make sure auto-frame calculation is only enabled when necessary - myTIA->enableAutoFrame(framerate <= 0); - return fbstatus; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void Console::initializeAudio() { - // Initialize the sound interface. - // The # of channels can be overridden in the AudioDialog box or on - // the commandline, but it can't be saved. - int framerate = myOSystem.settings().getInt("framerate"); - if(framerate > 0) myFramerate = float(framerate); - const string& sound = myProperties.get(Cartridge_Sound); - myOSystem.sound().close(); - 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); + myEmulationTiming + .updatePlaybackRate(myOSystem.sound().getSampleRate()) + .updatePlaybackPeriod(myOSystem.sound().getFragmentSize()) + .updateAudioQueueExtraFragments(myAudioSettings.bufferSize()) + .updateAudioQueueHeadroom(myAudioSettings.headroom()) + .updateSpeedFactor(myOSystem.settings().getFloat("speed")); + + (cout << "sample rate: " << myOSystem.sound().getSampleRate() << std::endl).flush(); + (cout << "fragment size: " << myOSystem.sound().getFragmentSize() << std::endl).flush(); + (cout << "prebuffer fragment count: " << myEmulationTiming.prebufferFragmentCount() << std::endl).flush(); + + createAudioQueue(); + myTIA->setAudioQueue(myAudioQueue); + + myOSystem.sound().open(myAudioQueue, &myEmulationTiming); } /* Original frying research and code by Fred Quimby. @@ -729,16 +727,11 @@ void Console::setTIAProperties() myDisplayFormat == "SECAM60") { // Assume we've got ~262 scanlines (NTSC-like format) - myFramerate = 60.0; - myConsoleInfo.InitialFrameRate = "60"; myTIA->setLayout(FrameLayout::ntsc); } else { // Assume we've got ~312 scanlines (PAL-like format) - myFramerate = 50.0; - myConsoleInfo.InitialFrameRate = "50"; - // PAL ROMs normally need at least 250 lines if (height != 0) height = std::max(height, 250u); @@ -747,6 +740,18 @@ void Console::setTIAProperties() myTIA->setYStart(ystart != 0 ? ystart : myAutodetectedYstart); myTIA->setHeight(height); + + myEmulationTiming.updateFrameLayout(myTIA->frameLayout()); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Console::createAudioQueue() +{ + myAudioQueue = make_shared( + myEmulationTiming.audioFragmentSize(), + myEmulationTiming.audioQueueCapacity(), + myProperties.get(Cartridge_Sound) == "STEREO" + ); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -967,11 +972,9 @@ void Console::generateColorLossPalette() } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void Console::setFramerate(float framerate) +float Console::getFramerate() const { - myFramerate = framerate; - myOSystem.setFramerate(framerate); - myOSystem.sound().setFrameRate(framerate); + return myTIA->frameBufferFrameRate(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1103,7 +1106,7 @@ uInt32 Console::ourPALPalette[256] = { 0x001759, 0, 0x00247c, 0, 0x1d469e, 0, 0x3f68c0, 0, // 288 b 0x618ae2, 0, 0x83acff, 0, 0xa5ceff, 0, 0xc7f0ff, 0, 0x12006d, 0, 0x34038f, 0, 0x5625b1, 0, 0x7847d3, 0, // 2a0 c - 0x9a69f5, 0, 0xb48cff, 0, 0xc9adff, 0, 0xe1d1ff, 0, // was ..0xbc8bff, 0xdeadff, 0xffcfff, + 0x9a69f5, 0, 0xb48cff, 0, 0xc9adff, 0, 0xe1d1ff, 0, // was ..0xbc8bff, 0xdeadff, 0xffcfff, 0x000070, 0, 0x161292, 0, 0x3834b4, 0, 0x5a56d6, 0, // 2b8 d 0x7c78f8, 0, 0x9e9aff, 0, 0xc0bcff, 0, 0xe2deff, 0, 0x000000, 0, 0x121212, 0, 0x242424, 0, 0x484848, 0, // 2d0 e diff --git a/src/emucore/Console.hxx b/src/emucore/Console.hxx index b258fba40..23835463c 100644 --- a/src/emucore/Console.hxx +++ b/src/emucore/Console.hxx @@ -27,6 +27,8 @@ class M6532; class Cartridge; class CompuMate; class Debugger; +class AudioQueue; +class AudioSettings; #include "bspf.hxx" #include "Control.hxx" @@ -36,6 +38,7 @@ class Debugger; #include "Serializable.hxx" #include "EventHandlerConstants.hxx" #include "NTSCFilter.hxx" +#include "EmulationTiming.hxx" #include "frame-manager/AbstractFrameManager.hxx" /** @@ -49,7 +52,6 @@ struct ConsoleInfo string Control0; string Control1; string DisplayFormat; - string InitialFrameRate; }; /** @@ -79,7 +81,7 @@ class Console : public Serializable @param props The properties for the cartridge */ Console(OSystem& osystem, unique_ptr& cart, - const Properties& props); + const Properties& props, AudioSettings& audioSettings); /** Destructor @@ -190,6 +192,11 @@ class Console : public Serializable */ void stateChanged(EventHandlerState state); + /** + Retrieve emulation timing provider. + */ + EmulationTiming& emulationTiming() { return myEmulationTiming; } + public: /** Toggle between NTSC/PAL/SECAM (and variants) display format. @@ -270,16 +277,9 @@ class Console : public Serializable void changeHeight(int direction); /** - Sets the framerate of the console, which in turn communicates - this to all applicable subsystems. + Returns the current framerate. */ - void setFramerate(float framerate); - - /** - Returns the framerate based on a number of factors - (whether 'framerate' is set, what display format is in use, etc) - */ - float getFramerate() const { return myFramerate; } + float getFramerate() const; /** Toggles the TIA bit specified in the method name. @@ -330,6 +330,11 @@ class Console : public Serializable */ void setTIAProperties(); + /** + Create the audio queue + */ + void createAudioQueue(); + /** Adds the left and right controllers to the console. */ @@ -389,6 +394,9 @@ class Console : public Serializable // The frame manager instance that is used during emulation. unique_ptr myFrameManager; + // The audio fragment queue that connects TIA and audio driver + shared_ptr myAudioQueue; + // Pointer to the Cartridge (the debugger needs it) unique_ptr myCart; @@ -404,9 +412,6 @@ class Console : public Serializable // The currently defined display format (NTSC/PAL/SECAM) string myDisplayFormat; - // The currently defined display framerate - float myFramerate; - // Display format currently in use uInt32 myCurrentFormat; @@ -423,6 +428,13 @@ class Console : public Serializable // Contains timing information for this console ConsoleTiming myConsoleTiming; + // Emulation timing provider. This ties together the timing of the core emulation loop + // and the parameters that govern audio synthesis + EmulationTiming myEmulationTiming; + + // The audio settings + AudioSettings& myAudioSettings; + // Table of RGB values for NTSC, PAL and SECAM static uInt32 ourNTSCPalette[256]; static uInt32 ourPALPalette[256]; diff --git a/src/emucore/DispatchResult.cxx b/src/emucore/DispatchResult.cxx new file mode 100644 index 000000000..e6ca383c9 --- /dev/null +++ b/src/emucore/DispatchResult.cxx @@ -0,0 +1,55 @@ +//============================================================================ +// +// MM MM 6666 555555 0000 2222 +// MMMM MMMM 66 66 55 00 00 22 22 +// MM MMM MM 66 55 00 00 22 +// MM M MM 66666 55555 00 00 22222 -- "A 6502 Microprocessor Emulator" +// MM MM 66 66 55 00 00 22 +// MM MM 66 66 55 55 00 00 22 +// MM MM 6666 5555 0000 222222 +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#include "DispatchResult.hxx" + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void DispatchResult::assertStatus(Status status) const +{ + if (myStatus != status) throw runtime_error("invalid status for operation"); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool DispatchResult::isSuccess() const +{ + return myStatus == Status::debugger || myStatus == Status::ok; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void DispatchResult::setOk(uInt32 cycles) +{ + myStatus = Status::ok; + myCycles = cycles; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void DispatchResult::setDebugger(uInt32 cycles, const string& message, int address, bool wasReadTrap) +{ + myStatus = Status::debugger; + myCycles = cycles; + myMessage = message; + myAddress = address; + myWasReadTrap = wasReadTrap; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void DispatchResult::setFatal(uInt32 cycles) +{ + myCycles = cycles; + + myStatus = Status::fatal; +} diff --git a/src/emucore/DispatchResult.hxx b/src/emucore/DispatchResult.hxx new file mode 100644 index 000000000..4bb1da098 --- /dev/null +++ b/src/emucore/DispatchResult.hxx @@ -0,0 +1,67 @@ +//============================================================================ +// +// MM MM 6666 555555 0000 2222 +// MMMM MMMM 66 66 55 00 00 22 22 +// MM MMM MM 66 55 00 00 22 +// MM M MM 66666 55555 00 00 22222 -- "A 6502 Microprocessor Emulator" +// MM MM 66 66 55 00 00 22 +// MM MM 66 66 55 55 00 00 22 +// MM MM 6666 5555 0000 222222 +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#ifndef DISPATCH_RESULT_HXX +#define DISPATCH_RESULT_HXX + +#include "bspf.hxx" + +class DispatchResult +{ + public: + enum class Status { invalid, ok, debugger, fatal }; + + public: + + DispatchResult() : myStatus(Status::invalid) {} + + Status getStatus() const { return myStatus; } + + uInt32 getCycles() const { return myCycles; } + + const string& getMessage() const { assertStatus(Status::debugger); return myMessage; } + + uInt16 getAddress() const { assertStatus(Status::debugger); return myAddress; } + + bool wasReadTrap() const { assertStatus(Status::debugger); return myWasReadTrap; } + + bool isSuccess() const; + + void setOk(uInt32 cycles); + + void setDebugger(uInt32 cycles, const string& message = "", int address = -1, bool wasReadTrap = false); + + void setFatal(uInt32 cycles); + + private: + + void assertStatus(Status status) const; + + private: + + Status myStatus; + + uInt32 myCycles; + + string myMessage; + + int myAddress; + + bool myWasReadTrap; +}; + +#endif // DISPATCH_RESULT_HXX diff --git a/src/emucore/EmulationTiming.cxx b/src/emucore/EmulationTiming.cxx new file mode 100644 index 000000000..80f29e084 --- /dev/null +++ b/src/emucore/EmulationTiming.cxx @@ -0,0 +1,202 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#include + +#include "EmulationTiming.hxx" + +namespace { + constexpr uInt32 AUDIO_HALF_FRAMES_PER_FRAGMENT = 1; + + uInt32 discreteDivCeil(uInt32 n, uInt32 d) + { + return n / d + ((n % d == 0) ? 0 : 1); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +EmulationTiming::EmulationTiming(FrameLayout frameLayout) : + myFrameLayout(frameLayout), + myPlaybackRate(44100), + myPlaybackPeriod(512), + myAudioQueueExtraFragments(1), + myAudioQueueHeadroom(2), + mySpeedFactor(1) +{ + recalculate(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +EmulationTiming& EmulationTiming::updateFrameLayout(FrameLayout frameLayout) +{ + myFrameLayout = frameLayout; + recalculate(); + + return *this; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +EmulationTiming& EmulationTiming::updatePlaybackRate(uInt32 playbackRate) +{ + myPlaybackRate = playbackRate; + recalculate(); + + return *this; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +EmulationTiming& EmulationTiming::updatePlaybackPeriod(uInt32 playbackPeriod) +{ + myPlaybackPeriod = playbackPeriod; + recalculate(); + + return *this; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +EmulationTiming& EmulationTiming::updateAudioQueueExtraFragments(uInt32 audioQueueExtraFragments) +{ + myAudioQueueExtraFragments = audioQueueExtraFragments; + recalculate(); + + return *this; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +EmulationTiming& EmulationTiming::updateAudioQueueHeadroom(uInt32 audioQueueHeadroom) +{ + myAudioQueueHeadroom = audioQueueHeadroom; + recalculate(); + + return *this; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +EmulationTiming& EmulationTiming::updateSpeedFactor(float speedFactor) +{ + mySpeedFactor = speedFactor; + recalculate(); + + return *this; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt32 EmulationTiming::maxCyclesPerTimeslice() const +{ + return myMaxCyclesPerTimeslice; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt32 EmulationTiming::minCyclesPerTimeslice() const +{ + return myMinCyclesPerTimeslice; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt32 EmulationTiming::linesPerFrame() const +{ + return myLinesPerFrame; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt32 EmulationTiming::cyclesPerFrame() const +{ + return myCyclesPerFrame; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt32 EmulationTiming::framesPerSecond() const +{ + return myFramesPerSecond; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt32 EmulationTiming::cyclesPerSecond() const +{ + return myCyclesPerSecond; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt32 EmulationTiming::audioFragmentSize() const +{ + return myAudioFragmentSize; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt32 EmulationTiming::audioSampleRate() const +{ + return myAudioSampleRate; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt32 EmulationTiming::audioQueueCapacity() const +{ + return myAudioQueueCapacity; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt32 EmulationTiming::prebufferFragmentCount() const +{ + return myPrebufferFragmentCount; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void EmulationTiming::recalculate() +{ + switch (myFrameLayout) { + case FrameLayout::ntsc: + myLinesPerFrame = 262; + break; + + case FrameLayout::pal: + myLinesPerFrame = 312; + break; + + default: + throw runtime_error("invalid frame layout"); + } + + switch (myFrameLayout) { + case FrameLayout::ntsc: + myFramesPerSecond = round(mySpeedFactor * 60); + break; + + case FrameLayout::pal: + myFramesPerSecond = round(mySpeedFactor * 50); + break; + + default: + throw runtime_error("invalid frame layout"); + } + + myCyclesPerFrame = 76 * myLinesPerFrame; + myMaxCyclesPerTimeslice = round(mySpeedFactor * myCyclesPerFrame * 2); + myMinCyclesPerTimeslice = round(mySpeedFactor * myCyclesPerFrame / 2); + myCyclesPerSecond = myCyclesPerFrame * myFramesPerSecond; + myAudioFragmentSize = round(mySpeedFactor * AUDIO_HALF_FRAMES_PER_FRAGMENT * myLinesPerFrame); + myAudioSampleRate = 2 * myLinesPerFrame * myFramesPerSecond; + + myPrebufferFragmentCount = discreteDivCeil( + myPlaybackPeriod * myAudioSampleRate, + myAudioFragmentSize * myPlaybackRate + ) + myAudioQueueHeadroom; + + myAudioQueueCapacity = std::max( + myPrebufferFragmentCount, + discreteDivCeil(myMaxCyclesPerTimeslice * myAudioSampleRate, myAudioFragmentSize * myCyclesPerSecond) + ) + myAudioQueueExtraFragments; +} diff --git a/src/emucore/EmulationTiming.hxx b/src/emucore/EmulationTiming.hxx new file mode 100644 index 000000000..578844e4e --- /dev/null +++ b/src/emucore/EmulationTiming.hxx @@ -0,0 +1,96 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#ifndef EMULATION_TIMING_HXX +#define EMULATION_TIMING_HXX + +#include "bspf.hxx" +#include "FrameLayout.hxx" + +class EmulationTiming { + public: + + EmulationTiming(FrameLayout frameLayout = FrameLayout::ntsc); + + EmulationTiming& updateFrameLayout(FrameLayout frameLayout); + + EmulationTiming& updatePlaybackRate(uInt32 playbackRate); + + EmulationTiming& updatePlaybackPeriod(uInt32 period); + + EmulationTiming& updateAudioQueueExtraFragments(uInt32 audioQueueExtraFragments); + + EmulationTiming& updateAudioQueueHeadroom(uInt32 audioQueueHeadroom); + + EmulationTiming& updateSpeedFactor(float speedFactor); + + uInt32 maxCyclesPerTimeslice() const; + + uInt32 minCyclesPerTimeslice() const; + + uInt32 linesPerFrame() const; + + uInt32 cyclesPerFrame() const; + + uInt32 framesPerSecond() const; + + uInt32 cyclesPerSecond() const; + + uInt32 audioFragmentSize() const; + + uInt32 audioSampleRate() const; + + uInt32 audioQueueCapacity() const; + + uInt32 prebufferFragmentCount() const; + + private: + + void recalculate(); + + private: + + FrameLayout myFrameLayout; + + uInt32 myPlaybackRate; + uInt32 myPlaybackPeriod; + uInt32 myAudioQueueExtraFragments; + uInt32 myAudioQueueHeadroom; + + uInt32 myMaxCyclesPerTimeslice; + uInt32 myMinCyclesPerTimeslice; + uInt32 myLinesPerFrame; + uInt32 myCyclesPerFrame; + uInt32 myFramesPerSecond; + uInt32 myCyclesPerSecond; + uInt32 myAudioFragmentSize; + uInt32 myAudioSampleRate; + uInt32 myAudioQueueCapacity; + uInt32 myPrebufferFragmentCount; + + float mySpeedFactor; + + private: + + EmulationTiming(const EmulationTiming&) = delete; + EmulationTiming(EmulationTiming&&) = delete; + EmulationTiming& operator=(const EmulationTiming&) = delete; + EmulationTiming& operator=(EmulationTiming&&) = delete; + +}; + +#endif // EMULATION_TIMING_HXX diff --git a/src/emucore/EmulationWorker.cxx b/src/emucore/EmulationWorker.cxx new file mode 100644 index 000000000..05dc1f650 --- /dev/null +++ b/src/emucore/EmulationWorker.cxx @@ -0,0 +1,335 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#include + +#include "EmulationWorker.hxx" +#include "DispatchResult.hxx" +#include "TIA.hxx" + +using namespace std::chrono; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +EmulationWorker::EmulationWorker() : myPendingSignal(Signal::none), myState(State::initializing) +{ + std::mutex mutex; + std::unique_lock lock(mutex); + std::condition_variable threadInitialized; + + myThread = std::thread( + &EmulationWorker::threadMain, this, &threadInitialized, &mutex + ); + + // Wait until the thread has acquired myThreadIsRunningMutex and moved on + while (myState == State::initializing) threadInitialized.wait(lock); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +EmulationWorker::~EmulationWorker() +{ + // This has to run in a block in order to release the mutex before joining + { + std::unique_lock lock(myThreadIsRunningMutex); + + if (myState != State::exception) { + signalQuit(); + myWakeupCondition.notify_one(); + } + } + + myThread.join(); + + handlePossibleException(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void EmulationWorker::handlePossibleException() +{ + if (myState == State::exception && myPendingException) { + std::exception_ptr ex = myPendingException; + // Make sure that the exception is not thrown a second time (destructor!!!) + myPendingException = nullptr; + + std::rethrow_exception(ex); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void EmulationWorker::start(uInt32 cyclesPerSecond, uInt32 maxCycles, uInt32 minCycles, DispatchResult* dispatchResult, TIA* tia) +{ + // Wait until any pending signal has been processed + waitUntilPendingSignalHasProcessed(); + + // Run in a block to release the mutex before notifying; this avoids an unecessary + // block that will waste a timeslice + { + // Aquire the mutex -> wait until the thread is suspended + std::unique_lock lock(myThreadIsRunningMutex); + + // Pass on possible exceptions + handlePossibleException(); + + // Make sure that we don't overwrite the exit condition. + // This case is hypothetical and cannot happen, but handling it does not hurt, either + if (myPendingSignal == Signal::quit) return; + + // NB: The thread does not suspend execution in State::initialized + if (myState != State::waitingForResume) + fatal("start called on running or dead worker"); + + // Store the parameters for emulation + myTia = tia; + myCyclesPerSecond = cyclesPerSecond; + myMaxCycles = maxCycles; + myMinCycles = minCycles; + myDispatchResult = dispatchResult; + + // Raise the signal... + myPendingSignal = Signal::resume; + } + + // ... and wakeup the thread + myWakeupCondition.notify_one(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt64 EmulationWorker::stop() +{ + // See EmulationWorker::start above for the gory details + waitUntilPendingSignalHasProcessed(); + + uInt64 totalCycles; + { + std::unique_lock lock(myThreadIsRunningMutex); + + // Paranoia: make sure that we don't doublecount an emulation timeslice + totalCycles = myTotalCycles; + myTotalCycles = 0; + + handlePossibleException(); + + if (myPendingSignal == Signal::quit) return totalCycles; + + // If the worker has stopped on its own, we return + if (myState == State::waitingForResume) return totalCycles; + + // NB: The thread does not suspend execution in State::initialized or State::running + if (myState != State::waitingForStop) + fatal("stop called on a dead worker"); + + myPendingSignal = Signal::stop; + } + + myWakeupCondition.notify_one(); + + return totalCycles; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void EmulationWorker::threadMain(std::condition_variable* initializedCondition, std::mutex* initializationMutex) +{ + std::unique_lock lock(myThreadIsRunningMutex); + + try { + { + // Wait until our parent releases the lock and sleeps + std::lock_guard guard(*initializationMutex); + + // Update the state... + myState = State::initialized; + + // ... and wake up our parent to notifiy that we have initialized. From this point, the + // parent can safely assume that we are running while the mutex is locked. + initializedCondition->notify_one(); + } + + // Loop until we have an exit condition + while (myPendingSignal != Signal::quit) handleWakeup(lock); + } + catch (...) { + // Store away the exception and the state accordingly + myPendingException = std::current_exception(); + myState = State::exception; + + // Raising the exit condition is consistent and makes shure that the main thread + // will not deadlock if an exception is raised while it is waiting for a signal + // to be processed. + signalQuit(); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void EmulationWorker::handleWakeup(std::unique_lock& lock) +{ + switch (myState) { + case State::initialized: + // Enter waitingForResume and sleep after initialization + myState = State::waitingForResume; + myWakeupCondition.wait(lock); + break; + + case State::waitingForResume: + handleWakeupFromWaitingForResume(lock); + break; + + case State::waitingForStop: + handleWakeupFromWaitingForStop(lock); + break; + + default: + fatal("wakeup in invalid worker state"); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void EmulationWorker::handleWakeupFromWaitingForResume(std::unique_lock& lock) +{ + switch (myPendingSignal) { + case Signal::resume: + // Clear the pending signal and notify the main thread + clearSignal(); + + // Reset virtual clock and cycle counter + myVirtualTime = high_resolution_clock::now(); + myTotalCycles = 0; + + // Enter emulation. This will emulate a timeslice and set the state upon completion. + dispatchEmulation(lock); + break; + + case Signal::none: + // Reenter sleep on spurious wakeups + myWakeupCondition.wait(lock); + break; + + case Signal::quit: + break; + + default: + fatal("invalid signal while waiting for resume"); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void EmulationWorker::handleWakeupFromWaitingForStop(std::unique_lock& lock) +{ + switch (myPendingSignal) { + case Signal::stop: + // Clear the pending signal and notify the main thread + clearSignal(); + + // Enter waiting for resume and sleep + myState = State::waitingForResume; + myWakeupCondition.wait(lock); + break; + + case Signal::none: + if (myVirtualTime <= high_resolution_clock::now()) + // The time allotted to the emulation timeslice has passed and we haven't been stopped? + // -> go for another emulation timeslice + dispatchEmulation(lock); + else + // Wakeup was spurious, reenter sleep + myWakeupCondition.wait_until(lock, myVirtualTime); + + break; + + case Signal::quit: + break; + + default: + fatal("invalid signal while waiting for stop"); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void EmulationWorker::dispatchEmulation(std::unique_lock& lock) +{ + // Technically, we could do without State::running, but it is cleaner and might be useful in the future + myState = State::running; + + uInt64 totalCycles = 0; + + do { + myTia->update(*myDispatchResult, totalCycles > 0 ? myMinCycles - totalCycles : myMaxCycles); + totalCycles += myDispatchResult->getCycles(); + } while (totalCycles < myMinCycles && myDispatchResult->getStatus() == DispatchResult::Status::ok); + + myTotalCycles += totalCycles; + + bool continueEmulating = false; + + if (myDispatchResult->getStatus() == DispatchResult::Status::ok) { + // If emulation finished successfully, we are free to go for another round + duration timesliceSeconds(static_cast(totalCycles) / static_cast(myCyclesPerSecond)); + myVirtualTime += duration_cast(timesliceSeconds); + + // If we aren't fast enough to keep up with the emulation, we stop immediatelly to avoid + // starving the system for processing time --- emulation will stutter anyway. + continueEmulating = myVirtualTime > high_resolution_clock::now(); + } + + if (continueEmulating) { + // If we are free to continue emulating, we sleep until either the timeslice has passed or we + // have been signalled from the main thread + myState = State::waitingForStop; + myWakeupCondition.wait_until(lock, myVirtualTime); + } else { + // If can't continue, we just stop and wait to be signalled + myState = State::waitingForResume; + myWakeupCondition.wait(lock); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void EmulationWorker::clearSignal() +{ + { + std::unique_lock lock(mySignalChangeMutex); + myPendingSignal = Signal::none; + } + + mySignalChangeCondition.notify_one(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void EmulationWorker::signalQuit() +{ + { + std::unique_lock lock(mySignalChangeMutex); + myPendingSignal = Signal::quit; + } + + mySignalChangeCondition.notify_one(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void EmulationWorker::waitUntilPendingSignalHasProcessed() +{ + std::unique_lock lock(mySignalChangeMutex); + + // White until there is no pending signal (or the exit condition has been raised) + while (myPendingSignal != Signal::none && myPendingSignal != Signal::quit) + mySignalChangeCondition.wait(lock); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void EmulationWorker::fatal(string message) +{ + (cerr << "FATAL in emulation worker: " << message << std::endl).flush(); + throw runtime_error(message); +} diff --git a/src/emucore/EmulationWorker.hxx b/src/emucore/EmulationWorker.hxx new file mode 100644 index 000000000..bacde5801 --- /dev/null +++ b/src/emucore/EmulationWorker.hxx @@ -0,0 +1,213 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +/* + * This class is the core of stella's scheduling. Scheduling is a two step process + * that is shared between the main loop in OSystem and this class. + * + * In emulation mode (as opposed to debugger, menu, etc.), each iteration of the main loop + * instructs the emulation worker to start emulation on a separate thread and then proceeds + * to render the last frame produced by the TIA (if any). After the frame has been rendered, + * the worker is stopped, and the main thread sleeps until the time allotted to the emulation + * timeslice (as calculated from the 6507 cycles that have passed) has been reached. After + * that, it iterates. + * + * The emulation worker does its own microscheduling. After emulating a timeslice, it sleeps + * until either the allotted time is up or it has been signalled to stop. If the time is up + * without being signalled, the worker will emulate another timeslice, etc. + * + * In combination, the scheduling in the main loop and the microscheduling in the worker + * ensure that the emulation continues to run even if rendering blocks, ensuring the real + * time scheduling required for cycle exact audio to work. + */ + +#ifndef EMULATION_WORKER_HXX +#define EMULATION_WORKER_HXX + +#include +#include +#include +#include +#include +#include + +#include "bspf.hxx" + +class TIA; +class DispatchResult; + +class EmulationWorker +{ + public: + + /** + The constructor starts the worker thread and waits until it has initialized. + */ + EmulationWorker(); + + /** + The destructor signals quit to the worker and joins. + */ + ~EmulationWorker(); + + /** + Wake up the worker and start emulation with the specified parameters. + */ + void start(uInt32 cyclesPerSecond, uInt32 maxCycles, uInt32 minCycles, DispatchResult* dispatchResult, TIA* tia); + + /** + Stop emulation and return the number of 6507 cycles emulated. + */ + uInt64 stop(); + + private: + + /** + Check whether an exception occurred on the thread and rethrow if appicable. + */ + void handlePossibleException(); + + /** + The main thread entry point. + Passing references into a thread is awkward and requires std::ref -> use pointers here + */ + void threadMain(std::condition_variable* initializedCondition, std::mutex* initializationMutex); + + /** + Handle thread wakeup after sleep depending on the thread state. + */ + void handleWakeup(std::unique_lock& lock); + + /** + Handle wakeup while sleeping and waiting to be resumed. + */ + void handleWakeupFromWaitingForResume(std::unique_lock& lock); + + /** + Handle wakeup while sleeping and waiting to be stopped (or for the timeslice + to expire). + */ + void handleWakeupFromWaitingForStop(std::unique_lock& lock); + + /** + Run the emulation, adjust the thread state according to the result and sleep. + */ + void dispatchEmulation(std::unique_lock& lock); + + /** + Clear any pending signal and wake up the main thread (if it is waiting for the signal + to be cleared). + */ + void clearSignal(); + + /** + * Signal quit and wake up the main thread if applicable. + */ + void signalQuit(); + + /** + Wait and sleep until a pending signal has been processed (or quit sigmalled). + This is called from the main thread. + */ + void waitUntilPendingSignalHasProcessed(); + + /** + Log a fatal error to cerr and throw a runtime exception. + */ + void fatal(string message); + + private: + /** + Thread state. + */ + enum class State { + // Initial state + initializing, + // Thread has initialized. From the point, myThreadIsRunningMutex is locked if and only if + // the thread is running. + initialized, + // Sleeping and waiting for emulation to be resumed + waitingForResume, + // Running and emulating + running, + // Sleeping and waiting for emulation to be stopped + waitingForStop, + // An exception occurred and the thread has terminated (or is terminating) + exception + }; + + /** + Thread behavior is controlled by signals that are raised prior to waking up the thread. + */ + enum class Signal { + // Resume emulation + resume, + // Stop emulation + stop, + // Quit (either during destruction or after an exception) + quit, + // No pending signal + none + }; + + private: + + // Worker thread + std::thread myThread; + + // Condition variable for waking up the thread + std::condition_variable myWakeupCondition; + // The thread is running if and only if while this mutex is locked + std::mutex myThreadIsRunningMutex; + + // Condition variable to signal changes to the pending signal + std::condition_variable mySignalChangeCondition; + // This mutex guards reading and writing the pending signal. + std::mutex mySignalChangeMutex; + + // Any exception on the worker thread is saved here to be rethrown on the main thread. + std::exception_ptr myPendingException; + + // Any pending signal (or Signal::none) + Signal myPendingSignal; + // The initial access to myState is not synchronized -> make this atomic + std::atomic myState; + + // Emulation parameters + TIA* myTia; + uInt32 myCyclesPerSecond; + uInt32 myMaxCycles; + uInt32 myMinCycles; + DispatchResult* myDispatchResult; + + // Total number of cycles during this emulation run + uInt64 myTotalCycles; + // 6507 time + std::chrono::time_point myVirtualTime; + + private: + + EmulationWorker(const EmulationWorker&) = delete; + + EmulationWorker(EmulationWorker&&) = delete; + + EmulationWorker& operator=(const EmulationWorker&) = delete; + + EmulationWorker& operator=(EmulationWorker&&) = delete; +}; + +#endif // EMULATION_WORKER_HXX diff --git a/src/emucore/FrameBuffer.cxx b/src/emucore/FrameBuffer.cxx index a0aa1f4a7..16017487d 100644 --- a/src/emucore/FrameBuffer.cxx +++ b/src/emucore/FrameBuffer.cxx @@ -48,13 +48,9 @@ FrameBuffer::FrameBuffer(OSystem& osystem) myPausedCount(0), myStatsEnabled(false), myLastScanlines(0), - myLastFrameRate(60), myGrabMouse(false), - myCurrentModeList(nullptr), - myTotalTime(0), - myTotalFrames(0) -{ -} + myCurrentModeList(nullptr) +{} // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - FrameBuffer::~FrameBuffer() @@ -272,29 +268,8 @@ void FrameBuffer::update() switch(myOSystem.eventHandler().state()) { case EventHandlerState::EMULATION: - { - // Run the console for one frame - // Note that the debugger can cause a breakpoint to occur, which changes - // the EventHandler state 'behind our back' - we need to check for that - myOSystem.console().tia().update(); - #ifdef DEBUGGER_SUPPORT - if(myOSystem.eventHandler().state() != EventHandlerState::EMULATION) break; - #endif - if(myOSystem.eventHandler().frying()) - myOSystem.console().fry(); - - // And update the screen - myTIASurface->render(); - - // Show frame statistics - if(myStatsMsg.enabled) - drawFrameStats(); - else - myLastFrameRate = myOSystem.console().getFramerate(); - myLastScanlines = myOSystem.console().tia().scanlinesLastFrame(); - myPausedCount = 0; - break; // EventHandlerState::EMULATION - } + // Do nothing; emulation mode is handled separately (see below) + break; case EventHandlerState::PAUSE: { @@ -356,6 +331,30 @@ void FrameBuffer::update() postFrameUpdate(); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void FrameBuffer::updateInEmulationMode() +{ + // Determine which mode we are in (from the EventHandler) + // Take care of S_EMULATE mode here, otherwise let the GUI + // figure out what to draw + + myTIASurface->render(); + + // Show frame statistics + if(myStatsMsg.enabled) + drawFrameStats(); + + myLastScanlines = myOSystem.console().tia().frameBufferScanlinesLastFrame(); + myPausedCount = 0; + + // Draw any pending messages + if(myMsg.enabled) + drawMessage(); + + // Do any post-frame stuff + postFrameUpdate(); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void FrameBuffer::showMessage(const string& message, MessagePosition position, bool force) @@ -367,6 +366,7 @@ void FrameBuffer::showMessage(const string& message, MessagePosition position, // Precompute the message coordinates myMsg.text = message; myMsg.counter = uInt32(myOSystem.frameRate()) << 1; // Show message for 2 seconds + if(myMsg.counter == 0) myMsg.counter = 60; myMsg.color = kBtnTextColor; myMsg.w = font().getStringWidth(myMsg.text) + 10; @@ -389,9 +389,9 @@ void FrameBuffer::drawFrameStats() myStatsMsg.surface->invalidate(); // draw scanlines - color = myOSystem.console().tia().scanlinesLastFrame() != myLastScanlines ? + color = myOSystem.console().tia().frameBufferScanlinesLastFrame() != myLastScanlines ? uInt32(kDbgColorRed) : myStatsMsg.color; - std::snprintf(msg, 30, "%3u", myOSystem.console().tia().scanlinesLastFrame()); + std::snprintf(msg, 30, "%3u", myOSystem.console().tia().frameBufferScanlinesLastFrame()); myStatsMsg.surface->drawString(font(), msg, xPos, YPOS, myStatsMsg.w, color, TextAlign::Left, 0, true, kBGColor); xPos += font().getStringWidth(msg); @@ -402,23 +402,10 @@ void FrameBuffer::drawFrameStats() myStatsMsg.w, myStatsMsg.color, TextAlign::Left, 0, true, kBGColor); xPos += font().getStringWidth(msg); - // draw the effective framerate - float frameRate; - const TimingInfo& ti = myOSystem.timingInfo(); - if(ti.totalFrames - myTotalFrames >= myLastFrameRate) - { - frameRate = 1000000.0 * (ti.totalFrames - myTotalFrames) / (ti.totalTime - myTotalTime); - if(frameRate > myOSystem.console().getFramerate() + 1) - frameRate = 1; - myTotalFrames = ti.totalFrames; - myTotalTime = ti.totalTime; - } - else - frameRate = myLastFrameRate; - std::snprintf(msg, 30, " @ %5.2ffps", frameRate); + std::snprintf(msg, 30, " @ %5.2ffps", myOSystem.console().getFramerate()); + myStatsMsg.surface->drawString(font(), msg, xPos, YPOS, myStatsMsg.w, myStatsMsg.color, TextAlign::Left, 0, true, kBGColor); - myLastFrameRate = frameRate; // draw bankswitching type string bsinfo = info.BankSwitch + @@ -751,8 +738,7 @@ void FrameBuffer::setAvailableVidModes(uInt32 baseWidth, uInt32 baseHeight) myDesktopSize.w, myDesktopSize.h); // Aspect ratio - bool ntsc = myOSystem.console().about().InitialFrameRate == "60"; - uInt32 aspect = myOSystem.settings().getInt(ntsc ? + uInt32 aspect = myOSystem.settings().getInt(myOSystem.console().timing() == ConsoleTiming::ntsc ? "tia.aspectn" : "tia.aspectp"); // Figure our the smallest zoom level we can use diff --git a/src/emucore/FrameBuffer.hxx b/src/emucore/FrameBuffer.hxx index 5958fed79..7a7e0a487 100644 --- a/src/emucore/FrameBuffer.hxx +++ b/src/emucore/FrameBuffer.hxx @@ -113,10 +113,16 @@ class FrameBuffer /** Updates the display, which depending on the current mode could mean - drawing the TIA, any pending menus, etc. + drawing the TIA, any pending menus, etc. Returns the numbers of CPU cycles + spent during emulation, or -1 if not applicable. */ void update(); + /** + There is a dedicated update method for emulation mode. + */ + void updateInEmulationMode(); + /** Shows a message onscreen. @@ -522,7 +528,6 @@ class FrameBuffer Message myStatsMsg; bool myStatsEnabled; uInt32 myLastScanlines; - float myLastFrameRate; bool myGrabMouse; @@ -540,9 +545,6 @@ class FrameBuffer // Holds UI palette data (standard and classic colours) static uInt32 ourGUIColors[3][kNumColors-256]; - uInt64 myTotalTime; - uInt64 myTotalFrames; - private: // Following constructors and assignment operators not supported FrameBuffer() = delete; diff --git a/src/emucore/M6502.cxx b/src/emucore/M6502.cxx index 19cf07ed8..cefd24d83 100644 --- a/src/emucore/M6502.cxx +++ b/src/emucore/M6502.cxx @@ -47,6 +47,7 @@ #include "System.hxx" #include "M6502.hxx" +#include "DispatchResult.hxx" // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - M6502::M6502(const Settings& settings) @@ -208,9 +209,9 @@ inline void M6502::handleHalt() } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool M6502::execute(uInt32 number) +void M6502::execute(uInt32 number, DispatchResult& result) { - const bool status = _execute(number); + _execute(number, result); #ifdef DEBUGGER_SUPPORT // Debugger hack: this ensures that stepping a "STA WSYNC" will actually end at the @@ -218,18 +219,27 @@ bool M6502::execute(uInt32 number) // the halt to take effect). This is safe because as we know that the next cycle will be a read // cycle anyway. handleHalt(); - - // Make sure that the hardware state matches the current system clock. This is necessary - // to maintain a consistent state for the debugger after stepping. - mySystem->tia().updateEmulation(); - mySystem->m6532().updateEmulation(); #endif - return status; + // Make sure that the hardware state matches the current system clock. This is necessary + // to maintain a consistent state for the debugger after stepping and to make sure + // that audio samples are generated for the whole timeslice. + mySystem->tia().updateEmulation(); + mySystem->m6532().updateEmulation(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -inline bool M6502::_execute(uInt32 number) +bool M6502::execute(uInt32 number) +{ + DispatchResult result; + + _execute(number, result); + + return result.isSuccess(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +inline void M6502::_execute(uInt32 cycles, DispatchResult& result) { // Clear all of the execution status bits except for the fatal error bit myExecutionStatus &= FatalErrorBit; @@ -239,32 +249,43 @@ inline bool M6502::_execute(uInt32 number) M6532& riot = mySystem->m6532(); #endif + uInt64 previousCycles = mySystem->cycles(); + uInt32 currentCycles = 0; + // Loop until execution is stopped or a fatal error occurs for(;;) { - for(; !myExecutionStatus && (number != 0); --number) + while (!myExecutionStatus && currentCycles < cycles * SYSTEM_CYCLES_PER_CPU) { #ifdef DEBUGGER_SUPPORT - if(myJustHitReadTrapFlag || myJustHitWriteTrapFlag) - { - bool read = myJustHitReadTrapFlag; - myJustHitReadTrapFlag = myJustHitWriteTrapFlag = false; + // Don't break if we haven't actually executed anything yet + if (currentCycles > 0) { + if(myJustHitReadTrapFlag || myJustHitWriteTrapFlag) + { + bool read = myJustHitReadTrapFlag; + myJustHitReadTrapFlag = myJustHitWriteTrapFlag = false; - if (startDebugger(myHitTrapInfo.message, myHitTrapInfo.address, read)) return true; + result.setDebugger(currentCycles, myHitTrapInfo.message, myHitTrapInfo.address, read); + return; + } + + if(myBreakPoints.isInitialized() && myBreakPoints.isSet(PC)) { + result.setDebugger(currentCycles, "BP: ", PC); + return; + } + + int cond = evalCondBreaks(); + if(cond > -1) + { + stringstream msg; + msg << "CBP[" << Common::Base::HEX2 << cond << "]: " << myCondBreakNames[cond]; + + result.setDebugger(currentCycles, msg.str()); + return; + } } - if(myBreakPoints.isInitialized() && myBreakPoints.isSet(PC) && startDebugger("BP: ", PC)) - return true; - - int cond = evalCondBreaks(); - if(cond > -1) - { - stringstream msg; - msg << "CBP[" << Common::Base::HEX2 << cond << "]: " << myCondBreakNames[cond]; - if (startDebugger(msg.str())) return true; - } - - cond = evalCondSaveStates(); + int cond = evalCondSaveStates(); if(cond > -1) { stringstream msg; @@ -294,6 +315,8 @@ inline bool M6502::_execute(uInt32 number) myExecutionStatus |= FatalErrorBit; } + currentCycles = (mySystem->cycles() - previousCycles); + #ifdef DEBUGGER_SUPPORT if(myStepStateByInstruction) { @@ -318,21 +341,24 @@ inline bool M6502::_execute(uInt32 number) if(myExecutionStatus & StopExecutionBit) { // Yes, so answer that everything finished fine - return true; + result.setOk(currentCycles); + return; } // See if a fatal error has occured if(myExecutionStatus & FatalErrorBit) { // Yes, so answer that something when wrong - return false; + result.setFatal(currentCycles + icycles); + return; } // See if we've executed the specified number of instructions - if(number == 0) + if (currentCycles >= cycles * SYSTEM_CYCLES_PER_CPU) { // Yes, so answer that everything finished fine - return true; + result.setOk(currentCycles); + return; } } } @@ -606,15 +632,4 @@ void M6502::updateStepStateByInstruction() myTrapConds.size(); } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bool M6502::startDebugger(const string& message, int address, bool read) -{ - handleHalt(); - - mySystem->tia().updateEmulation(); - mySystem->m6532().updateEmulation(); - - return myDebugger->start(message, address, read); -} - #endif // DEBUGGER_SUPPORT diff --git a/src/emucore/M6502.hxx b/src/emucore/M6502.hxx index c0512d658..26185e6ab 100644 --- a/src/emucore/M6502.hxx +++ b/src/emucore/M6502.hxx @@ -22,6 +22,7 @@ class Settings; class System; +class DispatchResult; #ifdef DEBUGGER_SUPPORT class Debugger; @@ -110,10 +111,14 @@ class M6502 : public Serializable is executed, someone stops execution, or an error occurs. Answers true iff execution stops normally. - @param number Indicates the number of instructions to execute - @return true iff execution stops normally + @param cycles Indicates the number of cycles to execute. Not that the actual + granularity of the CPU is instructions, so this is only accurate up to + a couple of cycles + @param result A DispatchResult object that will transport the result */ - bool execute(uInt32 number); + void execute(uInt32 cycles, DispatchResult& result); + + bool execute(uInt32 cycles); /** Tell the processor to stop executing instructions. Invoking this @@ -318,7 +323,7 @@ class M6502 : public Serializable This is the actual dispatch function that does the grunt work. M6502::execute wraps it and makes sure that any pending halt is processed before returning. */ - bool _execute(uInt32 number); + void _execute(uInt32 cycles, DispatchResult& result); #ifdef DEBUGGER_SUPPORT /** @@ -326,12 +331,6 @@ class M6502 : public Serializable with the CPU and update the flag accordingly. */ void updateStepStateByInstruction(); - - /** - Make sure that the current hardware state is up to date (TIA & RIOT) and dispatch - debugger. - */ - bool startDebugger(const string& message = "", int address = -1, bool read = true); #endif // DEBUGGER_SUPPORT private: diff --git a/src/emucore/OSystem.cxx b/src/emucore/OSystem.cxx index 12ac13fcc..5b05259a4 100644 --- a/src/emucore/OSystem.cxx +++ b/src/emucore/OSystem.cxx @@ -17,11 +17,6 @@ #include -#include -#ifdef HAVE_GETTIMEOFDAY - #include -#endif - #include "bspf.hxx" #include "MediaFactory.hxx" @@ -35,6 +30,8 @@ #include "CheatManager.hxx" #endif +#include + #include "FSNode.hxx" #include "MD5.hxx" #include "Cart.hxx" @@ -55,17 +52,19 @@ #include "SerialPort.hxx" #include "StateManager.hxx" #include "Version.hxx" +#include "TIA.hxx" +#include "DispatchResult.hxx" +#include "EmulationWorker.hxx" #include "OSystem.hxx" +using namespace std::chrono; + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - OSystem::OSystem() : myLauncherUsed(false), myQuitLoop(false) { - // Calculate startup time - myMillisAtStart = uInt32(time(nullptr) * 1000); - // Get built-in features #ifdef SOUND_SUPPORT myFeatures += "Sound "; @@ -91,6 +90,7 @@ OSystem::OSystem() myBuildInfo = info.str(); mySettings = MediaFactory::createSettings(*this); + myAudioSettings = AudioSettings(mySettings.get()); myRandom = make_unique(*this); } @@ -283,16 +283,6 @@ void OSystem::setConfigFile(const string& file) myConfigFile = FilesystemNode(file).getPath(); } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void OSystem::setFramerate(float framerate) -{ - if(framerate > 0.0) - { - myDisplayFrameRate = framerate; - myTimePerFrame = uInt32(1000000.0 / myDisplayFrameRate); - } -} - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - FBInitStatus OSystem::createFrameBuffer() { @@ -332,9 +322,9 @@ FBInitStatus OSystem::createFrameBuffer() void OSystem::createSound() { if(!mySound) - mySound = MediaFactory::createAudio(*this); + mySound = MediaFactory::createAudio(*this, myAudioSettings); #ifndef SOUND_SUPPORT - mySettings->setValue("sound", false); + myAudioSettings.setEnabled(false); #endif } @@ -408,9 +398,6 @@ string OSystem::createConsole(const FilesystemNode& rom, const string& md5sum, << getROMInfo(*myConsole) << endl; logMessage(buf.str(), 1); - // Update the timing info for a new console run - resetLoopTiming(); - myFrameBuffer->setCursorState(); // Also check if certain virtual buttons should be held down @@ -450,8 +437,6 @@ bool OSystem::createLauncher(const string& startdir) myLauncher->reStack(); myFrameBuffer->setCursorState(); - setFramerate(30); - resetLoopTiming(); status = true; } else @@ -565,7 +550,7 @@ unique_ptr OSystem::openConsole(const FilesystemNode& romfile, string& // Finally, create the cart with the correct properties if(cart) - console = make_unique(*this, cart, props); + console = make_unique(*this, cart, props, myAudioSettings); } return console; @@ -627,15 +612,6 @@ string OSystem::getROMInfo(const Console& console) return buf.str(); } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void OSystem::resetLoopTiming() -{ - myTimingInfo.start = myTimingInfo.virt = getTicks(); - myTimingInfo.current = 0; - myTimingInfo.totalTime = 0; - myTimingInfo.totalFrames = 0; -} - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void OSystem::validatePath(string& path, const string& setting, const string& defaultpath) @@ -653,68 +629,105 @@ void OSystem::validatePath(string& path, const string& setting, // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - uInt64 OSystem::getTicks() const { -#ifdef HAVE_GETTIMEOFDAY - // Gettimeofday natively refers to the UNIX epoch (a set time in the past) - timeval now; - gettimeofday(&now, nullptr); + return duration_cast > >(system_clock::now().time_since_epoch()).count(); +} - return uInt64(now.tv_sec) * 1000000 + now.tv_usec; -#else - // We use SDL_GetTicks, but add in the time when the application was - // initialized. This is necessary, since SDL_GetTicks only measures how - // long SDL has been running, which can be the same between multiple runs - // of the application. - return uInt64(SDL_GetTicks() + myMillisAtStart) * 1000; -#endif +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +float OSystem::frameRate() const +{ + return myConsole ? myConsole->getFramerate() : 0; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +double OSystem::dispatchEmulation(EmulationWorker& emulationWorker) +{ + if (!myConsole) return 0.; + + TIA& tia(myConsole->tia()); + EmulationTiming& timing(myConsole->emulationTiming()); + DispatchResult dispatchResult; + + // Check whether we have a frame pending for rendering... + bool framePending = tia.newFramePending(); + // ... and copy it to the frame buffer. It is important to do this before + // the worker is started to avoid racing. + if (framePending) tia.renderToFrameBuffer(); + + // Start emulation on a dedicated thread. It will do its own scheduling to sync 6507 and real time + // and will run until we stop the worker. + emulationWorker.start( + timing.cyclesPerSecond(), + timing.maxCyclesPerTimeslice(), + timing.minCyclesPerTimeslice(), + &dispatchResult, + &tia + ); + + // Render the frame. This may block, but emulation will continue to run on the worker, so the + // audio pipeline is kept fed :) + if (framePending) myFrameBuffer->updateInEmulationMode(); + + // Stop the worker and wait until it has finished + uInt64 totalCycles = emulationWorker.stop(); + + // Break or trap? -> start debugger + if (dispatchResult.getStatus() == DispatchResult::Status::debugger) myDebugger->start(); + + // Handle frying + if (dispatchResult.getStatus() == DispatchResult::Status::ok && myEventHandler->frying()) + myConsole->fry(); + + // Return the 6507 time used in seconds + return static_cast(totalCycles) / static_cast(timing.cyclesPerSecond()); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void OSystem::mainLoop() { - if(mySettings->getString("timing") == "sleep") + // Sleep-based wait: good for CPU, bad for graphical sync + bool busyWait = mySettings->getString("timing") != "sleep"; + // 6507 time + time_point virtualTime = high_resolution_clock::now(); + // The emulation worker + EmulationWorker emulationWorker; + + for(;;) { - // Sleep-based wait: good for CPU, bad for graphical sync - for(;;) - { - myTimingInfo.start = getTicks(); - myEventHandler->poll(myTimingInfo.start); - if(myQuitLoop) break; // Exit if the user wants to quit + myEventHandler->poll(getTicks()); + if(myQuitLoop) break; // Exit if the user wants to quit + + double timesliceSeconds; + + if (myEventHandler->state() == EventHandlerState::EMULATION) + // Dispatch emulation and render frame (if applicable) + timesliceSeconds = dispatchEmulation(emulationWorker); + else { + // Render the GUI with 30 Hz in all other modes + timesliceSeconds = 1. / 30.; myFrameBuffer->update(); - myTimingInfo.current = getTicks(); - myTimingInfo.virt += myTimePerFrame; - - // Timestamps may periodically go out of sync, particularly on systems - // that can have 'negative time' (ie, when the time seems to go backwards) - // This normally results in having a very large delay time, so we check - // for that and reset the timers when appropriate - if((myTimingInfo.virt - myTimingInfo.current) > (myTimePerFrame << 1)) - { - myTimingInfo.current = myTimingInfo.virt = getTicks(); - } - - if(myTimingInfo.current < myTimingInfo.virt) - SDL_Delay(uInt32(myTimingInfo.virt - myTimingInfo.current) / 1000); - - myTimingInfo.totalTime += (getTicks() - myTimingInfo.start); - myTimingInfo.totalFrames++; } - } - else - { - // Busy-wait: bad for CPU, good for graphical sync - for(;;) - { - myTimingInfo.start = getTicks(); - myEventHandler->poll(myTimingInfo.start); - if(myQuitLoop) break; // Exit if the user wants to quit - myFrameBuffer->update(); - myTimingInfo.virt += myTimePerFrame; - while(getTicks() < myTimingInfo.virt) - ; // busy-wait + duration timeslice(timesliceSeconds); + virtualTime += duration_cast(timeslice); + time_point now = high_resolution_clock::now(); - myTimingInfo.totalTime += (getTicks() - myTimingInfo.start); - myTimingInfo.totalFrames++; + // We allow 6507 time to lag behind by one frame max + double maxLag = myConsole + ? ( + static_cast(myConsole->emulationTiming().cyclesPerFrame()) / + static_cast(myConsole->emulationTiming().cyclesPerSecond()) + ) + : 0; + + if (duration_cast>(now - virtualTime).count() > maxLag) + // If 6507 time is lagging behind more than one frame we reset it to real time + virtualTime = now; + else if (virtualTime > now) { + // Wait until we have caught up with 6507 time + if (busyWait && myEventHandler->state() == EventHandlerState::EMULATION) { + while (high_resolution_clock::now() < virtualTime); + } + else std::this_thread::sleep_until(virtualTime); } } diff --git a/src/emucore/OSystem.hxx b/src/emucore/OSystem.hxx index 00432b8be..d52956fae 100644 --- a/src/emucore/OSystem.hxx +++ b/src/emucore/OSystem.hxx @@ -38,19 +38,13 @@ class Settings; class Sound; class StateManager; class VideoDialog; +class EmulationWorker; #include "FSNode.hxx" #include "FrameBufferConstants.hxx" #include "EventHandlerConstants.hxx" #include "bspf.hxx" - -struct TimingInfo { - uInt64 start; - uInt64 current; - uInt64 virt; - uInt64 totalTime; - uInt64 totalFrames; -}; +#include "AudioSettings.hxx" /** This class provides an interface for accessing operating system specific @@ -132,6 +126,11 @@ class OSystem Console& console() const { return *myConsole; } bool hasConsole() const; + /** + Get the audio settings ovject. + */ + AudioSettings& audioSettings() { return myAudioSettings; } + /** Get the serial port of the system. @@ -213,26 +212,11 @@ class OSystem CheatManager& cheat() const { return *myCheatManager; } #endif - /** - Set the framerate for the video system. It's placed in this class since - the mainLoop() method is defined here. - - @param framerate The video framerate to use - */ - virtual void setFramerate(float framerate); - /** Set all config file paths for the OSystem. */ void setConfigPaths(); - /** - Get the current framerate for the video system. - - @return The video framerate currently in use - */ - float frameRate() const { return myDisplayFrameRate; } - /** Return the default full/complete directory name for storing data. */ @@ -383,12 +367,6 @@ class OSystem */ const string& logMessages() const { return myLogMessages; } - /** - Return timing information (start time of console, current - number of frames rendered, etc. - */ - const TimingInfo& timingInfo() const { return myTimingInfo; } - public: ////////////////////////////////////////////////////////////////////// // The following methods are system-specific and can be overrided in @@ -406,6 +384,8 @@ class OSystem */ virtual uInt64 getTicks() const; + float frameRate() const; + /** This method runs the main loop. Since different platforms may use different timing methods and/or algorithms, this method can @@ -504,18 +484,12 @@ class OSystem // The list of log messages string myLogMessages; - // Number of times per second to iterate through the main loop - float myDisplayFrameRate; - - // Time per frame for a video update, based on the current framerate - uInt32 myTimePerFrame; - - // The time (in milliseconds) from the UNIX epoch when the application starts - uInt32 myMillisAtStart; - // Indicates whether to stop the main loop bool myQuitLoop; + // Audio settings + AudioSettings myAudioSettings; + private: string myBaseDir; string myStateDir; @@ -536,9 +510,6 @@ class OSystem string myFeatures; string myBuildInfo; - // Indicates whether the main processing loop should proceed - TimingInfo myTimingInfo; - private: /** Creates the various framebuffers/renderers available in this system. @@ -590,12 +561,6 @@ class OSystem */ string getROMInfo(const Console& console); - /** - Initializes the timing so that the mainloop is reset to its - initial values. - */ - void resetLoopTiming(); - /** Validate the directory name, and create it if necessary. Also, update the settings with the new name. For now, validation @@ -608,6 +573,8 @@ class OSystem void validatePath(string& path, const string& setting, const string& defaultpath); + double dispatchEmulation(EmulationWorker& emulationWorker); + // Following constructors and assignment operators not supported OSystem(const OSystem&) = delete; OSystem(OSystem&&) = delete; diff --git a/src/emucore/PropsSet.hxx b/src/emucore/PropsSet.hxx index 187110f63..9fc2cf927 100644 --- a/src/emucore/PropsSet.hxx +++ b/src/emucore/PropsSet.hxx @@ -115,7 +115,7 @@ class PropertiesSet /** Return the size of the myExternalProps list */ - uInt32 size() { return myExternalProps.size(); } + uInt64 size() const { return myExternalProps.size(); } private: using PropsList = std::map; diff --git a/src/emucore/Settings.cxx b/src/emucore/Settings.cxx index b54c366ed..04676a9d2 100644 --- a/src/emucore/Settings.cxx +++ b/src/emucore/Settings.cxx @@ -21,6 +21,7 @@ #include "OSystem.hxx" #include "Version.hxx" +#include "AudioSettings.hxx" #ifdef DEBUGGER_SUPPORT #include "DebuggerDialog.hxx" @@ -34,7 +35,7 @@ Settings::Settings(OSystem& osystem) { // Video-related options setInternal("video", ""); - setInternal("framerate", "0"); + setInternal("speed", "1.0"); setInternal("vsync", "true"); setInternal("fullscreen", "false"); setInternal("center", "false"); @@ -69,10 +70,14 @@ Settings::Settings(OSystem& osystem) setInternal("tv.bleed", "0.0"); // Sound options - setInternal("sound", "true"); - setInternal("fragsize", "512"); - setInternal("freq", "31400"); - setInternal("volume", "100"); + setInternal(AudioSettings::SETTING_ENABLED, AudioSettings::DEFAULT_ENABLED); + setInternal(AudioSettings::SETTING_PRESET, static_cast(AudioSettings::DEFAULT_PRESET)); + setInternal(AudioSettings::SETTING_SAMPLE_RATE, AudioSettings::DEFAULT_SAMPLE_RATE); + setInternal(AudioSettings::SETTING_FRAGMENT_SIZE, AudioSettings::DEFAULT_FRAGMENT_SIZE); + setInternal(AudioSettings::SETTING_BUFFER_SIZE, AudioSettings::DEFAULT_BUFFER_SIZE); + setInternal(AudioSettings::SETTING_HEADROOM, AudioSettings::DEFAULT_HEADROOM); + setInternal(AudioSettings::SETTING_RESAMPLING_QUALITY, static_cast(AudioSettings::DEFAULT_RESAMPLING_QUALITY)); + setInternal(AudioSettings::SETTING_VOLUME, AudioSettings::DEFAULT_VOLUME); // Input event options setInternal("keymap", ""); @@ -298,6 +303,10 @@ void Settings::validate() { string s; int i; + float f; + + f = getFloat("speed"); + if (f <= 0) setInternal("speed", "1.0"); s = getString("timing"); if(s != "sleep" && s != "busy") setInternal("timing", "sleep"); @@ -359,11 +368,7 @@ void Settings::validate() if(i < 0 || i > 6) setInternal("plr.tm.horizon", 5);*/ #ifdef SOUND_SUPPORT - i = getInt("volume"); - if(i < 0 || i > 100) setInternal("volume", "100"); - i = getInt("freq"); - if(!(i == 11025 || i == 22050 || i == 31400 || i == 44100 || i == 48000)) - setInternal("freq", "31400"); + AudioSettings::normalize(*this); #endif i = getInt("joydeadzone"); @@ -438,15 +443,19 @@ void Settings::usage() const << " -palette \n" - << " -framerate Display the given number of frames per second (0 to auto-calculate)\n" + << " -speed Run emulation at the given speed\n" << " -timing Use the given type of wait between frames\n" << " -uimessages <1|0> Show onscreen UI messages for different events\n" << endl #ifdef SOUND_SUPPORT - << " -sound <1|0> Enable sound generation\n" - << " -fragsize The size of sound fragments (must be a power of two)\n" - << " -freq Set sound sample output frequency (11025|22050|31400|44100|48000)\n" - << " -volume Set the volume (0 - 100)\n" + << " -audio.enabled <1|0> Enable audio\n" + << " -audio.preset <1-5> Audio preset (or 1 for custom)\n" + << " -audio.sample_rate Output sample rate (44100|48000|96000)\n" + << " -audio.fragment_size Fragment size (128|256|512|1024|2048|4096)\n" + << " -audio.buffer_size Max. number of additional half-frames to buffer (0 -- 20)\n" + << " -audio.headroom Additional half-frames to prebuffer (0 -- 20)\n" + << " -audio.resampling_quality <1-3> Resampling quality\n" + << " -audio.volume Vokume (0 -- 100)\n" << endl #endif << " -tia.zoom Use the specified zoom level (windowed mode) for TIA image\n" diff --git a/src/emucore/Sound.hxx b/src/emucore/Sound.hxx index ef56d642e..44e7b5db0 100644 --- a/src/emucore/Sound.hxx +++ b/src/emucore/Sound.hxx @@ -19,8 +19,9 @@ #define SOUND_HXX class OSystem; +class AudioQueue; +class EmulationTiming; -#include "Serializable.hxx" #include "bspf.hxx" /** @@ -29,7 +30,7 @@ class OSystem; @author Stephen Anthony */ -class Sound : public Serializable +class Sound { public: /** @@ -47,26 +48,11 @@ class Sound : public Serializable */ virtual void setEnabled(bool enable) = 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. */ - virtual void open() = 0; + virtual void open(shared_ptr, EmulationTiming*) = 0; /** Should be called to stop the sound system. Once called the sound @@ -81,20 +67,21 @@ class Sound : public Serializable */ virtual void mute(bool state) = 0; + /** + Get the fragment size. + */ + virtual uInt32 getFragmentSize() const = 0; + + /** + Get the sample rate. + */ + virtual uInt32 getSampleRate() const = 0; + /** Reset the sound device. */ 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, uInt64 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 @@ -102,7 +89,7 @@ class Sound : public Serializable @param percent The new volume percentage level for the sound device */ - virtual void setVolume(Int32 percent) = 0; + virtual void setVolume(uInt32 percent) = 0; /** Adjusts the volume of the sound device based on the given direction. diff --git a/src/emucore/TIASnd.cxx b/src/emucore/TIASnd.cxx deleted file mode 100644 index 23be53a4c..000000000 --- a/src/emucore/TIASnd.cxx +++ /dev/null @@ -1,388 +0,0 @@ -//============================================================================ -// -// SSSS tt lll lll -// SS SS tt ll ll -// SS tttttt eeee ll ll aaaa -// SSSS tt ee ee ll ll aa -// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" -// SS SS tt ee ll ll aa aa -// SSSS ttt eeeee llll llll aaaaa -// -// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony -// and the Stella Team -// -// See the file "License.txt" for information on usage and redistribution of -// this file, and for a DISCLAIMER OF ALL WARRANTIES. -//============================================================================ - -#include "System.hxx" -#include "TIAConstants.hxx" -#include "TIASnd.hxx" - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -TIASound::TIASound(Int32 outputFrequency) - : myChannelMode(Hardware2Stereo), - myOutputFrequency(outputFrequency), - myOutputCounter(0), - myVolumePercentage(100) -{ - reset(); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void TIASound::reset() -{ - // Fill the polynomials - polyInit(Bit4, 4, 4, 3); - polyInit(Bit5, 5, 5, 3); - polyInit(Bit9, 9, 9, 5); - - // Initialize instance variables - for(int chan = 0; chan <= 1; ++chan) - { - 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; - } - - myOutputCounter = 0; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void TIASound::outputFrequency(Int32 freq) -{ - myOutputFrequency = freq; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -string TIASound::channels(uInt32 hardware, bool stereo) -{ - if(hardware == 1) - myChannelMode = Hardware1; - else - myChannelMode = stereo ? Hardware2Stereo : Hardware2Mono; - - switch(myChannelMode) - { - case Hardware1: return "Hardware1"; - case Hardware2Mono: return "Hardware2Mono"; - case Hardware2Stereo: return "Hardware2Stereo"; - } - return EmptyString; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void TIASound::set(uInt16 address, uInt8 value) -{ - 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; - } -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -uInt8 TIASound::get(uInt16 address) const -{ - switch(address) - { - 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; - } -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void TIASound::volume(uInt32 percent) -{ - if(percent <= 100) - myVolumePercentage = percent; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void TIASound::process(Int16* buffer, uInt32 samples) -{ - // 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]; - - // 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) - { - // Process channel 0 - if (div_n_cnt0 > 1) - { - 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--; - } - break; - - 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; - } - } - - // 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; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void TIASound::polyInit(uInt8* poly, int size, int f0, int f1) -{ - int mask = (1 << size) - 1, x = mask; - - for(int i = 0; i < mask; i++) - { - 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) ); - } -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -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 deleted file mode 100644 index 51bb198d4..000000000 --- a/src/emucore/TIASnd.hxx +++ /dev/null @@ -1,183 +0,0 @@ -//============================================================================ -// -// SSSS tt lll lll -// SS SS tt ll ll -// SS tttttt eeee ll ll aaaa -// SSSS tt ee ee ll ll aa -// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" -// SS SS tt ee ll ll aa aa -// SSSS ttt eeeee llll llll aaaaa -// -// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony -// and the Stella Team -// -// See the file "License.txt" for information on usage and redistribution of -// this file, and for a DISCLAIMER OF ALL WARRANTIES. -//============================================================================ - -#ifndef TIASOUND_HXX -#define TIASOUND_HXX - -#include "bspf.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 -*/ -class TIASound -{ - public: - /** - Create a new TIA Sound object using the specified output frequency - */ - TIASound(Int32 outputFrequency = 31400); - - public: - /** - 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. - - @param hardware The number of channels supported by the sound system - @param stereo Whether to output the internal sound signals into 1 - or 2 channels - - @return Status of the channel configuration used - */ - string channels(uInt32 hardware, bool stereo); - - public: - /** - Sets the specified sound register to the given value - - @param address Register address - @param value Value to store in the register - */ - 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. - - @param buffer The location to store generated samples - @param samples The number of samples to generate - */ - void process(Int16* buffer, uInt32 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); - - private: - // Definitions for AUDCx (15, 16) - enum AUDCxRegister - { - 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 - }; - - 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). - - 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]; - - /* - 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 - TIASound(const TIASound&) = delete; - TIASound(TIASound&&) = delete; - TIASound& operator=(const TIASound&) = delete; - TIASound& operator=(TIASound&&) = delete; -}; - -#endif diff --git a/src/emucore/TIASurface.cxx b/src/emucore/TIASurface.cxx index d9cf8a5e1..783acbf73 100644 --- a/src/emucore/TIASurface.cxx +++ b/src/emucore/TIASurface.cxx @@ -138,11 +138,12 @@ const FBSurface& TIASurface::baseSurface(GUI::Rect& rect) const } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -uInt32 TIASurface::pixel(uInt32 idx, uInt8 shift) +uInt32 TIASurface::mapIndexedPixel(uInt8 indexedColor, uInt8 shift) { - return myPalette[*(myTIA->frameBuffer() + idx) | shift]; + return myPalette[indexedColor | shift]; } + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void TIASurface::setNTSC(NTSCFilter::Preset preset, bool show) { diff --git a/src/emucore/TIASurface.hxx b/src/emucore/TIASurface.hxx index 258462508..d7ae17291 100644 --- a/src/emucore/TIASurface.hxx +++ b/src/emucore/TIASurface.hxx @@ -73,10 +73,9 @@ class TIASurface const FBSurface& baseSurface(GUI::Rect& rect) const; /** - Get the TIA pixel associated with the given TIA buffer index, - shifting by the given offset (for greyscale values). - */ - uInt32 pixel(uInt32 idx, uInt8 shift = 0); + Use the palette to map a single indexed pixel color. This is used by the TIA output widget. + */ + uInt32 mapIndexedPixel(uInt8 indexedColor, uInt8 shift = 0); /** Get the NTSCFilter object associated with the framebuffer diff --git a/src/emucore/module.mk b/src/emucore/module.mk index a1e4cc00b..57b8fb133 100644 --- a/src/emucore/module.mk +++ b/src/emucore/module.mk @@ -51,8 +51,11 @@ MODULE_OBJS := \ src/emucore/CompuMate.o \ src/emucore/Console.o \ src/emucore/Control.o \ + src/emucore/DispatchResult.o \ src/emucore/Driving.o \ src/emucore/EventHandler.o \ + src/emucore/EmulationTiming.o \ + src/emucore/EmulationWorker.o \ src/emucore/FrameBuffer.o \ src/emucore/FBSurface.o \ src/emucore/FSNode.o \ @@ -75,7 +78,6 @@ MODULE_OBJS := \ src/emucore/Settings.o \ src/emucore/Switches.o \ src/emucore/System.o \ - src/emucore/TIASnd.o \ src/emucore/TIASurface.o \ src/emucore/Thumbulator.o diff --git a/src/emucore/tia/Audio.cxx b/src/emucore/tia/Audio.cxx new file mode 100644 index 000000000..74658f8c5 --- /dev/null +++ b/src/emucore/tia/Audio.cxx @@ -0,0 +1,168 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#include "Audio.hxx" +#include "AudioQueue.hxx" + +#include + +namespace { + constexpr double R_MAX = 30.; + constexpr double R = 1.; + + Int16 mixingTableEntry(uInt8 v, uInt8 vMax) + { + return static_cast( + floor(0x7fff * double(v) / double(vMax) * (R_MAX + R * double(vMax)) / (R_MAX + R * double(v))) + ); + } +} + + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Audio::Audio() + : myAudioQueue(nullptr), + myCurrentFragment(nullptr) +{ + for (uInt8 i = 0; i <= 0x1e; i++) myMixingTableSum[i] = mixingTableEntry(i, 0x1e); + for (uInt8 i = 0; i <= 0x0f; i++) myMixingTableIndividual[i] = mixingTableEntry(i, 0x0f); + + reset(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Audio::reset() +{ + myCounter = 0; + mySampleIndex = 0; + + myChannel0.reset(); + myChannel1.reset(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Audio::setAudioQueue(shared_ptr queue) +{ + myAudioQueue = queue; + + myCurrentFragment = myAudioQueue->enqueue(); + mySampleIndex = 0; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Audio::tick() +{ + switch (myCounter) { + case 9: + case 81: + myChannel0.phase0(); + myChannel1.phase0(); + + break; + + case 37: + case 149: + phase1(); + break; + } + + if (++myCounter == 228) myCounter = 0; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Audio::phase1() +{ + uInt8 sample0 = myChannel0.phase1(); + uInt8 sample1 = myChannel1.phase1(); + + if (!myAudioQueue) return; + + if (myAudioQueue->isStereo()) { + myCurrentFragment[2*mySampleIndex] = myMixingTableIndividual[sample0]; + myCurrentFragment[2*mySampleIndex + 1] = myMixingTableIndividual[sample1]; + } else { + myCurrentFragment[mySampleIndex] = myMixingTableSum[sample0 + sample1]; + } + + if (++mySampleIndex == myAudioQueue->fragmentSize()) { + mySampleIndex = 0; + myCurrentFragment = myAudioQueue->enqueue(myCurrentFragment); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +AudioChannel& Audio::channel0() +{ + return myChannel0; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +AudioChannel& Audio::channel1() +{ + return myChannel1; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +string Audio::name() const +{ + return "TIA_Audio"; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool Audio::save(Serializer& out) const +{ + try + { + out.putString(name()); + + out.putInt(myCounter); + + // The queue starts out pristine after loading, so we don't need to save + // any other parts of our state + + if (!myChannel0.save(out)) return false; + if (!myChannel1.save(out)) return false; + } + catch(...) + { + cerr << "ERROR: TIA_Audio::save" << endl; + return false; + } + + return true; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool Audio::load(Serializer& in) +{ + try + { + if (in.getString() != name()) return false; + + myCounter = in.getInt(); + + if (!myChannel0.load(in)) return false; + if (!myChannel1.load(in)) return false; + } + catch(...) + { + cerr << "ERROR: TIA_Audio::load" << endl; + return false; + } + + return true; +} diff --git a/src/emucore/tia/Audio.hxx b/src/emucore/tia/Audio.hxx new file mode 100644 index 000000000..968c30a9a --- /dev/null +++ b/src/emucore/tia/Audio.hxx @@ -0,0 +1,73 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#ifndef TIA_AUDIO_HXX +#define TIA_AUDIO_HXX + +#include "bspf.hxx" +#include "AudioChannel.hxx" +#include "Serializable.hxx" + +class AudioQueue; + +class Audio : public Serializable +{ + public: + Audio(); + + void reset(); + + void setAudioQueue(shared_ptr queue); + + void tick(); + + AudioChannel& channel0(); + + AudioChannel& channel1(); + + /** + Serializable methods (see that class for more information). + */ + bool save(Serializer& out) const override; + bool load(Serializer& in) override; + string name() const override; + + private: + void phase1(); + + private: + shared_ptr myAudioQueue; + + uInt8 myCounter; + + AudioChannel myChannel0; + AudioChannel myChannel1; + + Int16 myMixingTableSum[0x1e + 1]; + Int16 myMixingTableIndividual[0x0f + 1]; + + Int16* myCurrentFragment; + uInt32 mySampleIndex; + + private: + Audio(const Audio&) = delete; + Audio(Audio&&) = delete; + Audio& operator=(const Audio&) = delete; + Audio& operator=(Audio&&) = delete; +}; + +#endif // TIA_AUDIO_HXX diff --git a/src/emucore/tia/AudioChannel.cxx b/src/emucore/tia/AudioChannel.cxx new file mode 100644 index 000000000..0d344c846 --- /dev/null +++ b/src/emucore/tia/AudioChannel.cxx @@ -0,0 +1,207 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#include "AudioChannel.hxx" + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +AudioChannel::AudioChannel() +{ + reset(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioChannel::reset() +{ + myAudc = myAudv = myAudf = 0; + myClockEnable = myNoiseFeedback = myNoiseCounterBit4 = myPulseCounterHold = false; + myDivCounter = myPulseCounter = myNoiseCounter = 0; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioChannel::phase0() +{ + if (myClockEnable) { + myNoiseCounterBit4 = myNoiseCounter & 0x01; + + switch (myAudc & 0x03) { + case 0x00: + case 0x01: + myPulseCounterHold = false; + break; + + case 0x02: + myPulseCounterHold = (myNoiseCounter & 0x1e) != 0x02; + break; + + case 0x03: + myPulseCounterHold = !myNoiseCounterBit4; + break; + } + + switch (myAudc & 0x03) { + case 0x00: + myNoiseFeedback = + ((myPulseCounter ^ myNoiseCounter) & 0x01) || + !(myNoiseCounter || (myPulseCounter != 0x0a)) || + !(myAudc & 0x0c); + + break; + + default: + myNoiseFeedback = + ((myNoiseCounter & 0x04 ? 1 : 0) ^ (myNoiseCounter & 0x01)) || + myNoiseCounter == 0; + + break; + } + } + + myClockEnable = myDivCounter == myAudf; + + if (myDivCounter == myAudf || myDivCounter == 0x1f) { + myDivCounter = 0; + } else { + myDivCounter++; + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +uInt8 AudioChannel::phase1() +{ + bool pulseFeedback = false; + + if (myClockEnable) { + switch (myAudc >> 2) { + case 0x00: + pulseFeedback = + ((myPulseCounter & 0x02 ? 1 : 0) ^ (myPulseCounter & 0x01)) && + (myPulseCounter != 0x0a) && + (myAudc & 0x03); + + break; + + case 0x01: + pulseFeedback = !(myPulseCounter & 0x08); + break; + + case 0x02: + pulseFeedback = !myNoiseCounterBit4; + break; + + case 0x03: + pulseFeedback = !((myPulseCounter & 0x02) || !(myPulseCounter & 0x0e)); + break; + } + + myNoiseCounter >>= 1; + if (myNoiseFeedback) { + myNoiseCounter |= 0x10; + } + + if (!myPulseCounterHold) { + myPulseCounter = ~(myPulseCounter >> 1) & 0x07; + + if (pulseFeedback) { + myPulseCounter |= 0x08; + } + } + } + + return (myPulseCounter & 0x01) * myAudv; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioChannel::audc(uInt8 value) +{ + myAudc = value & 0x0f; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioChannel::audv(uInt8 value) +{ + myAudv = value & 0x0f; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioChannel::audf(uInt8 value) +{ + myAudf = value & 0x1f; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +string AudioChannel::name() const +{ + return "TIA_AudioChannel"; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool AudioChannel::save(Serializer& out) const +{ + try + { + out.putString(name()); + + out.putInt(myAudc); + out.putInt(myAudv); + out.putInt(myAudf); + + out.putBool(myClockEnable); + out.putBool(myNoiseFeedback); + out.putBool(myNoiseCounterBit4); + out.putBool(myPulseCounterHold); + + out.putInt(myDivCounter); + out.putInt(myPulseCounter); + out.putInt(myNoiseCounter); + } + catch(...) + { + cerr << "ERROR: TIA_AudioChannel::save" << endl; + return false; + } + + return true; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool AudioChannel::load(Serializer& in) +{ + try + { + if (in.getString() != name()) return false; + + myAudc = in.getInt(); + myAudv = in.getInt(); + myAudf = in.getInt(); + + myClockEnable = in.getBool(); + myNoiseFeedback = in.getBool(); + myNoiseCounterBit4 = in.getBool(); + myPulseCounterHold = in.getBool(); + + myDivCounter = in.getInt(); + myPulseCounter = in.getInt(); + myNoiseCounter = in.getInt(); + } + catch(...) + { + cerr << "ERROR: TIA_AudioChannel::load" << endl; + return false; + } + + return true; +} diff --git a/src/emucore/tia/AudioChannel.hxx b/src/emucore/tia/AudioChannel.hxx new file mode 100644 index 000000000..f72372a86 --- /dev/null +++ b/src/emucore/tia/AudioChannel.hxx @@ -0,0 +1,69 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#ifndef TIA_AUDIO_CHANNEL_HXX +#define TIA_AUDIO_CHANNEL_HXX + +#include "bspf.hxx" +#include "Serializable.hxx" + +class AudioChannel : public Serializable +{ + public: + AudioChannel(); + + void reset(); + + void phase0(); + + uInt8 phase1(); + + void audc(uInt8 value); + + void audf(uInt8 value); + + void audv(uInt8 value); + + /** + Serializable methods (see that class for more information). + */ + bool save(Serializer& out) const override; + bool load(Serializer& in) override; + string name() const override; + + private: + uInt8 myAudc; + uInt8 myAudv; + uInt8 myAudf; + + bool myClockEnable; + bool myNoiseFeedback; + bool myNoiseCounterBit4; + bool myPulseCounterHold; + + uInt8 myDivCounter; + uInt8 myPulseCounter; + uInt8 myNoiseCounter; + + private: + AudioChannel(const AudioChannel&); + AudioChannel(AudioChannel&&); + AudioChannel& operator=(const AudioChannel&); + AudioChannel& operator=(AudioChannel&&); +}; + +#endif // TIA_AUDIO_CHANNEL_HXX diff --git a/src/emucore/tia/PaddleReader.cxx b/src/emucore/tia/PaddleReader.cxx index f5a036145..dbc3d48aa 100644 --- a/src/emucore/tia/PaddleReader.cxx +++ b/src/emucore/tia/PaddleReader.cxx @@ -26,7 +26,7 @@ PaddleReader::PaddleReader() } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void PaddleReader::reset(double timestamp) +void PaddleReader::reset(uInt64 timestamp) { myU = 0; myIsDumped = false; @@ -38,7 +38,7 @@ void PaddleReader::reset(double timestamp) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void PaddleReader::vblank(uInt8 value, double timestamp) +void PaddleReader::vblank(uInt8 value, uInt64 timestamp) { bool oldIsDumped = myIsDumped; @@ -53,7 +53,7 @@ void PaddleReader::vblank(uInt8 value, double timestamp) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -uInt8 PaddleReader::inpt(double timestamp) +uInt8 PaddleReader::inpt(uInt64 timestamp) { updateCharge(timestamp); @@ -63,7 +63,7 @@ uInt8 PaddleReader::inpt(double timestamp) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void PaddleReader::update(double value, double timestamp, ConsoleTiming consoleTiming) +void PaddleReader::update(double value, uInt64 timestamp, ConsoleTiming consoleTiming) { if (consoleTiming != myConsoleTiming) { setConsoleTiming(consoleTiming); @@ -94,13 +94,13 @@ void PaddleReader::setConsoleTiming(ConsoleTiming consoleTiming) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void PaddleReader::updateCharge(double timestamp) +void PaddleReader::updateCharge(uInt64 timestamp) { if (myIsDumped) return; if (myValue >= 0) myU = USUPP * (1 - (1 - myU / USUPP) * - exp(-(timestamp - myTimestamp) / (myValue * RPOT + R0) / C / myClockFreq)); + exp(-static_cast(timestamp - myTimestamp) / (myValue * RPOT + R0) / C / myClockFreq)); myTimestamp = timestamp; } diff --git a/src/emucore/tia/PaddleReader.hxx b/src/emucore/tia/PaddleReader.hxx index 77ee3bdf3..544d1d7ee 100644 --- a/src/emucore/tia/PaddleReader.hxx +++ b/src/emucore/tia/PaddleReader.hxx @@ -30,14 +30,14 @@ class PaddleReader : public Serializable public: - void reset(double timestamp); + void reset(uInt64 timestamp); - void vblank(uInt8 value, double timestamp); + void vblank(uInt8 value, uInt64 timestamp); bool vblankDumped() const { return myIsDumped; } - uInt8 inpt(double timestamp); + uInt8 inpt(uInt64 timestamp); - void update(double value, double timestamp, ConsoleTiming consoleTiming); + void update(double value, uInt64 timestamp, ConsoleTiming consoleTiming); /** Serializable methods (see that class for more information). @@ -50,7 +50,7 @@ class PaddleReader : public Serializable void setConsoleTiming(ConsoleTiming timing); - void updateCharge(double timestamp); + void updateCharge(uInt64 timestamp); private: @@ -58,7 +58,7 @@ class PaddleReader : public Serializable double myU; double myValue; - double myTimestamp; + uInt64 myTimestamp; ConsoleTiming myConsoleTiming; double myClockFreq; diff --git a/src/emucore/tia/TIA.cxx b/src/emucore/tia/TIA.cxx index d1b55c0d4..11cc895f0 100644 --- a/src/emucore/tia/TIA.cxx +++ b/src/emucore/tia/TIA.cxx @@ -23,6 +23,8 @@ #include "DelayQueueIteratorImpl.hxx" #include "TIAConstants.hxx" #include "frame-manager/FrameManager.hxx" +#include "AudioQueue.hxx" +#include "DispatchResult.hxx" #ifdef DEBUGGER_SUPPORT #include "CartDebug.hxx" @@ -65,9 +67,8 @@ enum ResxCounter: uInt8 { static constexpr uInt8 resxLateHblankThreshold = 73; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -TIA::TIA(Console& console, Sound& sound, Settings& settings) +TIA::TIA(Console& console, Settings& settings) : myConsole(console), - mySound(sound), mySettings(settings), myFrameManager(nullptr), myPlayfield(~CollisionMask::playfield & 0x7FFF), @@ -116,6 +117,12 @@ void TIA::setFrameManager(AbstractFrameManager *frameManager) myFrameManager->setJitterFactor(myJitterFactor); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void TIA::setAudioQueue(shared_ptr queue) +{ + myAudio.setAudioQueue(queue); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void TIA::clearFrameManager() { @@ -138,7 +145,6 @@ void TIA::reset() myCollisionMask = 0; myLinesSinceChange = 0; myCollisionUpdateRequired = false; - myAutoFrameEnabled = false; myColorLossEnabled = myColorLossActive = false; myColorHBlank = 0; myLastCycle = 0; @@ -159,11 +165,12 @@ void TIA::reset() myInput0.reset(); myInput1.reset(); + myAudio.reset(); + myTimestamp = 0; for (PaddleReader& paddleReader : myPaddleReaders) paddleReader.reset(myTimestamp); - mySound.reset(); myDelayQueue.reset(); myCyclesAtFrameStart = 0; @@ -174,6 +181,11 @@ void TIA::reset() frameReset(); // Recalculate the size of the display } + myFrontBufferFrameRate = myFrameBufferFrameRate = 0; + myFrontBufferScanlines = myFrameBufferScanlines = 0; + + myNewFramePending = false; + // Must be done last, after all other items have reset enableFixedColors(mySettings.getBool(mySettings.getBool("dev.settings") ? "dev.debugcolors" : "plr.debugcolors")); setFixedColorPalette(mySettings.getString("tia.dbgcolors")); @@ -186,8 +198,9 @@ void TIA::reset() // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void TIA::frameReset() { + memset(myBackBuffer, 0, 160 * TIAConstants::frameBufferHeight); + memset(myFrontBuffer, 0, 160 * TIAConstants::frameBufferHeight); memset(myFramebuffer, 0, 160 * TIAConstants::frameBufferHeight); - myAutoFrameEnabled = mySettings.getInt("framerate") <= 0; enableColorLoss(mySettings.getBool("dev.settings") ? "dev.colorloss" : "plr.colorloss"); } @@ -227,8 +240,6 @@ bool TIA::save(Serializer& out) const { out.putString(name()); - if(!mySound.save(out)) return false; - if(!myDelayQueue.save(out)) return false; if(!myFrameManager->save(out)) return false; @@ -239,6 +250,7 @@ bool TIA::save(Serializer& out) const if(!myPlayer0.save(out)) return false; if(!myPlayer1.save(out)) return false; if(!myBall.save(out)) return false; + if(!myAudio.save(out)) return false; for (const PaddleReader& paddleReader : myPaddleReaders) if(!paddleReader.save(out)) return false; @@ -275,11 +287,14 @@ bool TIA::save(Serializer& out) const out.putLong(myTimestamp); - out.putBool(myAutoFrameEnabled); - out.putByteArray(myShadowRegisters, 64); out.putLong(myCyclesAtFrameStart); + + out.putInt(myFrameBufferScanlines); + out.putInt(myFrontBufferScanlines); + out.putDouble(myFrameBufferFrameRate); + out.putDouble(myFrontBufferFrameRate); } catch(...) { @@ -298,8 +313,6 @@ bool TIA::load(Serializer& in) if(in.getString() != name()) return false; - if(!mySound.load(in)) return false; - if(!myDelayQueue.load(in)) return false; if(!myFrameManager->load(in)) return false; @@ -310,6 +323,7 @@ bool TIA::load(Serializer& in) if(!myPlayer0.load(in)) return false; if(!myPlayer1.load(in)) return false; if(!myBall.load(in)) return false; + if(!myAudio.load(in)) return false; for (PaddleReader& paddleReader : myPaddleReaders) if(!paddleReader.load(in)) return false; @@ -346,11 +360,14 @@ bool TIA::load(Serializer& in) myTimestamp = in.getLong(); - myAutoFrameEnabled = in.getBool(); - in.getByteArray(myShadowRegisters, 64); myCyclesAtFrameStart = in.getLong(); + + myFrameBufferScanlines = in.getInt(); + myFrontBufferScanlines = in.getInt(); + myFrameBufferFrameRate = in.getDouble(); + myFrontBufferFrameRate = in.getDouble(); } catch(...) { @@ -521,33 +538,35 @@ bool TIA::poke(uInt16 address, uInt8 value) break; - //////////////////////////////////////////////////////////// - // FIXME - rework this when we add the new sound core case AUDV0: - mySound.set(address, value, mySystem->cycles()); + myAudio.channel0().audv(value); myShadowRegisters[address] = value; break; + case AUDV1: - mySound.set(address, value, mySystem->cycles()); + myAudio.channel1().audv(value); myShadowRegisters[address] = value; break; + case AUDF0: - mySound.set(address, value, mySystem->cycles()); + myAudio.channel0().audf(value); myShadowRegisters[address] = value; break; + case AUDF1: - mySound.set(address, value, mySystem->cycles()); + myAudio.channel1().audf(value); myShadowRegisters[address] = value; break; + case AUDC0: - mySound.set(address, value, mySystem->cycles()); + myAudio.channel0().audc(value); myShadowRegisters[address] = value; break; + case AUDC1: - mySound.set(address, value, mySystem->cycles()); + myAudio.channel1().audc(value); myShadowRegisters[address] = value; break; - //////////////////////////////////////////////////////////// case HMOVE: myDelayQueue.push(HMOVE, value, Delay::hmove); @@ -777,7 +796,10 @@ bool TIA::saveDisplay(Serializer& out) const { try { - out.putByteArray(myFramebuffer, 160*TIAConstants::frameBufferHeight); + out.putByteArray(myFramebuffer, 160* TIAConstants::frameBufferHeight); + out.putByteArray(myBackBuffer, 160 * TIAConstants::frameBufferHeight); + out.putByteArray(myFrontBuffer, 160 * TIAConstants::frameBufferHeight); + out.putBool(myNewFramePending); } catch(...) { @@ -795,6 +817,9 @@ bool TIA::loadDisplay(Serializer& in) { // Reset frame buffer pointer and data in.getByteArray(myFramebuffer, 160*TIAConstants::frameBufferHeight); + in.getByteArray(myBackBuffer, 160 * TIAConstants::frameBufferHeight); + in.getByteArray(myFrontBuffer, 160 * TIAConstants::frameBufferHeight); + myNewFramePending = in.getBool(); } catch(...) { @@ -806,9 +831,30 @@ bool TIA::loadDisplay(Serializer& in) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void TIA::update() +void TIA::update(DispatchResult& result, uInt32 maxCycles) { - mySystem->m6502().execute(25000); + mySystem->m6502().execute(maxCycles, result); + + updateEmulation(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void TIA::renderToFrameBuffer() +{ + if (!myNewFramePending) return; + + memcpy(myFramebuffer, myFrontBuffer, 160 * TIAConstants::frameBufferHeight); + + myFrameBufferFrameRate = myFrontBufferFrameRate; + myFrameBufferScanlines = myFrontBufferScanlines; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void TIA::update(uInt32 maxCycles) +{ + DispatchResult dispatchResult; + + update(dispatchResult, maxCycles); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1143,16 +1189,19 @@ void TIA::onFrameComplete() myCyclesAtFrameStart = mySystem->cycles(); if (myXAtRenderingStart > 0) - memset(myFramebuffer, 0, myXAtRenderingStart); + memset(myBackBuffer, 0, myXAtRenderingStart); // Blank out any extra lines not drawn this frame const Int32 missingScanlines = myFrameManager->missingScanlines(); if (missingScanlines > 0) - memset(myFramebuffer + 160 * myFrameManager->getY(), 0, missingScanlines * 160); + memset(myBackBuffer + 160 * myFrameManager->getY(), 0, missingScanlines * 160); - // Recalculate framerate, attempting to auto-correct for scanline 'jumps' - if(myAutoFrameEnabled) - myConsole.setFramerate(myFrameManager->frameRate()); + memcpy(myFrontBuffer, myBackBuffer, 160 * TIAConstants::frameBufferHeight); + + myFrontBufferFrameRate = frameRate(); + myFrontBufferScanlines = scanlinesLastFrame(); + + myNewFramePending = true; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1188,6 +1237,8 @@ void TIA::cycle(uInt32 colorClocks) if (++myHctr >= 228) nextLine(); + myAudio.tick(); + myTimestamp++; } } @@ -1261,7 +1312,7 @@ void TIA::applyRsync() myHctrDelta = 225 - myHctr; if (myFrameManager->isRendering()) - memset(myFramebuffer + myFrameManager->getY() * 160 + x, 0, 160 - x); + memset(myBackBuffer + myFrameManager->getY() * 160 + x, 0, 160 - x); myHctr = 225; } @@ -1300,7 +1351,7 @@ void TIA::cloneLastLine() if (!myFrameManager->isRendering() || y == 0) return; - uInt8* buffer = myFramebuffer; + uInt8* buffer = myBackBuffer; memcpy(buffer + y * 160, buffer + (y-1) * 160, 160); } @@ -1373,7 +1424,7 @@ void TIA::renderPixel(uInt32 x, uInt32 y) } } - myFramebuffer[y * 160 + x] = color; + myBackBuffer[y * 160 + x] = color; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1399,7 +1450,7 @@ void TIA::flushLineCache() void TIA::clearHmoveComb() { if (myFrameManager->isRendering() && myHstate == HState::blank) - memset(myFramebuffer + myFrameManager->getY() * 160, myColorHBlank, 8); + memset(myBackBuffer + myFrameManager->getY() * 160, myColorHBlank, 8); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/emucore/tia/TIA.hxx b/src/emucore/tia/TIA.hxx index 37e3af293..95c53d8ec 100644 --- a/src/emucore/tia/TIA.hxx +++ b/src/emucore/tia/TIA.hxx @@ -20,7 +20,6 @@ #include "bspf.hxx" #include "Console.hxx" -#include "Sound.hxx" #include "Settings.hxx" #include "Device.hxx" #include "Serializer.hxx" @@ -29,6 +28,7 @@ #include "DelayQueueIterator.hxx" #include "frame-manager/AbstractFrameManager.hxx" #include "FrameLayout.hxx" +#include "Audio.hxx" #include "Background.hxx" #include "Playfield.hxx" #include "Missile.hxx" @@ -40,6 +40,9 @@ #include "Control.hxx" #include "System.hxx" +class AudioQueue; +class DispatchResult; + /** This class is a device that emulates the Television Interface Adaptor found in the Atari 2600 and 7800 consoles. The Television Interface @@ -96,21 +99,26 @@ class TIA : public Device Create a new TIA for the specified console @param console The console the TIA is associated with - @param sound The sound object the TIA is associated with @param settings The settings object for this TIA device */ - TIA(Console& console, Sound& sound, Settings& settings); + TIA(Console& console, Settings& settings); virtual ~TIA() = default; public: /** - * Configure the frame manager. + Configure the frame manager. */ void setFrameManager(AbstractFrameManager *frameManager); /** - * Clear the configured frame manager and deteach the lifecycle callbacks. + Set the audio queue. This needs to be dynamic as the queue is created after + the timing has been determined. + */ + void setAudioQueue(shared_ptr audioQueue); + + /** + Clear the configured frame manager and deteach the lifecycle callbacks. */ void clearFrameManager(); @@ -192,7 +200,25 @@ class TIA : public Device desired frame rate to update the TIA. Invoking this method will update the graphics buffer and generate the corresponding audio samples. */ - void update(); + void update(DispatchResult& result, uInt32 maxCycles = 50000); + + void update(uInt32 maxCycles = 50000); + + /** + Did we generate a new frame? + */ + bool newFramePending() { return myNewFramePending; } + + /** + Render the pending frame to the framebuffer and clear the flag. + */ + void renderToFrameBuffer(); + + /** + Return the buffer that holds the currently drawing TIA frame + (the TIA output widget needs this). + */ + uInt8* outputBuffer() { return myBackBuffer; } /** Returns a pointer to the internal frame buffer. @@ -222,13 +248,12 @@ class TIA : public Device */ ConsoleTiming consoleTiming() const { return myConsole.timing(); } - /** - Enables/disables auto-frame calculation. If enabled, the TIA - re-adjusts the framerate at regular intervals. + float frameRate() const { return myFrameManager ? myFrameManager->frameRate() : 0; } - @param enabled Whether to enable or disable all auto-frame calculation - */ - void enableAutoFrame(bool enabled) { myAutoFrameEnabled = enabled; } + /** + The same, but for the frame in the frame buffer. + */ + float frameBufferFrameRate() const { return myFrameBufferFrameRate; } /** Enables/disables color-loss for PAL modes only. @@ -275,6 +300,11 @@ class TIA : public Device */ uInt32 scanlinesLastFrame() const { return myFrameManager->scanlinesLastFrame(); } + /** + The same, but for the frame in the frame buffer. + */ + uInt32 frameBufferScanlinesLastFrame() const { return myFrameBufferScanlines; } + /** Answers the total system cycles from the start of the emulation. */ @@ -604,7 +634,6 @@ class TIA : public Device private: Console& myConsole; - Sound& mySound; Settings& mySettings; /** @@ -641,6 +670,8 @@ class TIA : public Device Player myPlayer1; Ball myBall; + Audio myAudio; + /** * The paddle readout circuits. */ @@ -655,6 +686,19 @@ class TIA : public Device // Pointer to the internal color-index-based frame buffer uInt8 myFramebuffer[160 * TIAConstants::frameBufferHeight]; + // The frame is rendered to the backbuffer and only copied to the framebuffer + // upon completion + uInt8 myBackBuffer[160 * TIAConstants::frameBufferHeight]; + uInt8 myFrontBuffer[160 * TIAConstants::frameBufferHeight]; + + // We snapshot frame statistics when the back buffer is copied to the front buffer + // and when the front buffer is copied to the frame buffer + uInt32 myFrontBufferScanlines, myFrameBufferScanlines; + float myFrontBufferFrameRate, myFrameBufferFrameRate; + + // Did we emit a frame? + bool myNewFramePending; + /** * Setting this to true injects random values into undefined reads. */ @@ -740,11 +784,9 @@ class TIA : public Device uInt8 myColorHBlank; /** - * The total number of color clocks since emulation started. This is a - * double a) to avoid overflows and b) as it will enter floating point - * expressions in the paddle readout simulation anyway. + * The total number of color clocks since emulation started. */ - double myTimestamp; + uInt64 myTimestamp; /** * The "shadow registers" track the last written register value for the @@ -752,11 +794,6 @@ class TIA : public Device */ uInt8 myShadowRegisters[64]; - /** - * Automatic framerate correction based on number of scanlines. - */ - bool myAutoFrameEnabled; - /** * Indicates if color loss should be enabled or disabled. Color loss * occurs on PAL-like systems when the previous frame contains an odd diff --git a/src/emucore/tia/frame-manager/FrameManager.cxx b/src/emucore/tia/frame-manager/FrameManager.cxx index 18b1a54c1..c2010e326 100644 --- a/src/emucore/tia/frame-manager/FrameManager.cxx +++ b/src/emucore/tia/frame-manager/FrameManager.cxx @@ -47,9 +47,7 @@ FrameManager::FrameManager() myHeight(0), myFixedHeight(0), myYStart(0), - myJitterEnabled(false), - myStableFrameLines(-1), - myStableFrameHeightCountdown(0) + myJitterEnabled(false) { onLayoutChange(); } @@ -63,9 +61,6 @@ void FrameManager::onReset() myVsyncLines = 0; myY = 0; - myStableFrameLines = -1; - myStableFrameHeightCountdown = 0; - myJitterEmulation.reset(); } @@ -230,9 +225,6 @@ bool FrameManager::onSave(Serializer& out) const out.putBool(myJitterEnabled); - out.putInt(myStableFrameLines); - out.putInt(myStableFrameHeightCountdown); - return true; } @@ -257,8 +249,5 @@ bool FrameManager::onLoad(Serializer& in) myJitterEnabled = in.getBool(); - myStableFrameLines = in.getInt(); - myStableFrameHeightCountdown = in.getInt(); - return true; } diff --git a/src/emucore/tia/frame-manager/FrameManager.hxx b/src/emucore/tia/frame-manager/FrameManager.hxx index ee9297824..2f6a156af 100644 --- a/src/emucore/tia/frame-manager/FrameManager.hxx +++ b/src/emucore/tia/frame-manager/FrameManager.hxx @@ -100,9 +100,6 @@ class FrameManager: public AbstractFrameManager { bool myJitterEnabled; - Int32 myStableFrameLines; - uInt8 myStableFrameHeightCountdown; - JitterEmulation myJitterEmulation; private: diff --git a/src/emucore/tia/module.mk b/src/emucore/tia/module.mk index ef2adef45..d6302e805 100644 --- a/src/emucore/tia/module.mk +++ b/src/emucore/tia/module.mk @@ -9,7 +9,9 @@ MODULE_OBJS := \ src/emucore/tia/Ball.o \ src/emucore/tia/Background.o \ src/emucore/tia/LatchedInput.o \ - src/emucore/tia/PaddleReader.o + src/emucore/tia/PaddleReader.o \ + src/emucore/tia/Audio.o \ + src/emucore/tia/AudioChannel.o MODULE_DIRS += \ src/emucore/tia diff --git a/src/gui/AudioDialog.cxx b/src/gui/AudioDialog.cxx index 74c15668f..0a3b22aba 100644 --- a/src/gui/AudioDialog.cxx +++ b/src/gui/AudioDialog.cxx @@ -29,6 +29,7 @@ #include "Settings.hxx" #include "Sound.hxx" #include "Widget.hxx" +#include "AudioSettings.hxx" #include "AudioDialog.hxx" @@ -44,14 +45,14 @@ AudioDialog::AudioDialog(OSystem& osystem, DialogContainer& parent, fontWidth = font.getMaxCharWidth(), fontHeight = font.getFontHeight(); int xpos, ypos; - int lwidth = font.getStringWidth("Sample Size (*) "), + int lwidth = font.getStringWidth("Resampling quality "), pwidth = font.getStringWidth("512 bytes"); WidgetArray wid; VariantList items; // Set real dimensions - _w = 35 * fontWidth + HBORDER * 2; - _h = 7 * (lineHeight + 4) + VBORDER + _th; + _w = 45 * fontWidth + HBORDER * 2; + _h = 11 * (lineHeight + 4) + VBORDER + _th; xpos = HBORDER; ypos = VBORDER + _th; @@ -64,43 +65,80 @@ AudioDialog::AudioDialog(OSystem& osystem, DialogContainer& parent, xpos += INDENT; // Volume - myVolumeSlider = new SliderWidget(this, font, xpos, ypos, 11 * fontWidth + 5, lineHeight, - "Volume ", lwidth, 0, 4 * fontWidth, "%"); + myVolumeSlider = new SliderWidget(this, font, xpos, ypos, + "Volume ", 0, 0, 4 * fontWidth, "%"); myVolumeSlider->setMinValue(1); myVolumeSlider->setMaxValue(100); wid.push_back(myVolumeSlider); ypos += lineHeight + 4; + // + VarList::push_back(items, "Low quality, medium lag", static_cast(AudioSettings::Preset::lowQualityMediumLag)); + VarList::push_back(items, "High quality, medium lag", static_cast(AudioSettings::Preset::highQualityMediumLag)); + VarList::push_back(items, "High quality, low lag", static_cast(AudioSettings::Preset::highQualityLowLag)); + VarList::push_back(items, "Ultra quality, minimal lag", static_cast(AudioSettings::Preset::veryHighQualityVeryLowLag)); + VarList::push_back(items, "Custom", static_cast(AudioSettings::Preset::custom)); + myModePopup = new PopUpWidget(this, font, xpos, ypos, + font.getStringWidth("Ultry quality, minimal lag "), lineHeight, + items, "Mode (*) ", 0, kModeChanged); + wid.push_back(myModePopup); + ypos += lineHeight + 4; + xpos += INDENT; + // Fragment size - VarList::push_back(items, "128 bytes", "128"); - VarList::push_back(items, "256 bytes", "256"); - VarList::push_back(items, "512 bytes", "512"); - VarList::push_back(items, "1 KB", "1024"); - VarList::push_back(items, "2 KB", "2048"); - VarList::push_back(items, "4 KB", "4096"); + items.clear(); + VarList::push_back(items, "128 bytes", 128); + VarList::push_back(items, "256 bytes", 256); + VarList::push_back(items, "512 bytes", 512); + VarList::push_back(items, "1 KB", 1024); + VarList::push_back(items, "2 KB", 2048); + VarList::push_back(items, "4 KB", 4096); myFragsizePopup = new PopUpWidget(this, font, xpos, ypos, pwidth, lineHeight, - items, "Sample size (*) ", lwidth); + items, "Fragment size (*) ", lwidth); wid.push_back(myFragsizePopup); ypos += lineHeight + 4; // Output frequency items.clear(); - VarList::push_back(items, "11025 Hz", "11025"); - VarList::push_back(items, "22050 Hz", "22050"); - VarList::push_back(items, "31400 Hz", "31400"); - VarList::push_back(items, "44100 Hz", "44100"); - VarList::push_back(items, "48000 Hz", "48000"); + VarList::push_back(items, "44100 Hz", 44100); + VarList::push_back(items, "48000 Hz", 48000); + VarList::push_back(items, "96000 Hz", 96000); myFreqPopup = new PopUpWidget(this, font, xpos, ypos, pwidth, lineHeight, - items, "Frequency (*) ", lwidth); + items, "Sample rate (*) ", lwidth); wid.push_back(myFreqPopup); + ypos += lineHeight + 4; + + // Resampling quality + items.clear(); + VarList::push_back(items, "Low", static_cast(AudioSettings::ResamplingQuality::nearestNeightbour)); + VarList::push_back(items, "High", static_cast(AudioSettings::ResamplingQuality::lanczos_2)); + VarList::push_back(items, "Ultra", static_cast(AudioSettings::ResamplingQuality::lanczos_3)); + myResamplingPopup = new PopUpWidget(this, font, xpos, ypos, + pwidth, lineHeight, + items, "Resampling quality ", lwidth); + wid.push_back(myResamplingPopup); + ypos += lineHeight + 4; + + // Param 1 + myHeadroomSlider = new SliderWidget(this, font, xpos, ypos, + "Headroom ", 0, 0, 2 * fontWidth); + myHeadroomSlider->setMinValue(1); myHeadroomSlider->setMaxValue(AudioSettings::MAX_HEADROOM); + wid.push_back(myHeadroomSlider); + ypos += lineHeight + 4; + + // Param 2 + myBufferSizeSlider = new SliderWidget(this, font, xpos, ypos, + "Buffer size ", 0, 0, 2 * fontWidth); + myBufferSizeSlider->setMinValue(1); myBufferSizeSlider->setMaxValue(AudioSettings::MAX_BUFFER_SIZE); + wid.push_back(myBufferSizeSlider); // Add message concerning usage ypos = _h - fontHeight * 2 - 24; const GUI::Font& infofont = instance().frameBuffer().infoFont(); - new StaticTextWidget(this, infofont, HBORDER, ypos, + new StaticTextWidget(this, infofont, HBORDER, ypos, "(*) Requires application restart");/* , font.getStringWidth("(*) Requires application restart"), fontHeight, - "(*) Requires application restart", TextAlign::Left); + "(*) Requires application restart", TextAlign::Left);*/ // Add Defaults, OK and Cancel buttons addDefaultsOKCancelBGroup(wid, font); @@ -111,41 +149,69 @@ AudioDialog::AudioDialog(OSystem& osystem, DialogContainer& parent, // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void AudioDialog::loadConfig() { + AudioSettings& audioSettings = instance().audioSettings(); + // Volume - myVolumeSlider->setValue(instance().settings().getInt("volume")); - - // Fragsize - myFragsizePopup->setSelected(instance().settings().getString("fragsize"), "512"); - - // Output frequency - myFreqPopup->setSelected(instance().settings().getString("freq"), "31400"); + myVolumeSlider->setValue(audioSettings.volume()); // Enable sound - bool b = instance().settings().getBool("sound"); - mySoundEnableCheckbox->setState(b); + mySoundEnableCheckbox->setState(audioSettings.enabled()); + + // Preset / mode + myModePopup->setSelected(static_cast(audioSettings.preset())); + + updatePresetSettings(instance().audioSettings()); // Make sure that mutually-exclusive items are not enabled at the same time - handleSoundEnableChange(b); + handleSoundEnableChange(audioSettings.enabled()); + handleModeChange(audioSettings.enabled()); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioDialog::updatePresetSettings(AudioSettings& audioSettings) +{ + // Fragsize + myFragsizePopup->setSelected(audioSettings.fragmentSize()); + + // Output frequency + myFreqPopup->setSelected(audioSettings.sampleRate()); + + // Headroom + myHeadroomSlider->setValue(audioSettings.headroom()); + + // Buffer size + myBufferSizeSlider->setValue(audioSettings.bufferSize()); + + // Resampling quality + myResamplingPopup->setSelected(static_cast(audioSettings.resamplingQuality())); +} + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void AudioDialog::saveConfig() { - Settings& settings = instance().settings(); + AudioSettings audioSettings = instance().audioSettings(); // Volume - settings.setValue("volume", myVolumeSlider->getValue()); + audioSettings.setVolume(myVolumeSlider->getValue()); instance().sound().setVolume(myVolumeSlider->getValue()); - // Fragsize - settings.setValue("fragsize", myFragsizePopup->getSelectedTag().toString()); - - // Output frequency - settings.setValue("freq", myFreqPopup->getSelectedTag().toString()); - - // Enable/disable sound (requires a restart to take effect) + audioSettings.setEnabled(mySoundEnableCheckbox->getState()); instance().sound().setEnabled(mySoundEnableCheckbox->getState()); + AudioSettings::Preset preset = static_cast(myModePopup->getSelectedTag().toInt()); + audioSettings.setPreset(preset); + + if (preset == AudioSettings::Preset::custom) { + + // Fragsize + audioSettings.setFragmentSize(myFragsizePopup->getSelectedTag().toInt()); + audioSettings.setSampleRate(myFreqPopup->getSelectedTag().toInt()); + audioSettings.setHeadroom(myHeadroomSlider->getValue()); + audioSettings.setBufferSize(myBufferSizeSlider->getValue()); + audioSettings.setResamplingQuality(static_cast(myResamplingPopup->getSelectedTag().toInt())); + } + // Only force a re-initialization when necessary, since it can // be a time-consuming operation if(instance().hasConsole()) @@ -172,10 +238,33 @@ void AudioDialog::setDefaults() void AudioDialog::handleSoundEnableChange(bool active) { myVolumeSlider->setEnabled(active); - myFragsizePopup->setEnabled(active); - myFreqPopup->setEnabled(active); + myModePopup->setEnabled(active); + handleModeChange(active); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioDialog::handleModeChange(bool active) +{ + AudioSettings::Preset preset = static_cast(myModePopup->getSelectedTag().toInt()); + + AudioSettings audioSettings = instance().audioSettings(); + audioSettings.setPersistent(false); + audioSettings.setPreset(preset); + + (cout << "Preset: " << static_cast(preset) << std::endl).flush(); + + updatePresetSettings(audioSettings); + + bool userMode = active && preset == AudioSettings::Preset::custom; + + myFragsizePopup->setEnabled(userMode); + myFreqPopup->setEnabled(userMode); + myResamplingPopup->setEnabled(userMode); + myHeadroomSlider->setEnabled(userMode); + myBufferSizeSlider->setEnabled(userMode); +} + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void AudioDialog::handleCommand(CommandSender* sender, int cmd, int data, int id) @@ -195,6 +284,10 @@ void AudioDialog::handleCommand(CommandSender* sender, int cmd, handleSoundEnableChange(data == 1); break; + case kModeChanged: + handleModeChange(true); + break; + default: Dialog::handleCommand(sender, cmd, data, 0); break; diff --git a/src/gui/AudioDialog.hxx b/src/gui/AudioDialog.hxx index 1ca83be00..94e2666aa 100644 --- a/src/gui/AudioDialog.hxx +++ b/src/gui/AudioDialog.hxx @@ -26,6 +26,7 @@ class SliderWidget; class StaticTextWidget; class CheckboxWidget; class OSystem; +class AudioSettings; #include "bspf.hxx" @@ -41,17 +42,27 @@ class AudioDialog : public Dialog void setDefaults() override; void handleSoundEnableChange(bool active); + void handleModeChange(bool active); void handleCommand(CommandSender* sender, int cmd, int data, int id) override; private: enum { - kSoundEnableChanged = 'ADse' + kSoundEnableChanged = 'ADse', + kModeChanged = 'ADmc' }; + CheckboxWidget* mySoundEnableCheckbox; SliderWidget* myVolumeSlider; + PopUpWidget* myModePopup; PopUpWidget* myFragsizePopup; PopUpWidget* myFreqPopup; - CheckboxWidget* mySoundEnableCheckbox; + PopUpWidget* myResamplingPopup; + SliderWidget* myHeadroomSlider; + SliderWidget* myBufferSizeSlider; + + private: + + void updatePresetSettings(AudioSettings&); private: // Following constructors and assignment operators not supported diff --git a/src/gui/Dialog.cxx b/src/gui/Dialog.cxx index b84f87cba..6b620d6c6 100644 --- a/src/gui/Dialog.cxx +++ b/src/gui/Dialog.cxx @@ -795,7 +795,7 @@ bool Dialog::getResizableBounds(uInt32& w, uInt32& h) const if(instance().hasConsole()) { - ntsc = instance().console().about().InitialFrameRate == "60"; + ntsc = instance().console().tia().frameLayout() == FrameLayout::ntsc; } uInt32 aspect = instance().settings().getInt(ntsc ?"tia.aspectn" : "tia.aspectp"); diff --git a/src/gui/VideoDialog.cxx b/src/gui/VideoDialog.cxx index 1e5dec190..9de92335a 100644 --- a/src/gui/VideoDialog.cxx +++ b/src/gui/VideoDialog.cxx @@ -15,6 +15,8 @@ // this file, and for a DISCLAIMER OF ALL WARRANTIES. //============================================================================ +#include + #include "bspf.hxx" #include "Control.hxx" #include "Dialog.hxx" @@ -32,6 +34,45 @@ #include "TIASurface.hxx" #include "VideoDialog.hxx" +namespace { + // Emulation speed is a positive float that multiplies the framerate. However, the UI controls + // adjust speed in terms of a speedup factor (1/10, 1/9 .. 1/2, 1, 2, 3, .., 10). The following + // mapping and formatting functions implement this conversion. The speedup factor is represented + // by an integer value between -900 and 900 (0 means no speedup). + + constexpr int MAX_SPEED = 900; + constexpr int MIN_SPEED = -900; + constexpr int SPEED_STEP = 10; + + int mapSpeed(float speed) + { + speed = abs(speed); + + return BSPF::clamp( + static_cast(round(100 * (speed >= 1 ? speed - 1 : -1 / speed + 1))), + MIN_SPEED, MAX_SPEED + ); + } + + float unmapSpeed(int speed) + { + float f_speed = static_cast(speed) / 100; + + return speed < 0 ? -1 / (f_speed - 1) : 1 + f_speed; + } + + string formatSpeed(int speed) { + stringstream ss; + + ss + << (speed >= 0 ? "x " : "/ ") + << std::setw(4) << std::fixed << std::setprecision(2) + << (1 + static_cast(speed < 0 ? -speed : speed) / 100); + + return ss.str(); + } +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - VideoDialog::VideoDialog(OSystem& osystem, DialogContainer& parent, const GUI::Font& font, int max_w, int max_h) @@ -121,13 +162,13 @@ VideoDialog::VideoDialog(OSystem& osystem, DialogContainer& parent, wid.push_back(myPAspectRatio); ypos += lineHeight + VGAP; - // Framerate - myFrameRate = + // Speed + mySpeed = new SliderWidget(myTab, font, xpos, ypos-1, swidth, lineHeight, - "Frame rate ", lwidth, kFrameRateChanged, fontWidth * 6, "fps"); - myFrameRate->setMinValue(0); myFrameRate->setMaxValue(900); - myFrameRate->setStepValue(10); - wid.push_back(myFrameRate); + "Speed ", lwidth, kSpeedupChanged, fontWidth * 8, ""); + mySpeed->setMinValue(MIN_SPEED); mySpeed->setMaxValue(MAX_SPEED); + mySpeed->setStepValue(SPEED_STEP); + wid.push_back(mySpeed); ypos += lineHeight + VGAP; // Use sync to vblank @@ -141,7 +182,7 @@ VideoDialog::VideoDialog(OSystem& osystem, DialogContainer& parent, "(*) Requires application restart"); // Move over to the next column - xpos += myFrameRate->getWidth() + 16; + xpos += mySpeed->getWidth() + 16; ypos = VBORDER; // Fullscreen @@ -327,12 +368,10 @@ void VideoDialog::loadConfig() myNAspectRatio->setValue(instance().settings().getInt("tia.aspectn")); myPAspectRatio->setValue(instance().settings().getInt("tia.aspectp")); - // Framerate (0 or -1 means automatic framerate calculation) - int rate = instance().settings().getInt("framerate"); - myFrameRate->setValue(rate < 0 ? 0 : rate); - myFrameRate->setValueLabel(rate <= 0 ? "Auto" : - instance().settings().getString("framerate")); - myFrameRate->setValueUnit(rate <= 0 ? "" : "fps"); + // Emulation speed + int speed = mapSpeed(instance().settings().getFloat("speed")); + mySpeed->setValue(speed); + mySpeed->setValueLabel(formatSpeed(speed)); // Fullscreen myFullscreen->setState(instance().settings().getBool("fullscreen")); @@ -404,15 +443,10 @@ void VideoDialog::saveConfig() instance().settings().setValue("tia.aspectn", myNAspectRatio->getValueLabel()); instance().settings().setValue("tia.aspectp", myPAspectRatio->getValueLabel()); - // Framerate - int f = myFrameRate->getValue(); - instance().settings().setValue("framerate", f); - if(instance().hasConsole()) - { - // Make sure auto-frame calculation is only enabled when necessary - instance().console().tia().enableAutoFrame(f <= 0); - instance().console().setFramerate(float(f)); - } + // Speed + int speedup = mySpeed->getValue(); + instance().settings().setValue("speed", unmapSpeed(speedup)); + if (instance().hasConsole()) instance().console().initializeAudio(); // Fullscreen instance().settings().setValue("fullscreen", myFullscreen->getState()); @@ -486,7 +520,7 @@ void VideoDialog::setDefaults() myTIAInterpolate->setState(false); myNAspectRatio->setValue(91); myPAspectRatio->setValue(109); - myFrameRate->setValue(0); + mySpeed->setValue(0); myFullscreen->setState(false); //myFullScreenMode->setSelectedIndex(0); @@ -585,10 +619,8 @@ void VideoDialog::handleCommand(CommandSender* sender, int cmd, setDefaults(); break; - case kFrameRateChanged: - if(myFrameRate->getValue() == 0) - myFrameRate->setValueLabel("Auto"); - myFrameRate->setValueUnit(myFrameRate->getValue() == 0 ? "" : "fps"); + case kSpeedupChanged: + mySpeed->setValueLabel(formatSpeed(mySpeed->getValue())); break; case kTVModeChanged: diff --git a/src/gui/VideoDialog.hxx b/src/gui/VideoDialog.hxx index bec208387..8e50b3895 100644 --- a/src/gui/VideoDialog.hxx +++ b/src/gui/VideoDialog.hxx @@ -58,7 +58,7 @@ class VideoDialog : public Dialog CheckboxWidget* myTIAInterpolate; SliderWidget* myNAspectRatio; SliderWidget* myPAspectRatio; - SliderWidget* myFrameRate; + SliderWidget* mySpeed; CheckboxWidget* myFullscreen; //PopUpWidget* myFullScreenMode; @@ -99,7 +99,7 @@ class VideoDialog : public Dialog ButtonWidget* myCloneCustom; enum { - kFrameRateChanged = 'VDfr', + kSpeedupChanged = 'VDSp', kTVModeChanged = 'VDtv', diff --git a/src/macosx/stella.xcodeproj/project.pbxproj b/src/macosx/stella.xcodeproj/project.pbxproj index 4de23d9c1..aa63a229a 100644 --- a/src/macosx/stella.xcodeproj/project.pbxproj +++ b/src/macosx/stella.xcodeproj/project.pbxproj @@ -104,7 +104,6 @@ 2D91746109BA90380026E9FF /* TogglePixelWidget.hxx in Headers */ = {isa = PBXBuildFile; fileRef = 2D20F9FF08C603EC00A73076 /* TogglePixelWidget.hxx */; }; 2D91746209BA90380026E9FF /* ToggleWidget.hxx in Headers */ = {isa = PBXBuildFile; fileRef = 2D20FA0108C603EC00A73076 /* ToggleWidget.hxx */; }; 2D91746409BA90380026E9FF /* TiaZoomWidget.hxx in Headers */ = {isa = PBXBuildFile; fileRef = 2D6CC10408C811A600B8F642 /* TiaZoomWidget.hxx */; }; - 2D91746509BA90380026E9FF /* TIASnd.hxx in Headers */ = {isa = PBXBuildFile; fileRef = 2DE7242E08CE910900C889A8 /* TIASnd.hxx */; }; 2D91746609BA90380026E9FF /* AudioWidget.hxx in Headers */ = {isa = PBXBuildFile; fileRef = 2D2331900900B5EF00613B1F /* AudioWidget.hxx */; }; 2D91746909BA90380026E9FF /* EventMappingWidget.hxx in Headers */ = {isa = PBXBuildFile; fileRef = 2D05FF5F096E269100A518FE /* EventMappingWidget.hxx */; }; 2D91746A09BA90380026E9FF /* InputDialog.hxx in Headers */ = {isa = PBXBuildFile; fileRef = 2D05FF61096E269100A518FE /* InputDialog.hxx */; }; @@ -202,7 +201,6 @@ 2D91750309BA90380026E9FF /* TogglePixelWidget.cxx in Sources */ = {isa = PBXBuildFile; fileRef = 2D20F9FE08C603EC00A73076 /* TogglePixelWidget.cxx */; }; 2D91750409BA90380026E9FF /* ToggleWidget.cxx in Sources */ = {isa = PBXBuildFile; fileRef = 2D20FA0008C603EC00A73076 /* ToggleWidget.cxx */; }; 2D91750609BA90380026E9FF /* TiaZoomWidget.cxx in Sources */ = {isa = PBXBuildFile; fileRef = 2D6CC10308C811A600B8F642 /* TiaZoomWidget.cxx */; }; - 2D91750709BA90380026E9FF /* TIASnd.cxx in Sources */ = {isa = PBXBuildFile; fileRef = 2DE7242D08CE910900C889A8 /* TIASnd.cxx */; }; 2D91750809BA90380026E9FF /* AudioWidget.cxx in Sources */ = {isa = PBXBuildFile; fileRef = 2D23318F0900B5EF00613B1F /* AudioWidget.cxx */; }; 2D91750B09BA90380026E9FF /* EventMappingWidget.cxx in Sources */ = {isa = PBXBuildFile; fileRef = 2D05FF5E096E269100A518FE /* EventMappingWidget.cxx */; }; 2D91750C09BA90380026E9FF /* InputDialog.cxx in Sources */ = {isa = PBXBuildFile; fileRef = 2D05FF60096E269100A518FE /* InputDialog.cxx */; }; @@ -511,6 +509,9 @@ DCC527D610B9DA19005E1287 /* System.cxx in Sources */ = {isa = PBXBuildFile; fileRef = DCC527CE10B9DA19005E1287 /* System.cxx */; }; DCC527D710B9DA19005E1287 /* System.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCC527CF10B9DA19005E1287 /* System.hxx */; }; DCC527DB10B9DA6A005E1287 /* bspf.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCC527D810B9DA6A005E1287 /* bspf.hxx */; }; + DCC6A4B120A2622500863C59 /* Resampler.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCC6A4AE20A2622500863C59 /* Resampler.hxx */; }; + DCC6A4B220A2622500863C59 /* SimpleResampler.cxx in Sources */ = {isa = PBXBuildFile; fileRef = DCC6A4AF20A2622500863C59 /* SimpleResampler.cxx */; }; + DCC6A4B320A2622500863C59 /* SimpleResampler.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCC6A4B020A2622500863C59 /* SimpleResampler.hxx */; }; DCCA26B31FA64D5E000EE4D8 /* AbstractFrameManager.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCCA26B11FA64D5E000EE4D8 /* AbstractFrameManager.hxx */; }; DCCA26B41FA64D5E000EE4D8 /* FrameManager.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCCA26B21FA64D5E000EE4D8 /* FrameManager.hxx */; }; DCCF47DE14B60DEE00814FAB /* ControllerWidget.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCCF47DB14B60DEE00814FAB /* ControllerWidget.hxx */; }; @@ -564,6 +565,8 @@ DCDE17FB17724E5D00EB1AC6 /* ConfigPathDialog.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCDE17F717724E5D00EB1AC6 /* ConfigPathDialog.hxx */; }; DCDE17FC17724E5D00EB1AC6 /* SnapshotDialog.cxx in Sources */ = {isa = PBXBuildFile; fileRef = DCDE17F817724E5D00EB1AC6 /* SnapshotDialog.cxx */; }; DCDE17FD17724E5D00EB1AC6 /* SnapshotDialog.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCDE17F917724E5D00EB1AC6 /* SnapshotDialog.hxx */; }; + DCDFF08120B781B0001227C0 /* DispatchResult.cxx in Sources */ = {isa = PBXBuildFile; fileRef = DCDFF07F20B781B0001227C0 /* DispatchResult.cxx */; }; + DCDFF08220B781B0001227C0 /* DispatchResult.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCDFF08020B781B0001227C0 /* DispatchResult.hxx */; }; DCE395DB16CB0B2B008DB1E5 /* FSNodePOSIX.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCE395DA16CB0B2B008DB1E5 /* FSNodePOSIX.hxx */; }; DCE395EF16CB0B5F008DB1E5 /* FSNodeFactory.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCE395EA16CB0B5F008DB1E5 /* FSNodeFactory.hxx */; }; DCE395F016CB0B5F008DB1E5 /* FSNodeZIP.cxx in Sources */ = {isa = PBXBuildFile; fileRef = DCE395EB16CB0B5F008DB1E5 /* FSNodeZIP.cxx */; }; @@ -602,7 +605,6 @@ DCF3A6FD1DFC75E3008A8AF3 /* Playfield.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCF3A6E31DFC75E3008A8AF3 /* Playfield.hxx */; }; DCF3A6FE1DFC75E3008A8AF3 /* TIA.cxx in Sources */ = {isa = PBXBuildFile; fileRef = DCF3A6E41DFC75E3008A8AF3 /* TIA.cxx */; }; DCF3A6FF1DFC75E3008A8AF3 /* TIA.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCF3A6E51DFC75E3008A8AF3 /* TIA.hxx */; }; - DCF3A7021DFC76BC008A8AF3 /* TIATypes.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCF3A7011DFC76BC008A8AF3 /* TIATypes.hxx */; }; DCF467B80F93993B00B25D7A /* SoundNull.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCF467B40F93993B00B25D7A /* SoundNull.hxx */; }; DCF467BD0F9399F500B25D7A /* Version.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCF467BC0F9399F500B25D7A /* Version.hxx */; }; DCF467C20F939A1400B25D7A /* CartEF.cxx in Sources */ = {isa = PBXBuildFile; fileRef = DCF467BE0F939A1400B25D7A /* CartEF.cxx */; }; @@ -615,6 +617,8 @@ DCF7B0DF10A762FC007A2870 /* CartFA.cxx in Sources */ = {isa = PBXBuildFile; fileRef = DCF7B0DB10A762FC007A2870 /* CartFA.cxx */; }; DCF7B0E010A762FC007A2870 /* CartFA.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCF7B0DC10A762FC007A2870 /* CartFA.hxx */; }; DCFB9FAC1ECA2609004FD69B /* DelayQueueIteratorImpl.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCFB9FAB1ECA2609004FD69B /* DelayQueueIteratorImpl.hxx */; }; + DCFCDE7220C9E66500915CBE /* EmulationWorker.cxx in Sources */ = {isa = PBXBuildFile; fileRef = DCFCDE7020C9E66500915CBE /* EmulationWorker.cxx */; }; + DCFCDE7320C9E66500915CBE /* EmulationWorker.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCFCDE7120C9E66500915CBE /* EmulationWorker.hxx */; }; DCFF14CD18B0260300A20364 /* EventHandlerSDL2.cxx in Sources */ = {isa = PBXBuildFile; fileRef = DCFF14CB18B0260300A20364 /* EventHandlerSDL2.cxx */; }; DCFF14CE18B0260300A20364 /* EventHandlerSDL2.hxx in Headers */ = {isa = PBXBuildFile; fileRef = DCFF14CC18B0260300A20364 /* EventHandlerSDL2.hxx */; }; DCFFE59D12100E1400DFA000 /* ComboDialog.cxx in Sources */ = {isa = PBXBuildFile; fileRef = DCFFE59B12100E1400DFA000 /* ComboDialog.cxx */; }; @@ -625,8 +629,20 @@ E0306E0F1F93E916003DDD52 /* JitterEmulation.cxx in Sources */ = {isa = PBXBuildFile; fileRef = E0306E091F93E915003DDD52 /* JitterEmulation.cxx */; }; E0306E101F93E916003DDD52 /* FrameLayoutDetector.cxx in Sources */ = {isa = PBXBuildFile; fileRef = E0306E0A1F93E916003DDD52 /* FrameLayoutDetector.cxx */; }; E0306E111F93E916003DDD52 /* JitterEmulation.hxx in Headers */ = {isa = PBXBuildFile; fileRef = E0306E0B1F93E916003DDD52 /* JitterEmulation.hxx */; }; + E034A5EE209FB25D00C89E9E /* EmulationTiming.cxx in Sources */ = {isa = PBXBuildFile; fileRef = E034A5EC209FB25C00C89E9E /* EmulationTiming.cxx */; }; + E034A5EF209FB25D00C89E9E /* EmulationTiming.hxx in Headers */ = {isa = PBXBuildFile; fileRef = E034A5ED209FB25C00C89E9E /* EmulationTiming.hxx */; }; E0406FB61F81A85400A82AE0 /* AbstractFrameManager.cxx in Sources */ = {isa = PBXBuildFile; fileRef = E0DFDD781F81A358000F3505 /* AbstractFrameManager.cxx */; }; E0406FB81F81A85400A82AE0 /* FrameManager.cxx in Sources */ = {isa = PBXBuildFile; fileRef = E0DFDD7B1F81A358000F3505 /* FrameManager.cxx */; }; + E09F413B201E901D004A3391 /* AudioQueue.hxx in Headers */ = {isa = PBXBuildFile; fileRef = E09F4139201E901C004A3391 /* AudioQueue.hxx */; }; + E09F413C201E901D004A3391 /* AudioQueue.cxx in Sources */ = {isa = PBXBuildFile; fileRef = E09F413A201E901D004A3391 /* AudioQueue.cxx */; }; + E09F4141201E9050004A3391 /* Audio.hxx in Headers */ = {isa = PBXBuildFile; fileRef = E09F413D201E904F004A3391 /* Audio.hxx */; }; + E09F4142201E9050004A3391 /* Audio.cxx in Sources */ = {isa = PBXBuildFile; fileRef = E09F413E201E904F004A3391 /* Audio.cxx */; }; + E09F4143201E9050004A3391 /* AudioChannel.cxx in Sources */ = {isa = PBXBuildFile; fileRef = E09F413F201E904F004A3391 /* AudioChannel.cxx */; }; + E09F4144201E9050004A3391 /* AudioChannel.hxx in Headers */ = {isa = PBXBuildFile; fileRef = E09F4140201E904F004A3391 /* AudioChannel.hxx */; }; + E0DCD3A720A64E96000B614E /* LanczosResampler.hxx in Headers */ = {isa = PBXBuildFile; fileRef = E0DCD3A320A64E95000B614E /* LanczosResampler.hxx */; }; + E0DCD3A820A64E96000B614E /* LanczosResampler.cxx in Sources */ = {isa = PBXBuildFile; fileRef = E0DCD3A420A64E95000B614E /* LanczosResampler.cxx */; }; + E0DCD3A920A64E96000B614E /* ConvolutionBuffer.hxx in Headers */ = {isa = PBXBuildFile; fileRef = E0DCD3A520A64E96000B614E /* ConvolutionBuffer.hxx */; }; + E0DCD3AA20A64E96000B614E /* ConvolutionBuffer.cxx in Sources */ = {isa = PBXBuildFile; fileRef = E0DCD3A620A64E96000B614E /* ConvolutionBuffer.cxx */; }; /* End PBXBuildFile section */ /* Begin PBXBuildRule section */ @@ -874,8 +890,6 @@ 2DE2DF8D0627AE34006BEC99 /* Sound.hxx */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.cpp.h; path = Sound.hxx; sourceTree = ""; }; 2DE2DF8E0627AE34006BEC99 /* Switches.cxx */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.cpp.cpp; path = Switches.cxx; sourceTree = ""; }; 2DE2DF8F0627AE34006BEC99 /* Switches.hxx */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.cpp.h; path = Switches.hxx; sourceTree = ""; }; - 2DE7242D08CE910900C889A8 /* TIASnd.cxx */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.cpp.cpp; path = TIASnd.cxx; sourceTree = ""; }; - 2DE7242E08CE910900C889A8 /* TIASnd.hxx */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.cpp.h; path = TIASnd.hxx; sourceTree = ""; }; 2DEB3D4C0629BD24007EBBD3 /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = System/Library/Frameworks/OpenGL.framework; sourceTree = SDKROOT; }; 2DEF21F808BC033500B246B4 /* CheckListWidget.cxx */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.cpp.cpp; path = CheckListWidget.cxx; sourceTree = ""; }; 2DEF21F908BC033500B246B4 /* CheckListWidget.hxx */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.cpp.h; path = CheckListWidget.hxx; sourceTree = ""; }; @@ -1187,6 +1201,9 @@ DCC527CE10B9DA19005E1287 /* System.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = System.cxx; sourceTree = ""; }; DCC527CF10B9DA19005E1287 /* System.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = System.hxx; sourceTree = ""; }; DCC527D810B9DA6A005E1287 /* bspf.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = bspf.hxx; sourceTree = ""; }; + DCC6A4AE20A2622500863C59 /* Resampler.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Resampler.hxx; path = audio/Resampler.hxx; sourceTree = ""; }; + DCC6A4AF20A2622500863C59 /* SimpleResampler.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = SimpleResampler.cxx; path = audio/SimpleResampler.cxx; sourceTree = ""; }; + DCC6A4B020A2622500863C59 /* SimpleResampler.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = SimpleResampler.hxx; path = audio/SimpleResampler.hxx; sourceTree = ""; }; DCCA26B11FA64D5E000EE4D8 /* AbstractFrameManager.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = AbstractFrameManager.hxx; sourceTree = ""; }; DCCA26B21FA64D5E000EE4D8 /* FrameManager.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = FrameManager.hxx; sourceTree = ""; }; DCCF47DB14B60DEE00814FAB /* ControllerWidget.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ControllerWidget.hxx; sourceTree = ""; }; @@ -1240,6 +1257,8 @@ DCDE17F717724E5D00EB1AC6 /* ConfigPathDialog.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ConfigPathDialog.hxx; sourceTree = ""; }; DCDE17F817724E5D00EB1AC6 /* SnapshotDialog.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SnapshotDialog.cxx; sourceTree = ""; }; DCDE17F917724E5D00EB1AC6 /* SnapshotDialog.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SnapshotDialog.hxx; sourceTree = ""; }; + DCDFF07F20B781B0001227C0 /* DispatchResult.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = DispatchResult.cxx; sourceTree = ""; }; + DCDFF08020B781B0001227C0 /* DispatchResult.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = DispatchResult.hxx; sourceTree = ""; }; DCE395DA16CB0B2B008DB1E5 /* FSNodePOSIX.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = FSNodePOSIX.hxx; sourceTree = ""; }; DCE395EA16CB0B5F008DB1E5 /* FSNodeFactory.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = FSNodeFactory.hxx; sourceTree = ""; }; DCE395EB16CB0B5F008DB1E5 /* FSNodeZIP.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = FSNodeZIP.cxx; sourceTree = ""; }; @@ -1278,7 +1297,6 @@ DCF3A6E31DFC75E3008A8AF3 /* Playfield.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Playfield.hxx; sourceTree = ""; }; DCF3A6E41DFC75E3008A8AF3 /* TIA.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TIA.cxx; sourceTree = ""; }; DCF3A6E51DFC75E3008A8AF3 /* TIA.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TIA.hxx; sourceTree = ""; }; - DCF3A7011DFC76BC008A8AF3 /* TIATypes.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TIATypes.hxx; sourceTree = ""; }; DCF467B40F93993B00B25D7A /* SoundNull.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SoundNull.hxx; sourceTree = ""; }; DCF467BC0F9399F500B25D7A /* Version.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Version.hxx; sourceTree = ""; }; DCF467BE0F939A1400B25D7A /* CartEF.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CartEF.cxx; sourceTree = ""; }; @@ -1291,6 +1309,8 @@ DCF7B0DB10A762FC007A2870 /* CartFA.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CartFA.cxx; sourceTree = ""; }; DCF7B0DC10A762FC007A2870 /* CartFA.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CartFA.hxx; sourceTree = ""; }; DCFB9FAB1ECA2609004FD69B /* DelayQueueIteratorImpl.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = DelayQueueIteratorImpl.hxx; sourceTree = ""; }; + DCFCDE7020C9E66500915CBE /* EmulationWorker.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = EmulationWorker.cxx; sourceTree = ""; }; + DCFCDE7120C9E66500915CBE /* EmulationWorker.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = EmulationWorker.hxx; sourceTree = ""; }; DCFF14CB18B0260300A20364 /* EventHandlerSDL2.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = EventHandlerSDL2.cxx; sourceTree = ""; }; DCFF14CC18B0260300A20364 /* EventHandlerSDL2.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = EventHandlerSDL2.hxx; sourceTree = ""; }; DCFFE59B12100E1400DFA000 /* ComboDialog.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ComboDialog.cxx; sourceTree = ""; }; @@ -1301,6 +1321,18 @@ E0306E091F93E915003DDD52 /* JitterEmulation.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = JitterEmulation.cxx; sourceTree = ""; }; E0306E0A1F93E916003DDD52 /* FrameLayoutDetector.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = FrameLayoutDetector.cxx; sourceTree = ""; }; E0306E0B1F93E916003DDD52 /* JitterEmulation.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = JitterEmulation.hxx; sourceTree = ""; }; + E034A5EC209FB25C00C89E9E /* EmulationTiming.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = EmulationTiming.cxx; sourceTree = ""; }; + E034A5ED209FB25C00C89E9E /* EmulationTiming.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = EmulationTiming.hxx; sourceTree = ""; }; + E09F4139201E901C004A3391 /* AudioQueue.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = AudioQueue.hxx; sourceTree = ""; }; + E09F413A201E901D004A3391 /* AudioQueue.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = AudioQueue.cxx; sourceTree = ""; }; + E09F413D201E904F004A3391 /* Audio.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Audio.hxx; sourceTree = ""; }; + E09F413E201E904F004A3391 /* Audio.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Audio.cxx; sourceTree = ""; }; + E09F413F201E904F004A3391 /* AudioChannel.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = AudioChannel.cxx; sourceTree = ""; }; + E09F4140201E904F004A3391 /* AudioChannel.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = AudioChannel.hxx; sourceTree = ""; }; + E0DCD3A320A64E95000B614E /* LanczosResampler.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = LanczosResampler.hxx; path = audio/LanczosResampler.hxx; sourceTree = ""; }; + E0DCD3A420A64E95000B614E /* LanczosResampler.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = LanczosResampler.cxx; path = audio/LanczosResampler.cxx; sourceTree = ""; }; + E0DCD3A520A64E96000B614E /* ConvolutionBuffer.hxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = ConvolutionBuffer.hxx; path = audio/ConvolutionBuffer.hxx; sourceTree = ""; }; + E0DCD3A620A64E96000B614E /* ConvolutionBuffer.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = ConvolutionBuffer.cxx; path = audio/ConvolutionBuffer.cxx; sourceTree = ""; }; E0DFDD781F81A358000F3505 /* AbstractFrameManager.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = AbstractFrameManager.cxx; sourceTree = ""; }; E0DFDD7B1F81A358000F3505 /* FrameManager.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = FrameManager.cxx; sourceTree = ""; }; F5A47A9D01A0482F01D3D55B /* SDLMain.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = SDLMain.h; sourceTree = SOURCE_ROOT; }; @@ -1578,6 +1610,9 @@ 2D6050C5089876F300C6DE89 /* common */ = { isa = PBXGroup; children = ( + DCC6A4AD20A2620D00863C59 /* audio */, + E09F413A201E901D004A3391 /* AudioQueue.cxx */, + E09F4139201E901C004A3391 /* AudioQueue.hxx */, DC79F81017A88D9E00288B91 /* Base.cxx */, DC79F81117A88D9E00288B91 /* Base.hxx */, DCC527D810B9DA6A005E1287 /* bspf.hxx */, @@ -1756,8 +1791,14 @@ 2DE2DF3B0627AE07006BEC99 /* Control.hxx */, DC932D3F0F278A5200FEFEFC /* DefProps.hxx */, DCC527C910B9DA19005E1287 /* Device.hxx */, + DCDFF07F20B781B0001227C0 /* DispatchResult.cxx */, + DCDFF08020B781B0001227C0 /* DispatchResult.hxx */, 2DE2DF3E0627AE07006BEC99 /* Driving.cxx */, 2DE2DF3F0627AE07006BEC99 /* Driving.hxx */, + E034A5EC209FB25C00C89E9E /* EmulationTiming.cxx */, + E034A5ED209FB25C00C89E9E /* EmulationTiming.hxx */, + DCFCDE7020C9E66500915CBE /* EmulationWorker.cxx */, + DCFCDE7120C9E66500915CBE /* EmulationWorker.hxx */, 2DE2DF410627AE07006BEC99 /* Event.hxx */, 2D733D6E062895B2006265D9 /* EventHandler.cxx */, 2D733D6F062895B2006265D9 /* EventHandler.hxx */, @@ -1815,11 +1856,8 @@ DCD2839612E39F1200A808DC /* Thumbulator.cxx */, DCD2839712E39F1200A808DC /* Thumbulator.hxx */, DCE903E31DF5DCD10080A7F3 /* tia */, - 2DE7242D08CE910900C889A8 /* TIASnd.cxx */, - 2DE7242E08CE910900C889A8 /* TIASnd.hxx */, DC2AADAC194F389C0026C7A4 /* TIASurface.cxx */, DC2AADAD194F389C0026C7A4 /* TIASurface.hxx */, - DCF3A7011DFC76BC008A8AF3 /* TIATypes.hxx */, DC1B2EC21E50036100F62837 /* TrakBall.hxx */, ); path = emucore; @@ -2020,6 +2058,20 @@ path = tv_filters; sourceTree = ""; }; + DCC6A4AD20A2620D00863C59 /* audio */ = { + isa = PBXGroup; + children = ( + E0DCD3A620A64E96000B614E /* ConvolutionBuffer.cxx */, + E0DCD3A520A64E96000B614E /* ConvolutionBuffer.hxx */, + E0DCD3A420A64E95000B614E /* LanczosResampler.cxx */, + E0DCD3A320A64E95000B614E /* LanczosResampler.hxx */, + DCC6A4AE20A2622500863C59 /* Resampler.hxx */, + DCC6A4AF20A2622500863C59 /* SimpleResampler.cxx */, + DCC6A4B020A2622500863C59 /* SimpleResampler.hxx */, + ); + name = audio; + sourceTree = ""; + }; DCCC0C9109C3541E0088BFF1 /* cheat */ = { isa = PBXGroup; children = ( @@ -2070,6 +2122,10 @@ DCE903E31DF5DCD10080A7F3 /* tia */ = { isa = PBXGroup; children = ( + E09F413E201E904F004A3391 /* Audio.cxx */, + E09F413D201E904F004A3391 /* Audio.hxx */, + E09F413F201E904F004A3391 /* AudioChannel.cxx */, + E09F4140201E904F004A3391 /* AudioChannel.hxx */, DCF3A6CD1DFC75E3008A8AF3 /* Background.cxx */, DCF3A6CE1DFC75E3008A8AF3 /* Background.hxx */, DCF3A6CF1DFC75E3008A8AF3 /* Ball.cxx */, @@ -2124,11 +2180,13 @@ buildActionMask = 2147483647; files = ( 2D9173CB09BA90380026E9FF /* SDLMain.h in Headers */, + E09F4141201E9050004A3391 /* Audio.hxx in Headers */, 2D9173CC09BA90380026E9FF /* Booster.hxx in Headers */, 2D9173CD09BA90380026E9FF /* Cart.hxx in Headers */, 2D9173CE09BA90380026E9FF /* Cart2K.hxx in Headers */, 2D9173CF09BA90380026E9FF /* Cart3F.hxx in Headers */, DC3EE86D1E2C0E6D00905161 /* zlib.h in Headers */, + E034A5EF209FB25D00C89E9E /* EmulationTiming.hxx in Headers */, DC3EE86A1E2C0E6D00905161 /* trees.h in Headers */, 2D9173D009BA90380026E9FF /* Cart4K.hxx in Headers */, 2D9173D109BA90380026E9FF /* CartAR.hxx in Headers */, @@ -2171,6 +2229,7 @@ 2D9173EE09BA90380026E9FF /* Serializer.hxx in Headers */, 2D9173EF09BA90380026E9FF /* Sound.hxx in Headers */, 2D9173F009BA90380026E9FF /* Switches.hxx in Headers */, + E09F413B201E901D004A3391 /* AudioQueue.hxx in Headers */, 2D9173F909BA90380026E9FF /* EventHandler.hxx in Headers */, 2D9173FA09BA90380026E9FF /* FrameBuffer.hxx in Headers */, 2D9173FB09BA90380026E9FF /* Settings.hxx in Headers */, @@ -2186,6 +2245,7 @@ 2D91740309BA90380026E9FF /* Command.hxx in Headers */, DC3EE85B1E2C0E6D00905161 /* deflate.h in Headers */, 2D91740409BA90380026E9FF /* Dialog.hxx in Headers */, + E09F4144201E9050004A3391 /* AudioChannel.hxx in Headers */, 2D91740509BA90380026E9FF /* DialogContainer.hxx in Headers */, DC6D39881A3CE65000171E71 /* CartWDWidget.hxx in Headers */, 2D91740609BA90380026E9FF /* GameInfoDialog.hxx in Headers */, @@ -2223,6 +2283,7 @@ DC3EE86F1E2C0E6D00905161 /* zutil.h in Headers */, 2D91742509BA90380026E9FF /* EditTextWidget.hxx in Headers */, DCB87E581A104C1E00BF2A3B /* MediaFactory.hxx in Headers */, + E0DCD3A720A64E96000B614E /* LanczosResampler.hxx in Headers */, 2D91742809BA90380026E9FF /* PackedBitArray.hxx in Headers */, 2D91742909BA90380026E9FF /* TIADebug.hxx in Headers */, 2D91742A09BA90380026E9FF /* YaccParser.hxx in Headers */, @@ -2246,6 +2307,7 @@ 2D91745509BA90380026E9FF /* CpuWidget.hxx in Headers */, 2D91745609BA90380026E9FF /* DataGridOpsWidget.hxx in Headers */, 2D91745709BA90380026E9FF /* DataGridWidget.hxx in Headers */, + DCC6A4B120A2622500863C59 /* Resampler.hxx in Headers */, DCF3A6EC1DFC75E3008A8AF3 /* DelayQueue.hxx in Headers */, DCA078341F8C1B04008EFEE5 /* LinkedObjectPool.hxx in Headers */, 2D91745809BA90380026E9FF /* DebuggerDialog.hxx in Headers */, @@ -2267,7 +2329,6 @@ 2D91746109BA90380026E9FF /* TogglePixelWidget.hxx in Headers */, 2D91746209BA90380026E9FF /* ToggleWidget.hxx in Headers */, 2D91746409BA90380026E9FF /* TiaZoomWidget.hxx in Headers */, - 2D91746509BA90380026E9FF /* TIASnd.hxx in Headers */, DC1BC6672066B4390076F74A /* PKeyboardHandler.hxx in Headers */, 2D91746609BA90380026E9FF /* AudioWidget.hxx in Headers */, 2D91746909BA90380026E9FF /* EventMappingWidget.hxx in Headers */, @@ -2285,6 +2346,7 @@ DC8078EB0B4BD697005E9305 /* UIDialog.hxx in Headers */, DCEECE570B5E5E540021D754 /* Cart0840.hxx in Headers */, DCE3BBFA0C95CEDC00A671DF /* RomInfoWidget.hxx in Headers */, + DCC6A4B320A2622500863C59 /* SimpleResampler.hxx in Headers */, DC6DC91F205DB879004A5FC3 /* PhysicalJoystick.hxx in Headers */, DC0984860D3985160073C852 /* CartSB.hxx in Headers */, DCEC585E1E945175002F0246 /* DelayQueueIterator.hxx in Headers */, @@ -2298,6 +2360,7 @@ DC4AC6F40DC8DAEF00CD3AD2 /* SaveKey.hxx in Headers */, DC173F770E2CAC1E00320F94 /* ContextMenu.hxx in Headers */, DC0DF86A0F0DAAF500B0F1F3 /* GlobalPropsDialog.hxx in Headers */, + E0DCD3A920A64E96000B614E /* ConvolutionBuffer.hxx in Headers */, DC5D2C520F117CFD004D1660 /* Rect.hxx in Headers */, DC71EAA81FDA070D008827CB /* CartMNetworkWidget.hxx in Headers */, DC5D2C530F117CFD004D1660 /* StellaFont.hxx in Headers */, @@ -2309,7 +2372,6 @@ DC932D450F278A5200FEFEFC /* Serializable.hxx in Headers */, DC932D460F278A5200FEFEFC /* SerialPort.hxx in Headers */, DC9EA8880F729A36000452B5 /* KidVid.hxx in Headers */, - DCF3A7021DFC76BC008A8AF3 /* TIATypes.hxx in Headers */, DCF467B80F93993B00B25D7A /* SoundNull.hxx in Headers */, DCBDDE9F1D6A5F2F009DF1E9 /* Cart3EPlus.hxx in Headers */, DCF467BD0F9399F500B25D7A /* Version.hxx in Headers */, @@ -2350,6 +2412,7 @@ DC74D6A2138D4D7E00F05C5C /* StringParser.hxx in Headers */, DC2874071F8F2278004BF21A /* TrapArray.hxx in Headers */, DCCA26B41FA64D5E000EE4D8 /* FrameManager.hxx in Headers */, + DCFCDE7320C9E66500915CBE /* EmulationWorker.hxx in Headers */, DC6C726313CDEA0A008A5975 /* LoggerDialog.hxx in Headers */, DC8C1BAE14B25DE7006440EE /* CartCM.hxx in Headers */, DCF3A6FB1DFC75E3008A8AF3 /* Player.hxx in Headers */, @@ -2386,6 +2449,7 @@ DC6DC921205DB879004A5FC3 /* PJoystickHandler.hxx in Headers */, DCAAE5DF1715887B0080BB82 /* CartEFSCWidget.hxx in Headers */, DCAAE5E11715887B0080BB82 /* CartEFWidget.hxx in Headers */, + DCDFF08220B781B0001227C0 /* DispatchResult.hxx in Headers */, DCAAE5E31715887B0080BB82 /* CartF0Widget.hxx in Headers */, DCAAE5E51715887B0080BB82 /* CartF4SCWidget.hxx in Headers */, DCAAE5E71715887B0080BB82 /* CartF4Widget.hxx in Headers */, @@ -2551,6 +2615,7 @@ DC3EE8671E2C0E6D00905161 /* inftrees.c in Sources */, 2D91747609BA90380026E9FF /* Cart.cxx in Sources */, 2D91747709BA90380026E9FF /* Cart2K.cxx in Sources */, + E034A5EE209FB25D00C89E9E /* EmulationTiming.cxx in Sources */, 2D91747809BA90380026E9FF /* Cart3F.cxx in Sources */, 2D91747909BA90380026E9FF /* Cart4K.cxx in Sources */, 2D91747A09BA90380026E9FF /* CartAR.cxx in Sources */, @@ -2559,9 +2624,11 @@ DCF3A6FC1DFC75E3008A8AF3 /* Playfield.cxx in Sources */, 2D91747C09BA90380026E9FF /* CartDPC.cxx in Sources */, 2D91747D09BA90380026E9FF /* CartE0.cxx in Sources */, + E0DCD3AA20A64E96000B614E /* ConvolutionBuffer.cxx in Sources */, 2D91747E09BA90380026E9FF /* CartE7.cxx in Sources */, DC9616321F817830008A2206 /* PointingDeviceWidget.cxx in Sources */, 2D91747F09BA90380026E9FF /* CartF4.cxx in Sources */, + DCFCDE7220C9E66500915CBE /* EmulationWorker.cxx in Sources */, 2D91748009BA90380026E9FF /* CartF4SC.cxx in Sources */, 2D91748109BA90380026E9FF /* CartF6.cxx in Sources */, 2D91748209BA90380026E9FF /* CartF6SC.cxx in Sources */, @@ -2578,6 +2645,7 @@ 2D91748F09BA90380026E9FF /* Keyboard.cxx in Sources */, 2D91749009BA90380026E9FF /* M6532.cxx in Sources */, 2D91749109BA90380026E9FF /* MD5.cxx in Sources */, + E09F4143201E9050004A3391 /* AudioChannel.cxx in Sources */, DC44019E1F1A5D01008C08F6 /* ColorWidget.cxx in Sources */, 2D91749309BA90380026E9FF /* Paddles.cxx in Sources */, 2D91749409BA90380026E9FF /* Props.cxx in Sources */, @@ -2650,6 +2718,7 @@ 2D9174FA09BA90380026E9FF /* DebuggerDialog.cxx in Sources */, DCF3A6E91DFC75E3008A8AF3 /* Ball.cxx in Sources */, 2D9174FB09BA90380026E9FF /* PromptWidget.cxx in Sources */, + DCDFF08120B781B0001227C0 /* DispatchResult.cxx in Sources */, 2D9174FC09BA90380026E9FF /* RamWidget.cxx in Sources */, DC2AADAE194F389C0026C7A4 /* CartDASH.cxx in Sources */, 2D9174FD09BA90380026E9FF /* RomListWidget.cxx in Sources */, @@ -2668,7 +2737,7 @@ DC71EA9D1FDA06D2008827CB /* CartE78K.cxx in Sources */, DC73BD851915E5B1003FAFAD /* FBSurfaceSDL2.cxx in Sources */, DCDDEAC41F5DBF0400C67366 /* RewindManager.cxx in Sources */, - 2D91750709BA90380026E9FF /* TIASnd.cxx in Sources */, + E09F413C201E901D004A3391 /* AudioQueue.cxx in Sources */, DC71EA9F1FDA06D2008827CB /* CartMNetwork.cxx in Sources */, 2D91750809BA90380026E9FF /* AudioWidget.cxx in Sources */, 2D91750B09BA90380026E9FF /* EventMappingWidget.cxx in Sources */, @@ -2753,6 +2822,7 @@ DC8C1BAD14B25DE7006440EE /* CartCM.cxx in Sources */, DCDDEAC61F5DBF0400C67366 /* StateManager.cxx in Sources */, DC8C1BAF14B25DE7006440EE /* CompuMate.cxx in Sources */, + E09F4142201E9050004A3391 /* Audio.cxx in Sources */, DC8C1BB114B25DE7006440EE /* MindLink.cxx in Sources */, DCCF47DF14B60DEE00814FAB /* JoystickWidget.cxx in Sources */, DCCF49B714B7544A00814FAB /* PaddleWidget.cxx in Sources */, @@ -2774,6 +2844,7 @@ DC3EE8601E2C0E6D00905161 /* gzwrite.c in Sources */, DCAAE5D71715887B0080BB82 /* Cart4KWidget.cxx in Sources */, DCDA03B01A2009BB00711920 /* CartWD.cxx in Sources */, + DCC6A4B220A2622500863C59 /* SimpleResampler.cxx in Sources */, DCAAE5D91715887B0080BB82 /* Cart0840Widget.cxx in Sources */, DCAAE5DB1715887B0080BB82 /* CartCVWidget.cxx in Sources */, DCAAE5DE1715887B0080BB82 /* CartEFSCWidget.cxx in Sources */, @@ -2802,6 +2873,7 @@ DC676A4D1729A0B000E4E73D /* CartDPCWidget.cxx in Sources */, DC676A4F1729A0B000E4E73D /* CartE0Widget.cxx in Sources */, DC676A511729A0B000E4E73D /* CartE7Widget.cxx in Sources */, + E0DCD3A820A64E96000B614E /* LanczosResampler.cxx in Sources */, DC676A531729A0B000E4E73D /* CartFA2Widget.cxx in Sources */, DC676A551729A0B000E4E73D /* CartFEWidget.cxx in Sources */, DC676A591729A0B000E4E73D /* CartSBWidget.cxx in Sources */, diff --git a/src/windows/SettingsWINDOWS.cxx b/src/windows/SettingsWINDOWS.cxx index 7820b0937..432509b3f 100644 --- a/src/windows/SettingsWINDOWS.cxx +++ b/src/windows/SettingsWINDOWS.cxx @@ -20,6 +20,4 @@ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - SettingsWINDOWS::SettingsWINDOWS(OSystem& osystem) : Settings(osystem) -{ - setInternal("fragsize", "1024"); -} +{} diff --git a/src/windows/Stella.vcxproj b/src/windows/Stella.vcxproj index 9f49220f1..9de68a454 100644 --- a/src/windows/Stella.vcxproj +++ b/src/windows/Stella.vcxproj @@ -228,6 +228,10 @@ + + + + @@ -323,10 +327,15 @@ + + + + + @@ -410,7 +419,6 @@ - @@ -511,6 +519,11 @@ + + + + + @@ -620,12 +633,17 @@ + + + + + @@ -733,7 +751,6 @@ - diff --git a/src/windows/Stella.vcxproj.filters b/src/windows/Stella.vcxproj.filters index 511d26624..4c27c863a 100644 --- a/src/windows/Stella.vcxproj.filters +++ b/src/windows/Stella.vcxproj.filters @@ -55,6 +55,12 @@ {ffa3642d-aa8a-43a5-8ac5-acd8878dd091} + + {49d8ea64-20c1-45f1-9dc9-b39c17d7cabd} + + + {000e4a6b-8cd6-43db-8253-8255c7efa706} + @@ -237,9 +243,6 @@ Source Files\emucore - - Source Files\emucore - Source Files\cheat @@ -900,6 +903,33 @@ Source Files + + Source Files\emucore\tia + + + Source Files\emucore\tia + + + Source Files + + + Source Files\emucore + + + Source Files\audio + + + Source Files\audio + + + Source Files\audio + + + Source Files\emucore + + + Source Files\emucore + @@ -1112,9 +1142,6 @@ Header Files\emucore - - Header Files\emucore - Header Files\debugger @@ -1844,6 +1871,36 @@ Header Files + + Header Files\emucore\tia + + + Header Files\emucore\tia + + + Header Files + + + Header Files\emucore + + + Header Files\audio + + + Header Files\audio + + + Header Files\audio + + + Header Files\audio + + + Header Files\emucore + + + Header Files\emucore +