diff --git a/assets b/assets
index 946cf0082..cb7a863e4 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 946cf00829416b879881427d4e2fe09a09cb79ce
+Subproject commit cb7a863e4ab5a563828436be63c9f8cbbf09b4c5
diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx
index d557bd39c..9340e46c9 100644
--- a/source/funkin/data/song/SongData.hx
+++ b/source/funkin/data/song/SongData.hx
@@ -4,6 +4,7 @@ import flixel.util.typeLimit.OneOfTwo;
 import funkin.data.song.SongRegistry;
 import thx.semver.Version;
 
+@:nullSafety
 class SongMetadata
 {
   /**
@@ -42,7 +43,7 @@ class SongMetadata
   public var timeChanges:Array<SongTimeChange>;
 
   /**
-   * Defaults to `default` or `''`. Populated later.
+   * Defaults to `Constants.DEFAULT_VARIATION`. Populated later.
    */
   @:jignored
   public var variation:String;
@@ -228,10 +229,10 @@ class SongMusicData
   public var timeChanges:Array<SongTimeChange>;
 
   /**
-   * Defaults to `default` or `''`. Populated later.
+   * Defaults to `Constants.DEFAULT_VARIATION`. Populated later.
    */
   @:jignored
-  public var variation:String = Constants.DEFAULT_VARIATION;
+  public var variation:String;
 
   public function new(songName:String, artist:String, variation:String = 'default')
   {
@@ -375,6 +376,9 @@ class SongChartData
   @:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY)
   public var generatedBy:String;
 
+  /**
+   * Defaults to `Constants.DEFAULT_VARIATION`. Populated later.
+   */
   @:jignored
   public var variation:String;
 
diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx
index cf2da14f7..889fca707 100644
--- a/source/funkin/data/song/SongRegistry.hx
+++ b/source/funkin/data/song/SongRegistry.hx
@@ -156,7 +156,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
     return cleanMetadata(parser.value, variation);
   }
 
-  public function parseEntryMetadataWithMigration(id:String, ?variation:String, version:thx.semver.Version):Null<SongMetadata>
+  public function parseEntryMetadataWithMigration(id:String, variation:String, version:thx.semver.Version):Null<SongMetadata>
   {
     variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
 
@@ -192,7 +192,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
     }
   }
 
-  function parseEntryMetadata_v2_0_0(id:String, variation:String = ""):Null<SongMetadata>
+  function parseEntryMetadata_v2_0_0(id:String, ?variation:String):Null<SongMetadata>
   {
     variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
 
diff --git a/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx
index e852dff0a..b5a6f36be 100644
--- a/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx
@@ -1,11 +1,14 @@
 package funkin.ui.debug.charting;
 
-import openfl.utils.Assets;
 import flixel.system.FlxAssets.FlxSoundAsset;
 import flixel.system.FlxSound;
-import funkin.play.character.BaseCharacter.CharacterType;
 import flixel.system.FlxSound;
+import funkin.audio.VoicesGroup;
+import funkin.play.character.BaseCharacter.CharacterType;
+import funkin.util.FileUtil;
+import haxe.io.Bytes;
 import haxe.io.Path;
+import openfl.utils.Assets;
 
 /**
  * Functions for loading audio for the chart editor.
@@ -17,16 +20,18 @@ import haxe.io.Path;
 class ChartEditorAudioHandler
 {
   /**
-   * Loads a vocal track from an absolute file path.
+   * Loads and stores byte data for a vocal track from an absolute file path
+   *
    * @param path The absolute path to the audio file.
-   * @param charKey The character to load the vocal track for.
+   * @param charId The character this vocal track will be for.
+   * @param instId The instrumental this vocal track will be for.
    * @return Success or failure.
    */
-  static function loadVocalsFromPath(state:ChartEditorState, path:Path, charKey:String = 'default'):Bool
+  static function loadVocalsFromPath(state:ChartEditorState, path:Path, charId:String, instId:String = ''):Bool
   {
     #if sys
-    var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path.toString());
-    return loadVocalsFromBytes(state, fileBytes, charKey);
+    var fileBytes:Bytes = sys.io.File.getBytes(path.toString());
+    return loadVocalsFromBytes(state, fileBytes, charId, instId);
     #else
     trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
     return false;
@@ -34,137 +39,235 @@ class ChartEditorAudioHandler
   }
 
   /**
-   * Load a vocal track for a given song and character and add it to the voices group.
+   * Loads and stores byte data for a vocal track from an asset
    *
-   * @param path ID of the asset.
-   * @param charKey Character to load the vocal track for.
+   * @param path The path to the asset. Use `Paths` to build this.
+   * @param charId The character this vocal track will be for.
+   * @param instId The instrumental this vocal track will be for.
    * @return Success or failure.
    */
-  static function loadVocalsFromAsset(state:ChartEditorState, path:String, charType:CharacterType = OTHER):Bool
+  static function loadVocalsFromAsset(state:ChartEditorState, path:String, charId:String, instId:String = ''):Bool
   {
-    var vocalTrack:FlxSound = FlxG.sound.load(path, 1.0, false);
+    var trackData:Null<Bytes> = Assets.getBytes(path);
+    if (trackData != null)
+    {
+      return loadVocalsFromBytes(state, trackData, charId, instId);
+    }
+    return false;
+  }
+
+  /**
+   * Loads and stores byte data for a vocal track
+   *
+   * @param bytes The audio byte data.
+   * @param charId The character this vocal track will be for.
+   * @param instId The instrumental this vocal track will be for.
+   */
+  static function loadVocalsFromBytes(state:ChartEditorState, bytes:Bytes, charId:String, instId:String = ''):Bool
+  {
+    var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}';
+    state.audioVocalTrackData.set(trackId, bytes);
+    return true;
+  }
+
+  /**
+   * Loads and stores byte data for an instrumental track from an absolute file path
+   *
+   * @param path The absolute path to the audio file.
+   * @param instId The instrumental this vocal track will be for.
+   * @return Success or failure.
+   */
+  static function loadInstFromPath(state:ChartEditorState, path:Path, instId:String = ''):Bool
+  {
+    #if sys
+    var fileBytes:Bytes = sys.io.File.getBytes(path.toString());
+    return loadInstFromBytes(state, fileBytes, instId);
+    #else
+    trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
+    return false;
+    #end
+  }
+
+  /**
+   * Loads and stores byte data for an instrumental track from an asset
+   *
+   * @param path The path to the asset. Use `Paths` to build this.
+   * @param instId The instrumental this vocal track will be for.
+   * @return Success or failure.
+   */
+  static function loadInstFromAsset(state:ChartEditorState, path:String, instId:String = ''):Bool
+  {
+    var trackData:Null<Bytes> = Assets.getBytes(path);
+    if (trackData != null)
+    {
+      return loadInstFromBytes(state, trackData, instId);
+    }
+    return false;
+  }
+
+  /**
+   * Loads and stores byte data for a vocal track
+   *
+   * @param bytes The audio byte data.
+   * @param charId The character this vocal track will be for.
+   * @param instId The instrumental this vocal track will be for.
+   */
+  static function loadInstFromBytes(state:ChartEditorState, bytes:Bytes, instId:String = ''):Bool
+  {
+    if (instId == '') instId = 'default';
+    state.audioInstTrackData.set(instId, bytes);
+    return true;
+  }
+
+  public static function switchToInstrumental(state:ChartEditorState, instId:String = '', playerId:String, opponentId:String):Bool
+  {
+    var result:Bool = playInstrumental(state, instId);
+    if (!result) return false;
+
+    stopExistingVocals(state);
+    result = playVocals(state, BF, playerId, instId);
+    if (!result) return false;
+    result = playVocals(state, DAD, opponentId, instId);
+    if (!result) return false;
+
+    return true;
+  }
+
+  /**
+   * Tell the Chart Editor to select a specific instrumental track, that is already loaded.
+   */
+  static function playInstrumental(state:ChartEditorState, instId:String = ''):Bool
+  {
+    if (instId == '') instId = 'default';
+    var instTrackData:Null<Bytes> = state.audioInstTrackData.get(instId);
+    var instTrack:Null<FlxSound> = buildFlxSoundFromBytes(instTrackData);
+    if (instTrack == null) return false;
+
+    stopExistingInstrumental(state);
+    state.audioInstTrack = instTrack;
+    state.postLoadInstrumental();
+    return true;
+  }
+
+  static function stopExistingInstrumental(state:ChartEditorState):Void
+  {
+    if (state.audioInstTrack != null)
+    {
+      state.audioInstTrack.stop();
+      state.audioInstTrack.destroy();
+      state.audioInstTrack = null;
+    }
+  }
+
+  /**
+   * Tell the Chart Editor to select a specific vocal track, that is already loaded.
+   */
+  static function playVocals(state:ChartEditorState, charType:CharacterType, charId:String, instId:String = ''):Bool
+  {
+    var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}';
+    var vocalTrackData:Null<Bytes> = state.audioVocalTrackData.get(trackId);
+    var vocalTrack:Null<FlxSound> = buildFlxSoundFromBytes(vocalTrackData);
+
+    if (state.audioVocalTrackGroup == null) state.audioVocalTrackGroup = new VoicesGroup();
+
     if (vocalTrack != null)
     {
       switch (charType)
       {
-        case CharacterType.BF:
-          if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.addPlayerVoice(vocalTrack);
-          state.audioVocalTrackData.set(state.currentSongCharacterPlayer, Assets.getBytes(path));
-        case CharacterType.DAD:
-          if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.addOpponentVoice(vocalTrack);
-          state.audioVocalTrackData.set(state.currentSongCharacterOpponent, Assets.getBytes(path));
+        case BF:
+          state.audioVocalTrackGroup.addPlayerVoice(vocalTrack);
+          return true;
+        case DAD:
+          state.audioVocalTrackGroup.addOpponentVoice(vocalTrack);
+          return true;
+        case OTHER:
+          state.audioVocalTrackGroup.add(vocalTrack);
+          return true;
         default:
-          if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.add(vocalTrack);
-          state.audioVocalTrackData.set('default', Assets.getBytes(path));
+          // Do nothing.
       }
-
-      return true;
     }
     return false;
   }
 
-  /**
-   * Loads a vocal track from audio byte data.
-   */
-  static function loadVocalsFromBytes(state:ChartEditorState, bytes:haxe.io.Bytes, charKey:String = ''):Bool
+  static function stopExistingVocals(state:ChartEditorState):Void
   {
-    var openflSound:openfl.media.Sound = new openfl.media.Sound();
-    openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length);
-    var vocalTrack:FlxSound = FlxG.sound.load(openflSound, 1.0, false);
-    if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.add(vocalTrack);
-    state.audioVocalTrackData.set(charKey, bytes);
-    return true;
-  }
-
-  /**
-   * Loads an instrumental from an absolute file path, replacing the current instrumental.
-   *
-   * @param path The absolute path to the audio file.
-   *
-   * @return Success or failure.
-   */
-  static function loadInstrumentalFromPath(state:ChartEditorState, path:Path):Bool
-  {
-    #if sys
-    // Validate file extension.
-    if (path.ext != null && !ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext))
+    if (state.audioVocalTrackGroup != null)
     {
-      return false;
+      state.audioVocalTrackGroup.clear();
     }
-
-    var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path.toString());
-    return loadInstrumentalFromBytes(state, fileBytes, '${path.file}.${path.ext}');
-    #else
-    trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
-    return false;
-    #end
-  }
-
-  /**
-   * Loads an instrumental from audio byte data, replacing the current instrumental.
-   * @param bytes The audio byte data.
-   * @param fileName The name of the file, if available. Used for notifications.
-   * @return Success or failure.
-   */
-  static function loadInstrumentalFromBytes(state:ChartEditorState, bytes:haxe.io.Bytes, fileName:String = null):Bool
-  {
-    if (bytes == null)
-    {
-      return false;
-    }
-
-    var openflSound:openfl.media.Sound = new openfl.media.Sound();
-    openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length);
-    state.audioInstTrack = FlxG.sound.load(openflSound, 1.0, false);
-    state.audioInstTrack.autoDestroy = false;
-    state.audioInstTrack.pause();
-
-    state.audioInstTrackData = bytes;
-
-    state.postLoadInstrumental();
-
-    return true;
-  }
-
-  /**
-   * Loads an instrumental from an OpenFL asset, replacing the current instrumental.
-   * @param path The path to the asset. Use `Paths` to build this.
-   * @return Success or failure.
-   */
-  static function loadInstrumentalFromAsset(state:ChartEditorState, path:String):Bool
-  {
-    var instTrack:FlxSound = FlxG.sound.load(path, 1.0, false);
-    if (instTrack != null)
-    {
-      state.audioInstTrack = instTrack;
-
-      state.audioInstTrackData = Assets.getBytes(path);
-
-      state.postLoadInstrumental();
-      return true;
-    }
-
-    return false;
   }
 
   /**
    * Play a sound effect.
    * Automatically cleans up after itself and recycles previous FlxSound instances if available, for performance.
+   * @param path The path to the sound effect. Use `Paths` to build this.
    */
   public static function playSound(path:String):Void
   {
     var snd:FlxSound = FlxG.sound.list.recycle(FlxSound) ?? new FlxSound();
-
     var asset:Null<FlxSoundAsset> = FlxG.sound.cache(path);
     if (asset == null)
     {
       trace('WARN: Failed to play sound $path, asset not found.');
       return;
     }
-
     snd.loadEmbedded(asset);
     snd.autoDestroy = true;
     FlxG.sound.list.add(snd);
     snd.play();
   }
+
+  /**
+   * Convert byte data into a playable sound.
+   *
+   * @param input The byte data.
+   * @return The playable sound, or `null` if loading failed.
+   */
+  public static function buildFlxSoundFromBytes(input:Null<Bytes>):Null<FlxSound>
+  {
+    if (input == null) return null;
+
+    var openflSound:openfl.media.Sound = new openfl.media.Sound();
+    openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(input), input.length);
+    var output:FlxSound = FlxG.sound.load(openflSound, 1.0, false);
+    return output;
+  }
+
+  static function makeZIPEntriesFromInstrumentals(state:ChartEditorState):Array<haxe.zip.Entry>
+  {
+    var zipEntries = [];
+
+    for (key in state.audioInstTrackData.keys())
+    {
+      if (key == 'default')
+      {
+        var data:Null<Bytes> = state.audioInstTrackData.get('default');
+        if (data == null) continue;
+        zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', data));
+      }
+      else
+      {
+        var data:Null<Bytes> = state.audioInstTrackData.get(key);
+        if (data == null) continue;
+        zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst-${key}.ogg', data));
+      }
+    }
+
+    return zipEntries;
+  }
+
+  static function makeZIPEntriesFromVocals(state:ChartEditorState):Array<haxe.zip.Entry>
+  {
+    var zipEntries = [];
+
+    for (key in state.audioVocalTrackData.keys())
+    {
+      var data:Null<Bytes> = state.audioVocalTrackData.get(key);
+      if (data == null) continue;
+      zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-${key}.ogg', data));
+    }
+
+    return zipEntries;
+  }
 }
diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
index 736851d16..30f0381c6 100644
--- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
@@ -83,7 +83,7 @@ class ChartEditorDialogHandler
     var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_WELCOME_LAYOUT, true, closable);
     if (dialog == null) throw 'Could not locate Welcome dialog';
 
-    // Add handlers to the "Create From Song" section.
+    // 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';
     linkCreateBasic.onClick = function(_event) {
@@ -94,7 +94,20 @@ class ChartEditorDialogHandler
       //
       // Create Song Wizard
       //
-      openCreateSongWizard(state, false);
+      openCreateSongWizardBasic(state, false);
+    }
+
+    // Create New Song "Erect/Nightmare"
+    var linkCreateErect:Null<Link> = dialog.findComponent('splashCreateFromSongErect', Link);
+    if (linkCreateErect == null) throw 'Could not locate splashCreateFromSongErect link in Welcome dialog';
+    linkCreateErect.onClick = function(_event) {
+      // Hide the welcome dialog
+      dialog.hideDialog(DialogButton.CANCEL);
+
+      //
+      // Create Song Wizard
+      //
+      openCreateSongWizardErect(state, false);
     }
 
     var linkImportChartLegacy:Null<Link> = dialog.findComponent('splashImportChartLegacy', Link);
@@ -237,34 +250,112 @@ class ChartEditorDialogHandler
     };
   }
 
-  public static function openCreateSongWizard(state:ChartEditorState, closable:Bool):Void
+  public static function openCreateSongWizardBasic(state:ChartEditorState, closable:Bool):Void
   {
-    // Step 1. Upload Instrumental
-    var uploadInstDialog:Dialog = openUploadInstDialog(state, closable);
-    uploadInstDialog.onDialogClosed = function(_event) {
+    // Step 1. Song Metadata
+    var songMetadataDialog:Dialog = openSongMetadataDialog(state);
+    songMetadataDialog.onDialogClosed = function(_event) {
       state.isHaxeUIDialogOpen = false;
       if (_event.button == DialogButton.APPLY)
       {
-        // Step 2. Song Metadata
-        var songMetadataDialog:Dialog = openSongMetadataDialog(state);
-        songMetadataDialog.onDialogClosed = function(_event) {
+        // Step 2. Upload Instrumental
+        var uploadInstDialog:Dialog = openUploadInstDialog(state, closable);
+        uploadInstDialog.onDialogClosed = function(_event) {
           state.isHaxeUIDialogOpen = false;
           if (_event.button == DialogButton.APPLY)
           {
             // Step 3. Upload Vocals
             // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard.
-            openUploadVocalsDialog(state, false); // var uploadVocalsDialog:Dialog
+            var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog
+            uploadVocalsDialog.onDialogClosed = function(_event) {
+              state.isHaxeUIDialogOpen = false;
+              state.switchToCurrentInstrumental();
+              state.postLoadInstrumental();
+            }
           }
           else
           {
-            // User cancelled the wizard! Back to the welcome dialog.
+            // User cancelled the wizard at Step 2! Back to the welcome dialog.
             openWelcomeDialog(state);
           }
         };
       }
       else
       {
-        // User cancelled the wizard! Back to the welcome dialog.
+        // User cancelled the wizard at Step 1! Back to the welcome dialog.
+        openWelcomeDialog(state);
+      }
+    };
+  }
+
+  public static function openCreateSongWizardErect(state:ChartEditorState, closable:Bool):Void
+  {
+    // Step 1. Song Metadata
+    var songMetadataDialog:Dialog = openSongMetadataDialog(state);
+    songMetadataDialog.onDialogClosed = function(_event) {
+      state.isHaxeUIDialogOpen = false;
+      if (_event.button == DialogButton.APPLY)
+      {
+        // Step 2. Upload Instrumental
+        var uploadInstDialog:Dialog = openUploadInstDialog(state, closable);
+        uploadInstDialog.onDialogClosed = function(_event) {
+          state.isHaxeUIDialogOpen = false;
+          if (_event.button == DialogButton.APPLY)
+          {
+            // Step 3. Upload Vocals
+            // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard.
+            var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog
+            uploadVocalsDialog.onDialogClosed = function(_event) {
+              state.switchToCurrentInstrumental();
+              // Step 4. Song Metadata (Erect)
+              var songMetadataDialogErect:Dialog = openSongMetadataDialog(state, 'erect');
+              songMetadataDialogErect.onDialogClosed = function(_event) {
+                state.isHaxeUIDialogOpen = false;
+                if (_event.button == DialogButton.APPLY)
+                {
+                  // Switch to the Erect variation so uploading the instrumental applies properly.
+                  state.selectedVariation = 'erect';
+
+                  // Step 5. Upload Instrumental (Erect)
+                  var uploadInstDialogErect:Dialog = openUploadInstDialog(state, closable);
+                  uploadInstDialogErect.onDialogClosed = function(_event) {
+                    state.isHaxeUIDialogOpen = false;
+                    if (_event.button == DialogButton.APPLY)
+                    {
+                      // Step 6. Upload Vocals (Erect)
+                      // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard.
+                      var uploadVocalsDialogErect:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog
+                      uploadVocalsDialogErect.onDialogClosed = function(_event) {
+                        state.isHaxeUIDialogOpen = false;
+                        state.switchToCurrentInstrumental();
+                        state.postLoadInstrumental();
+                      }
+                    }
+                    else
+                    {
+                      // User cancelled the wizard at Step 5! Back to the welcome dialog.
+                      openWelcomeDialog(state);
+                    }
+                  };
+                }
+                else
+                {
+                  // User cancelled the wizard at Step 4! Back to the welcome dialog.
+                  openWelcomeDialog(state);
+                }
+              }
+            }
+          }
+          else
+          {
+            // User cancelled the wizard at Step 2! Back to the welcome dialog.
+            openWelcomeDialog(state);
+          }
+        };
+      }
+      else
+      {
+        // User cancelled the wizard at Step 1! Back to the welcome dialog.
         openWelcomeDialog(state);
       }
     };
@@ -302,6 +393,8 @@ class ChartEditorDialogHandler
       Cursor.cursorMode = Default;
     }
 
+    var instId:String = state.currentInstrumentalId;
+
     var onDropFile:String->Void;
 
     instrumentalBox.onClick = function(_event) {
@@ -309,14 +402,14 @@ class ChartEditorDialogHandler
         {label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile:SelectedFileInfo) {
           if (selectedFile != null && selectedFile.bytes != null)
           {
-            if (ChartEditorAudioHandler.loadInstrumentalFromBytes(state, selectedFile.bytes))
+            if (ChartEditorAudioHandler.loadInstFromBytes(state, selectedFile.bytes, instId))
             {
               trace('Selected file: ' + selectedFile.fullPath);
               #if !mac
               NotificationManager.instance.addNotification(
                 {
                   title: 'Success',
-                  body: 'Loaded instrumental track (${selectedFile.name})',
+                  body: 'Loaded instrumental track (${selectedFile.name}) for variation (${state.selectedVariation})',
                   type: NotificationType.Success,
                   expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
                 });
@@ -333,7 +426,7 @@ class ChartEditorDialogHandler
               NotificationManager.instance.addNotification(
                 {
                   title: 'Failure',
-                  body: 'Failed to load instrumental track (${selectedFile.name})',
+                  body: 'Failed to load instrumental track (${selectedFile.name}) for variation (${state.selectedVariation})',
                   type: NotificationType.Error,
                   expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
                 });
@@ -346,14 +439,14 @@ class ChartEditorDialogHandler
     onDropFile = function(pathStr:String) {
       var path:Path = new Path(pathStr);
       trace('Dropped file (${path})');
-      if (ChartEditorAudioHandler.loadInstrumentalFromPath(state, path))
+      if (ChartEditorAudioHandler.loadInstFromPath(state, path, instId))
       {
         // Tell the user the load was successful.
         #if !mac
         NotificationManager.instance.addNotification(
           {
             title: 'Success',
-            body: 'Loaded instrumental track (${path.file}.${path.ext})',
+            body: 'Loaded instrumental track (${path.file}.${path.ext}) for variation (${state.selectedVariation})',
             type: NotificationType.Success,
             expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
           });
@@ -370,7 +463,7 @@ class ChartEditorDialogHandler
         }
         else
         {
-          'Failed to load instrumental track (${path.file}.${path.ext})';
+          'Failed to load instrumental track (${path.file}.${path.ext}) for variation (${state.selectedVariation})';
         }
 
         // Tell the user the load was successful.
@@ -457,11 +550,18 @@ class ChartEditorDialogHandler
    * @return The dialog to open.
    */
   @:haxe.warning("-WVarInit")
-  public static function openSongMetadataDialog(state:ChartEditorState):Dialog
+  public static function openSongMetadataDialog(state:ChartEditorState, ?targetVariation:String):Dialog
   {
+    if (targetVariation == null) targetVariation = Constants.DEFAULT_VARIATION;
+
     var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT, true, false);
     if (dialog == null) throw 'Could not locate Song Metadata dialog';
 
+    if (targetVariation != Constants.DEFAULT_VARIATION)
+    {
+      dialog.title = 'New Chart - Provide Song Metadata (${targetVariation.toTitleCase()})';
+    }
+
     var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
     if (buttonCancel == null) throw 'Could not locate dialogCancel button in Song Metadata dialog';
     buttonCancel.onClick = function(_event) {
@@ -574,7 +674,11 @@ class ChartEditorDialogHandler
 
     var dialogContinue:Null<Button> = dialog.findComponent('dialogContinue', Button);
     if (dialogContinue == null) throw 'Could not locate dialogContinue button in Song Metadata dialog';
-    dialogContinue.onClick = (_event) -> dialog.hideDialog(DialogButton.APPLY);
+    dialogContinue.onClick = (_event) -> {
+      state.songMetadata.set(targetVariation, newSongMetadata);
+
+      dialog.hideDialog(DialogButton.APPLY);
+    }
 
     return dialog;
   }
@@ -587,6 +691,7 @@ class ChartEditorDialogHandler
    */
   public static function openUploadVocalsDialog(state:ChartEditorState, closable:Bool = true):Dialog
   {
+    var instId:String = state.currentInstrumentalId;
     var charIdsForVocals:Array<String> = [];
 
     var charData:SongCharacterData = state.currentSongMetadata.playData.characters;
@@ -633,14 +738,14 @@ class ChartEditorDialogHandler
         trace('Selected file: $pathStr');
         var path:Path = new Path(pathStr);
 
-        if (ChartEditorAudioHandler.loadVocalsFromPath(state, path, charKey))
+        if (ChartEditorAudioHandler.loadVocalsFromPath(state, path, charKey, instId))
         {
           // Tell the user the load was successful.
           #if !mac
           NotificationManager.instance.addNotification(
             {
               title: 'Success',
-              body: 'Loaded vocal track for $charName (${path.file}.${path.ext})',
+              body: 'Loaded vocals for $charName (${path.file}.${path.ext}), variation ${state.selectedVariation}',
               type: NotificationType.Success,
               expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
             });
@@ -656,21 +761,14 @@ class ChartEditorDialogHandler
         }
         else
         {
-          var message:String = if (!ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext ?? ''))
-          {
-            'File format (${path.ext}) not supported for vocal track (${path.file}.${path.ext})';
-          }
-          else
-          {
-            'Failed to load vocal track (${path.file}.${path.ext})';
-          }
+          trace('Failed to load vocal track (${path.file}.${path.ext})');
 
           // Vocals failed to load.
           #if !mac
           NotificationManager.instance.addNotification(
             {
               title: 'Failure',
-              body: message,
+              body: 'Failed to load vocal track (${path.file}.${path.ext}) for variation (${state.selectedVariation})',
               type: NotificationType.Error,
               expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
             });
@@ -690,14 +788,46 @@ class ChartEditorDialogHandler
             if (selectedFile != null && selectedFile.bytes != null)
             {
               trace('Selected file: ' + selectedFile.name);
-              #if FILE_DROP_SUPPORTED
-              vocalsEntryLabel.text = 'Vocals for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
-              #else
-              vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${selectedFile.name}';
-              #end
-              ChartEditorAudioHandler.loadVocalsFromBytes(state, selectedFile.bytes, charKey);
-              dialogNoVocals.hidden = true;
-              removeDropHandler(onDropFile);
+              if (ChartEditorAudioHandler.loadVocalsFromBytes(state, selectedFile.bytes, charKey, instId))
+              {
+                // Tell the user the load was successful.
+                #if !mac
+                NotificationManager.instance.addNotification(
+                  {
+                    title: 'Success',
+                    body: 'Loaded vocals for $charName (${selectedFile.name}), variation ${state.selectedVariation}',
+                    type: NotificationType.Success,
+                    expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
+                  });
+                #end
+                #if FILE_DROP_SUPPORTED
+                vocalsEntryLabel.text = 'Vocals for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
+                #else
+                vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${selectedFile.name}';
+                #end
+
+                dialogNoVocals.hidden = true;
+              }
+              else
+              {
+                trace('Failed to load vocal track (${selectedFile.fullPath})');
+
+                #if !mac
+                NotificationManager.instance.addNotification(
+                  {
+                    title: 'Failure',
+                    body: 'Failed to load vocal track (${selectedFile.name}) for variation (${state.selectedVariation})',
+                    type: NotificationType.Error,
+                    expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
+                  });
+                #end
+
+                #if FILE_DROP_SUPPORTED
+                vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
+                #else
+                vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
+                #end
+              }
             }
         });
       }
diff --git a/source/funkin/ui/debug/charting/ChartEditorImportExportHandler.hx b/source/funkin/ui/debug/charting/ChartEditorImportExportHandler.hx
index f116ad3f1..4d8ff18cb 100644
--- a/source/funkin/ui/debug/charting/ChartEditorImportExportHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorImportExportHandler.hx
@@ -36,7 +36,7 @@ class ChartEditorImportExportHandler
     for (metadata in rawSongMetadata)
     {
       if (metadata == null) continue;
-      var variation = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation;
+      var variation = (metadata.variation == null || metadata.variation == '') ? Constants.DEFAULT_VARIATION : metadata.variation;
 
       // Clone to prevent modifying the original.
       var metadataClone:SongMetadata = metadata.clone(variation);
@@ -52,23 +52,44 @@ class ChartEditorImportExportHandler
 
     state.clearVocals();
 
-    ChartEditorAudioHandler.loadInstrumentalFromAsset(state, Paths.inst(songId));
-
-    var diff:Null<SongDifficulty> = song.getDifficulty(state.selectedDifficulty);
-    var voiceList:Array<String> = diff != null ? diff.buildVoiceList() : [];
-    if (voiceList.length == 2)
+    var variations:Array<String> = state.availableVariations;
+    for (variation in variations)
     {
-      ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[0], BF);
-      ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[1], DAD);
-    }
-    else
-    {
-      for (voicePath in voiceList)
+      if (variation == Constants.DEFAULT_VARIATION)
       {
-        ChartEditorAudioHandler.loadVocalsFromAsset(state, voicePath);
+        ChartEditorAudioHandler.loadInstFromAsset(state, Paths.inst(songId));
+      }
+      else
+      {
+        ChartEditorAudioHandler.loadInstFromAsset(state, Paths.inst(songId, '-$variation'), variation);
       }
     }
 
+    for (difficultyId in song.listDifficulties())
+    {
+      var diff:Null<SongDifficulty> = song.getDifficulty(difficultyId);
+      if (diff == null) continue;
+
+      var instId:String = diff.variation == Constants.DEFAULT_VARIATION ? '' : diff.variation;
+      var voiceList:Array<String> = diff.buildVoiceList(); // SongDifficulty accounts for variation already.
+
+      if (voiceList.length == 2)
+      {
+        ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[0], diff.characters.player, instId);
+        ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[1], diff.characters.opponent, instId);
+      }
+      else if (voiceList.length == 1)
+      {
+        ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[0], diff.characters.player, instId);
+      }
+      else
+      {
+        trace('[WARN] Strange quantity of voice paths for difficulty ${difficultyId}: ${voiceList.length}');
+      }
+    }
+
+    state.switchToCurrentInstrumental();
+
     state.refreshMetadataToolbox();
 
     #if !mac
@@ -148,13 +169,8 @@ class ChartEditorImportExportHandler
       }
     }
 
-    if (state.audioInstTrackData != null) zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', state.audioInstTrackData));
-    for (charId in state.audioVocalTrackData.keys())
-    {
-      var entryData = state.audioVocalTrackData.get(charId);
-      if (entryData == null) continue;
-      zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-$charId.ogg', entryData));
-    }
+    if (state.audioInstTrackData != null) zipEntries.concat(ChartEditorAudioHandler.makeZIPEntriesFromInstrumentals(state));
+    if (state.audioVocalTrackData != null) zipEntries.concat(ChartEditorAudioHandler.makeZIPEntriesFromVocals(state));
 
     trace('Exporting ${zipEntries.length} files to ZIP...');
 
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 5e4dded91..2f5222cd5 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -461,6 +461,8 @@ class ChartEditorState extends HaxeUIState
     notePreviewDirty = true;
     notePreviewViewportBoundsDirty = true;
     this.scrollPositionInPixels = this.scrollPositionInPixels;
+    // Characters have probably changed too.
+    healthIconsDirty = true;
 
     return isViewDownscroll;
   }
@@ -519,8 +521,14 @@ class ChartEditorState extends HaxeUIState
    */
   var selectedVariation(default, set):String = Constants.DEFAULT_VARIATION;
 
+  /**
+   * Setter called when we are switching variations.
+   * We will likely need to switch instrumentals as well.
+   */
   function set_selectedVariation(value:String):String
   {
+    // Don't update if we're already on the variation.
+    if (selectedVariation == value) return selectedVariation;
     selectedVariation = value;
 
     // Make sure view is updated when the variation changes.
@@ -528,6 +536,8 @@ class ChartEditorState extends HaxeUIState
     notePreviewDirty = true;
     notePreviewViewportBoundsDirty = true;
 
+    switchToCurrentInstrumental();
+
     return selectedVariation;
   }
 
@@ -548,6 +558,23 @@ class ChartEditorState extends HaxeUIState
     return selectedDifficulty;
   }
 
+  /**
+   * The instrumental ID which is currently selected.
+   */
+  var currentInstrumentalId(get, set):String;
+
+  function get_currentInstrumentalId():String
+  {
+    var instId:Null<String> = currentSongMetadata.playData.characters.instrumental;
+    if (instId == null || instId == '') instId = (selectedVariation == Constants.DEFAULT_VARIATION) ? '' : selectedVariation;
+    return instId;
+  }
+
+  function set_currentInstrumentalId(value:String):String
+  {
+    return currentSongMetadata.playData.characters.instrumental = value;
+  }
+
   /**
    * The character ID for the character which is currently selected.
    */
@@ -592,6 +619,11 @@ class ChartEditorState extends HaxeUIState
    */
   var noteDisplayDirty:Bool = true;
 
+  /**
+   * Whether the selected charactesr have been modified and the health icons need to be updated.
+   */
+  var healthIconsDirty:Bool = true;
+
   /**
    * Whether the note preview graphic needs to be FULLY rebuilt.
    */
@@ -773,28 +805,29 @@ class ChartEditorState extends HaxeUIState
 
   /**
    * The audio track for the instrumental.
+   * Replaced when switching instrumentals.
    * `null` until an instrumental track is loaded.
    */
   var audioInstTrack:Null<FlxSound> = null;
 
   /**
-   * The raw byte data for the instrumental audio track.
+   * The raw byte data for the instrumental audio tracks.
+   * Key is the instrumental name.
    * `null` until an instrumental track is loaded.
    */
-  var audioInstTrackData:Null<Bytes> = null;
+  var audioInstTrackData:Map<String, Bytes> = [];
 
   /**
    * The audio track for the vocals.
    * `null` until vocal track(s) are loaded.
+   * When switching characters, the elements of the VoicesGroup will be swapped to match the new character.
    */
   var audioVocalTrackGroup:Null<VoicesGroup> = null;
 
   /**
    * A map of the audio tracks for each character's vocals.
-   * - Keys are the character IDs.
-   * - Values are the FlxSound objects to play that character's vocals.
-   *
-   * When switching characters, the elements of the VoicesGroup will be swapped to match the new character.
+   * - Keys are `characterId-variation` (with `characterId` being the default variation).
+   * - Values are the byte data for the audio track.
    */
   var audioVocalTrackData:Map<String, Bytes> = [];
 
@@ -1045,30 +1078,6 @@ class ChartEditorState extends HaxeUIState
     return currentSongMetadata.artist = value;
   }
 
-  var currentSongCharacterPlayer(get, set):String;
-
-  function get_currentSongCharacterPlayer():String
-  {
-    return currentSongMetadata.playData.characters.player;
-  }
-
-  function set_currentSongCharacterPlayer(value:String):String
-  {
-    return currentSongMetadata.playData.characters.player = value;
-  }
-
-  var currentSongCharacterOpponent(get, set):String;
-
-  function get_currentSongCharacterOpponent():String
-  {
-    return currentSongMetadata.playData.characters.opponent;
-  }
-
-  function set_currentSongCharacterOpponent(value:String):String
-  {
-    return currentSongMetadata.playData.characters.opponent = value;
-  }
-
   /**
    * SIGNALS
    */
@@ -1379,7 +1388,7 @@ class ChartEditorState extends HaxeUIState
     gridPlayhead.add(playheadBlock);
 
     // Character icons.
-    healthIconDad = new HealthIcon(currentSongCharacterOpponent);
+    healthIconDad = new HealthIcon(currentSongMetadata.playData.characters.opponent);
     healthIconDad.autoUpdate = false;
     healthIconDad.size.set(0.5, 0.5);
     healthIconDad.x = gridTiledSprite.x - 15 - (HealthIcon.HEALTH_ICON_SIZE * 0.5);
@@ -1387,7 +1396,7 @@ class ChartEditorState extends HaxeUIState
     add(healthIconDad);
     healthIconDad.zIndex = 30;
 
-    healthIconBF = new HealthIcon(currentSongCharacterPlayer);
+    healthIconBF = new HealthIcon(currentSongMetadata.playData.characters.player);
     healthIconBF.autoUpdate = false;
     healthIconBF.size.set(0.5, 0.5);
     healthIconBF.x = gridTiledSprite.x + gridTiledSprite.width + 15;
@@ -1484,6 +1493,12 @@ class ChartEditorState extends HaxeUIState
     return bounds;
   }
 
+  public function switchToCurrentInstrumental():Void
+  {
+    ChartEditorAudioHandler.switchToInstrumental(this, currentInstrumentalId, currentSongMetadata.playData.characters.player,
+      currentSongMetadata.playData.characters.opponent);
+  }
+
   function setNotePreviewViewportBounds(bounds:FlxRect = null):Void
   {
     if (notePreviewViewport == null)
@@ -1691,6 +1706,7 @@ class ChartEditorState extends HaxeUIState
     });
 
     addUIClickListener('menubarItemAbout', _ -> ChartEditorDialogHandler.openAboutDialog(this));
+    addUIClickListener('menubarItemWelcomeDialog', _ -> ChartEditorDialogHandler.openWelcomeDialog(this, true));
 
     addUIClickListener('menubarItemUserGuide', _ -> ChartEditorDialogHandler.openUserGuideDialog(this));
 
@@ -1713,6 +1729,11 @@ class ChartEditorState extends HaxeUIState
     });
     setUICheckboxSelected('menuBarItemThemeDark', currentTheme == ChartEditorTheme.Dark);
 
+    addUIClickListener('menubarItemPlayPause', _ -> toggleAudioPlayback());
+
+    addUIClickListener('menubarItemLoadInstrumental', _ -> ChartEditorDialogHandler.openUploadInstDialog(this, true));
+    addUIClickListener('menubarItemLoadVocals', _ -> ChartEditorDialogHandler.openUploadVocalsDialog(this, true));
+
     addUIChangeListener('menubarItemMetronomeEnabled', event -> isMetronomeEnabled = event.value);
     setUICheckboxSelected('menubarItemMetronomeEnabled', isMetronomeEnabled);
 
@@ -1726,7 +1747,7 @@ class ChartEditorState extends HaxeUIState
     if (instVolumeLabel != null)
     {
       addUIChangeListener('menubarItemVolumeInstrumental', function(event:UIEvent) {
-        var volume:Float = event?.value ?? 0 / 100.0;
+        var volume:Float = (event?.value ?? 0) / 100.0;
         if (audioInstTrack != null) audioInstTrack.volume = volume;
         instVolumeLabel.text = 'Instrumental - ${Std.int(event.value)}%';
       });
@@ -1736,7 +1757,7 @@ class ChartEditorState extends HaxeUIState
     if (vocalsVolumeLabel != null)
     {
       addUIChangeListener('menubarItemVolumeVocals', function(event:UIEvent) {
-        var volume:Float = event?.value ?? 0 / 100.0;
+        var volume:Float = (event?.value ?? 0) / 100.0;
         if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = volume;
         vocalsVolumeLabel.text = 'Vocals - ${Std.int(event.value)}%';
       });
@@ -2986,6 +3007,12 @@ class ChartEditorState extends HaxeUIState
    */
   function handleHealthIcons():Void
   {
+    if (healthIconsDirty)
+    {
+      if (healthIconBF != null) healthIconBF.characterId = currentSongMetadata.playData.characters.player;
+      if (healthIconDad != null) healthIconDad.characterId = currentSongMetadata.playData.characters.opponent;
+    }
+
     // Right align the BF health icon.
     if (healthIconBF != null)
     {
@@ -3413,11 +3440,11 @@ class ChartEditorState extends HaxeUIState
     {
       playerPreviewDirty = false;
 
-      if (currentSongCharacterPlayer != charPlayer.charId)
+      if (currentSongMetadata.playData.characters.player != charPlayer.charId)
       {
-        if (healthIconBF != null) healthIconBF.characterId = currentSongCharacterPlayer;
+        if (healthIconBF != null) healthIconBF.characterId = currentSongMetadata.playData.characters.player;
 
-        charPlayer.loadCharacter(currentSongCharacterPlayer);
+        charPlayer.loadCharacter(currentSongMetadata.playData.characters.player);
         charPlayer.characterType = CharacterType.BF;
         charPlayer.flip = true;
         charPlayer.targetScale = 0.5;
@@ -3449,11 +3476,11 @@ class ChartEditorState extends HaxeUIState
     {
       opponentPreviewDirty = false;
 
-      if (currentSongCharacterOpponent != charPlayer.charId)
+      if (currentSongMetadata.playData.characters.opponent != charPlayer.charId)
       {
-        if (healthIconDad != null) healthIconDad.characterId = currentSongCharacterOpponent;
+        if (healthIconDad != null) healthIconDad.characterId = currentSongMetadata.playData.characters.opponent;
 
-        charPlayer.loadCharacter(currentSongCharacterOpponent);
+        charPlayer.loadCharacter(currentSongMetadata.playData.characters.opponent);
         charPlayer.characterType = CharacterType.DAD;
         charPlayer.flip = false;
         charPlayer.targetScale = 0.5;
@@ -3833,9 +3860,9 @@ class ChartEditorState extends HaxeUIState
       switch (noteData.getStrumlineIndex())
       {
         case 0: // Player
-          if (hitsoundsEnabledPlayer) ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-09'));
+          if (hitsoundsEnabledPlayer) ChartEditorAudioHandler.playSound(Paths.sound('ui/chart-editor/playerHitsound'));
         case 1: // Opponent
-          if (hitsoundsEnabledOpponent) ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-010'));
+          if (hitsoundsEnabledOpponent) ChartEditorAudioHandler.playSound(Paths.sound('ui/chart-editor/opponentHitsound'));
       }
     }
   }
@@ -4072,12 +4099,18 @@ class ChartEditorState extends HaxeUIState
 
       buildSpectrogram(audioInstTrack);
     }
+    else
+    {
+      trace('[WARN] Instrumental track was null!');
+    }
 
+    // Pretty much everything is going to need to be reset.
     scrollPositionInPixels = 0;
     playheadPositionInPixels = 0;
     notePreviewDirty = true;
     notePreviewViewportBoundsDirty = true;
     noteDisplayDirty = true;
+    healthIconsDirty = true;
     moveSongToScrollPosition();
   }