summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/level.dart91
-rw-r--r--lib/simfile.dart109
2 files changed, 181 insertions, 19 deletions
diff --git a/lib/level.dart b/lib/level.dart
index 1391f8a..01083e2 100644
--- a/lib/level.dart
+++ b/lib/level.dart
@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart';
+import 'package:sense_the_rhythm/simfile.dart';
class Level extends StatefulWidget {
const Level({super.key, required this.stepmaniaFolderPath});
@@ -12,8 +13,27 @@ class Level extends StatefulWidget {
State<Level> createState() => _LevelState();
}
+enum ArrowDirection {
+ left(Icons.arrow_back),
+ down(Icons.arrow_downward),
+ up(Icons.arrow_upward),
+ right(Icons.arrow_forward);
+
+ const ArrowDirection(this.icon);
+
+ final IconData icon;
+}
+
+class Note {
+ final double time;
+ final ArrowDirection direction;
+
+ const Note({required this.time, required this.direction});
+}
+
class _LevelState extends State<Level> {
final player = AudioPlayer();
+ Simfile? simfile;
bool _isPlaying = true;
Duration? _duration;
Duration? _position;
@@ -21,6 +41,8 @@ class _LevelState extends State<Level> {
StreamSubscription? _durationSubscription;
StreamSubscription? _positionSubscription;
+ List<Note> notes = [];
+
@override
void setState(VoidCallback fn) {
// Subscriptions only can be closed asynchronously,
@@ -51,10 +73,7 @@ class _LevelState extends State<Level> {
_positionSubscription = player.onPositionChanged.listen(
(p) => setState(() => _position = p),
);
- }
- @override
- Widget build(BuildContext context) {
player.onDurationChanged.listen((Duration d) {
// print('Max duration: $d');
setState(() => _duration = d);
@@ -65,12 +84,47 @@ class _LevelState extends State<Level> {
setState(() => _position = p);
});
+ String simfilePath = Directory(widget.stepmaniaFolderPath)
+ .listSync()
+ .firstWhere((entity) => entity.path.endsWith('.sm'),
+ orElse: () => File(''))
+ .path;
+
String audioPath = Directory(widget.stepmaniaFolderPath)
.listSync()
.firstWhere((entity) => entity.path.endsWith('.ogg'),
orElse: () => File(''))
.path;
+
+ simfile = Simfile(simfilePath);
+ simfile!.load();
+
+ double bpm = simfile!.bpms.entries.first.value;
+
+ for (final (measureIndex, measure)
+ in simfile!.chartSimplest!.measures!.indexed) {
+ for (final (noteIndex, noteData) in measure.indexed) {
+ int arrowIndex = noteData.indexOf('1');
+ if (arrowIndex < 0 || arrowIndex > 3) {
+ continue;
+ }
+ notes.add(Note(
+ time: (measureIndex * 4.0 +
+ (noteIndex.toDouble() / measure.length) * 4.0) *
+ 1.0 /
+ bpm +
+ simfile!.offset / 60.0,
+ direction: ArrowDirection.values[arrowIndex]));
+ }
+ }
+
+ print(audioPath);
+
player.play(DeviceFileSource(audioPath));
+ }
+
+ @override
+ Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
@@ -107,18 +161,16 @@ class _LevelState extends State<Level> {
)),
),
body: Stack(children: [
- Arrow(
- position: -100.0,
- ),
- Arrow(
- position: 00.0,
- ),
- Arrow(
- position: 100.0,
- ),
- Arrow(
- position: 200.0,
- ),
+ ...notes.map((note) {
+ return Arrow(
+ position: _position != null
+ ? (note.time - _position!.inMilliseconds / 60000.0) *
+ 20 *
+ MediaQuery.of(context).size.height
+ : 0.0,
+ direction: note.direction,
+ );
+ }),
Positioned(
top: 50,
width: MediaQuery.of(context).size.width,
@@ -155,15 +207,16 @@ class _LevelState extends State<Level> {
class Arrow extends StatelessWidget {
final double position;
+ final ArrowDirection direction;
- const Arrow({super.key, required this.position});
+ const Arrow({super.key, required this.position, required this.direction});
@override
Widget build(BuildContext context) {
return Positioned(
- left: MediaQuery.of(context).size.width / 2 - 25, // Center the arrow
- top: position,
- child: Icon(size: 100, Icons.arrow_forward),
+ left: MediaQuery.of(context).size.width / 2 - 50, // Center the arrow
+ bottom: position + 50,
+ child: Icon(size: 100, color: Colors.redAccent.shade400, direction.icon),
);
}
}
diff --git a/lib/simfile.dart b/lib/simfile.dart
new file mode 100644
index 0000000..2174de9
--- /dev/null
+++ b/lib/simfile.dart
@@ -0,0 +1,109 @@
+import 'dart:ffi';
+import 'dart:io';
+
+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;
+}
+
+class Simfile {
+ String path;
+ String? lines;
+
+ // tags of simfile
+ Map<String, String> tags = {};
+
+ Chart? chartSimplest;
+
+ Map<double, double> bpms = {};
+ double offset = 0;
+
+ Simfile(this.path);
+
+ void load() {
+ lines = File(path).readAsStringSync();
+
+ RegExp commentsRegExp = RegExp(r'//.*$');
+ lines = lines?.replaceAll(commentsRegExp, '');
+ RegExp fieldDataRegExp = RegExp(r'#([^;]+):([^;]*);');
+
+ for (final fieldData in fieldDataRegExp.allMatches(lines!)) {
+ 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;
+ continue;
+ }
+
+ 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];
+
+ 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);
+ }
+ chart.measures = measures;
+ chartSimplest = chart;
+ }
+ }
+ }
+}