Sense_the_Rhythm/lib/utils/simfile.dart

176 lines
4.8 KiB
Dart
Raw Normal View History

2024-12-28 01:29:12 +01:00
import 'dart:io';
import 'package:audioplayers/audioplayers.dart';
2024-12-28 01:29:12 +01:00
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 = {};
2024-12-28 01:29:12 +01:00
}
class Simfile {
String? directoryPath;
String simfilePath;
String? audioPath;
String? bannerPath;
2024-12-28 01:29:12 +01:00
String? lines;
Duration? duration;
2024-12-28 01:29:12 +01:00
// tags of simfile
Map<String, String> tags = {};
Chart? chartSimplest;
Map<double, double> bpms = {};
double offset = 0;
Simfile(this.simfilePath);
2024-12-28 01:29:12 +01:00
2025-01-14 17:35:24 +01:00
/// 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];
2025-01-14 17:35:24 +01:00
// 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);
}
2025-01-14 17:35:24 +01:00
// for now only use the first bpm value
double bpm = bpms.entries.first.value;
2025-01-14 17:35:24 +01:00
// 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;
}
2024-12-28 01:29:12 +01:00
}
chart.measures = measures;
chartSimplest = chart;
}
}
2025-01-14 17:35:24 +01:00
/// 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]!;
2025-01-14 17:35:24 +01:00
if (keys[0] == "BPMS") {
for (final pairRaw in value.split(',')) {
List<String> pair = pairRaw.split('=');
if (pair.length != 2) {
continue;
2024-12-28 01:29:12 +01:00
}
double time = double.parse(pair[0]);
double bpm = double.parse(pair[1]);
bpms[time] = bpm;
2024-12-28 01:29:12 +01:00
}
}
if (keys[0] == "OFFSET") {
offset = double.parse(value);
}
if (keys[0] != "NOTES") {
tags[keys[0]] = value;
return;
}
_parseChart(keys: keys, value: value);
}
2025-01-14 17:35:24 +01:00
/// load the simfile
Future<bool> load() async {
directoryPath = File(simfilePath).parent.path;
lines = File(simfilePath).readAsStringSync();
2025-01-14 17:35:24 +01:00
// remove comments
RegExp commentsRegExp = RegExp(r'//.*$');
lines = lines?.replaceAll(commentsRegExp, '');
2025-01-14 17:35:24 +01:00
// find all tags
RegExp fieldDataRegExp = RegExp(r'#([^;]+):([^;]*);');
2025-01-14 17:35:24 +01:00
// parse all tags
for (final fieldData in fieldDataRegExp.allMatches(lines!)) {
try {
_parseTag(fieldData);
} catch (err) {
return false;
}
}
2025-01-14 17:35:24 +01:00
// 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;
}
}
2025-01-14 17:35:24 +01:00
// 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;
2024-12-28 01:29:12 +01:00
}
}