diff --git a/src/core/frontend/input.h b/src/core/frontend/input.h
index 277b70e53b..fb2ce2514c 100644
--- a/src/core/frontend/input.h
+++ b/src/core/frontend/input.h
@@ -33,7 +33,7 @@ public:
     virtual bool GetAnalogDirectionStatus(AnalogDirection direction) const {
         return {};
     }
-    virtual bool SetRumblePlay(f32 amp_high, f32 amp_low, f32 freq_high, f32 freq_low) const {
+    virtual bool SetRumblePlay(f32 amp_low, f32 freq_low, f32 amp_high, f32 freq_high) const {
         return {};
     }
 };
diff --git a/src/core/hle/service/hid/controllers/npad.cpp b/src/core/hle/service/hid/controllers/npad.cpp
index 924f209c0e..cc54b164dc 100644
--- a/src/core/hle/service/hid/controllers/npad.cpp
+++ b/src/core/hle/service/hid/controllers/npad.cpp
@@ -698,16 +698,57 @@ void Controller_NPad::VibrateController(const std::vector<DeviceHandle>& vibrati
             continue;
         }
 
+        // Filter out vibrations with equivalent values to reduce unnecessary state changes.
+        if (vibration_values[i].amp_low ==
+                latest_vibration_values[npad_index][device_index].amp_low &&
+            vibration_values[i].amp_high ==
+                latest_vibration_values[npad_index][device_index].amp_high) {
+            continue;
+        }
+
+        // Filter out non-zero vibrations that are within 0.015625 absolute amplitude of each other.
+        if ((vibration_values[i].amp_low != 0.0f || vibration_values[i].amp_high != 0.0f) &&
+            (latest_vibration_values[npad_index][device_index].amp_low != 0.0f ||
+             latest_vibration_values[npad_index][device_index].amp_high != 0.0f) &&
+            (abs(vibration_values[i].amp_low -
+                 latest_vibration_values[npad_index][device_index].amp_low) < 0.015625f &&
+             abs(vibration_values[i].amp_high -
+                 latest_vibration_values[npad_index][device_index].amp_high) < 0.015625f)) {
+            continue;
+        }
+
         using namespace Settings::NativeButton;
         const auto& button_state = buttons[npad_index];
 
         // TODO: Vibrate left/right vibration motors independently if possible.
-        button_state[A - BUTTON_HID_BEGIN]->SetRumblePlay(
-            vibration_values[i].amp_high * Settings::values.vibration_strength.GetValue() / 100,
+        const bool success = button_state[A - BUTTON_HID_BEGIN]->SetRumblePlay(
             vibration_values[i].amp_low * Settings::values.vibration_strength.GetValue() / 100,
-            vibration_values[i].freq_high, vibration_values[i].freq_low);
+            vibration_values[i].freq_low,
+            vibration_values[i].amp_high * Settings::values.vibration_strength.GetValue() / 100,
+            vibration_values[i].freq_high);
 
-        latest_vibration_values[npad_index][device_index] = vibration_values[i];
+        if (success) {
+            switch (connected_controllers[npad_index].type) {
+            case NPadControllerType::None:
+                UNREACHABLE();
+                break;
+            case NPadControllerType::ProController:
+            case NPadControllerType::Handheld:
+            case NPadControllerType::JoyDual:
+                // Since we can't vibrate motors independently yet, we can reduce state changes by
+                // assigning all 3 device indices the current vibration value.
+                latest_vibration_values[npad_index][0] = vibration_values[i];
+                latest_vibration_values[npad_index][1] = vibration_values[i];
+                latest_vibration_values[npad_index][2] = vibration_values[i];
+                break;
+            case NPadControllerType::JoyLeft:
+            case NPadControllerType::JoyRight:
+            case NPadControllerType::Pokeball:
+            default:
+                latest_vibration_values[npad_index][device_index] = vibration_values[i];
+                break;
+            }
+        }
     }
 }
 
diff --git a/src/input_common/sdl/sdl_impl.cpp b/src/input_common/sdl/sdl_impl.cpp
index 10883e2d9e..18fb2ac5e2 100644
--- a/src/input_common/sdl/sdl_impl.cpp
+++ b/src/input_common/sdl/sdl_impl.cpp
@@ -80,30 +80,24 @@ public:
         return static_cast<float>(state.axes.at(axis)) / (32767.0f * range);
     }
 
-    bool RumblePlay(f32 amp_low, f32 amp_high, u32 time) {
-        const u16 raw_amp_low = static_cast<u16>(amp_low * 0xFFFF);
-        const u16 raw_amp_high = static_cast<u16>(amp_high * 0xFFFF);
-        // Lower drastically the number of state changes
-        if (raw_amp_low >> 11 == last_state_rumble_low >> 11 &&
-            raw_amp_high >> 11 == last_state_rumble_high >> 11) {
-            if (raw_amp_low + raw_amp_high != 0 ||
-                last_state_rumble_low + last_state_rumble_high == 0) {
-                return false;
-            }
-        }
-        // Don't change state if last vibration was < 20ms
-        const auto now = std::chrono::system_clock::now();
-        if (std::chrono::duration_cast<std::chrono::milliseconds>(now - last_vibration) <
-            std::chrono::milliseconds(20)) {
-            return raw_amp_low + raw_amp_high == 0;
+    bool RumblePlay(u16 amp_low, u16 amp_high) {
+        using std::chrono::duration_cast;
+        using std::chrono::milliseconds;
+        using std::chrono::steady_clock;
+
+        // Prevent vibrations less than 10ms apart from each other.
+        if (duration_cast<milliseconds>(steady_clock::now() - last_vibration) < milliseconds(10)) {
+            return false;
+        };
+
+        last_vibration = steady_clock::now();
+
+        if (sdl_controller != nullptr) {
+            return SDL_GameControllerRumble(sdl_controller.get(), amp_low, amp_high, 0) == 0;
+        } else if (sdl_joystick != nullptr) {
+            return SDL_JoystickRumble(sdl_joystick.get(), amp_low, amp_high, 0) == 0;
         }
 
-        last_vibration = now;
-        last_state_rumble_low = raw_amp_low;
-        last_state_rumble_high = raw_amp_high;
-        if (sdl_joystick) {
-            SDL_JoystickRumble(sdl_joystick.get(), raw_amp_low, raw_amp_high, time);
-        }
         return false;
     }
 
@@ -172,13 +166,13 @@ private:
     } state;
     std::string guid;
     int port;
-    u16 last_state_rumble_high = 0;
-    u16 last_state_rumble_low = 0;
-    std::chrono::time_point<std::chrono::system_clock> last_vibration;
     std::unique_ptr<SDL_Joystick, decltype(&SDL_JoystickClose)> sdl_joystick;
     std::unique_ptr<SDL_GameController, decltype(&SDL_GameControllerClose)> sdl_controller;
     mutable std::mutex mutex;
 
+    // This is the timepoint of the last vibration and is used to ensure vibrations are 10ms apart.
+    std::chrono::steady_clock::time_point last_vibration;
+
     // Motion is initialized without PID values as motion input is not aviable for SDL2
     MotionInput motion{0.0f, 0.0f, 0.0f};
 };
@@ -327,10 +321,12 @@ public:
         return joystick->GetButton(button);
     }
 
-    bool SetRumblePlay(f32 amp_high, f32 amp_low, f32 freq_high, f32 freq_low) const override {
-        const f32 new_amp_low = pow(amp_low, 0.5f) * (3.0f - 2.0f * pow(amp_low, 0.15f));
-        const f32 new_amp_high = pow(amp_high, 0.5f) * (3.0f - 2.0f * pow(amp_high, 0.15f));
-        return joystick->RumblePlay(new_amp_low, new_amp_high, 250);
+    bool SetRumblePlay(f32 amp_low, f32 freq_low, f32 amp_high, f32 freq_high) const override {
+        const u16 processed_amp_low =
+            static_cast<u16>(pow(amp_low, 0.5f) * (3.0f - 2.0f * pow(amp_low, 0.15f)) * 0xFFFF);
+        const u16 processed_amp_high =
+            static_cast<u16>(pow(amp_high, 0.5f) * (3.0f - 2.0f * pow(amp_high, 0.15f)) * 0xFFFF);
+        return joystick->RumblePlay(processed_amp_low, processed_amp_high);
     }
 
 private: