Skip to content

Commit

Permalink
Add option to export to MIDI in Hitsound Studio.
Browse files Browse the repository at this point in the history
  • Loading branch information
OliBomby committed Jan 20, 2024
1 parent 726c4cb commit 1163169
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 8 deletions.
81 changes: 76 additions & 5 deletions Mapping_Tools/Classes/HitsoundStuff/MidiExporter.cs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -37,7 +40,7 @@ public static void SaveToFile(string fileName, int[] bankNumbers, int[] patchNum
if (channelIndex == -1) {
channels.Add(new Tuple<int, int>(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);
Expand All @@ -58,18 +61,86 @@ public static void SaveToFile(string fileName, int[] bankNumbers, int[] patchNum
}

private static int FindChannel(List<Tuple<int, int>> 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<SamplePackage> 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<Tuple<int, int>>();

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<int, int>(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);
}
}
}
}
20 changes: 19 additions & 1 deletion Mapping_Tools/Viewmodels/HitsoundStudioVm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -87,6 +93,8 @@ public HitsoundExportMode HitsoundExportModeSetting {
RaisePropertyChanged(nameof(StandardExtraSettingsVisibility));
RaisePropertyChanged(nameof(CoincidingExtraSettingsVisibility));
RaisePropertyChanged(nameof(StoryboardExtraSettingsVisibility));
RaisePropertyChanged(nameof(MidiExtraSettingsVisibility));
RaisePropertyChanged(nameof(SampleExportSettingsVisibility));
}
}
}
Expand All @@ -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<HitsoundExportMode> HitsoundExportModes => Enum.GetValues(typeof(HitsoundExportMode)).Cast<HitsoundExportMode>();

Expand Down Expand Up @@ -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;
Expand All @@ -216,7 +233,8 @@ public HitsoundStudioVm(string baseBeatmap, Sample defaultSample, ObservableColl
public enum HitsoundExportMode {
Standard,
Coinciding,
Storyboard
Storyboard,
Midi,
}
}
}
13 changes: 11 additions & 2 deletions Mapping_Tools/Views/HitsoundStudio/HitsoundStudioExportDialog.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,22 @@
<CheckBox Content="Export hitsound map" IsChecked="{Binding ExportMap}" Margin="0 15 0 0"
ToolTip="Check this to export the hitsound beatmap."/>
<CheckBox Content="Export hitsound samples" IsChecked="{Binding ExportSamples}"
Visibility="{Binding SampleExportSettingsVisibility}"
ToolTip="Check this to export the custom hitsound samples."/>
<CheckBox Content="Show results" IsChecked="{Binding ShowResults}"
ToolTip="Check this to see the resulting number of custom indices, hitsound samples, and index changes."/>
<CheckBox Content="Delete all files in export folder" IsChecked="{Binding DeleteAllInExportFirst}"
ToolTip="Check this to automatically delete all files in the export folder before exporting. When using this be sure there aren't any important files in your export folder."/>
<CheckBox Name="UsePreviousSampleSchemaBox" Content="Use previous sample schema" IsChecked="{Binding UsePreviousSampleSchema}"
ToolTip="Check this to use the same samples as the previous export."/>
ToolTip="Check this to use the same samples as the previous export."
Visibility="{Binding SampleExportSettingsVisibility}"/>
<CheckBox Content="Allow growth of previous sample schema" IsChecked="{Binding AllowGrowthPreviousSampleSchema}"
IsEnabled="{Binding ElementName=UsePreviousSampleSchemaBox, Path=IsChecked}"
Visibility="{Binding SampleExportSettingsVisibility}"
ToolTip="Alows expansion of the previous sample schema to always fit the need of the current export. If you dont check this, then the sample schema of this export needs to be a subset of the previous sample schema."/>
<ComboBox Margin="0 20 0 0" ItemsSource="{Binding HitsoundExportModes}" SelectedItem="{Binding HitsoundExportModeSetting}"
Style="{StaticResource MaterialDesignFloatingHintComboBox}" materialDesign:HintAssist.Hint="Export mode"
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."/>
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."/>
<TextBox Margin="0,5,0,0"
Style="{StaticResource MaterialDesignFloatingHintTextBox}" materialDesign:HintAssist.Hint="Note grouping leniency"
ToolTip="Maximum time in millisecond that can be between two hitsounds while treated as being at the same time."
Expand Down Expand Up @@ -89,15 +92,21 @@
<CheckBox Content="Add regular hitsounds" IsChecked="{Binding AddCoincidingRegularHitsounds}" Margin="0 5 0 0"
Visibility="{Binding CoincidingExtraSettingsVisibility}"
ToolTip="Check this to add whistles, claps, finishes, and samplesets ontop of the filename hitsounding used by coinciding export."/>
<CheckBox Content="Add green line volume" IsChecked="{Binding AddGreenLineVolumeToMidi}" Margin="0 5 0 0"
Visibility="{Binding MidiExtraSettingsVisibility}"
ToolTip="Check this to add a track to the MIDI with volume changes from timing points."/>
<ComboBox Margin="0 10 0 0" ItemsSource="{Binding HitsoundExportGameModes}" SelectedItem="{Binding HitsoundExportGameMode}"
Style="{StaticResource MaterialDesignFloatingHintComboBox}" materialDesign:HintAssist.Hint="Export gamemode"
Visibility="{Binding SampleExportSettingsVisibility}"
ToolTip="Choose the gamemode for the exported hitsound map."/>

<ComboBox Margin="0 15 0 0" ItemsSource="{Binding SampleExportFormatDisplayNames}" SelectedItem="{Binding SingleSampleExportFormatDisplay}"
Style="{StaticResource MaterialDesignFloatingHintComboBox}" materialDesign:HintAssist.Hint="Sample file format"
Visibility="{Binding SampleExportSettingsVisibility}"
ToolTip="Choose the file format for exported sound samples. Default will match the encoding of the source files."/>
<ComboBox Margin="0 5 0 0" ItemsSource="{Binding SampleExportFormatDisplayNames}" SelectedItem="{Binding MixedSampleExportFormatDisplay}"
Style="{StaticResource MaterialDesignFloatingHintComboBox}" materialDesign:HintAssist.Hint="Mixed sample file format"
Visibility="{Binding SampleExportSettingsVisibility}"
ToolTip="Choose the file format for mixed exported sound samples. Mixing only occurs in the Standard hitsound export mode."/>
</StackPanel>

Expand Down
25 changes: 25 additions & 0 deletions Mapping_Tools/Views/HitsoundStudio/HitsoundStudioView.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<SamplePackage> 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
Expand Down

0 comments on commit 1163169

Please sign in to comment.