Sense_the_Rhythm/lib/utils/simfile.dart
2025-01-14 17:35:24 +01:00

176 lines
4.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}