From c44deed5b8a0bf3310f004b7065a8fb3436cb629 Mon Sep 17 00:00:00 2001 From: shockdude Date: Wed, 26 Jun 2019 01:40:02 -0700 Subject: [PATCH] support more djh2: tempomap, chart author --- Sharktooth/Mub/Mub.cs | 4 +- Sharktooth/Mub/MubEntry.cs | 6 +- Sharktooth/Mub/MubExport.cs | 133 ++++++++++++++++++++---- Sharktooth/Mub/MubImport.cs | 172 +++++++++++++++++++++++++++---- Sharktooth/Mub/MubTempoMarker.cs | 34 ++++++ Sharktooth/Sharktooth.csproj | 1 + 6 files changed, 306 insertions(+), 44 deletions(-) create mode 100644 Sharktooth/Mub/MubTempoMarker.cs diff --git a/Sharktooth/Mub/Mub.cs b/Sharktooth/Mub/Mub.cs index 8d81634..e3f2637 100644 --- a/Sharktooth/Mub/Mub.cs +++ b/Sharktooth/Mub/Mub.cs @@ -89,7 +89,7 @@ private static Mub FromStream(Stream stream) float length = ar.ReadSingle(); int wordOffset = ar.ReadInt32(); - mub.Entries.Add(new MubEntry(start, mod, length, wordOffset > 0 && words.ContainsKey(wordOffset) ? words[wordOffset] : "")); + mub.Entries.Add(new MubEntry(start, mod, length, wordOffset, wordOffset > 0 && words.ContainsKey(wordOffset) ? words[wordOffset] : "")); } return mub; @@ -148,7 +148,7 @@ private byte[] CreateData() aw.Write((float)entry.Start); aw.Write((int)entry.Modifier); aw.Write((float)entry.Length); - aw.Write(stringOffsets.ContainsKey(entry.Text) ? stringOffsets[entry.Text] : 0); + aw.Write(stringOffsets.ContainsKey(entry.Text) ? stringOffsets[entry.Text] : entry.Data); } // Writes string blob diff --git a/Sharktooth/Mub/MubEntry.cs b/Sharktooth/Mub/MubEntry.cs index 0f297ae..032e7b6 100644 --- a/Sharktooth/Mub/MubEntry.cs +++ b/Sharktooth/Mub/MubEntry.cs @@ -8,19 +8,21 @@ namespace Sharktooth.Mub { public struct MubEntry { - public MubEntry(float start, int mod, float length, string text = "") + public MubEntry(float start, int mod, float length, int data = 0, string text = "") { Start = start; Modifier = mod; Length = length; + Data = data; Text = text; } public float Start { get; set; } // Measure percentage, 0-index public int Modifier { get; set; } public float Length { get; set; } + public int Data { get; set; } public string Text { get; set; } - public override string ToString() => $"{Start:0.000}, 0x{Modifier:X8}, {Length:0.000}, \"{Text}\""; + public override string ToString() => $"{Start:0.000}, 0x{Modifier:X8}, {Length:0.000}, 0x{Data:X8}, \"{Text}\""; } } diff --git a/Sharktooth/Mub/MubExport.cs b/Sharktooth/Mub/MubExport.cs index 924bf76..00c56f8 100644 --- a/Sharktooth/Mub/MubExport.cs +++ b/Sharktooth/Mub/MubExport.cs @@ -1,9 +1,7 @@ -using System; +using NAudio.Midi; +using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; -using NAudio.Midi; namespace Sharktooth.Mub { @@ -11,6 +9,7 @@ public class MubExport { private const int DELTA_TICKS_PER_QUARTER = 480; private Mub _mub; + private List tempoMarkers; public MubExport(Mub mub) { @@ -29,14 +28,91 @@ public void Export(string path) MidiFile.Export(path, mid); } + private long NotePosToTicks(double pos, bool useTempoMarkers = true) + { + int ticksPerMeasure = DELTA_TICKS_PER_QUARTER * 4; // Assume 4/4 + + if (useTempoMarkers && tempoMarkers != null) + { + for (int i = 0; i < tempoMarkers.Count; ++i) + { + if (i == tempoMarkers.Count - 1 || tempoMarkers[i + 1].AbsolutePos > pos) + { + return (long)Math.Round(tempoMarkers[i].GetBeatPos(pos) * ticksPerMeasure); + } + } + throw new Exception("Error converting note position using tempo markers"); + } + return (long)Math.Round(pos * ticksPerMeasure); + } + private List CreateTempoTrack() { List track = new List(); + float chartBPM = 0; + int chartUsPerQuarterNote = 500000; // 120 BPM + // These are redundant but ehh track.Add(new NAudio.Midi.TextEvent("mubTempo", MetaEventType.SequenceTrackName, 0)); track.Add(new TimeSignatureEvent(0, 4, 2, 24, 8)); // 4/4 ts - track.Add(new TempoEvent(500000, 0)); // 120 bpm + + // get chart BPM + foreach (var entry in _mub.Entries) + { + if (entry.Modifier == 0x0B000002) + { + if (chartBPM > 0) + { + throw new Exception("Mub has more than one Chart BPM!"); + } + chartBPM = BitConverter.ToSingle(BitConverter.GetBytes(entry.Data), 0); + } + } + + // without a chart BPM, can't do an accurate tempomap so don't even try without one. + if (chartBPM > 0) + { + chartUsPerQuarterNote = (int)Math.Round(60000000 / chartBPM); + + foreach (var entry in _mub.Entries) + { + if (entry.Modifier == 0x0B000001) + { + long start = NotePosToTicks(entry.Start, false); + track.Add(new TempoEvent(entry.Data, start)); + if (tempoMarkers == null) + { + tempoMarkers = new List(); + } + tempoMarkers.Add(new MubTempoMarker(entry.Start, entry.Data, chartUsPerQuarterNote)); + } + } + if (tempoMarkers != null) + { + tempoMarkers.Sort((x, y) => + { + if (x.BeatPos < y.BeatPos) + return -1; + else if (x.BeatPos > y.BeatPos) + return 1; + else + return 0; + }); + MubTempoMarker temp; + for (int i=1; i CreateTrack() List track = new List(); track.Add(new NAudio.Midi.TextEvent("NOTES", MetaEventType.SequenceTrackName, 0)); - int ticksPerMeasure = DELTA_TICKS_PER_QUARTER * 4; // Assume 4/4 foreach (var entry in _mub.Entries) { - long start = (long)(entry.Start * ticksPerMeasure); - long end = (long)(start + (entry.Length * ticksPerMeasure)); + long start = NotePosToTicks(entry.Start); + long end = NotePosToTicks(entry.Start + entry.Length); // DJH2 effect type - 0x05FFFFFF through 0x06000009 // Should not be added to the notes track - if ((entry.Modifier & 0xFF000000) == 0x06000000 || entry.Modifier == 0x05FFFFFF) continue; + if ((entry.Modifier & 0xFF000000) == 0x06000000 + || entry.Modifier == 0x05FFFFFF) continue; + + // BPM type - 0x0Bxxxxxx + if ((entry.Modifier & 0xFF000000) == 0x0B000000) + { + if (entry.Modifier == 0x0B000002) + { + float chartBpm = BitConverter.ToSingle(BitConverter.GetBytes(entry.Data), 0); + track.Add(new NAudio.Midi.TextEvent(chartBpm.ToString(), MetaEventType.CuePoint, 0)); + } + continue; + } if ((entry.Modifier & 0xFFFFFF) == 0xFFFFFF) { - // Text event? + // Text Event? if (!string.IsNullOrEmpty(entry.Text)) - track.Add(new NAudio.Midi.TextEvent(entry.Text, MetaEventType.TextEvent, start)); + { + // Author? + if ((entry.Modifier & 0xFF000000) == 0x0A000000) + { + track.Add(new NAudio.Midi.TextEvent(entry.Text, MetaEventType.Copyright, 0)); + } + // Just a section + else + { + track.Add(new NAudio.Midi.TextEvent(entry.Text, MetaEventType.TextEvent, start)); + } + } continue; } @@ -76,8 +174,8 @@ private List CreateTrack() track.Add(new NAudio.Midi.TextEvent(entry.Text, MetaEventType.TextEvent, start)); } - track.Add(new NoteEvent(start, 1, MidiCommandCode.NoteOn, entry.Modifier & 0xFF, 100)); - track.Add(new NoteEvent(end, 1, MidiCommandCode.NoteOff, entry.Modifier & 0xFF, 100)); + track.Add(new NoteEvent(start, 1, MidiCommandCode.NoteOn, entry.Modifier & 0xFF, entry.Data + 1)); + track.Add(new NoteEvent(end, 1, MidiCommandCode.NoteOff, entry.Modifier & 0xFF, entry.Data + 1)); } track.Sort((x, y) => @@ -99,11 +197,10 @@ private List CreateEffectsTrack() { List effects = null; - int ticksPerMeasure = DELTA_TICKS_PER_QUARTER * 4; // Assume 4/4 foreach (var entry in _mub.Entries) { - long start = (long)(entry.Start * ticksPerMeasure); - long end = (long)(start + (entry.Length * ticksPerMeasure)); + long start = NotePosToTicks(entry.Start); + long end = NotePosToTicks(entry.Start + entry.Length); if ((entry.Modifier & 0xFF000000) == 0x06000000) { @@ -112,8 +209,8 @@ private List CreateEffectsTrack() effects = new List(); effects.Add(new NAudio.Midi.TextEvent("EFFECTS", MetaEventType.SequenceTrackName, 0)); } - effects.Add(new NoteEvent(start, 1, MidiCommandCode.NoteOn, entry.Modifier & 0xFF, 100)); - effects.Add(new NoteEvent(end, 1, MidiCommandCode.NoteOff, entry.Modifier & 0xFF, 100)); + effects.Add(new NoteEvent(start, 1, MidiCommandCode.NoteOn, entry.Modifier & 0xFF, entry.Data + 1)); + effects.Add(new NoteEvent(end, 1, MidiCommandCode.NoteOff, entry.Modifier & 0xFF, entry.Data + 1)); continue; } } diff --git a/Sharktooth/Mub/MubImport.cs b/Sharktooth/Mub/MubImport.cs index 088dd5a..e1da7a7 100644 --- a/Sharktooth/Mub/MubImport.cs +++ b/Sharktooth/Mub/MubImport.cs @@ -10,18 +10,42 @@ namespace Sharktooth.Mub public class MubImport { private MidiFile _mid; + private List tempoMarkers; public MubImport(string midPath) { _mid = new MidiFile(midPath); } - + + private double NoteTicksToPos(long ticks, bool useTempoMarkers = true) + { + double ticksPerMeasure = DeltaTicksPerQuarter * 4; // Assume 4/4 + double pos = ticks / ticksPerMeasure; + + if (useTempoMarkers && tempoMarkers != null) + { + for (int i = 0; i < tempoMarkers.Count; ++i) + { + if (i == tempoMarkers.Count - 1 || tempoMarkers[i + 1].BeatPos > pos) + { + return tempoMarkers[i].GetAbsolutePos(pos); + } + } + throw new Exception("Error converting note position using tempo markers"); + } + return pos; + } + public Mub ExportToMub() { var existingTracks = _mid.Events .Skip(1) .ToDictionary(x => GetTrackName(x), y => y); + var tempoTrack = _mid.Events[0]; + int chartUsPerQuarterNote = 500000; // 120 BPM + float bpm = 0; + var noteTrack = existingTracks.Keys .Where(x => x == "NOTES") .Select(x => existingTracks[x]) @@ -34,32 +58,136 @@ public Mub ExportToMub() var mubNotes = new List(); - var notes = noteTrack - .Where(x => x is NoteOnEvent) - .Select(x => x as NoteOnEvent); + var bpmEvents = noteTrack + .Where(x => x is TextEvent te + && (te.MetaEventType == MetaEventType.CuePoint)) + .Select(x => x as TextEvent); - foreach (var note in notes) + foreach (var textNote in bpmEvents) { - if (note.Velocity <= 0) - continue; + if (bpm != 0) + { + throw new Exception("Too many Cue (BPM) events"); + } + try + { + bpm = float.Parse(textNote.Text, System.Globalization.CultureInfo.InvariantCulture); + if (bpm <= 0) + { + throw new FormatException("BPM cannot be 0 or negative"); + } + chartUsPerQuarterNote = (int)Math.Round(60000000 / bpm); + } + catch (FormatException e) + { + Console.WriteLine($"Invalid BPM found in Cue TextEvent: {textNote.Text}"); + throw e; + } + } - mubNotes.Add(new MubEntry((note.AbsoluteTime / (DeltaTicksPerQuarter * 4)), - note.NoteNumber, - (note.NoteLength / (DeltaTicksPerQuarter * 4)))); + var tempoEvents = tempoTrack + .Where(x => x is TempoEvent ts) + .Select(x => x as TempoEvent); + + foreach (var tempoEvent in tempoEvents) + { + double pos = NoteTicksToPos(tempoEvent.AbsoluteTime, false); + int usPerQuarterNote = tempoEvent.MicrosecondsPerQuarterNote; + if (tempoMarkers == null) + { + tempoMarkers = new List(); + if (bpm == 0) + { + chartUsPerQuarterNote = usPerQuarterNote; + bpm = (float)60000000 / chartUsPerQuarterNote; + mubNotes.Add(new MubEntry(0.0f, + 0x0B_00_00_02, + 0.0f, + BitConverter.ToInt32(BitConverter.GetBytes(bpm), 0))); + } + } + tempoMarkers.Add(new MubTempoMarker(pos, usPerQuarterNote, chartUsPerQuarterNote)); + } + if (tempoMarkers != null) + { + tempoMarkers.Sort((x, y) => + { + if (x.BeatPos < y.BeatPos) + return -1; + else if (x.BeatPos > y.BeatPos) + return 1; + else + return 0; + }); + MubTempoMarker temp; + for (int i = 0; i < tempoMarkers.Count; ++i) + { + if (i > 0) + { + temp = tempoMarkers[i]; + temp.AbsolutePos = tempoMarkers[i - 1].GetAbsolutePos(tempoMarkers[i].BeatPos); + tempoMarkers[i] = temp; + } + + mubNotes.Add(new MubEntry((float)tempoMarkers[i].BeatPos, + 0x0B_00_00_01, + 0.0f, + tempoMarkers[i].UsPerQuarterNote)); + } } var metaEvents = noteTrack - .Where(x => x is TextEvent te - && (te.MetaEventType == MetaEventType.TextEvent - || te.MetaEventType == MetaEventType.Lyric)) - .Select(x => x as TextEvent); + .Where(x => x is TextEvent te + && (te.MetaEventType == MetaEventType.TextEvent + || te.MetaEventType == MetaEventType.Lyric + || te.MetaEventType == MetaEventType.Copyright + || te.MetaEventType == MetaEventType.CuePoint)) + .Select(x => x as TextEvent); foreach (var textNote in metaEvents) { - mubNotes.Add(new MubEntry((textNote.AbsoluteTime / (DeltaTicksPerQuarter * 4)), - 0x09_FF_FF_FF, + // Sections and Lyrics + if (textNote.MetaEventType == MetaEventType.TextEvent || textNote.MetaEventType == MetaEventType.Lyric) + { + mubNotes.Add(new MubEntry((float)NoteTicksToPos(textNote.AbsoluteTime), + 0x09_FF_FF_FF, + 0.0f, + 0, + textNote.Text)); + } + // Author + else if (textNote.MetaEventType == MetaEventType.Copyright) + { + mubNotes.Add(new MubEntry((float)NoteTicksToPos(textNote.AbsoluteTime), + 0x0A_FF_FF_FF, 0.0f, + 0, textNote.Text)); + } + // BPM + // don't include if there are no tempomarkers for some reason, since we can't guarantee + // the cue BPM & chart BPM are the same. + else if (textNote.MetaEventType == MetaEventType.CuePoint && tempoMarkers != null) + { + mubNotes.Add(new MubEntry((float)NoteTicksToPos(textNote.AbsoluteTime), + 0x0B_00_00_02, + 0.0f, + BitConverter.ToInt32(BitConverter.GetBytes(bpm),0))); + } + } + + var notes = noteTrack + .Where(x => x is NoteOnEvent) + .Select(x => x as NoteOnEvent); + + foreach (var note in notes) + { + double start = NoteTicksToPos(note.AbsoluteTime); + double end = NoteTicksToPos(note.AbsoluteTime + note.NoteLength); + mubNotes.Add(new MubEntry((float)start, + note.NoteNumber, + (float)(end - start), + note.Velocity - 1)); } // DJ Hero 2 effects @@ -76,12 +204,12 @@ public Mub ExportToMub() foreach (var effect in effects) { - if (effect.Velocity <= 0) - continue; - - mubNotes.Add(new MubEntry((effect.AbsoluteTime / (DeltaTicksPerQuarter * 4)), + double start = NoteTicksToPos(effect.AbsoluteTime); + double end = NoteTicksToPos(effect.AbsoluteTime + effect.NoteLength); + mubNotes.Add(new MubEntry((float)start, effect.NoteNumber + 0x06_00_00_00, - (effect.NoteLength / (DeltaTicksPerQuarter * 4)))); + (float)(end - start), + effect.Velocity - 1)); } } @@ -104,6 +232,6 @@ private static string GetTrackName(IList track) return (trackNameEv as TextEvent)?.Text; } - public float DeltaTicksPerQuarter => _mid.DeltaTicksPerQuarterNote; + public double DeltaTicksPerQuarter => _mid.DeltaTicksPerQuarterNote; } } diff --git a/Sharktooth/Mub/MubTempoMarker.cs b/Sharktooth/Mub/MubTempoMarker.cs new file mode 100644 index 0000000..d48e30c --- /dev/null +++ b/Sharktooth/Mub/MubTempoMarker.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Sharktooth.Mub +{ + public struct MubTempoMarker + { + public MubTempoMarker(double beatPos, int usPerQuarterNote, int chartUsPerQuarterNote) + { + ChartUsPerQuarterNote = chartUsPerQuarterNote; + UsPerQuarterNote = usPerQuarterNote; + BeatPos = beatPos; + AbsolutePos = beatPos; + } + + public int ChartUsPerQuarterNote { get; } + public int UsPerQuarterNote { get; } + public double BeatPos { set; get; } + public double AbsolutePos { set; get; } + + public double GetBeatPos(double notePos) + { + return BeatPos + (notePos - AbsolutePos) * ChartUsPerQuarterNote / UsPerQuarterNote; + } + + public double GetAbsolutePos(double notePos) + { + return AbsolutePos + (notePos - BeatPos) * UsPerQuarterNote / ChartUsPerQuarterNote; + } + } +} diff --git a/Sharktooth/Sharktooth.csproj b/Sharktooth/Sharktooth.csproj index d1a61f0..5d85edb 100644 --- a/Sharktooth/Sharktooth.csproj +++ b/Sharktooth/Sharktooth.csproj @@ -58,6 +58,7 @@ +