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>? measures; Map beats = {}; } class Simfile { String? directoryPath; String simfilePath; String? audioPath; String? bannerPath; String? lines; Duration? duration; // tags of simfile Map tags = {}; Chart? chartSimplest; Map bpms = {}; double offset = 0; Simfile(this.simfilePath); /// parses a chart tag with metadata [keys] and note data [value] void _parseChart({required List 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> measures = []; for (final measureRaw in value.split(',')) { List 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 keys = fieldData[1]!.split(':').map((key) => key.trim()).toList(); String value = fieldData[2]!; if (keys[0] == "BPMS") { for (final pairRaw in value.split(',')) { List 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 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; } }