diff options
Diffstat (limited to 'lib/utils/simfile.dart')
-rw-r--r-- | lib/utils/simfile.dart | 175 |
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; + } +} |