diff --git a/docs/index.html b/docs/index.html index daab10049..df6d923cd 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4068,10 +4068,10 @@

Stella supports viewing images and ROM properties of the currently selected ROM in the ROM launcher. Image support is automatic, as long as your - image directory contains snapshots in the appropriate format. The label - (if existing) and the number of matching snapshots are displayed under the - current one. The mouse or the ROM Launcher hotkeys can be used to browse - multiple images or a ROM.

+ image directory contains any images in the appropriate format. The label (if + existing) and the number of matching images are displayed under the + current image. The mouse, the ROM Launcher hotkeys or the controller can be + used to browse multiple images of a ROM.

Notes:
  • The images can have PNG or JPG format.
  • diff --git a/docs/index_r77.html b/docs/index_r77.html index bf3a3a8a2..982c281c8 100644 --- a/docs/index_r77.html +++ b/docs/index_r77.html @@ -160,6 +160,16 @@ Button 2 or 6 close the menu without any action. + + Button 1 + Left + - + Display previous image + + + Button 1 + Right + - + Display next image + Button 2 SKILL P2 diff --git a/src/common/SoundSDL2.cxx b/src/common/SoundSDL2.cxx index e66799cc4..9e07564e3 100644 --- a/src/common/SoundSDL2.cxx +++ b/src/common/SoundSDL2.cxx @@ -74,11 +74,6 @@ SoundSDL2::~SoundSDL2() if(!myIsInitializedFlag) return; - if(myWavDevice) - { - SDL_CloseAudioDevice(myWavDevice); - SDL_FreeWAV(myWavBuffer); - } SDL_CloseAudioDevice(myDevice); SDL_QuitSubSystem(SDL_INIT_AUDIO); } @@ -165,7 +160,7 @@ void SoundSDL2::open(shared_ptr audioQueue, openDevice(); myEmulationTiming = emulationTiming; - myWavSpeed = 262 * 60 * 2. / myEmulationTiming->audioSampleRate(); + myWavHandler.setSpeed(262 * 60 * 2. / myEmulationTiming->audioSampleRate()); Logger::debug("SoundSDL2::open started ..."); @@ -214,11 +209,7 @@ void SoundSDL2::mute(bool state) { myAudioSettings.setEnabled(!state); if(state) - { - SDL_LockAudioDevice(myDevice); myVolumeFactor = 0; - SDL_UnlockAudioDevice(myDevice); - } else setVolume(myAudioSettings.volume()); } @@ -242,10 +233,10 @@ bool SoundSDL2::pause(bool state) const bool oldstate = SDL_GetAudioDeviceStatus(myDevice) == SDL_AUDIO_PAUSED; if(myIsInitializedFlag) + { SDL_PauseAudioDevice(myDevice, state ? 1 : 0); - if(myWavDevice) - SDL_PauseAudioDevice(myWavDevice, state ? 1 : 0); - + myWavHandler.pause(state); + } return oldstate; } @@ -255,10 +246,7 @@ void SoundSDL2::setVolume(uInt32 volume) if(myIsInitializedFlag && (volume <= 100)) { myAudioSettings.setVolume(volume); - - SDL_LockAudioDevice(myDevice); myVolumeFactor = myAudioSettings.enabled() ? static_cast(volume) / 100.F : 0; - SDL_UnlockAudioDevice(myDevice); } } @@ -336,15 +324,6 @@ string SoundSDL2::about() const return buf.str(); } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void SoundSDL2::processFragment(float* stream, uInt32 length) -{ - myResampler->fillFragment(stream, length); - - for(uInt32 i = 0; i < length; ++i) - stream[i] *= myVolumeFactor; -} - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void SoundSDL2::initResampler() { @@ -395,12 +374,21 @@ void SoundSDL2::initResampler() } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void SoundSDL2::callback(void* udata, uInt8* stream, int len) +void SoundSDL2::callback(void* object, uInt8* stream, int len) { - auto* self = static_cast(udata); + auto* self = static_cast(object); if(self->myAudioQueue) - self->processFragment(reinterpret_cast(stream), len >> 2); + { + // The stream is 32-bit float (even though this callback is 8-bits), since + // the resampler and TIA audio subsystem always generate float samples + auto* s = reinterpret_cast(stream); + const uInt32 length = len >> 2; + self->myResampler->fillFragment(s, length); + + for(uInt32 i = 0; i < length; ++i) // TODO - perhaps move into Resampler + s[i] *= SoundSDL2::myVolumeFactor; + } else SDL_memset(stream, 0, len); } @@ -409,128 +397,160 @@ void SoundSDL2::callback(void* udata, uInt8* stream, int len) bool SoundSDL2::playWav(const string& fileName, const uInt32 position, const uInt32 length) { - // Load WAV file - if(fileName != myWavFilename || myWavBuffer == nullptr) - { - if(myWavBuffer) - { - SDL_FreeWAV(myWavBuffer); - myWavBuffer = nullptr; - } - if(SDL_LoadWAV(fileName.c_str(), &myWavSpec, &myWavBuffer, &myWavLength) == nullptr) - return false; - // Set the callback function - myWavSpec.callback = wavCallback; - myWavSpec.userdata = nullptr; - //myWavSpec.samples = 4096; // decrease for smaller samples; - } - if(position > myWavLength) - return false; + const char* device = myDeviceId ? myDevices.at(myDeviceId).first.c_str() : nullptr; - myWavFilename = fileName; - - myWavLen = length - ? std::min(length, myWavLength - position) - : myWavLength; - myWavPos = myWavBuffer + position; - - // Open audio device - if(!myWavDevice) - { - const char* device = myDeviceId ? myDevices.at(myDeviceId).first.c_str() : nullptr; - - myWavDevice = SDL_OpenAudioDevice(device, 0, &myWavSpec, nullptr, 0); - if(!myWavDevice) - return false; - // Play audio - SDL_PauseAudioDevice(myWavDevice, 0); - } - return true; + return myWavHandler.play(fileName, device, position, length); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void SoundSDL2::stopWav() { - if(myWavBuffer) - { - // Clean up - myWavLen = 0; - SDL_CloseAudioDevice(myWavDevice); - myWavDevice = 0; - SDL_FreeWAV(myWavBuffer); - myWavBuffer = nullptr; - } - if(myWavCvtBuffer) - { - myWavCvtBuffer.reset(); - myWavCvtBufferSize = 0; - } + myWavHandler.stop(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - uInt32 SoundSDL2::wavSize() const { - return myWavBuffer ? myWavLen : 0; + return myWavHandler.size(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void SoundSDL2::wavCallback(void* udata, uInt8* stream, int len) +bool SoundSDL2::WavHandlerSDL2::play( + const string& fileName, const char* device, + const uInt32 position, const uInt32 length +) { - SDL_memset(stream, myWavSpec.silence, len); - if(myWavLen) + // Load WAV file + if(fileName != myFilename || myBuffer == nullptr) { - if(myWavSpeed != 1.0) + if(myBuffer) { - const int origLen = len; - len = std::round(len / myWavSpeed); - const int newFreq = - std::round(static_cast(myWavSpec.freq) * origLen / len); - - if(static_cast(len) > myWavLen) - len = myWavLen; - - SDL_AudioCVT cvt; - SDL_BuildAudioCVT(&cvt, myWavSpec.format, myWavSpec.channels, myWavSpec.freq, - myWavSpec.format, myWavSpec.channels, newFreq); - SDL_assert(cvt.needed); // Obviously, this one is always needed. - cvt.len = len * myWavSpec.channels; // Mono 8 bit sample frames - - if(!myWavCvtBuffer || - myWavCvtBufferSize < static_cast(cvt.len * cvt.len_mult)) - { - myWavCvtBufferSize = cvt.len * cvt.len_mult; - myWavCvtBuffer = make_unique(myWavCvtBufferSize); - } - cvt.buf = myWavCvtBuffer.get(); - - // Read original data into conversion buffer - SDL_memcpy(cvt.buf, myWavPos, cvt.len); - SDL_ConvertAudio(&cvt); - // Mix volume adjusted WAV data into silent buffer - SDL_MixAudioFormat(stream, cvt.buf, myWavSpec.format, cvt.len_cvt, - SDL_MIX_MAXVOLUME * myVolumeFactor); + SDL_FreeWAV(myBuffer); + myBuffer = nullptr; } - else - { - if(static_cast(len) > myWavLen) - len = myWavLen; + SDL_zero(mySpec); + if(SDL_LoadWAV(fileName.c_str(), &mySpec, &myBuffer, &myLength) == nullptr) + return false; - // Mix volume adjusted WAV data into silent buffer - SDL_MixAudioFormat(stream, myWavPos, myWavSpec.format, len, - SDL_MIX_MAXVOLUME * myVolumeFactor); - } - myWavPos += len; - myWavLen -= len; + // Set the callback function + mySpec.callback = callback; + mySpec.userdata = this; + } + if(position > myLength) + return false; + + myFilename = fileName; + + myRemaining = length + ? std::min(length, myLength - position) + : myLength; + myPos = myBuffer + position; + + // Open audio device + if(!myDevice) + { + myDevice = SDL_OpenAudioDevice(device, 0, &mySpec, nullptr, 0); + if(!myDevice) + return false; + // Play audio + SDL_PauseAudioDevice(myDevice, 0); + } + return true; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void SoundSDL2::WavHandlerSDL2::stop() +{ + if(myBuffer) + { + // Clean up + myRemaining = 0; + SDL_CloseAudioDevice(myDevice); myDevice = 0; + SDL_FreeWAV(myBuffer); myBuffer = nullptr; + } + if(myCvtBuffer) + { + myCvtBuffer.reset(); + myCvtBufferSize = 0; } } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void SoundSDL2::WavHandlerSDL2::processWav(uInt8* stream, uInt32 len) +{ + SDL_memset(stream, mySpec.silence, len); + if(myRemaining) + { + if(mySpeed != 1.0) + { + const int origLen = len; + len = std::round(len / mySpeed); + const int newFreq = + std::round(static_cast(mySpec.freq) * origLen / len); + + if(static_cast(len) > myRemaining) + len = myRemaining; + + SDL_AudioCVT cvt; + SDL_BuildAudioCVT(&cvt, mySpec.format, mySpec.channels, mySpec.freq, + mySpec.format, mySpec.channels, newFreq); + SDL_assert(cvt.needed); // Obviously, this one is always needed. + cvt.len = len * mySpec.channels; // Mono 8 bit sample frames + + if(!myCvtBuffer || + myCvtBufferSize < static_cast(cvt.len * cvt.len_mult)) + { + myCvtBufferSize = cvt.len * cvt.len_mult; + myCvtBuffer = make_unique(myCvtBufferSize); + } + cvt.buf = myCvtBuffer.get(); + + // Read original data into conversion buffer + SDL_memcpy(cvt.buf, myPos, cvt.len); + SDL_ConvertAudio(&cvt); + // Mix volume adjusted WAV data into silent buffer + SDL_MixAudioFormat(stream, cvt.buf, mySpec.format, cvt.len_cvt, + SDL_MIX_MAXVOLUME * SoundSDL2::myVolumeFactor); + } + else + { + if(static_cast(len) > myRemaining) + len = myRemaining; + + // Mix volume adjusted WAV data into silent buffer + SDL_MixAudioFormat(stream, myPos, mySpec.format, len, + SDL_MIX_MAXVOLUME * myVolumeFactor); + } + myPos += len; + myRemaining -= len; + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void SoundSDL2::WavHandlerSDL2::callback(void* object, uInt8* stream, int len) +{ + static_cast(object)->processWav( + stream, static_cast(len)); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +SoundSDL2::WavHandlerSDL2::~WavHandlerSDL2() +{ + if(myDevice) + { + SDL_CloseAudioDevice(myDevice); + SDL_FreeWAV(myBuffer); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void SoundSDL2::WavHandlerSDL2::pause(bool state) const +{ + if(myDevice) + SDL_PauseAudioDevice(myDevice, state ? 1 : 0); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - float SoundSDL2::myVolumeFactor = 0.F; -SDL_AudioSpec SoundSDL2::myWavSpec; // audio output format -uInt8* SoundSDL2::myWavPos = nullptr; // pointer to the audio buffer to be played -uInt32 SoundSDL2::myWavLen = 0; // remaining length of the sample we have to play -double SoundSDL2::myWavSpeed = 1.0; -unique_ptr SoundSDL2::myWavCvtBuffer; -uInt32 SoundSDL2::myWavCvtBufferSize = 0; #endif // SOUND_SUPPORT diff --git a/src/common/SoundSDL2.hxx b/src/common/SoundSDL2.hxx index 2753afb78..016d1a612 100644 --- a/src/common/SoundSDL2.hxx +++ b/src/common/SoundSDL2.hxx @@ -125,7 +125,7 @@ class SoundSDL2 : public Sound @param position The position to start playing @param length The played length - @return True, if the WAV file can be played + @return True if the WAV file can be played, else false */ bool playWav(const string& fileName, const uInt32 position = 0, const uInt32 length = 0) override; @@ -142,7 +142,7 @@ class SoundSDL2 : public Sound */ uInt32 wavSize() const override; - protected: + private: /** This method is called to query the audio devices. @@ -150,17 +150,6 @@ class SoundSDL2 : public Sound */ void queryHardware(VariantList& devices) override; - /** - Invoked by the sound callback to process the next sound fragment. - The stream is 16-bits (even though the callback is 8-bits), since - the TIASnd class always generates signed 16-bit stereo samples. - - @param stream Pointer to the start of the fragment - @param length Length of the fragment - */ - void processFragment(float* stream, uInt32 length); - - private: /** The actual sound device is opened only when absolutely necessary. Typically this will only happen once per program run, but it can also @@ -171,47 +160,75 @@ class SoundSDL2 : public Sound void initResampler(); private: + AudioSettings& myAudioSettings; + // Indicates if the sound device was successfully initialized bool myIsInitializedFlag{false}; // Audio specification structure SDL_AudioSpec myHardwareSpec; + SDL_AudioDeviceID myDevice{0}; uInt32 myDeviceId{0}; - SDL_AudioDeviceID myDevice{0}; - shared_ptr myAudioQueue; + unique_ptr myResampler; EmulationTiming* myEmulationTiming{nullptr}; Int16* myCurrentFragment{nullptr}; bool myUnderrun{false}; - unique_ptr myResampler; + string myAboutString; - AudioSettings& myAudioSettings; + /** + This class implements WAV file playback using the SDL2 sound API. + */ + class WavHandlerSDL2 + { + public: + explicit WavHandlerSDL2() = default; + ~WavHandlerSDL2(); - // WAV file sound variables - string myWavFilename; - uInt32 myWavLength{0}; - SDL_AudioDeviceID myWavDevice{0}; - uInt8* myWavBuffer{nullptr}; + bool play(const string& fileName, const char* device, + const uInt32 position, const uInt32 length); + void stop(); + uInt32 size() const { return myBuffer ? myRemaining : 0; } + + void setSpeed(const double speed) { mySpeed = speed; } + void pause(bool state) const; + + private: + string myFilename; + uInt32 myLength{0}; + SDL_AudioDeviceID myDevice{0}; + uInt8* myBuffer{nullptr}; + double mySpeed{1.0}; + unique_ptr myCvtBuffer; + uInt32 myCvtBufferSize{0}; + SDL_AudioSpec mySpec; // audio output format + uInt8* myPos{nullptr}; // pointer to the audio buffer to be played + uInt32 myRemaining{0}; // remaining length of the sample we have to play + + private: + // Callback function invoked by the SDL Audio library when it needs data + void processWav(uInt8* stream, uInt32 len); + static void callback(void* object, uInt8* stream, int len); + + // Following constructors and assignment operators not supported + WavHandlerSDL2(const WavHandlerSDL2&) = delete; + WavHandlerSDL2(WavHandlerSDL2&&) = delete; + WavHandlerSDL2& operator=(const WavHandlerSDL2&) = delete; + WavHandlerSDL2& operator=(WavHandlerSDL2&&) = delete; + }; + + WavHandlerSDL2 myWavHandler; static float myVolumeFactor; // Current volume level (0 - 100) - static double myWavSpeed; - static unique_ptr myWavCvtBuffer; - static uInt32 myWavCvtBufferSize; - static SDL_AudioSpec myWavSpec; // audio output format - static uInt8* myWavPos; // pointer to the audio buffer to be played - static uInt32 myWavLen; // remaining length of the sample we have to play - - string myAboutString; private: // Callback functions invoked by the SDL Audio library when it needs data - static void callback(void* udata, uInt8* stream, int len); - static void wavCallback(void* udata, uInt8* stream, int len); + static void callback(void* object, uInt8* stream, int len); // Following constructors and assignment operators not supported SoundSDL2() = delete; diff --git a/src/gui/DialogContainer.cxx b/src/gui/DialogContainer.cxx index 35e5cf601..cdca03d44 100644 --- a/src/gui/DialogContainer.cxx +++ b/src/gui/DialogContainer.cxx @@ -67,7 +67,7 @@ void DialogContainer::updateTime(uInt64 time) // Joystick has been pressed long if(myCurrentButtonDown.stick != -1 && myButtonLongPressTime < myTime) { - myButtonLongPress = true; + myIgnoreButtonUp = true; activeDialog->handleJoyDown(myCurrentButtonDown.stick, myCurrentButtonDown.button, true); myButtonLongPressTime = myButtonRepeatTime = myTime + _REPEAT_NONE; } @@ -75,8 +75,12 @@ void DialogContainer::updateTime(uInt64 time) // Joystick axis still pressed if(myCurrentAxisDown.stick != -1 && myAxisRepeatTime < myTime) { - activeDialog->handleJoyAxis(myCurrentAxisDown.stick, myCurrentAxisDown.axis, - myCurrentAxisDown.adir); + if(myCurrentButtonDown.stick == myCurrentAxisDown.stick) + activeDialog->handleJoyAxis(myCurrentAxisDown.stick, myCurrentAxisDown.axis, + myCurrentAxisDown.adir, myCurrentButtonDown.button); + else + activeDialog->handleJoyAxis(myCurrentAxisDown.stick, myCurrentAxisDown.axis, + myCurrentAxisDown.adir); myAxisRepeatTime = myTime + _REPEAT_SUSTAIN_DELAY; } @@ -323,14 +327,18 @@ void DialogContainer::handleJoyBtnEvent(int stick, int button, bool pressed) // Send the event to the dialog box on the top of the stack Dialog* activeDialog = myDialogStack.top(); - if(pressed && myButtonRepeatTime < myTime) // prevent pending repeats after enabling repeat again + if(pressed) { - myCurrentButtonDown.stick = stick; - myCurrentButtonDown.button = button; - myButtonRepeatTime = myTime + (activeDialog->repeatEnabled() ? _REPEAT_INITIAL_DELAY : _REPEAT_NONE); - myButtonLongPressTime = myTime + _LONG_PRESS_DELAY; + if(myButtonRepeatTime < myTime || // prevent pending repeats after enabling repeat again + myButtonRepeatTime + _REPEAT_INITIAL_DELAY > myTime) // ignore blocking delays + { + myCurrentButtonDown.stick = stick; + myCurrentButtonDown.button = button; + myButtonRepeatTime = myTime + (activeDialog->repeatEnabled() ? _REPEAT_INITIAL_DELAY : _REPEAT_NONE); + myButtonLongPressTime = myTime + _LONG_PRESS_DELAY; - activeDialog->handleJoyDown(stick, button); + activeDialog->handleJoyDown(stick, button); + } } else { @@ -340,8 +348,8 @@ void DialogContainer::handleJoyBtnEvent(int stick, int button, bool pressed) myCurrentButtonDown.stick = myCurrentButtonDown.button = -1; myButtonRepeatTime = myButtonLongPressTime = 0; } - if (myButtonLongPress) - myButtonLongPress = false; + if(myIgnoreButtonUp) + myIgnoreButtonUp = false; else activeDialog->handleJoyUp(stick, button); } @@ -356,7 +364,8 @@ void DialogContainer::handleJoyAxisEvent(int stick, JoyAxis axis, JoyDir adir, i // Send the event to the dialog box on the top of the stack Dialog* activeDialog = myDialogStack.top(); - // Prevent long button press in button/axis combinations + // Prevent button repeats and long button press in button/axis combinations + myButtonRepeatTime = myTime + _REPEAT_NONE; myButtonLongPressTime = myTime + _REPEAT_NONE; // Only stop firing events if it's the current stick @@ -374,6 +383,8 @@ void DialogContainer::handleJoyAxisEvent(int stick, JoyAxis axis, JoyDir adir, i myCurrentAxisDown.adir = adir; myAxisRepeatTime = myTime + (activeDialog->repeatEnabled() ? _REPEAT_INITIAL_DELAY : _REPEAT_NONE); } + if(adir != JoyDir::NONE) + myIgnoreButtonUp = true; // prevent button released events activeDialog->handleJoyAxis(stick, axis, adir, button); } @@ -386,7 +397,8 @@ void DialogContainer::handleJoyHatEvent(int stick, int hat, JoyHatDir hdir, int // Send the event to the dialog box on the top of the stack Dialog* activeDialog = myDialogStack.top(); - // Prevent long button press in button/hat combinations + // Prevent button repeats and long button press in button/hat combinations + myButtonRepeatTime = myTime + _REPEAT_NONE; myButtonLongPressTime = myTime + _REPEAT_NONE; // Only stop firing events if it's the current stick diff --git a/src/gui/DialogContainer.hxx b/src/gui/DialogContainer.hxx index 69c6f5d66..44ef0cf2d 100644 --- a/src/gui/DialogContainer.hxx +++ b/src/gui/DialogContainer.hxx @@ -214,7 +214,7 @@ class DialogContainer } myCurrentButtonDown; uInt64 myButtonRepeatTime{0}; uInt64 myButtonLongPressTime{0}; - bool myButtonLongPress{false}; + bool myIgnoreButtonUp{false}; // For continuous 'joy axis down' events struct { diff --git a/src/gui/LauncherDialog.cxx b/src/gui/LauncherDialog.cxx index 37f8c6b53..5ddcc8bd9 100644 --- a/src/gui/LauncherDialog.cxx +++ b/src/gui/LauncherDialog.cxx @@ -952,12 +952,13 @@ Event::Type LauncherDialog::getJoyAxisEvent(int stick, JoyAxis axis, JoyDir adir break; case Event::UITabPrev: - // TODO: check with controller, then update doc (R77 too) myRomImageWidget->changeImage(-1); + myEventHandled = true; break; case Event::UITabNext: myRomImageWidget->changeImage(1); + myEventHandled = true; break; default: @@ -1136,11 +1137,13 @@ void LauncherDialog::openContextMenu(int x, int y) { // Dynamically create context menu for ROM list options + bool addCancel = false; if(x < 0 || y < 0) { // Long pressed button, determine position from currently selected list item x = myList->getLeft() + myList->getWidth() / 2; y = myList->getTop() + (myList->getSelected() - myList->currentPos() + 1) * _font.getLineHeight(); + addCancel = true; } struct ContextItem { @@ -1219,6 +1222,8 @@ void LauncherDialog::openContextMenu(int x, int y) // items.push_back(ContextItem("Options" + ELLIPSIS, "Ctrl+O", "options")); //} } + if(addCancel) + items.push_back(ContextItem("Cancel", "")); // closes the context menu and does nothing // Format items for menu VariantList varItems;