diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index af3281c4b..4bd5b6a51 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -236,6 +236,11 @@ class PlayState extends MusicBeatSubState
    */
   public var cameraZoomTween:FlxTween;
 
+  /**
+   * An FlxTween that changes the additive speed to the desired amount.
+   */
+  public var scrollSpeedTweens:Array<FlxTween> = [];
+
   /**
    * The camera follow point from the last stage.
    * Used to persist the position of the `cameraFollowPosition` between levels.
@@ -822,6 +827,8 @@ class PlayState extends MusicBeatSubState
     {
       if (!assertChartExists()) return;
 
+      prevScrollTargets = [];
+
       dispatchEvent(new ScriptEvent(SONG_RETRY));
 
       resetCamera();
@@ -1204,6 +1211,15 @@ class PlayState extends MusicBeatSubState
         cameraTweensPausedBySubState.add(cameraZoomTween);
       }
 
+      for (tween in scrollSpeedTweens)
+      {
+        if (tween != null && tween.active)
+        {
+          tween.active = false;
+          cameraTweensPausedBySubState.add(tween);
+        }
+      }
+
       // Pause the countdown.
       Countdown.pauseCountdown();
     }
@@ -3045,8 +3061,9 @@ class PlayState extends MusicBeatSubState
     // Stop camera zooming on beat.
     cameraZoomRate = 0;
 
-    // Cancel camera tweening if it's active.
+    // Cancel camera and scroll tweening if it's active.
     cancelAllCameraTweens();
+    cancelScrollSpeedTweens();
 
     // If the opponent is GF, zoom in on the opponent.
     // Else, if there is no GF, zoom in on BF.
@@ -3268,6 +3285,60 @@ class PlayState extends MusicBeatSubState
     cancelCameraZoomTween();
   }
 
+  var prevScrollTargets:Array<Dynamic> = []; // used to snap scroll speed when things go unruely
+
+  /**
+   * The magical function that shall tween the scroll speed.
+   */
+  public function tweenScrollSpeed(?speed:Float, ?duration:Float, ?ease:Null<Float->Float>, strumlines:Array<String>):Void
+  {
+    // Cancel the current tween if it's active.
+    cancelScrollSpeedTweens();
+
+    // Snap to previous event value to prevent the tween breaking when another event cancels the previous tween.
+    for (i in prevScrollTargets)
+    {
+      var value:Float = i[0];
+      var strum:Strumline = Reflect.getProperty(this, i[1]);
+      strum.scrollSpeed = value;
+    }
+
+    // for next event, clean array.
+    prevScrollTargets = [];
+
+    for (i in strumlines)
+    {
+      var value:Float = speed;
+      var strum:Strumline = Reflect.getProperty(this, i);
+
+      if (duration == 0)
+      {
+        strum.scrollSpeed = value;
+      }
+      else
+      {
+        scrollSpeedTweens.push(FlxTween.tween(strum,
+          {
+            'scrollSpeed': value
+          }, duration, {ease: ease}));
+      }
+      // make sure charts dont break if the charter is dumb and stupid
+      prevScrollTargets.push([value, i]);
+    }
+  }
+
+  public function cancelScrollSpeedTweens()
+  {
+    for (tween in scrollSpeedTweens)
+    {
+      if (tween != null)
+      {
+        tween.cancel();
+      }
+    }
+    scrollSpeedTweens = [];
+  }
+
   #if (debug || FORCE_DEBUG_VERSION)
   /**
    * Jumps forward or backward a number of sections in the song.
diff --git a/source/funkin/play/event/ScrollSpeedEvent.hx b/source/funkin/play/event/ScrollSpeedEvent.hx
new file mode 100644
index 000000000..c752d2f6d
--- /dev/null
+++ b/source/funkin/play/event/ScrollSpeedEvent.hx
@@ -0,0 +1,176 @@
+package funkin.play.event;
+
+import flixel.tweens.FlxTween;
+import flixel.FlxCamera;
+import flixel.tweens.FlxEase;
+// Data from the chart
+import funkin.data.song.SongData;
+import funkin.data.song.SongData.SongEventData;
+// Data from the event schema
+import funkin.play.event.SongEvent;
+import funkin.data.event.SongEventSchema;
+import funkin.data.event.SongEventSchema.SongEventFieldType;
+
+/**
+ * This class represents a handler for scroll speed events.
+ *
+ * Example: Scroll speed change of both strums from 1x to 1.3x:
+ * ```
+ * {
+ *   'e': 'ScrollSpeed',
+ *   "v": {
+ *      "scroll": "1.3",
+ *      "duration": "4",
+ *      "ease": "linear",
+ *      "strumline": "both",
+ *      "absolute": false
+ *    }
+ * }
+ * ```
+ */
+class ScrollSpeedEvent extends SongEvent
+{
+  public function new()
+  {
+    super('ScrollSpeed');
+  }
+
+  static final DEFAULT_SCROLL:Float = 1;
+  static final DEFAULT_DURATION:Float = 4.0;
+  static final DEFAULT_EASE:String = 'linear';
+  static final DEFAULT_ABSOLUTE:Bool = false;
+  static final DEFAULT_STRUMLINE:String = 'both'; // my special little trick
+
+  public override function handleEvent(data:SongEventData):Void
+  {
+    // Does nothing if there is no PlayState.
+    if (PlayState.instance == null) return;
+
+    var scroll:Float = data.getFloat('scroll') ?? DEFAULT_SCROLL;
+
+    var duration:Float = data.getFloat('duration') ?? DEFAULT_DURATION;
+
+    var ease:String = data.getString('ease') ?? DEFAULT_EASE;
+
+    var strumline:String = data.getString('strumline') ?? DEFAULT_STRUMLINE;
+
+    var absolute:Bool = data.getBool('absolute') ?? DEFAULT_ABSOLUTE;
+
+    var strumlineNames:Array<String> = [];
+
+    if (!absolute)
+    {
+      // If absolute is set to false, do the awesome multiplicative thing
+      scroll = scroll * (PlayState.instance?.currentChart?.scrollSpeed ?? 1.0);
+    }
+
+    switch (strumline)
+    {
+      case 'both':
+        strumlineNames = ['playerStrumline', 'opponentStrumline'];
+      default:
+        strumlineNames = [strumline + 'Strumline'];
+    }
+    // If it's a string, check the value.
+    switch (ease)
+    {
+      case 'INSTANT':
+        PlayState.instance.tweenScrollSpeed(scroll, 0, null, strumlineNames);
+      default:
+        var durSeconds = Conductor.instance.stepLengthMs * duration / 1000;
+        var easeFunction:Null<Float->Float> = Reflect.field(FlxEase, ease);
+        if (easeFunction == null)
+        {
+          trace('Invalid ease function: $ease');
+          return;
+        }
+
+        PlayState.instance.tweenScrollSpeed(scroll, durSeconds, easeFunction, strumlineNames);
+    }
+  }
+
+  public override function getTitle():String
+  {
+    return 'Scroll Speed';
+  }
+
+  /**
+   * ```
+   * {
+   *   'scroll': FLOAT, // Target scroll level.
+   *   'duration': FLOAT, // Duration in steps.
+   *   'ease': ENUM, // Easing function.
+   *   'strumline': ENUM, // Which strumline to change
+   *   'absolute': BOOL, // True to set the scroll speed to the target level, false to set the scroll speed to (target level x base scroll speed)
+   * }
+   * @return SongEventSchema
+   */
+  public override function getEventSchema():SongEventSchema
+  {
+    return new SongEventSchema([
+      {
+        name: 'scroll',
+        title: 'Target Value',
+        defaultValue: 1.0,
+        step: 0.1,
+        type: SongEventFieldType.FLOAT,
+        units: 'x'
+      },
+      {
+        name: 'duration',
+        title: 'Duration',
+        defaultValue: 4.0,
+        step: 0.5,
+        type: SongEventFieldType.FLOAT,
+        units: 'steps'
+      },
+      {
+        name: 'ease',
+        title: 'Easing Type',
+        defaultValue: 'linear',
+        type: SongEventFieldType.ENUM,
+        keys: [
+          'Linear' => 'linear',
+          'Instant (Ignores Duration)' => 'INSTANT',
+          'Sine In' => 'sineIn',
+          'Sine Out' => 'sineOut',
+          'Sine In/Out' => 'sineInOut',
+          'Quad In' => 'quadIn',
+          'Quad Out' => 'quadOut',
+          'Quad In/Out' => 'quadInOut',
+          'Cube In' => 'cubeIn',
+          'Cube Out' => 'cubeOut',
+          'Cube In/Out' => 'cubeInOut',
+          'Quart In' => 'quartIn',
+          'Quart Out' => 'quartOut',
+          'Quart In/Out' => 'quartInOut',
+          'Quint In' => 'quintIn',
+          'Quint Out' => 'quintOut',
+          'Quint In/Out' => 'quintInOut',
+          'Expo In' => 'expoIn',
+          'Expo Out' => 'expoOut',
+          'Expo In/Out' => 'expoInOut',
+          'Smooth Step In' => 'smoothStepIn',
+          'Smooth Step Out' => 'smoothStepOut',
+          'Smooth Step In/Out' => 'smoothStepInOut',
+          'Elastic In' => 'elasticIn',
+          'Elastic Out' => 'elasticOut',
+          'Elastic In/Out' => 'elasticInOut'
+        ]
+      },
+      {
+        name: 'strumline',
+        title: 'Target Strumline',
+        defaultValue: 'both',
+        type: SongEventFieldType.ENUM,
+        keys: ['Both' => 'both', 'Player' => 'player', 'Opponent' => 'opponent']
+      },
+      {
+        name: 'absolute',
+        title: 'Absolute',
+        defaultValue: false,
+        type: SongEventFieldType.BOOL,
+      }
+    ]);
+  }
+}
diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx
index 472c38bba..0e4b6645f 100644
--- a/source/funkin/play/notes/Strumline.hx
+++ b/source/funkin/play/notes/Strumline.hx
@@ -52,6 +52,14 @@ class Strumline extends FlxSpriteGroup
    */
   public var conductorInUse(get, set):Conductor;
 
+  // Used in-game to control the scroll speed within a song
+  public var scrollSpeed:Float = 1.0;
+
+  public function resetScrollSpeed():Void
+  {
+    scrollSpeed = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0;
+  }
+
   var _conductorInUse:Null<Conductor>;
 
   function get_conductorInUse():Conductor
@@ -134,6 +142,7 @@ class Strumline extends FlxSpriteGroup
     this.refresh();
 
     this.onNoteIncoming = new FlxTypedSignal<NoteSprite->Void>();
+    resetScrollSpeed();
 
     for (i in 0...KEY_COUNT)
     {
@@ -297,7 +306,6 @@ class Strumline extends FlxSpriteGroup
     // var vwoosh:Float = (strumTime < Conductor.songPosition) && vwoosh ? 2.0 : 1.0;
     // ^^^ commented this out... do NOT make it move faster as it moves offscreen!
     var vwoosh:Float = 1.0;
-    var scrollSpeed:Float = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0;
 
     return
       Constants.PIXELS_PER_MS * (conductorInUse.songPosition - strumTime - Conductor.instance.inputOffset) * scrollSpeed * vwoosh * (Preferences.downscroll ? 1 : -1);
@@ -553,6 +561,7 @@ class Strumline extends FlxSpriteGroup
     {
       playStatic(dir);
     }
+    resetScrollSpeed();
   }
 
   public function applyNoteData(data:Array<SongNoteData>):Void
@@ -719,6 +728,7 @@ class Strumline extends FlxSpriteGroup
 
     if (holdNoteSprite != null)
     {
+      holdNoteSprite.parentStrumline = this;
       holdNoteSprite.noteData = note;
       holdNoteSprite.strumTime = note.time;
       holdNoteSprite.noteDirection = note.getDirection();
diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx
index 056a6a5a9..b358d7f03 100644
--- a/source/funkin/play/notes/SustainTrail.hx
+++ b/source/funkin/play/notes/SustainTrail.hx
@@ -32,6 +32,7 @@ class SustainTrail extends FlxSprite
   public var sustainLength(default, set):Float = 0; // millis
   public var fullSustainLength:Float = 0;
   public var noteData:Null<SongNoteData>;
+  public var parentStrumline:Strumline;
 
   public var cover:NoteHoldCover = null;
 
@@ -119,7 +120,7 @@ class SustainTrail extends FlxSprite
 
     // CALCULATE SIZE
     graphicWidth = graphic.width / 8 * zoom; // amount of notes * 2
-    graphicHeight = sustainHeight(sustainLength, getScrollSpeed());
+    graphicHeight = sustainHeight(sustainLength, parentStrumline?.scrollSpeed ?? 1.0);
     // instead of scrollSpeed, PlayState.SONG.speed
 
     flipY = Preferences.downscroll;
@@ -135,9 +136,21 @@ class SustainTrail extends FlxSprite
     this.active = true; // This NEEDS to be true for the note to be drawn!
   }
 
-  function getScrollSpeed():Float
+  function getBaseScrollSpeed()
   {
-    return PlayState?.instance?.currentChart?.scrollSpeed ?? 1.0;
+    return (PlayState.instance?.currentChart?.scrollSpeed ?? 1.0);
+  }
+
+  var previousScrollSpeed:Float = 1;
+
+  override function update(elapsed)
+  {
+    super.update(elapsed);
+    if (previousScrollSpeed != (parentStrumline?.scrollSpeed ?? 1.0))
+    {
+      triggerRedraw();
+    }
+    previousScrollSpeed = parentStrumline?.scrollSpeed ?? 1.0;
   }
 
   /**
@@ -155,12 +168,16 @@ class SustainTrail extends FlxSprite
     if (s < 0.0) s = 0.0;
 
     if (sustainLength == s) return s;
-
-    graphicHeight = sustainHeight(s, getScrollSpeed());
     this.sustainLength = s;
+    triggerRedraw();
+    return this.sustainLength;
+  }
+
+  function triggerRedraw()
+  {
+    graphicHeight = sustainHeight(sustainLength, parentStrumline?.scrollSpeed ?? 1.0);
     updateClipping();
     updateHitbox();
-    return this.sustainLength;
   }
 
   public override function updateHitbox():Void
@@ -178,7 +195,7 @@ class SustainTrail extends FlxSprite
    */
   public function updateClipping(songTime:Float = 0):Void
   {
-    var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), getScrollSpeed()), 0, graphicHeight);
+    var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), parentStrumline?.scrollSpeed ?? 1.0), 0, graphicHeight);
     if (clipHeight <= 0.1)
     {
       visible = false;
diff --git a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
index 7c20358a4..ded48abe3 100644
--- a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
+++ b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
@@ -60,11 +60,11 @@ class ChartEditorHoldNoteSprite extends SustainTrail
   {
     if (lerp)
     {
-      sustainLength = FlxMath.lerp(sustainLength, h / (getScrollSpeed() * Constants.PIXELS_PER_MS), 0.25);
+      sustainLength = FlxMath.lerp(sustainLength, h / (getBaseScrollSpeed() * Constants.PIXELS_PER_MS), 0.25);
     }
     else
     {
-      sustainLength = h / (getScrollSpeed() * Constants.PIXELS_PER_MS);
+      sustainLength = h / (getBaseScrollSpeed() * Constants.PIXELS_PER_MS);
     }
 
     fullSustainLength = sustainLength;