Skip to content

Instantly share code, notes, and snippets.

@zardoru
Last active July 13, 2016 14:01
Show Gist options
  • Save zardoru/5298155 to your computer and use it in GitHub Desktop.
Save zardoru/5298155 to your computer and use it in GitHub Desktop.
Stepmania converter to osu!mania in c#. fixed a few bugs.
/* use this one. github is not cooperating. */
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.Text.RegularExpressions;
/* god my c# is so messy.
* I'm not fond of proper C#. I rather like C, really
*/
namespace StepmaniaConverter
{
class fPair
{
public float Beat, Value;
};
public class BpmPair
{
public float Beatspace, Time;
}
public enum ENoteType { Hold, Tap };
public class Note
{
public ENoteType NoteType;
public float BStart, BEnd; // Beat Start and End
public float TStart, TEnd; // Time Start and End
public uint Track;
};
public class Difficulty
{
public String chartName;
public List<Note> Notes;
public uint keyCount = 0;
/* iterate through this one to get the timing sections. */
public List<BpmPair> OsuTimingSections;
public String style = "";
};
public class Converter
{
Regex commandFmt = new Regex("#(\\w+):(.+)?;");
public String artistName = "Unknown";
public String songName = "Untitled";
public String chartAuthor = "SM2OM";
public String songFile = "music.mp3";
public String Background = "";
List<fPair> BpmChanges = new List<fPair>();
List<fPair> Stops = new List<fPair>();
public List<Difficulty> Difficulties;
public float Offset = 0;
public float PreviewPoint = 0;
int DifficultyCount
{
get { return Difficulties.Count; }
set {}
}
public int trackPos(uint col, uint totalcol)
{
double xbase = (512.0/totalcol);
double minus = (256.0/totalcol);
int xpos = (int)((double)(xbase) * (double)(col+1) - (double)minus);
return xpos;
}
uint keyCountFromStyle(String style)
{
switch (style)
{
case "kb7-single":
return 7;
case "dance-single":
return 4;
case "dance-solo":
return 6;
case "dance-double":
return 8;
case "pump-single":
return 5;
case "pump-double":
return 10;
}
return 4;
}
/* Read a command */
String ReadCommand(System.IO.StreamReader In)
{
String Retval = "";
while (In.Peek() != ';' && !In.EndOfStream)
{
while ((In.Peek() == '\n') && !In.EndOfStream)
In.Read(); // Discard newline.
if (In.Peek() != ';' && !In.EndOfStream)
Retval += (char)In.Read();
}
while (In.Peek() == ';')
In.Read();
// Console.WriteLine((char)In.Peek());
Retval += ";";
return Retval;
}
/* parse a x=y list.*/
void ParseLists(String Value, bool BPM) // if BPM is false, work with the stops list.
{
List<fPair> outList;
string[] Pairs;
fPair NewElement;
if (BPM)
outList = BpmChanges;
else
outList = Stops;
Pairs = Value.Split(',');
foreach (string Element in Pairs)
{
string[] Values;
float nBeat, nValue;
Values = Element.Split('=');
if (Values.GetLength(0) > 1) // hey we got an actual pair?
{
float.TryParse(Values[0], out nBeat);
float.TryParse(Values[1], out nValue);
NewElement = new fPair();
NewElement.Beat = nBeat;
NewElement.Value = nValue;
outList.Add(NewElement);
}
}
}
/* give us the bpm at this point in time */
float BpmForBeat(float beat)
{
float best = 0;
foreach (fPair value in BpmChanges)
{
if (value.Beat <= beat) // aww yeah we're the closest bpm in range.
best = value.Value;
}
return best;
}
/* give us a stop at the current time */
float StopForBeat(float beat)
{
foreach (fPair value in Stops)
{
if (Math.Abs(value.Beat - beat) < 0.00001)
return value.Value * 1000;
}
return 0; // nope no stop here
}
float sumStopInterval(float beat, float nextbeat)
{
float time = 0;
foreach (fPair value in Stops)
{
if (value.Beat > beat && value.Beat < nextbeat)
time += value.Value;
}
return time;
}
float sumBPMInterval(float totalTime, float last_bpm, float beat, float next_beat, out float alt_bpm)
{
float timeSum = 0;
List<fPair> BpmList = new List<fPair>();
foreach (fPair pair in BpmChanges)
{
if ( ((pair.Beat - beat) > 0.0001) && ((next_beat - pair.Beat) > 0.0001) )
{
BpmList.Add(pair);
}
}
if (BpmList.Count > 0)
{
float spb = 60 / last_bpm;
// Calculate value for time between first bpm change in the interval and the current beat
timeSum += (spb * (BpmList[0].Beat - beat)) * 1000;
try
{
for (int index = 0; index < BpmList.Count; index++)
{
// Calculate time for all values in between
timeSum += (60000.0f / BpmList[index].Value) * (BpmList[index + 1].Beat - BpmList[index].Beat);
}
}
catch (SystemException) { }
// Calculate value for last bpm change in the list and our next beat.
timeSum += 60000.0f / BpmList[BpmList.Count - 1].Value * (next_beat - BpmList[BpmList.Count - 1].Beat);
alt_bpm = BpmList[BpmList.Count - 1].Value;
}
else
alt_bpm = last_bpm;
return timeSum;
}
/* Parse a #notes tag. */
Difficulty ParseNotes(String Value)
{
string[] data, measures;
string notes;
float currentTime = -Offset * 1000/* + 70 */; // 70 ms is osu's standard drift from sm
float beat = 0, prev_beat = 0;
float cur_bpm = 0;
float[] pendingTracks;
List<Note> Notes;
Difficulty Diff = new Difficulty();
Notes = new List<Note>();
Diff.Notes = Notes;
// Console.WriteLine("about to write dem notes\n");
// eliminate measure comments
Value = Regex.Replace(Value, "\\/\\/ measure \\d+\r", "");
// eliminate whitespace
Value = Regex.Replace(Value, "\\s", "");
// eliminate carriage returns
Value = Value.Replace("\r", " ");
data = Value.Split(':');
if (data.Length < 6)
throw new System.Exception("invalid data while parsing .sm file");
// get key count
Diff.keyCount = keyCountFromStyle(data[0]);
Diff.style = data[0];
// make up space for holds
pendingTracks = new float[Diff.keyCount];
Diff.OsuTimingSections = new List<BpmPair>();
/*foreach (string val in data)
{
Console.WriteLine(val);
}*/
Diff.chartName = data[2];
// now we work with the notes.
notes = data[5];
measures = notes.Split(',');
foreach (string measure in measures)
{
long rows = measure.Length / Diff.keyCount;
float fractionperrow = 4.0f/rows; /* measure len / rows. assume mlen = 4*/
uint currentTrack = 0, mCount = 0;
float mspb = 0;
// Console.WriteLine("measure rows = {0}\n", rows);
while (rows > 0)
{
/* this is only for timing sections */
if (cur_bpm != BpmForBeat(beat))
{
// add a bpm change
BpmPair newPair = new BpmPair();
newPair.Beatspace = 60000 / BpmForBeat(beat);
newPair.Time = currentTime;
Diff.OsuTimingSections.Add(newPair);
}
if (StopForBeat(beat) != 0)
{
// add a stop.
BpmPair newpair = new BpmPair();
newpair.Beatspace = 60000.0f / 0.000000001f; // a small enough value
newpair.Time = currentTime;
Diff.OsuTimingSections.Add(newpair);
newpair = new BpmPair();
newpair.Beatspace = 60000 / BpmForBeat(beat);
newpair.Time = currentTime + StopForBeat(beat);
Diff.OsuTimingSections.Add(newpair);
}
/* the next is for notes and everything else*/
cur_bpm = BpmForBeat(beat);
mspb = 60000 / cur_bpm;
switch (measure[(int)mCount])
{
case '1':
Note newNote = new Note();
newNote.NoteType = ENoteType.Tap;
newNote.BStart = beat;
newNote.TStart = currentTime;
newNote.BEnd = 0;
newNote.Track = currentTrack;
Notes.Add(newNote);
break;
case '2': // hold start
case '4': // roll start
pendingTracks[currentTrack] = currentTime;
break;
case '3':
newNote = new Note();
newNote.NoteType = ENoteType.Tap;
newNote.BEnd = beat;
newNote.TStart = pendingTracks[currentTrack];
newNote.TEnd = currentTime;
newNote.Track = currentTrack;
Notes.Add(newNote);
break;
default:
break;
}
mCount++;
currentTrack++;
if (currentTrack == Diff.keyCount)
{
float alt_bpm;
currentTrack = 0;
rows--;
currentTime += fractionperrow * mspb + StopForBeat(beat);
prev_beat = beat;
beat += fractionperrow;
currentTime += sumStopInterval(prev_beat, beat);
currentTime += sumBPMInterval(currentTime, cur_bpm, prev_beat, beat, out alt_bpm);
cur_bpm = alt_bpm;
}
}
}
return Diff;
}
/* Read the chart's headers */
public void ReadHeader(String Filename, bool readNotes = false)
{
System.IO.StreamReader Stream = new System.IO.StreamReader(Filename);
String Line;
Difficulties = new List<Difficulty>();
while (commandFmt.IsMatch((Line = ReadCommand(Stream))))
{
MatchCollection matches = commandFmt.Matches(Line);
foreach (Match m in matches)
{
String Key = m.Groups[1].Value;
String Value = m.Groups[2].Value;
/* Console.WriteLine("{0}: {1}\n", Key, Value); */
/* Information relevant only to the metadata */
switch (Key)
{
case "TITLE":
songName = Value;
break;
case "OFFSET":
float.TryParse(Value, out Offset);
break;
case "ARTIST":
artistName = Value;
break;
case "CREDIT":
chartAuthor = Value;
break;
case "SAMPLESTART":
float.TryParse(Value, out PreviewPoint);
break;
case "MUSIC":
songFile = Value;
break;
case "BACKGROUND":
Background = Value;
break;
default: break;
}
if (readNotes)
{
switch (Key)
{
case "BPMS":
ParseLists(Value, true);
break;
case "STOPS":
ParseLists(Value, false);
break;
case "NOTES":
Difficulties.Add(ParseNotes(Value));
break;
}
}
}
}
}
}
public class EZConverter
{
public static void Convert(String filename, int soundVolume, String prefix = "")
{
/* Example usage */
Converter SMC = new Converter();
System.IO.StreamWriter swout;
SMC.ReadHeader(filename, true);
foreach (Difficulty Diff in SMC.Difficulties)
{
swout = new System.IO.StreamWriter(prefix + String.Format("{0} - {1} ({2}) [{3}-{4}].osu",
SMC.artistName, SMC.songName, SMC.chartAuthor, Diff.chartName, Diff.style));
swout.AutoFlush = true;
swout.WriteLine("osu file format v12");
swout.WriteLine("");
swout.WriteLine("[General]");
swout.WriteLine("AudioFilename: {0}", SMC.songFile);
swout.WriteLine("AudioLeadIn: 2");
swout.WriteLine("PreviewTime: {0}", SMC.PreviewPoint * 1000);
swout.WriteLine("Countdown: 0");
swout.WriteLine("SampleSet: Normal");
swout.WriteLine("StackLeniency: 0.7");
swout.WriteLine("Mode: 3");
swout.WriteLine("LetterboxInBreaks: 0");
swout.WriteLine("");
swout.WriteLine("[Editor]");
swout.WriteLine("DistanceSpacing: 0.8");
swout.WriteLine("BeatDivisor: 4");
swout.WriteLine("GridSize: 8");
swout.WriteLine("");
swout.WriteLine("[Metadata]");
swout.WriteLine("Title:{0}", SMC.songName);
swout.WriteLine("TitleUnicide:{0}", SMC.songName);
swout.WriteLine("Artist:{0}", SMC.artistName);
swout.WriteLine("ArtistUnicode:{0}", SMC.artistName);
swout.WriteLine("Creator:{0}", SMC.chartAuthor);
swout.WriteLine("Version:{0}", Diff.chartName);
swout.WriteLine("Source: ");
swout.WriteLine("Tags: ");
swout.WriteLine("BeatmapID:0");
swout.WriteLine("BeatmapSetID:-1");
swout.WriteLine("");
swout.WriteLine("[Difficulty]");
swout.WriteLine("HPDrainRate:5"); // changeme
swout.WriteLine("CircleSize:{0}", Diff.keyCount);
swout.WriteLine("OverallDifficulty:7");
swout.WriteLine("ApproachRate:10");
swout.WriteLine("SliderMultiplier:1.4");
swout.WriteLine("SliderTickRate:1");
swout.WriteLine("");
swout.WriteLine("[Events]");
swout.WriteLine("//Background and Video events");
if (SMC.Background.Length > 1) // hey, a background
{
swout.WriteLine("0,0,\"{0}\"", SMC.Background);
}
swout.WriteLine("//Break Periods");
swout.WriteLine("//Storyboard Layer 0 (Background)");
swout.WriteLine("//Storyboard Layer 1 (Fail)");
swout.WriteLine("//Storyboard Layer 2 (Pass)");
swout.WriteLine("//Storyboard Layer 3 (Foreground)");
swout.WriteLine("//Storyboard Sound Samples");
swout.WriteLine("//Background Colour Transformations");
swout.WriteLine("3,100,163,162,255");
swout.WriteLine("");
swout.WriteLine("[TimingPoints]");
foreach (BpmPair point in Diff.OsuTimingSections)
{
swout.WriteLine("{0},{1},4,1,0,{2},1,0", point.Time, point.Beatspace, soundVolume);
}
swout.WriteLine("[HitObjects]");
foreach (Note cNote in Diff.Notes)
{
if (cNote.TEnd == 0) // normal note
swout.WriteLine("{0},{1},{2},1,0\n", SMC.trackPos(cNote.Track, Diff.keyCount),
384 / 2, (int)cNote.TStart);
else // hold note
swout.WriteLine("{0},0,{1},128,0,{2}:1:0:0\n",
SMC.trackPos(cNote.Track, Diff.keyCount), (int)cNote.TStart, (int)cNote.TEnd);
}
}
}
public static void Main()
{
EZConverter.Convert("moyar.sm", 15);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment