diff --git a/assets b/assets
index 118b62295..c1cea2051 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 118b622953171aaf127cb160538e21bc468620e2
+Subproject commit c1cea20513dfa93e3e74a0db98498b2fd8da50fc
diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx
index 54b66605c..dcf7f9f0d 100644
--- a/source/funkin/save/Save.hx
+++ b/source/funkin/save/Save.hx
@@ -1,16 +1,19 @@
 package funkin.save;
 
 import flixel.util.FlxSave;
-import funkin.save.migrator.SaveDataMigrator;
-import thx.semver.Version;
 import funkin.Controls.Device;
 import funkin.save.migrator.RawSaveData_v1_0_0;
+import funkin.save.migrator.SaveDataMigrator;
+import funkin.ui.debug.charting.ChartEditorState.LiveInputStyle;
+import funkin.ui.debug.charting.ChartEditorThemeHandler.ChartEditorTheme;
+import thx.semver.Version;
 
 @:nullSafety
 @:forward(volume, mute)
 abstract Save(RawSaveData)
 {
-  public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.0";
+  // Version 2.0.1 adds attributes to `optionsChartEditor`, that should return default values if they are null.
+  public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.1";
   public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x";
 
   // We load this version's saves from a new save path, to maintain SOME level of backwards compatibility.
@@ -94,6 +97,18 @@ abstract Save(RawSaveData)
         optionsChartEditor:
           {
             // Reasonable defaults.
+            previousFiles: [],
+            noteQuant: 3,
+            liveInputStyle: LiveInputStyle.None,
+            theme: ChartEditorTheme.Light,
+            playtestStartTime: false,
+            downscroll: false,
+            metronomeEnabled: true,
+            hitsoundsEnabledPlayer: true,
+            hitsoundsEnabledOpponent: true,
+            instVolume: 1.0,
+            voicesVolume: 1.0,
+            playbackSpeed: 1.0,
           },
       };
   }
@@ -124,7 +139,9 @@ abstract Save(RawSaveData)
 
   function set_ngSessionId(value:Null<String>):Null<String>
   {
-    return this.api.newgrounds.sessionId = value;
+    this.api.newgrounds.sessionId = value;
+    flush();
+    return this.api.newgrounds.sessionId;
   }
 
   public var enabledModIds(get, set):Array<String>;
@@ -136,7 +153,213 @@ abstract Save(RawSaveData)
 
   function set_enabledModIds(value:Array<String>):Array<String>
   {
-    return this.mods.enabledMods = value;
+    this.mods.enabledMods = value;
+    flush();
+    return this.mods.enabledMods;
+  }
+
+  public var chartEditorPreviousFiles(get, set):Array<String>;
+
+  function get_chartEditorPreviousFiles():Array<String>
+  {
+    if (this.optionsChartEditor.previousFiles == null) this.optionsChartEditor.previousFiles = [];
+
+    return this.optionsChartEditor.previousFiles;
+  }
+
+  function set_chartEditorPreviousFiles(value:Array<String>):Array<String>
+  {
+    // Set and apply.
+    this.optionsChartEditor.previousFiles = value;
+    flush();
+    return this.optionsChartEditor.previousFiles;
+  }
+
+  public var chartEditorNoteQuant(get, set):Int;
+
+  function get_chartEditorNoteQuant():Int
+  {
+    if (this.optionsChartEditor.noteQuant == null) this.optionsChartEditor.noteQuant = 3;
+
+    return this.optionsChartEditor.noteQuant;
+  }
+
+  function set_chartEditorNoteQuant(value:Int):Int
+  {
+    // Set and apply.
+    this.optionsChartEditor.noteQuant = value;
+    flush();
+    return this.optionsChartEditor.noteQuant;
+  }
+
+  public var chartEditorLiveInputStyle(get, set):LiveInputStyle;
+
+  function get_chartEditorLiveInputStyle():LiveInputStyle
+  {
+    if (this.optionsChartEditor.liveInputStyle == null) this.optionsChartEditor.liveInputStyle = LiveInputStyle.None;
+
+    return this.optionsChartEditor.liveInputStyle;
+  }
+
+  function set_chartEditorLiveInputStyle(value:LiveInputStyle):LiveInputStyle
+  {
+    // Set and apply.
+    this.optionsChartEditor.liveInputStyle = value;
+    flush();
+    return this.optionsChartEditor.liveInputStyle;
+  }
+
+  public var chartEditorDownscroll(get, set):Bool;
+
+  function get_chartEditorDownscroll():Bool
+  {
+    if (this.optionsChartEditor.downscroll == null) this.optionsChartEditor.downscroll = false;
+
+    return this.optionsChartEditor.downscroll;
+  }
+
+  function set_chartEditorDownscroll(value:Bool):Bool
+  {
+    // Set and apply.
+    this.optionsChartEditor.downscroll = value;
+    flush();
+    return this.optionsChartEditor.downscroll;
+  }
+
+  public var chartEditorPlaytestStartTime(get, set):Bool;
+
+  function get_chartEditorPlaytestStartTime():Bool
+  {
+    if (this.optionsChartEditor.playtestStartTime == null) this.optionsChartEditor.playtestStartTime = false;
+
+    return this.optionsChartEditor.playtestStartTime;
+  }
+
+  function set_chartEditorPlaytestStartTime(value:Bool):Bool
+  {
+    // Set and apply.
+    this.optionsChartEditor.playtestStartTime = value;
+    flush();
+    return this.optionsChartEditor.playtestStartTime;
+  }
+
+  public var chartEditorTheme(get, set):ChartEditorTheme;
+
+  function get_chartEditorTheme():ChartEditorTheme
+  {
+    if (this.optionsChartEditor.theme == null) this.optionsChartEditor.theme = ChartEditorTheme.Light;
+
+    return this.optionsChartEditor.theme;
+  }
+
+  function set_chartEditorTheme(value:ChartEditorTheme):ChartEditorTheme
+  {
+    // Set and apply.
+    this.optionsChartEditor.theme = value;
+    flush();
+    return this.optionsChartEditor.theme;
+  }
+
+  public var chartEditorMetronomeEnabled(get, set):Bool;
+
+  function get_chartEditorMetronomeEnabled():Bool
+  {
+    if (this.optionsChartEditor.metronomeEnabled == null) this.optionsChartEditor.metronomeEnabled = true;
+
+    return this.optionsChartEditor.metronomeEnabled;
+  }
+
+  function set_chartEditorMetronomeEnabled(value:Bool):Bool
+  {
+    // Set and apply.
+    this.optionsChartEditor.metronomeEnabled = value;
+    flush();
+    return this.optionsChartEditor.metronomeEnabled;
+  }
+
+  public var chartEditorHitsoundsEnabledPlayer(get, set):Bool;
+
+  function get_chartEditorHitsoundsEnabledPlayer():Bool
+  {
+    if (this.optionsChartEditor.hitsoundsEnabledPlayer == null) this.optionsChartEditor.hitsoundsEnabledPlayer = true;
+
+    return this.optionsChartEditor.hitsoundsEnabledPlayer;
+  }
+
+  function set_chartEditorHitsoundsEnabledPlayer(value:Bool):Bool
+  {
+    // Set and apply.
+    this.optionsChartEditor.hitsoundsEnabledPlayer = value;
+    flush();
+    return this.optionsChartEditor.hitsoundsEnabledPlayer;
+  }
+
+  public var chartEditorHitsoundsEnabledOpponent(get, set):Bool;
+
+  function get_chartEditorHitsoundsEnabledOpponent():Bool
+  {
+    if (this.optionsChartEditor.hitsoundsEnabledOpponent == null) this.optionsChartEditor.hitsoundsEnabledOpponent = true;
+
+    return this.optionsChartEditor.hitsoundsEnabledOpponent;
+  }
+
+  function set_chartEditorHitsoundsEnabledOpponent(value:Bool):Bool
+  {
+    // Set and apply.
+    this.optionsChartEditor.hitsoundsEnabledOpponent = value;
+    flush();
+    return this.optionsChartEditor.hitsoundsEnabledOpponent;
+  }
+
+  public var chartEditorInstVolume(get, set):Float;
+
+  function get_chartEditorInstVolume():Float
+  {
+    if (this.optionsChartEditor.instVolume == null) this.optionsChartEditor.instVolume = 1.0;
+
+    return this.optionsChartEditor.instVolume;
+  }
+
+  function set_chartEditorInstVolume(value:Float):Float
+  {
+    // Set and apply.
+    this.optionsChartEditor.instVolume = value;
+    flush();
+    return this.optionsChartEditor.instVolume;
+  }
+
+  public var chartEditorVoicesVolume(get, set):Float;
+
+  function get_chartEditorVoicesVolume():Float
+  {
+    if (this.optionsChartEditor.voicesVolume == null) this.optionsChartEditor.voicesVolume = 1.0;
+
+    return this.optionsChartEditor.voicesVolume;
+  }
+
+  function set_chartEditorVoicesVolume(value:Float):Float
+  {
+    // Set and apply.
+    this.optionsChartEditor.voicesVolume = value;
+    flush();
+    return this.optionsChartEditor.voicesVolume;
+  }
+
+  public var chartEditorPlaybackSpeed(get, set):Float;
+
+  function get_chartEditorPlaybackSpeed():Float
+  {
+    if (this.optionsChartEditor.playbackSpeed == null) this.optionsChartEditor.playbackSpeed = 1.0;
+
+    return this.optionsChartEditor.playbackSpeed;
+  }
+
+  function set_chartEditorPlaybackSpeed(value:Float):Float
+  {
+    // Set and apply.
+    this.optionsChartEditor.playbackSpeed = value;
+    flush();
+    return this.optionsChartEditor.playbackSpeed;
   }
 
   /**
@@ -699,4 +922,77 @@ typedef SaveControlsData =
 /**
  * An anonymous structure containing all the user's options and preferences, specific to the Chart Editor.
  */
-typedef SaveDataChartEditorOptions = {};
+typedef SaveDataChartEditorOptions =
+{
+  /**
+   * Previous files opened in the Chart Editor.
+   * @default `[]`
+   */
+  var ?previousFiles:Array<String>;
+
+  /**
+   * Note snapping level in the Chart Editor.
+   * @default `3`
+   */
+  var ?noteQuant:Int;
+
+  /**
+   * Live input style in the Chart Editor.
+   * @default `LiveInputStyle.None`
+   */
+  var ?liveInputStyle:LiveInputStyle;
+
+  /**
+   * Theme in the Chart Editor.
+   * @default `ChartEditorTheme.Light`
+   */
+  var ?theme:ChartEditorTheme;
+
+  /**
+   * Downscroll in the Chart Editor.
+   * @default `false`
+   */
+  var ?downscroll:Bool;
+
+  /**
+   * Metronome sounds in the Chart Editor.
+   * @default `true`
+   */
+  var ?metronomeEnabled:Bool;
+
+  /**
+   * If true, playtest songs from the current position in the Chart Editor.
+   * @default `false`
+   */
+  var ?playtestStartTime:Bool;
+
+  /**
+   * Player note hit sounds in the Chart Editor.
+   * @default `true`
+   */
+  var ?hitsoundsEnabledPlayer:Bool;
+
+  /**
+   * Opponent note hit sounds in the Chart Editor.
+   * @default `true`
+   */
+  var ?hitsoundsEnabledOpponent:Bool;
+
+  /**
+   * Instrumental volume in the Chart Editor.
+   * @default `1.0`
+   */
+  var ?instVolume:Float;
+
+  /**
+   * Voices volume in the Chart Editor.
+   * @default `1.0`
+   */
+  var ?voicesVolume:Float;
+
+  /**
+   * Playback speed in the Chart Editor.
+   * @default `1.0`
+   */
+  var ?playbackSpeed:Float;
+};
diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
index dd5ddb06c..dd874577a 100644
--- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
@@ -84,11 +84,47 @@ class ChartEditorDialogHandler
     var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_WELCOME_LAYOUT, true, closable);
     if (dialog == null) throw 'Could not locate Welcome dialog';
 
+    state.isHaxeUIDialogOpen = true;
     dialog.onDialogClosed = function(_event) {
+      state.isHaxeUIDialogOpen = false;
       // Called when the Welcome dialog is closed while it is closable.
       state.stopWelcomeMusic();
     }
 
+    #if sys
+    var splashRecentContainer:Null<VBox> = dialog.findComponent('splashRecentContainer', VBox);
+    if (splashRecentContainer == null) throw 'Could not locate splashRecentContainer in Welcome dialog';
+
+    for (chartPath in state.previousWorkingFilePaths)
+    {
+      var linkRecentChart:Link = new FunkinLink();
+      linkRecentChart.text = chartPath;
+      linkRecentChart.onClick = function(_event) {
+        dialog.hideDialog(DialogButton.CANCEL);
+        state.stopWelcomeMusic();
+
+        // Load chart from file
+        ChartEditorImportExportHandler.loadFromFNFCPath(state, chartPath);
+      }
+
+      if (!FileUtil.doesFileExist(chartPath))
+      {
+        trace('Previously loaded chart file (${chartPath}) does not exist, disabling link...');
+        linkRecentChart.disabled = true;
+      }
+
+      splashRecentContainer.addComponent(linkRecentChart);
+    }
+    #else
+    var splashRecentContainer:Null<VBox> = dialog.findComponent('splashRecentContainer', VBox);
+    if (splashRecentContainer == null) throw 'Could not locate splashRecentContainer in Welcome dialog';
+
+    var webLoadLabel:Label = new Label();
+    webLoadLabel.text = 'Click the button below to load a chart file (.fnfc) from your computer.';
+
+    splashRecentContainer.add(webLoadLabel);
+    #end
+
     // Create New Song "Easy/Normal/Hard"
     var linkCreateBasic:Null<Link> = dialog.findComponent('splashCreateFromSongBasic', Link);
     if (linkCreateBasic == null) throw 'Could not locate splashCreateFromSongBasic link in Welcome dialog';
@@ -180,6 +216,7 @@ class ChartEditorDialogHandler
     if (dialog == null) throw 'Could not locate Upload Chart dialog';
 
     dialog.onDialogClosed = function(_event) {
+      state.isHaxeUIDialogOpen = false;
       if (_event.button == DialogButton.APPLY)
       {
         // Simply let the dialog close.
@@ -194,6 +231,7 @@ class ChartEditorDialogHandler
     var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
     if (buttonCancel == null) throw 'Could not locate dialogCancel button in Upload Chart dialog';
 
+    state.isHaxeUIDialogOpen = true;
     buttonCancel.onClick = function(_event) {
       dialog.hideDialog(DialogButton.CANCEL);
     }
@@ -232,6 +270,10 @@ class ChartEditorDialogHandler
                   });
                 #end
 
+                trace(selectedFile.name);
+                trace(selectedFile.text);
+                trace(selectedFile.isBinary);
+                trace(selectedFile.fullPath);
                 if (selectedFile.fullPath != null) state.currentWorkingFilePath = selectedFile.fullPath;
                 dialog.hideDialog(DialogButton.APPLY);
                 removeDropHandler(onDropFile);
@@ -689,7 +731,9 @@ class ChartEditorDialogHandler
 
     var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
     if (buttonCancel == null) throw 'Could not locate dialogCancel button in Song Metadata dialog';
+    state.isHaxeUIDialogOpen = true;
     buttonCancel.onClick = function(_event) {
+      state.isHaxeUIDialogOpen = false;
       dialog.hideDialog(DialogButton.CANCEL);
     }
 
@@ -1411,7 +1455,9 @@ class ChartEditorDialogHandler
     var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
     if (buttonCancel == null) throw 'Could not locate dialogCancel button in Import Chart dialog';
 
+    state.isHaxeUIDialogOpen = true;
     buttonCancel.onClick = function(_event) {
+      state.isHaxeUIDialogOpen = false;
       dialog.hideDialog(DialogButton.CANCEL);
     }
 
@@ -1596,7 +1642,9 @@ class ChartEditorDialogHandler
 
     // If all validators succeeded, this callback is called.
 
+    state.isHaxeUIDialogOpen = true;
     variationForm.onSubmit = function(_event) {
+      state.isHaxeUIDialogOpen = false;
       trace('Add Variation dialog submitted, validation succeeded!');
 
       var dialogVariationName:Null<TextField> = dialog.findComponent('dialogVariationName', TextField);
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index d392c2c06..831afe738 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -1,25 +1,18 @@
 package funkin.ui.debug.charting;
 
-import funkin.play.stage.StageData;
-import funkin.play.character.CharacterData.CharacterDataParser;
-import funkin.play.character.CharacterData;
-import flixel.system.FlxAssets.FlxSoundAsset;
-import flixel.math.FlxMath;
-import haxe.ui.components.TextField;
-import haxe.ui.components.DropDown;
-import haxe.ui.components.NumberStepper;
-import haxe.ui.containers.Frame;
 import flixel.addons.display.FlxSliceSprite;
 import flixel.addons.display.FlxTiledSprite;
+import flixel.addons.transition.FlxTransitionableState;
 import flixel.FlxCamera;
 import flixel.FlxSprite;
 import flixel.FlxSubState;
 import flixel.group.FlxSpriteGroup;
-import flixel.addons.transition.FlxTransitionableState;
 import flixel.input.keyboard.FlxKey;
+import flixel.math.FlxMath;
 import flixel.math.FlxPoint;
 import flixel.math.FlxRect;
 import flixel.sound.FlxSound;
+import flixel.system.FlxAssets.FlxSoundAsset;
 import flixel.tweens.FlxEase;
 import flixel.tweens.FlxTween;
 import flixel.tweens.misc.VarTween;
@@ -29,28 +22,31 @@ import flixel.util.FlxTimer;
 import funkin.audio.visualize.PolygonSpectogram;
 import funkin.audio.VoicesGroup;
 import funkin.data.notestyle.NoteStyleRegistry;
-import funkin.data.notestyle.NoteStyleRegistry;
+import funkin.data.song.SongData.SongCharacterData;
+import funkin.data.song.SongData.SongChartData;
+import funkin.data.song.SongData.SongEventData;
+import funkin.data.song.SongData.SongMetadata;
+import funkin.data.song.SongData.SongNoteData;
+import funkin.data.song.SongDataUtils;
+import funkin.data.song.SongRegistry;
 import funkin.input.Cursor;
 import funkin.input.TurboKeyHandler;
 import funkin.modding.events.ScriptEvent;
 import funkin.play.character.BaseCharacter.CharacterType;
+import funkin.play.character.CharacterData;
+import funkin.play.character.CharacterData.CharacterDataParser;
 import funkin.play.HealthIcon;
 import funkin.play.notes.NoteSprite;
 import funkin.play.notes.Strumline;
 import funkin.play.PlayState;
 import funkin.play.song.Song;
-import funkin.data.song.SongData.SongChartData;
-import funkin.data.song.SongRegistry;
-import funkin.data.song.SongData.SongEventData;
-import funkin.data.song.SongData.SongMetadata;
-import funkin.data.song.SongData.SongNoteData;
-import funkin.data.song.SongData.SongCharacterData;
-import funkin.data.song.SongDataUtils;
-import funkin.ui.debug.charting.ChartEditorCommand;
+import funkin.play.stage.StageData;
+import funkin.save.Save;
 import funkin.ui.debug.charting.ChartEditorCommand;
 import funkin.ui.debug.charting.ChartEditorThemeHandler.ChartEditorTheme;
 import funkin.ui.debug.charting.ChartEditorToolboxHandler.ChartEditorToolMode;
 import funkin.ui.haxeui.components.CharacterPlayer;
+import funkin.ui.haxeui.components.FunkinMenuItem;
 import funkin.ui.haxeui.HaxeUIState;
 import funkin.util.Constants;
 import funkin.util.DateUtil;
@@ -61,10 +57,14 @@ import funkin.util.WindowUtil;
 import haxe.DynamicAccess;
 import haxe.io.Bytes;
 import haxe.io.Path;
+import haxe.ui.components.DropDown;
 import haxe.ui.components.Label;
+import haxe.ui.components.NumberStepper;
 import haxe.ui.components.Slider;
+import haxe.ui.components.TextField;
 import haxe.ui.containers.dialogs.CollapsibleDialog;
-import haxe.ui.containers.menus.MenuItem;
+import haxe.ui.containers.Frame;
+import haxe.ui.containers.menus.Menu;
 import haxe.ui.containers.TreeView;
 import haxe.ui.containers.TreeViewNode;
 import haxe.ui.core.Component;
@@ -594,27 +594,6 @@ class ChartEditorState extends HaxeUIState
     return selectedCharacter;
   }
 
-  /**
-   * Whether the user is currently in Pattern Mode.
-   * This overrides the chart editor's normal behavior.
-   */
-  var isInPatternMode(default, set):Bool = false;
-
-  function set_isInPatternMode(value:Bool):Bool
-  {
-    isInPatternMode = value;
-
-    // Make sure view is updated when we change modes.
-    noteDisplayDirty = true;
-    notePreviewDirty = true;
-    notePreviewViewportBoundsDirty = true;
-    this.scrollPositionInPixels = 0;
-
-    return isInPatternMode;
-  }
-
-  var currentPattern:String = '';
-
   /**
    * Whether the note display render group has been modified and needs to be updated.
    * This happens when we scroll or add/remove notes, and need to update what notes are displayed and where.
@@ -1183,6 +1162,11 @@ class ChartEditorState extends HaxeUIState
    */
   var playbarHeadLayout:Null<Component> = null;
 
+  /**
+   * The submenu in the menubar containing recently opened files.
+   */
+  var menubarOpenRecent:Null<Menu> = null;
+
   /**
    * The playbar head slider.
    */
@@ -1237,10 +1221,50 @@ class ChartEditorState extends HaxeUIState
    */
   var params:Null<ChartEditorParams>;
 
+  /**
+   * A list of previous working file paths.
+   * Also known as the "recent files" list.
+   */
+  public var previousWorkingFilePaths:Array<String> = [];
+
   /**
    * The current file path which the chart editor is working with.
    */
-  public var currentWorkingFilePath:Null<String>;
+  public var currentWorkingFilePath(get, set):Null<String>;
+
+  function get_currentWorkingFilePath():Null<String>
+  {
+    return previousWorkingFilePaths[0];
+  }
+
+  function set_currentWorkingFilePath(value:Null<String>):Null<String>
+  {
+    if (value == null) return null;
+
+    if (value == previousWorkingFilePaths[0]) return value;
+
+    if (previousWorkingFilePaths.contains(value))
+    {
+      // Move the path to the front of the list.
+      previousWorkingFilePaths.remove(value);
+      previousWorkingFilePaths.unshift(value);
+    }
+    else
+    {
+      // Add the path to the front of the list.
+      previousWorkingFilePaths.unshift(value);
+    }
+
+    while (previousWorkingFilePaths.length > Constants.MAX_PREVIOUS_WORKING_FILES)
+    {
+      // Remove the oldest path.
+      previousWorkingFilePaths.pop();
+    }
+
+    populateOpenRecentMenu();
+
+    return value;
+  }
 
   public function new(?params:ChartEditorParams)
   {
@@ -1260,6 +1284,8 @@ class ChartEditorState extends HaxeUIState
     // Show the mouse cursor.
     Cursor.show();
 
+    loadPreferences();
+
     fixCamera();
 
     // Get rid of any music from the previous state.
@@ -1280,6 +1306,7 @@ class ChartEditorState extends HaxeUIState
     buildSelectionBox();
 
     buildAdditionalUI();
+    populateOpenRecentMenu();
 
     // Setup the onClick listeners for the UI after it's been created.
     setupUIListeners();
@@ -1322,8 +1349,80 @@ class ChartEditorState extends HaxeUIState
   {
     this.welcomeMusic.loadEmbedded(Paths.music('chartEditorLoop/chartEditorLoop'));
     this.welcomeMusic.looped = true;
-    // this.welcomeMusic.play();
-    // fadeInWelcomeMusic();
+  }
+
+  public function loadPreferences():Void
+  {
+    var save:Save = Save.get();
+
+    previousWorkingFilePaths = save.chartEditorPreviousFiles;
+    noteSnapQuantIndex = save.chartEditorNoteQuant;
+    currentLiveInputStyle = save.chartEditorLiveInputStyle;
+    isViewDownscroll = save.chartEditorDownscroll;
+    playtestStartTime = save.chartEditorPlaytestStartTime;
+    currentTheme = save.chartEditorTheme;
+    isMetronomeEnabled = save.chartEditorMetronomeEnabled;
+    hitsoundsEnabledPlayer = save.chartEditorHitsoundsEnabledPlayer;
+    hitsoundsEnabledOpponent = save.chartEditorHitsoundsEnabledOpponent;
+
+    // audioInstTrack.volume = save.chartEditorInstVolume;
+    // audioInstTrack.pitch = save.chartEditorPlaybackSpeed;
+    // audioVocalTrackGroup.volume = save.chartEditorVoicesVolume;
+    // audioVocalTrackGroup.pitch = save.chartEditorPlaybackSpeed;
+  }
+
+  public function writePreferences():Void
+  {
+    var save:Save = Save.get();
+
+    save.chartEditorPreviousFiles = previousWorkingFilePaths;
+    save.chartEditorNoteQuant = noteSnapQuantIndex;
+    save.chartEditorLiveInputStyle = currentLiveInputStyle;
+    save.chartEditorDownscroll = isViewDownscroll;
+    save.chartEditorPlaytestStartTime = playtestStartTime;
+    save.chartEditorTheme = currentTheme;
+    save.chartEditorMetronomeEnabled = isMetronomeEnabled;
+    save.chartEditorHitsoundsEnabledPlayer = hitsoundsEnabledPlayer;
+    save.chartEditorHitsoundsEnabledOpponent = hitsoundsEnabledOpponent;
+
+    // save.chartEditorInstVolume = audioInstTrack.volume;
+    // save.chartEditorVoicesVolume = audioVocalTrackGroup.volume;
+    // save.chartEditorPlaybackSpeed = audioInstTrack.pitch;
+  }
+
+  public function populateOpenRecentMenu():Void
+  {
+    if (menubarOpenRecent == null) return;
+
+    #if sys
+    menubarOpenRecent.clear();
+
+    for (chartPath in previousWorkingFilePaths)
+    {
+      var menuItemRecentChart:FunkinMenuItem = new FunkinMenuItem();
+      menuItemRecentChart.text = chartPath;
+      menuItemRecentChart.onClick = function(_event) {
+        stopWelcomeMusic();
+
+        // Load chart from file
+        ChartEditorImportExportHandler.loadFromFNFCPath(this, chartPath);
+      }
+
+      if (!FileUtil.doesFileExist(chartPath))
+      {
+        trace('Previously loaded chart file (${chartPath}) does not exist, disabling link...');
+        menuItemRecentChart.disabled = true;
+      }
+      else
+      {
+        menuItemRecentChart.disabled = false;
+      }
+
+      menubarOpenRecent.addComponent(menuItemRecentChart);
+    }
+    #else
+    menubarOpenRecent.hide();
+    #end
   }
 
   public function fadeInWelcomeMusic():Void
@@ -1540,7 +1639,10 @@ class ChartEditorState extends HaxeUIState
   function setNotePreviewViewportBounds(bounds:FlxRect = null):Void
   {
     if (notePreviewViewport == null)
-      throw 'ERROR: Tried to set note preview viewport bounds, but notePreviewViewport is null! Check ChartEditorThemeHandler.updateTheme().';
+    {
+      trace('[WARN] Tried to set note preview viewport bounds, but notePreviewViewport is null!');
+      return;
+    }
 
     if (bounds == null)
     {
@@ -1646,9 +1748,11 @@ class ChartEditorState extends HaxeUIState
 
     add(playbarHeadLayout);
 
+    menubarOpenRecent = findComponent('menubarOpenRecent', Menu);
+    if (menubarOpenRecent == null) throw "Could not find menubarOpenRecent!";
+
     // Setup notifications.
     @:privateAccess
-    // NotificationManager.GUTTER_SIZE = 56;
     NotificationManager.GUTTER_SIZE = 20;
   }
 
@@ -1886,10 +1990,13 @@ class ChartEditorState extends HaxeUIState
   {
     saveDataDirty = false;
 
-    // Auto-save the chart.
+    // Auto-save preferences.
+    writePreferences();
 
+    // Auto-save the chart.
     #if html5
     // Auto-save to local storage.
+    // TODO: Implement this.
     #else
     // Auto-save to temp file.
     ChartEditorImportExportHandler.exportAllSongData(this, true);
@@ -3835,7 +3942,7 @@ class ChartEditorState extends HaxeUIState
       commandHistoryDirty = false;
 
       // Update the Undo and Redo buttons.
-      var undoButton:Null<MenuItem> = findComponent('menubarItemUndo', MenuItem);
+      var undoButton:Null<FunkinMenuItem> = findComponent('menubarItemUndo', FunkinMenuItem);
 
       if (undoButton != null)
       {
@@ -3857,7 +3964,7 @@ class ChartEditorState extends HaxeUIState
         trace('undoButton is null');
       }
 
-      var redoButton:Null<MenuItem> = findComponent('menubarItemRedo', MenuItem);
+      var redoButton:Null<FunkinMenuItem> = findComponent('menubarItemRedo', FunkinMenuItem);
 
       if (redoButton != null)
       {
diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx
index edd95f946..d37505b4c 100644
--- a/source/funkin/util/Constants.hx
+++ b/source/funkin/util/Constants.hx
@@ -391,6 +391,11 @@ class Constants
    */
   public static final GHOST_TAPPING:Bool = false;
 
+  /**
+   * The maximum number of previous file paths for the Chart Editor to remember.
+   */
+  public static final MAX_PREVIOUS_WORKING_FILES:Int = 10;
+
   /**
    * The separator between an asset library and the asset path.
    */
diff --git a/source/funkin/util/FileUtil.hx b/source/funkin/util/FileUtil.hx
index 72c9c43f1..5fae983d4 100644
--- a/source/funkin/util/FileUtil.hx
+++ b/source/funkin/util/FileUtil.hx
@@ -344,13 +344,22 @@ class FileUtil
   public static function readBytesFromPath(path:String):Bytes
   {
     #if sys
-    if (!sys.FileSystem.exists(path)) return null;
+    if (!doesFileExist(path)) return null;
     return sys.io.File.getBytes(path);
     #else
     return null;
     #end
   }
 
+  public static function doesFileExist(path:String):Bool
+  {
+    #if sys
+    return sys.FileSystem.exists(path);
+    #else
+    return false;
+    #end
+  }
+
   /**
    * Browse for a file to read and execute a callback once we have a file reference.
    * Works great on HTML5 or desktop.
@@ -434,7 +443,7 @@ class FileUtil
       case Force:
         sys.io.File.saveContent(path, data);
       case Skip:
-        if (!sys.FileSystem.exists(path))
+        if (!doesFileExist(path))
         {
           sys.io.File.saveContent(path, data);
         }
@@ -443,7 +452,7 @@ class FileUtil
           throw 'File already exists: $path';
         }
       case Ask:
-        if (sys.FileSystem.exists(path))
+        if (doesFileExist(path))
         {
           // TODO: We don't have the technology to use native popups yet.
         }
@@ -475,7 +484,7 @@ class FileUtil
       case Force:
         sys.io.File.saveBytes(path, data);
       case Skip:
-        if (!sys.FileSystem.exists(path))
+        if (!doesFileExist(path))
         {
           sys.io.File.saveBytes(path, data);
         }
@@ -484,7 +493,7 @@ class FileUtil
           throw 'File already exists: $path';
         }
       case Ask:
-        if (sys.FileSystem.exists(path))
+        if (doesFileExist(path))
         {
           // TODO: We don't have the technology to use native popups yet.
         }
@@ -523,7 +532,7 @@ class FileUtil
   public static function createDirIfNotExists(dir:String):Void
   {
     #if sys
-    if (!sys.FileSystem.exists(dir))
+    if (!doesFileExist(dir))
     {
       sys.FileSystem.createDirectory(dir);
     }