diff --git a/Mapping_Tools/Classes/HitsoundStuff/MidiExporter.cs b/Mapping_Tools/Classes/HitsoundStuff/MidiExporter.cs index 8d8207c3..29c4da9b 100644 --- a/Mapping_Tools/Classes/HitsoundStuff/MidiExporter.cs +++ b/Mapping_Tools/Classes/HitsoundStuff/MidiExporter.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; +using Mapping_Tools.Classes.BeatmapHelper; +using Mapping_Tools.Classes.MathUtil; using NAudio.Midi; namespace Mapping_Tools.Classes.HitsoundStuff { @@ -37,7 +40,7 @@ public static void SaveToFile(string fileName, int[] bankNumbers, int[] patchNum if (channelIndex == -1) { channels.Add(new Tuple(bankNumbers[i], patchNumbers[i])); - channelIndex = channels.Count; + channelIndex = channels.Count >= 10 ? channels.Count + 1 : channels.Count; // Dont use the percussion channel collection.AddEvent(new ControlChangeEvent(time, channelIndex, MidiController.BankSelect, bankNumbers[i] >> 7), trackNumber); collection.AddEvent(new ControlChangeEvent(time, channelIndex, MidiController.BankSelectLsb, (byte)bankNumbers[i] & 0x01111111), trackNumber); collection.AddEvent(new PatchChangeEvent(time, channelIndex, patchNumbers[i]), trackNumber); @@ -58,18 +61,86 @@ public static void SaveToFile(string fileName, int[] bankNumbers, int[] patchNum } private static int FindChannel(List> channels, int bank, int patch) { + if (bank == 128) + return 10; // Standard MIDI percussion channel + for (int i = 0; i < channels.Count; i++) { var item = channels[i]; if (item.Item1 == bank && item.Item2 == patch) { - return i + 1; + return i >= 9 ? i + 2 : i + 1; // We dont want to output to the percussion channel } } return -1; } - private static int CalculateMicrosecondsPerQuaterNote(int bpm) { - return 60 * 1000 * 1000 / bpm; + private static int CalculateMicrosecondsPerQuaterNote(double bpm) { + return (int) (60 * 1000 * 1000 / bpm); + } + + public static void ExportAsMidi(List samplePackages, Beatmap baseBeatmap, string fileName, bool addGreenLineVolume) { + const int midiFileType = 0; + const int ticksPerQuarterNote = 120; + + const int trackNumber = 0; + + int microsecondsPerQuaterNote = baseBeatmap.BeatmapTiming.Redlines.Count > 0 ? + CalculateMicrosecondsPerQuaterNote(baseBeatmap.BeatmapTiming.Redlines[0].GetBpm()) : 1000000; + long tick = baseBeatmap.BeatmapTiming.Redlines.Count > 0 ? + (long) (baseBeatmap.BeatmapTiming.Redlines[0].Offset * 1000 / microsecondsPerQuaterNote * ticksPerQuarterNote) : 0; + + var collection = new MidiEventCollection(midiFileType, ticksPerQuarterNote); + + collection.AddEvent(new TextEvent("Note stream", MetaEventType.TextEvent, tick), trackNumber); + collection.AddEvent(new TempoEvent(microsecondsPerQuaterNote, tick), trackNumber); + + foreach (var samplePackage in samplePackages) { + tick = (long) (samplePackage.Time * 1000 / microsecondsPerQuaterNote * ticksPerQuarterNote); + + var channels = new List>(); + + foreach (var sample in samplePackage.Samples) { + var sampleArgs = sample.SampleArgs; + + int tickDuration = (int) Math.Max(sampleArgs.Length * 1000 / microsecondsPerQuaterNote * ticksPerQuarterNote, 0); + int bank = Math.Max(sampleArgs.Bank, 0); + int patch = MathHelper.Clamp(sampleArgs.Patch, 0, 127); + int key = MathHelper.Clamp(sampleArgs.Key, 0, 127); + int velocity = MathHelper.Clamp(sampleArgs.Velocity, 0, 127); + + var channelIndex = FindChannel(channels, bank, patch); + + if (channelIndex == -1) { + channels.Add(new Tuple(bank, patch)); + + channelIndex = channels.Count >= 10 ? channels.Count + 1 : channels.Count; // Dont use the percussion channel + collection.AddEvent(new ControlChangeEvent(tick, channelIndex, MidiController.BankSelect, bank >> 7), trackNumber); + collection.AddEvent(new ControlChangeEvent(tick, channelIndex, MidiController.BankSelectLsb, (byte)bank & 0x01111111), trackNumber); + collection.AddEvent(new PatchChangeEvent(tick, channelIndex, patch), trackNumber); + } + + collection.AddEvent(new NoteOnEvent(tick, channelIndex, key, velocity, tickDuration), trackNumber); + collection.AddEvent(new NoteEvent(tick + tickDuration, channelIndex, MidiCommandCode.NoteOff, key, 0), trackNumber); + } + } + + if (addGreenLineVolume) { + // Add the greenline volume of the base beatmap as a track with volume change events + const int volumeTrackNumber = 1; + + collection.AddEvent(new TextEvent("Green line volume", MetaEventType.TextEvent, 0), volumeTrackNumber); + + foreach (var tp in baseBeatmap.BeatmapTiming.TimingPoints) { + tick = (long) (tp.Offset * 1000 / microsecondsPerQuaterNote * ticksPerQuarterNote); + + for (int i = 1; i <= 16; i++) { + collection.AddEvent(new ControlChangeEvent(tick, i, MidiController.MainVolume, (int) (tp.Volume * 127 / 100)), volumeTrackNumber); + } + } + } + + collection.PrepareForExport(); + MidiFile.Export(fileName, collection); } - } + } } \ No newline at end of file diff --git a/Mapping_Tools/Viewmodels/HitsoundStudioVm.cs b/Mapping_Tools/Viewmodels/HitsoundStudioVm.cs index 5ad728f4..3cb93ebe 100644 --- a/Mapping_Tools/Viewmodels/HitsoundStudioVm.cs +++ b/Mapping_Tools/Viewmodels/HitsoundStudioVm.cs @@ -77,6 +77,12 @@ public bool AddCoincidingRegularHitsounds { set => Set(ref addCoincidingRegularHitsounds, value); } + private bool addGreenLineVolumeToMidi; + public bool AddGreenLineVolumeToMidi { + get => addGreenLineVolumeToMidi; + set => Set(ref addGreenLineVolumeToMidi, value); + } + public SampleSchema PreviousSampleSchema { get; set; } private HitsoundExportMode hitsoundExportModeSetting; @@ -87,6 +93,8 @@ public HitsoundExportMode HitsoundExportModeSetting { RaisePropertyChanged(nameof(StandardExtraSettingsVisibility)); RaisePropertyChanged(nameof(CoincidingExtraSettingsVisibility)); RaisePropertyChanged(nameof(StoryboardExtraSettingsVisibility)); + RaisePropertyChanged(nameof(MidiExtraSettingsVisibility)); + RaisePropertyChanged(nameof(SampleExportSettingsVisibility)); } } } @@ -102,6 +110,14 @@ public HitsoundExportMode HitsoundExportModeSetting { [JsonIgnore] public Visibility StoryboardExtraSettingsVisibility => HitsoundExportModeSetting == HitsoundExportMode.Storyboard ? Visibility.Visible : Visibility.Collapsed; + + [JsonIgnore] + public Visibility MidiExtraSettingsVisibility => + HitsoundExportModeSetting == HitsoundExportMode.Midi ? Visibility.Visible : Visibility.Collapsed; + + [JsonIgnore] + public Visibility SampleExportSettingsVisibility => + HitsoundExportModeSetting == HitsoundExportMode.Midi ? Visibility.Collapsed : Visibility.Visible; public IEnumerable HitsoundExportModes => Enum.GetValues(typeof(HitsoundExportMode)).Cast(); @@ -205,6 +221,7 @@ public HitsoundStudioVm(string baseBeatmap, Sample defaultSample, ObservableColl ExportSamples = true; DeleteAllInExportFirst = false; AddCoincidingRegularHitsounds = true; + AddGreenLineVolumeToMidi = true; HitsoundExportModeSetting = HitsoundExportMode.Standard; HitsoundExportGameMode = GameMode.Standard; ZipLayersLeniency = 15; @@ -216,7 +233,8 @@ public HitsoundStudioVm(string baseBeatmap, Sample defaultSample, ObservableColl public enum HitsoundExportMode { Standard, Coinciding, - Storyboard + Storyboard, + Midi, } } } diff --git a/Mapping_Tools/Views/HitsoundStudio/HitsoundStudioExportDialog.xaml b/Mapping_Tools/Views/HitsoundStudio/HitsoundStudioExportDialog.xaml index ccd167dd..138b94b3 100644 --- a/Mapping_Tools/Views/HitsoundStudio/HitsoundStudioExportDialog.xaml +++ b/Mapping_Tools/Views/HitsoundStudio/HitsoundStudioExportDialog.xaml @@ -35,19 +35,22 @@ + ToolTip="Check this to use the same samples as the previous export." + Visibility="{Binding SampleExportSettingsVisibility}"/> + ToolTip="Choose which format to export the hitsounds as. The 'Standard' option will generate hitsounds that can be copied to osu! standard beatmaps. The 'Coinciding' option combined with the osu! mania gamemode will export a lossless representation of the hitsound layers. The 'Midi' option will export a single MIDI file with all the MIDI information of the hitsound layers."/> + diff --git a/Mapping_Tools/Views/HitsoundStudio/HitsoundStudioView.xaml.cs b/Mapping_Tools/Views/HitsoundStudio/HitsoundStudioView.xaml.cs index acca32b9..ad70556d 100644 --- a/Mapping_Tools/Views/HitsoundStudio/HitsoundStudioView.xaml.cs +++ b/Mapping_Tools/Views/HitsoundStudio/HitsoundStudioView.xaml.cs @@ -15,6 +15,7 @@ using Mapping_Tools.Classes.HitsoundStuff; using Mapping_Tools.Classes.MathUtil; using Mapping_Tools.Classes.SystemTools; +using Mapping_Tools.Classes.ToolHelpers; using Mapping_Tools.Viewmodels; using MaterialDesignThemes.Wpf; using NAudio.Wave; @@ -241,6 +242,30 @@ private string Make_Hitsounds(HitsoundStudioVm arg, BackgroundWorker worker, DoW if (arg.ExportSamples) { HitsoundExporter.ExportLoadedSamples(loadedSamples, arg.ExportFolder, sampleNames, arg.SingleSampleExportFormat, comparer); } + } else if (arg.HitsoundExportModeSetting == HitsoundStudioVm.HitsoundExportMode.Midi) { + List samplePackages = HitsoundConverter.ZipLayers(arg.HitsoundLayers, arg.DefaultSample, 0, false); + var beatmap = EditorReaderStuff.GetNewestVersionOrNot(arg.BaseBeatmap).Beatmap; + + if (arg.ShowResults) { + result = $"Number of notes: {samplePackages.SelectMany(o => o.Samples).Count()}, " + + $"Number of volume changes: {(arg.AddGreenLineVolumeToMidi ? beatmap.BeatmapTiming.TimingPoints.Count : 0)}"; + } + + UpdateProgressBar(worker, 20); + + if (arg.DeleteAllInExportFirst && arg.ExportMap) { + // Delete all files in the export folder before filling it again + DirectoryInfo di = new DirectoryInfo(arg.ExportFolder); + foreach (FileInfo file in di.GetFiles()) { + file.Delete(); + } + } + + UpdateProgressBar(worker, 40); + + if (arg.ExportMap) { + MidiExporter.ExportAsMidi(samplePackages, beatmap, Path.Combine(arg.ExportFolder, arg.HitsoundDiffName + ".mid"), arg.AddGreenLineVolumeToMidi); + } } // Open export folder