From ee8734ce148cb364948edf79cdf1e3d8ec6e9b42 Mon Sep 17 00:00:00 2001 From: thrust26 Date: Sun, 6 Dec 2020 12:08:25 +0100 Subject: [PATCH] added sound to Time Machine playback fixed playback speed updated docs --- Changes.txt | 2 + docs/index.html | 92 +++++++++++++++++--------------- src/common/HighScoresManager.cxx | 17 +++--- src/common/HighScoresManager.hxx | 3 -- src/emucore/EventHandler.cxx | 1 - src/emucore/FrameBuffer.cxx | 22 ++++---- src/emucore/OSystem.cxx | 10 +++- src/emucore/tia/Audio.cxx | 50 ++++++++++++++--- src/emucore/tia/Audio.hxx | 4 ++ src/gui/WhatsNewDialog.cxx | 1 + 10 files changed, 128 insertions(+), 74 deletions(-) diff --git a/Changes.txt b/Changes.txt index 73f089554..171fa9160 100644 --- a/Changes.txt +++ b/Changes.txt @@ -28,6 +28,8 @@ * Added dynamic tooltips to most debugger items. + * Added sound to Time Machine playback. + * Increased sample size for CDFJ+. * Fixed autofire bug for trackball controllers. diff --git a/docs/index.html b/docs/index.html index 2cbe5c10d..05ab607e0 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1828,8 +1828,8 @@ Playback the Time Machine from current state (without sound, from the TM dialog only) - Shift - Shift + Space + Space Start/Stop playback (exist/enters the Time Machine dialog) @@ -2259,10 +2259,10 @@ Current timeShows the time of the currently selected state, relative to the first one 'Start/Stop' buttonStarts or stops the Time Machine - 'Exit' buttonExits the dialog and continues emulation, starting at current state + 'Exit' buttonExits the dialog and continues emulation from the current state 'Rewind All' buttonNavigates back to the begin of the timeline 'Rewind One' buttonNavigates back by one state - 'Playback' buttonStarts playback, starting at the current state (with sound disabled) + 'Playback' buttonStarts playback from the current state 'Unwind One' buttonNavigates forward by one state 'Unwind All' buttonNavigates forward to the end of the timeline 'Save All' buttonSaves all Time Machine states to disk @@ -2288,43 +2288,50 @@ High Scores - supported games) this has been done already.

+

To save a score, the High Score dialog can be opened by pressing 'Insert' + any time while a game is played. It will provide the current variation and + score and allow the user to add this as a new high score. Of course this + makes most sense when a game is over. Note: In multiplayer games, only the + score of the first player can be saved.

+

High Scores dialog:

- + + + + +
     + + + + + + + + + +
ItemDescription
Top rowDisplays the current game's name.
VariationBy default the current game's variation is + selected. By changing the variation, the high scores of other + variations can be reviewed.
High scores tableThis table displays up to ten high scores + for the current game variation. +
    +
  • Besides 'Rank' and 'Score' an optional special value (e.g. + 'Level', 'Wave' or 'Round') is displayed.
  • +
  • In the 'Name' column, the player's initials are displayed. + These can be entered when a new high score is added to the list.
  • +
  • 'Date' and 'Time' record when the high score was added.
  • +
  • The buttons at the right allow deleting individual high + scores from the list.
  • +
+
MD5/PropsDisplay the checksums of the current ROM and the + high score properties defined for it. This can be useful for + comparing and verifying high scores.
ResetResets all high scores of the currently selected + variation.
SaveSaves the updated high scores and closes the dialog. +
CancelCloses the dialog without saving.
-

This dialog can be opened by pressing 'Insert' any time while a game is - played. It will provide the current variation and score and allow the user - to add this as a new high score. Of course this makes most sense when a game - is over. Note: In multiplayer games, only the score of the first player can be - saved.

-

The dialog items are explained in the following two tables.

- - - - - - - - - - + +
ItemDescription
Top rowDisplays the current game's name.
VariationBy default the current game's variation is - selected. By changing the variation, the high scores of other - variations can be reviewed.
High scores tableThis table displays up to ten high scores - for the current game variation. -
    -
  • Besides 'Rank' and 'Score' an optional special value (e.g. - 'Level', 'Wave' or 'Round') is displayed.
  • -
  • In the 'Name' column, the player's initials are displayed. - These can be entered when a new high score is added to the list.
  • -
  • 'Date' and 'Time' record when the high score was added.
  • -
  • The buttons at the right allow deleting individual high - scores from the list.
  • -
-
MD5/PropsDisplay the checksums of the current ROM and the - high score properties defined for it. This can be useful for - comparing and verifying high scores.
ResetResets all high scores of the currently selected - variation.
SaveSaves the updated high scores and closes the dialog. -
CancelCloses the dialog without saving.
+

For details how to configure high scores definintions for a game see High Scores Properties.


@@ -3511,7 +3518,7 @@

User Interface Settings dialog (2 tabs):

- +


     @@ -3530,12 +3537,13 @@ - + - + - + +
    

diff --git a/src/common/HighScoresManager.cxx b/src/common/HighScoresManager.cxx index 6c16d589a..59946b29b 100644 --- a/src/common/HighScoresManager.cxx +++ b/src/common/HighScoresManager.cxx @@ -422,33 +422,28 @@ string HighScoresManager::md5Props() const bool HighScoresManager::scoreInvert() const { json jprops; + return scoreInvert(properties(jprops)); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Int32 HighScoresManager::special() const { + if(!myOSystem.hasConsole()) + return NO_VALUE; + json jprops; uInt16 addr = specialAddress(properties(jprops)); if (addr == DEFAULT_ADDRESS) return NO_VALUE; - return special(addr, specialBCD(jprops), specialZeroBased(jprops)); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Int32 HighScoresManager::special(uInt16 addr, bool varBCD, bool zeroBased) const -{ - if (!myOSystem.hasConsole()) - return NO_VALUE; - Int32 var = peek(addr); - if (varBCD) + if(specialBCD(jprops)) var = fromBCD(var); - var += zeroBased ? 1 : 0; + var += specialZeroBased(jprops) ? 1 : 0; return var; } diff --git a/src/common/HighScoresManager.hxx b/src/common/HighScoresManager.hxx index 19b1ee2f2..6768840c4 100644 --- a/src/common/HighScoresManager.hxx +++ b/src/common/HighScoresManager.hxx @@ -131,9 +131,6 @@ class HighScoresManager */ uInt32 numAddrBytes(Int32 digits, Int32 trailing) const; - - Int32 special(uInt16 addr, bool varBCD, bool zeroBased) const; - // Retrieve current values (using game's properties) Int32 numVariations() const; const string specialLabel() const; diff --git a/src/emucore/EventHandler.cxx b/src/emucore/EventHandler.cxx index d908cd82c..94ae7c82c 100644 --- a/src/emucore/EventHandler.cxx +++ b/src/emucore/EventHandler.cxx @@ -2388,7 +2388,6 @@ void EventHandler::enterPlayBackMode() { #ifdef GUI_SUPPORT setState(EventHandlerState::PLAYBACK); - myOSystem.sound().mute(true); // sound does not work in playback mode #endif } diff --git a/src/emucore/FrameBuffer.cxx b/src/emucore/FrameBuffer.cxx index 3ee961795..a1b1ba294 100644 --- a/src/emucore/FrameBuffer.cxx +++ b/src/emucore/FrameBuffer.cxx @@ -25,6 +25,7 @@ #include "Settings.hxx" #include "TIA.hxx" #include "Sound.hxx" +#include "AudioSettings.hxx" #include "MediaFactory.hxx" #include "FBSurface.hxx" @@ -440,23 +441,24 @@ void FrameBuffer::update(UpdateMode mode) case EventHandlerState::PLAYBACK: { static Int32 frames = 0; - RewindManager& r = myOSystem.state().rewindManager(); bool success = true; - Int64 frameCycles = 76 * std::max(myOSystem.console().tia().scanlinesLastFrame(), 240); if(--frames <= 0) { - r.unwindStates(1); - // get time between current and next state - uInt64 startCycles = r.getCurrentCycles(); + RewindManager& r = myOSystem.state().rewindManager(); + uInt64 prevCycles = r.getCurrentCycles(); + success = r.unwindStates(1); - // display larger state gaps faster - frames = std::sqrt((myOSystem.console().tia().cycles() - startCycles) / frameCycles); - if(success) - r.rewindStates(1); + Int64 frameCycles = 76 * std::max(myOSystem.console().tia().scanlinesLastFrame(), 240); + + // Use time between previous and current state + // to playback larger state gaps faster + frames = std::sqrt((r.getCurrentCycles() - prevCycles) / frameCycles); + + // TODO: test and verify with CS + //myOSystem.sound().mute(uInt32(frames) > myOSystem.audioSettings().headroom() / 2 + 1); } - redraw |= success; if(redraw) myTIASurface->render(); diff --git a/src/emucore/OSystem.cxx b/src/emucore/OSystem.cxx index 4ad8b035e..40271748c 100644 --- a/src/emucore/OSystem.cxx +++ b/src/emucore/OSystem.cxx @@ -839,7 +839,15 @@ void OSystem::mainLoop() if (myEventHandler->state() == EventHandlerState::EMULATION) // Dispatch emulation and render frame (if applicable) timesliceSeconds = dispatchEmulation(emulationWorker); - else { + else if(myEventHandler->state() == EventHandlerState::PLAYBACK) + { + // Playback at emulation speed + timesliceSeconds = static_cast(myConsole->tia().scanlinesLastFrame() * 76) / + static_cast(myConsole->emulationTiming().cyclesPerSecond()); + myFrameBuffer->update(); + } + else + { // Render the GUI with 60 Hz in all other modes timesliceSeconds = 1. / 60.; myFrameBuffer->update(); diff --git a/src/emucore/tia/Audio.cxx b/src/emucore/tia/Audio.cxx index 4ddb78179..bbf3e2071 100644 --- a/src/emucore/tia/Audio.cxx +++ b/src/emucore/tia/Audio.cxx @@ -86,16 +86,26 @@ void Audio::phase1() uInt8 sample0 = myChannel0.phase1(); uInt8 sample1 = myChannel1.phase1(); - if (!myAudioQueue) return; + addSample(sample0, sample1); +#ifdef GUI_SUPPORT + mySamples.push_back(sample0 | (sample1 << 4)); +#endif +} - if (myAudioQueue->isStereo()) { - myCurrentFragment[2*mySampleIndex] = myMixingTableIndividual[sample0]; - myCurrentFragment[2*mySampleIndex + 1] = myMixingTableIndividual[sample1]; - } else { +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Audio::addSample(uInt8 sample0, uInt8 sample1) +{ + 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()) { + if(++mySampleIndex == myAudioQueue->fragmentSize()) { mySampleIndex = 0; myCurrentFragment = myAudioQueue->enqueue(myCurrentFragment); } @@ -125,6 +135,16 @@ bool Audio::save(Serializer& out) const if (!myChannel0.save(out)) return false; if (!myChannel1.save(out)) return false; + #ifdef GUI_SUPPORT + out.putLong(uInt64(mySamples.size())); + out.putByteArray(mySamples.data(), mySamples.size()); + + // TODO: check if this improves sound of playback for larger state gaps + //out.putInt(mySampleIndex); + //out.putShortArray((uInt16*)myCurrentFragment, myAudioQueue->fragmentSize()); + + mySamples.clear(); + #endif } catch(...) { @@ -144,6 +164,24 @@ bool Audio::load(Serializer& in) if (!myChannel0.load(in)) return false; if (!myChannel1.load(in)) return false; + #ifdef GUI_SUPPORT + uInt64 sampleSize = in.getLong(); + unique_ptr samples = make_unique(sampleSize); + in.getByteArray(samples.get(), sampleSize); + + //mySampleIndex = in.getInt(); + //in.getShortArray((uInt16*)myCurrentFragment, myAudioQueue->fragmentSize()); + + // Feed all loaded samples into the audio queue + for(size_t i = 0; i < sampleSize; i++) + { + uInt8 sample = samples[i]; + uInt8 sample0 = sample & 0x0f; + uInt8 sample1 = sample >> 4; + + addSample(sample0, sample1); + } + #endif } catch(...) { diff --git a/src/emucore/tia/Audio.hxx b/src/emucore/tia/Audio.hxx index 5ab53ecae..d4158bf72 100644 --- a/src/emucore/tia/Audio.hxx +++ b/src/emucore/tia/Audio.hxx @@ -47,6 +47,7 @@ class Audio : public Serializable private: void phase1(); + void addSample(uInt8 sample0, uInt8 sample1); private: shared_ptr myAudioQueue; @@ -61,6 +62,9 @@ class Audio : public Serializable Int16* myCurrentFragment{nullptr}; uInt32 mySampleIndex{0}; + #ifdef GUI_SUPPORT + mutable ByteArray mySamples; + #endif private: Audio(const Audio&) = delete; diff --git a/src/gui/WhatsNewDialog.cxx b/src/gui/WhatsNewDialog.cxx index 07914b894..aac4171f2 100644 --- a/src/gui/WhatsNewDialog.cxx +++ b/src/gui/WhatsNewDialog.cxx @@ -53,6 +53,7 @@ WhatsNewDialog::WhatsNewDialog(OSystem& osystem, DialogContainer& parent, const add(ypos, "added wildcard support to launcher dialog filter"); add(ypos, "added option to search subdirectories in launcher"); add(ypos, "added tooltips to many UI items"); + add(ypos, "added sound to Time Machine playback"); add(ypos, "increased sample size for CDFJ+"); add(ypos, ELLIPSIS + " (for a complete list see 'docs/Changes.txt')"); #endif