1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
|
import 'dart:io';
import 'package:audioplayers/audioplayers.dart';
enum Difficulty { Beginner, Easy, Medium, Hard, Challenge, Edit }
// These are the standard note values:
//
// 0 – No note
// 1 – Normal note
// 2 – Hold head
// 3 – Hold/Roll tail
// 4 – Roll head
// M – Mine (or other negative note)
//
// Later versions of StepMania accept other note values which may not work in older versions:
//
// K – Automatic keysound
// L – Lift note
// F – Fake note
RegExp noteTypes = RegExp(r'^([012345MKLF]+)\s*([,;])?');
class Chart {
String? chartType;
// Description/author
String? author;
// Difficulty (one of Beginner, Easy, Medium, Hard, Challenge, Edit)
Difficulty? difficulty;
// Numerical meter
int? numericalMeter;
// Groove radar values, generated by the program
String? radarValues;
List<List<String>>? measures;
Map<double, String> beats = {};
}
class Simfile {
String? directoryPath;
String simfilePath;
String? audioPath;
String? bannerPath;
String? lines;
Duration? duration;
// tags of simfile
Map<String, String> tags = {};
Chart? chartSimplest;
Map<double, double> bpms = {};
double offset = 0;
Simfile(this.simfilePath);
/// parses a chart tag with metadata [keys] and note data [value]
void _parseChart({required List<String> keys, required String value}) {
Chart chart = Chart();
chart.chartType = keys[1];
chart.author = keys[2];
chart.difficulty = Difficulty.values.byName(keys[3]);
chart.numericalMeter = int.parse(keys[4]);
chart.radarValues = keys[5];
// find simplest chart
if (chartSimplest == null ||
(chart.difficulty!.index <= chartSimplest!.difficulty!.index &&
chart.numericalMeter! <= chartSimplest!.numericalMeter!)) {
List<List<String>> measures = [];
for (final measureRaw in value.split(',')) {
List<String> measure = [];
for (final noteRaw in measureRaw.split('\n')) {
String note = noteRaw.trim();
if (noteTypes.hasMatch(note)) {
measure.add(note);
}
}
measures.add(measure);
}
// for now only use the first bpm value
double bpm = bpms.entries.first.value;
// calculate timing for all notes based on offset, bpm and measure
for (final (measureIndex, measure) in measures.indexed) {
for (final (noteIndex, noteData) in measure.indexed) {
double beat = measureIndex * 4.0 +
(noteIndex.toDouble() / measure.length) * 4.0;
double minutesPerBeat = 1.0 / bpm;
double offsetMinutes = offset / 60.0;
chart.beats[beat * minutesPerBeat + offsetMinutes] = noteData;
}
}
chart.measures = measures;
chartSimplest = chart;
}
}
/// parse a tag based on a regex match [fieldData] and parsing the value based
/// on the key
void _parseTag(RegExpMatch fieldData) {
List<String> keys =
fieldData[1]!.split(':').map((key) => key.trim()).toList();
String value = fieldData[2]!;
if (keys[0] == "BPMS") {
for (final pairRaw in value.split(',')) {
List<String> pair = pairRaw.split('=');
if (pair.length != 2) {
continue;
}
double time = double.parse(pair[0]);
double bpm = double.parse(pair[1]);
bpms[time] = bpm;
}
}
if (keys[0] == "OFFSET") {
offset = double.parse(value);
}
if (keys[0] != "NOTES") {
tags[keys[0]] = value;
return;
}
_parseChart(keys: keys, value: value);
}
/// load the simfile
Future<bool> load() async {
directoryPath = File(simfilePath).parent.path;
lines = File(simfilePath).readAsStringSync();
// remove comments
RegExp commentsRegExp = RegExp(r'//.*$');
lines = lines?.replaceAll(commentsRegExp, '');
// find all tags
RegExp fieldDataRegExp = RegExp(r'#([^;]+):([^;]*);');
// parse all tags
for (final fieldData in fieldDataRegExp.allMatches(lines!)) {
try {
_parseTag(fieldData);
} catch (err) {
return false;
}
}
// searching for audio and banned in the directory is more robust than using
// values from metadata as they are wrong more often
for (FileSystemEntity entity in Directory(directoryPath!).listSync()) {
if (entity.path.endsWith('.ogg')) {
audioPath = entity.path;
}
if (entity.path.endsWith('anner.png')) {
bannerPath = entity.path;
}
}
// dont use this simfile of files are missing
if (audioPath == null) return false;
if (bannerPath == null) return false;
// get duration from audio
AudioPlayer audioplayer = AudioPlayer();
await audioplayer.setSource(DeviceFileSource(audioPath!));
duration = await audioplayer.getDuration();
audioplayer.dispose();
return true;
}
}
|