sense_the_rythm

rythm game for ESense Earable
git clone git://source.orangerot.dev:/university/sense_the_rythm.git
Log | Files | Refs | README | LICENSE

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 }