summaryrefslogtreecommitdiff
path: root/lib/utils/simfile.dart
diff options
context:
space:
mode:
Diffstat (limited to 'lib/utils/simfile.dart')
-rw-r--r--lib/utils/simfile.dart175
1 files changed, 175 insertions, 0 deletions
diff --git a/lib/utils/simfile.dart b/lib/utils/simfile.dart
new file mode 100644
index 0000000..71613a9
--- /dev/null
+++ b/lib/utils/simfile.dart
@@ -0,0 +1,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;
+ }
+}