private static void ProcessChannelNotes(Match match, int barOffset, BpmGraph barToBeat, BpmGraph bpmGraph, Dictionary <int, List <PartialData> > channelData) { int barNum = ParseMmm(match.Groups[1]) + barOffset; int lane = ParseXY(match.Groups[2]); int channel = ParseXY(match.Groups[3]); ProcessNotes(match.Groups[4], (barOff, type, width) => { double beat = barToBeat.BeatAt(barNum + barOff); var time = bpmGraph.FromBeat(beat); var pos = new LanePosition(lane * 2, width * 2); if (!channelData.ContainsKey(channel)) { channelData.Add(channel, new List <PartialData>()); } channelData[channel].Add(new PartialData(type, pos, time)); }); }
private Chart ParseChart(string[] tagData, BpmGraph bpm) { //0 will be the note tag, skip string chartType = tagData[1].Replace(" ", ""); int keysPerMeasure = 4; //currently hardcoded for dance-single string description = tagData[2].Replace(" ", ""); string difficulty = tagData[3].Replace(" ", ""); int meter = int.Parse(tagData[4].Replace(" ", "")); //grooveRadar data below; we're ignoring this for now //String grooves[] = tagData[5].replaceAll(" ", "").split(","); string[] measureLines = tagData[6].Replace(" ", "").Split(','); var notes = new List <RhythmEvent>(); for (int i = 0; i != measureLines.Length; i++) { parseMeasure(i, measureLines[i], keysPerMeasure, notes, bpm); } return(new Chart(difficulty, bpm, notes)); }
private readonly Regex _arrow = CompileRule("mmm5x: notes"); //Hex bar, B36 startLane, notes public Song ParseSong(string filename) { var theSourceFile = new FileInfo(filename); var reader = theSourceFile.OpenText(); var data = new List <string>(); while (true) { string line = reader.ReadLine(); if (line == null) { break; } if (line.StartsWith("#")) { data.Add(line.Substring(1)); } } bool[] hasBeenRead = new bool[data.Count]; int ticksPerBeat = 480; //default int beatsPerBar = 4; //default TODO this is being ignored! should be slowing down approach rate/effective BPM when increased int barOffset = 0; //default Song song = new Song(); var barSections = new List <BarSection>(); var bpmDefinitions = new Dictionary <int, double>();//BPM ID -> BPM value string difficulty = null; //PASS 1: construct base rhythm model for (int i = 0; i < data.Count; i++) { if (hasBeenRead[i]) { continue; } var line = data[i]; //assume we read this line (simplifies our code a bit) hasBeenRead[i] = true; //now try all regex rules for this stage in sequence: var match = _barLength.Match(line); if (match.Success) { barSections.Add(new BarSection(ParseMmm(match.Groups[1]), ParseInt(match.Groups[2]))); continue; } match = _bpmDef.Match(line); if (match.Success) { bpmDefinitions.Add(ParseZz(match.Groups[1]), ParseDouble(match.Groups[2])); continue; } //also parse song info while we're at it: match = _wave.Match(line); if (match.Success) { song.AudioFile = match.Groups[1].Value; continue; } match = _waveOffset.Match(line); if (match.Success) { song.Offset = ParseDouble(match.Groups[1]); continue; } //reach here, nothing matched, so we didn't read this line after all hasBeenRead[i] = false; } //We now have enough information to construct the Bar->Beat converter var barToBeat = BpmGraph.ParseBars(barSections); var bpmChanges = new List <BeatEvent>(); //PASS 2: construct internal rhythm model for (int i = 0; i < data.Count; i++) { if (hasBeenRead[i]) { continue; } var line = data[i]; //assume we read this line (simplifies our code a bit) hasBeenRead[i] = true; //now try all regex rules for this stage in sequence: var match = _bpmChange.Match(line); if (match.Success) { bpmChanges.Add(new BeatEvent(barToBeat.BeatAt(ParseMmm(match.Groups[1])), bpmDefinitions[ParseInt(match.Groups[2])])); continue; } //reach here, nothing matched, so we didn't read this line after all hasBeenRead[i] = false; } //We can now construct our BPM Graph, which means we now have everything needed to start parsing notes. bpmChanges.Sort((a, b) => a.beat.CompareTo(b.beat)); BpmGraph bpmGraph = BpmGraph.ParseBpm(bpmChanges, Array.Empty <BeatEvent>()); var notes = new List <RhythmEvent>(); var holdData = new Dictionary <int, List <PartialData> >(); var slideData = new Dictionary <int, List <PartialData> >(); var airData = new Dictionary <int, List <PartialData> >(); //PASS 3: read all note data for (int i = 0; i < data.Count; i++) { if (hasBeenRead[i]) { continue; } var line = data[i]; //assume we read this line (simplifies our code a bit) hasBeenRead[i] = true; //now try all regex rules for this stage in sequence: var match = _tap.Match(line); if (match.Success) { int barNum = ParseMmm(match.Groups[1]) + barOffset; int lane = ParseXY(match.Groups[2]); ProcessNotes(match.Groups[3], (barOff, type, width) => { double beat = barToBeat.BeatAt(barNum + barOff); var time = bpmGraph.FromBeat(beat); var pos = new LanePosition(lane * 2, width * 2); switch (type) { case 1: //standard notes.Add(new SimpleNote(time, pos, false)); break; case 2: //golden notes.Add(new SimpleNote(time, pos, true)); break; case 3: //swipe notes.Add(new SwipeNote(time, pos)); break; case 4: //mine notes.Add(new Mine(time, pos)); break; //TODO types 5 & 6 default: Debug.LogError($"Ignoring unknown tap type {type} at time {time} position {pos}"); break; } }); continue; } match = _arrow.Match(line); if (match.Success) { int barNum = ParseMmm(match.Groups[1]) + barOffset; int lane = ParseXY(match.Groups[2]); ProcessNotes(match.Groups[3], (barOff, type, width) => { double beat = barToBeat.BeatAt(barNum + barOff); var time = bpmGraph.FromBeat(beat); var pos = new LanePosition(lane * 2, width * 2); bool isUp = true; int arrowShift = 0; //convert the 6 cases to up/down/left/right/center information switch (type) { case 1: isUp = true; arrowShift = 0; break; case 2: isUp = false; arrowShift = 0; break; case 3: isUp = true; arrowShift = -1; break; case 4: isUp = true; arrowShift = +1; break; case 5: isUp = false; arrowShift = +1; //inverted because down break; case 6: isUp = false; arrowShift = -1; //inverted because down break; default: Debug.LogError($"Unknown Air Arrow {type} at time {time} position {pos}"); break; } notes.Add(new AirArrow(time, pos, isUp, arrowShift)); }); continue; } //matching holds, airholds and slides are basically exactly the same, the only thing that changes is the output array match = _hold.Match(line); if (match.Success) { ProcessChannelNotes(match, barOffset, barToBeat, bpmGraph, holdData); continue; } match = _airHold.Match(line); if (match.Success) { ProcessChannelNotes(match, barOffset, barToBeat, bpmGraph, airData); continue; } match = _slide.Match(line); if (match.Success) { ProcessChannelNotes(match, barOffset, barToBeat, bpmGraph, slideData); continue; } //reach here, nothing matched, so we didn't read this line after all hasBeenRead[i] = false; } //all note data has now been read. We'll now assemble all multi-part notes. //sort note data and store length so we can use this for lookups on start/end of holds (TODO) notes.Sort((a, b) => a.Time.Beats.CompareTo(b.Time.Beats)); int noteDataLength = notes.Count; //TODO a lot of code below for (air)holds/slides is shared, may be useful to combine these into a generic function //holds: foreach (var holdChannel in holdData.Values) { //first sort all hold data in this channel holdChannel.Sort((a, b) => a.Time.Beats.CompareTo(b.Time.Beats)); //now walk through each in sequence with a state machine to assemble the holds PartialData?startingPoint = null; foreach (var dataPoint in holdChannel) { switch (dataPoint.Type) { case 1: //start if (startingPoint != null) { Debug.LogError($"Hold started on channel with already active hold at {startingPoint?.Time}"); } startingPoint = dataPoint; break; case 2: //end if (startingPoint == null) { Debug.LogError($"Attempt to end hold at {dataPoint.Time} without a starting point."); continue; } var holdNote = new HoldNote(startingPoint.Value.Time, dataPoint.Pos, new SlidePoint[1]); holdNote.SlidePoints[0] = new SlidePoint(holdNote, dataPoint.Time, holdNote.Position); notes.Add(holdNote); startingPoint = null; break; case 3: //relay Debug.LogWarning($"Skipping hold relay point"); //TODO (will be fixed if we merge slide and hold parsing code) break; default: Debug.LogError($"Unknown hold point type {dataPoint.Type} found at {dataPoint.Time}"); break; } } } //air holds: foreach (var airChannel in airData.Values) { //first sort all hold data in this channel airChannel.Sort((a, b) => a.Time.Beats.CompareTo(b.Time.Beats)); //now walk through each in sequence with a state machine to assemble the holds PartialData?startingPoint = null; foreach (var dataPoint in airChannel) { switch (dataPoint.Type) { case 1: //start if (startingPoint != null) { Debug.LogError($"Air hold started on channel with already active hold at {startingPoint?.Time}"); } startingPoint = dataPoint; break; case 2: //end if (startingPoint == null) { Debug.LogError($"Attempt to end air hold at {dataPoint.Time} without a starting point."); continue; } var airHold = new AirHold(startingPoint.Value.Time, dataPoint.Pos, new SlidePoint[1]); airHold.SlidePoints[0] = new SlidePoint(airHold, dataPoint.Time, airHold.Position); notes.Add(airHold); notes.Add(new AirAction(dataPoint.Time, dataPoint.Pos)); startingPoint = null; break; case 3: //relay notes.Add(new AirAction(dataPoint.Time, dataPoint.Pos)); break; default: Debug.LogError($"Unknown hold point type {dataPoint.Type} found at {dataPoint.Time}"); break; } } } //slides: foreach (var slideChannel in slideData.Values) { //first sort all hold data in this channel slideChannel.Sort((a, b) => a.Time.Beats.CompareTo(b.Time.Beats)); //now walk through each in sequence with a state machine to assemble the holds var dataPoints = new List <PartialData>(); foreach (var dataPoint in slideChannel) { switch (dataPoint.Type) { case 1: //start if (dataPoints.Count > 0) { Debug.LogError($"Hold started on channel with already active slide at {dataPoints[0].Time}"); } dataPoints.Add(dataPoint); break; case 2: //end if (dataPoints.Count == 0) { Debug.LogError($"Attempt to end slide at {dataPoint.Time} without a starting point."); continue; } dataPoints.Add(dataPoint); //we now have all points, create a SlideNote from this: var slidePoints = new List <SlidePoint>(); var slideNote = new SlideNote(dataPoints[0].Time, dataPoints[0].Pos, null); Note previous = slideNote; SlidePoint.AnchorNote anchor = null; for (int i = 1; i < dataPoints.Count; i++) //start from 1, the initial point is the base SlideNote { var pointHere = dataPoints[i]; if (pointHere.Type == 4) { //anchor point anchor = new SlidePoint.AnchorNote(pointHere.Time, pointHere.Pos); continue; } var newPoint = new SlidePoint(previous, pointHere.Time, pointHere.Pos); newPoint.AnchorPoint = anchor; //writes anchor if set, null otherwise anchor = null; newPoint.Visible = pointHere.Type == 3; //set visible/invisible slidePoints.Add(newPoint); previous = newPoint; } slideNote.SlidePoints = slidePoints.ToArray(); notes.Add(slideNote); dataPoints.Clear(); break; case 3: //relay case 4: //bezier anchor case 5: //invisible relay if (dataPoints.Count == 0) { Debug.LogError($"Attempt to add points to slide at {dataPoint.Time} without a starting point."); continue; } dataPoints.Add(dataPoint); break; default: Debug.LogError($"Unknown slide point type {dataPoint.Type} found at {dataPoint.Time}"); break; } } } //finally, sort the notes and return the finished chart notes.Sort((a, b) => a.Time.Beats.CompareTo(b.Time.Beats)); //TODO store difficulty in here var chart = new Chart("undefined", bpmGraph, notes); song.Charts = new List <Chart>(); song.Charts.Add(chart); return(song); }
private void parseState(double offset, string keys, List <RhythmEvent> notes, BpmGraph bpm) { int totalActions = keys.Count(key => "124LF".Contains(key + "")) + _activeHoldCount; int laneWidth = 32 / keys.Length; for (int i = 0; i < keys.Length; i++) { switch (keys[i]) { case '0': //none break; case '1': //normal notes.Add(new SimpleNote(bpm.FromBeat(offset), new LanePosition(i * laneWidth, laneWidth), totalActions >= 2)); break; case 'M': //mine notes.Add(new Mine(bpm.FromBeat(offset), new LanePosition(i * laneWidth, laneWidth))); break; case 'F': //fake //notes.Add(new Mine(bpm.FromBeat(offset), new LanePosition(i * laneWidth, laneWidth), true)); break; case '2': //start hold case '4': //start roll { notes.Add(new SimpleNote(bpm.FromBeat(offset), new LanePosition(i * laneWidth, laneWidth), keys[i] == '4')); var hold = new HoldNote(bpm.FromBeat(offset), new LanePosition(i * laneWidth, laneWidth), new SlidePoint[1]); notes.Add(hold); _activeHoldCount++; _activeHolds[i * laneWidth] = hold; } break; case '3': // end hold/roll { var activeHold = _activeHolds[i * laneWidth]; activeHold.SlidePoints[0] = new SlidePoint(activeHold, bpm.FromBeat(offset), activeHold.Position); _activeHolds[i * laneWidth] = null; _activeHoldCount--; } break; //TODO holds, rolls, lifts, fakes etc. } } }
private void parseMeasure(int beat, string keyData, int keysPerMeasure, List <RhythmEvent> notes, BpmGraph bpm) { if (keyData.Length % keysPerMeasure != 0) { throw new ArgumentException("incorrect number of keys in measure: " + keyData); } int states = keyData.Length / keysPerMeasure; for (int i = 0; i != states; i++) { double offset = (beat + (1.0 / states) * i) * 4; parseState(offset, keyData.Substring(i * keysPerMeasure, keysPerMeasure), notes, bpm); } }
public Song ParseSong(string filename) { var theSourceFile = new FileInfo(filename); var reader = theSourceFile.OpenText(); var contentsBuffer = new StringBuilder(); string line; do { line = reader.ReadLine(); if (line != null) { contentsBuffer.AppendLine(line); } } while (line != null); var contents = contentsBuffer.ToString(); contents = Regex.Replace(contents, @"//.*.\n", ""); contents = Regex.Replace(contents, @"\n|\r", ""); contents = Regex.Replace(contents, @"\t", " "); contents = Regex.Replace(contents, @",#NOTES", ";#NOTES"); var song = new Song(); var charts = new List <Chart>(); var bpmChanges = new List <BeatEvent>(); var bpmStops = new List <BeatEvent>(); BpmGraph bpmGraph = null; string title = ""; string subTitle = ""; string artist = ""; string musicFile = ""; double offset = Double.NaN; foreach (var command in contents.Split(';')) { string[] bits = command.Split(':'); //0=command, 1=data (more for #NOTES) if (bits.Length == 1) // blank given, enter blank in manually { bits = new string[2]; bits[0] = command.Replace(":", ""); bits[1] = ""; } if (!bits[0].StartsWith("#")) { //System.out.println("warning: \"" + command + "\" is not a tag"); continue; } bits[0] = bits[0].Substring(1).ToLower(); //remove # and make lowercase (now case insensitive) switch (bits[0]) { case "title": if (title == "") { title = bits[1]; } break; case "subtitle": if (subTitle == "") { subTitle = bits[1]; } break; case "titletranslit": if (bits[1].Trim() != "") { title = bits[1]; } break; case "subtitletranslit": if (bits[1].Trim() != "") { subTitle = bits[1]; } break; case "artist": case "credit": artist = bits[1]; break; case "music": musicFile = bits[1]; break; case "offset": offset = double.Parse(bits[1], CultureInfo.InvariantCulture); break; case "notes": if (bpmGraph == null) { bpmGraph = BpmGraph.ParseBpm(bpmChanges, bpmStops); } if (bits[1].Replace(" ", "") == "dance-single") { charts.Add(ParseChart(bits, bpmGraph)); } else { //System.out.println("chart type " + bits[1] + ", ignoring"); } break; case "bpms": { string[] bpms = bits[1].Split(','); foreach (var bpm in bpms) { if (bpm != "") { bpmChanges.Add(new BeatEvent(bpm)); } } } break; case "stops": { string[] stops = bits[1].Split(','); if (bits[1] != "") { foreach (var stop in stops) { if (stop != "") { bpmStops.Add(new BeatEvent(stop)); } } } } break; default: //System.out.println("Unknown tag \"" + bits[0] + "\", ignoring"); break; } } song.Name = title; song.AudioFile = musicFile; song.Charts = charts; song.Offset = offset; return(song); }