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