summaryrefslogtreecommitdiff
path: root/lib/utils/simfile.dart
blob: 71613a9cedf9050101ecde1e3f198ef249b7d65d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
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;
  }
}