summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/arrows.dart57
-rw-r--r--lib/level.dart257
-rw-r--r--lib/simfile.dart131
3 files changed, 359 insertions, 86 deletions
diff --git a/lib/arrows.dart b/lib/arrows.dart
new file mode 100644
index 0000000..ff53e02
--- /dev/null
+++ b/lib/arrows.dart
@@ -0,0 +1,57 @@
+import 'package:flutter/material.dart';
+
+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;
+ double position = 0;
+ bool? wasHit;
+
+ Note({required this.time, required this.direction});
+}
+
+class Arrows extends StatelessWidget {
+ final List<Note> notes;
+ final double position;
+
+ const Arrows({super.key, required this.notes, required this.position});
+
+ @override
+ Widget build(BuildContext context) {
+ return Stack(
+ children: notes.map((note) {
+ double position = note.position * 10000; // * 20 * MediaQuery.of(context).size.height;
+
+ return Arrow(
+ position: position,
+ direction: note.direction,
+ );
+ }).toList());
+ }
+}
+
+class Arrow extends StatelessWidget {
+ final double position;
+ final ArrowDirection direction;
+
+ const Arrow({super.key, required this.position, required this.direction});
+
+ @override
+ Widget build(BuildContext context) {
+ return Positioned(
+ 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/level.dart b/lib/level.dart
index 1391f8a..aa042f4 100644
--- a/lib/level.dart
+++ b/lib/level.dart
@@ -3,6 +3,9 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart';
+import 'package:flutter/services.dart';
+import 'package:sense_the_rhythm/arrows.dart';
+import 'package:sense_the_rhythm/simfile.dart';
class Level extends StatefulWidget {
const Level({super.key, required this.stepmaniaFolderPath});
@@ -12,8 +15,16 @@ class Level extends StatefulWidget {
State<Level> createState() => _LevelState();
}
+class InputDirection {
+ bool up = false;
+ bool down = false;
+ bool left = false;
+ bool right = false;
+}
+
class _LevelState extends State<Level> {
final player = AudioPlayer();
+ Simfile? simfile;
bool _isPlaying = true;
Duration? _duration;
Duration? _position;
@@ -21,6 +32,11 @@ class _LevelState extends State<Level> {
StreamSubscription? _durationSubscription;
StreamSubscription? _positionSubscription;
+ final FocusNode _focusNode = FocusNode();
+ InputDirection inputDirection = InputDirection();
+
+ List<Note> notes = [];
+
@override
void setState(VoidCallback fn) {
// Subscriptions only can be closed asynchronously,
@@ -51,10 +67,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);
@@ -63,85 +76,172 @@ class _LevelState extends State<Level> {
player.onPositionChanged.listen((Duration p) {
// print('Current position: $p');
setState(() => _position = p);
+ for (final note in notes) {
+ note.position = note.time - p.inMilliseconds / 60000.0;
+ if (note.wasHit != null) {
+ continue;
+ }
+ if (note.position.abs() < 0.5 * 1.0 / 60.0) {
+ bool keypressCorrect = false;
+ switch (note.direction) {
+ case ArrowDirection.up:
+ keypressCorrect = inputDirection.up;
+ break;
+ case ArrowDirection.down:
+ keypressCorrect = inputDirection.down;
+ break;
+ case ArrowDirection.right:
+ keypressCorrect = inputDirection.right;
+ break;
+ case ArrowDirection.left:
+ keypressCorrect = inputDirection.left;
+ break;
+ }
+ if (keypressCorrect) {
+ print("you hit!");
+ note.wasHit = true;
+
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Great!'),
+ duration: Duration(milliseconds: 500),
+ ),
+ );
+ }
+ } else if (note.position < -0.5 * 1.0 / 60.0) {
+ print("Missed");
+ note.wasHit = false;
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Missed!'),
+ duration: Duration(milliseconds: 500),
+ ),
+ );
+ }
+ }
});
+ 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();
+
+ simfile!.chartSimplest!.beats.forEach((time, noteData) {
+ int arrowIndex = noteData.indexOf('1');
+ if (arrowIndex < 0 || arrowIndex > 3) {
+ return;
+ }
+ notes.add(Note(time: time, direction: ArrowDirection.values[arrowIndex]));
+ });
+
+ print(audioPath);
+
player.play(DeviceFileSource(audioPath));
- return Scaffold(
- appBar: AppBar(
- leading: IconButton(
- icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow),
- onPressed: () {
- if (_isPlaying) {
- player.pause();
- setState(() {
- _isPlaying = false;
- });
- } else {
- player.resume();
- setState(() {
- _isPlaying = true;
- });
- }
- },
- ),
- title: Text(widget.stepmaniaFolderPath.split('/').last),
- actions: [
- IconButton(
- icon: Icon(Icons.close),
- onPressed: () => Navigator.pop(context))
- ],
- bottom: PreferredSize(
- preferredSize: Size(double.infinity, 1.0),
- child: LinearProgressIndicator(
- value: (_duration != null &&
- _position != null &&
- _position!.inMilliseconds > 0 &&
- _position!.inMilliseconds < _duration!.inMilliseconds)
- ? _position!.inMilliseconds / _duration!.inMilliseconds
- : 0.0,
- )),
- ),
- body: Stack(children: [
- Arrow(
- position: -100.0,
- ),
- Arrow(
- position: 00.0,
- ),
- Arrow(
- position: 100.0,
- ),
- Arrow(
- position: 200.0,
- ),
- Positioned(
- top: 50,
- width: MediaQuery.of(context).size.width,
- left: 0,
- child: Text(
- "Great!",
- textScaler: TextScaler.linear(4),
- textAlign: TextAlign.center,
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return KeyboardListener(
+ focusNode: _focusNode,
+ autofocus: true,
+ onKeyEvent: (event) {
+ bool isDown = false;
+ if (event is KeyDownEvent) {
+ isDown = true;
+ } else if (event is KeyUpEvent) {
+ isDown = false;
+ } else {
+ return;
+ }
+ switch (event.logicalKey) {
+ case LogicalKeyboardKey.arrowUp:
+ inputDirection.up = isDown;
+ break;
+ case LogicalKeyboardKey.arrowDown:
+ inputDirection.down = isDown;
+ break;
+ case LogicalKeyboardKey.arrowLeft:
+ inputDirection.left = isDown;
+ break;
+ case LogicalKeyboardKey.arrowRight:
+ inputDirection.right = isDown;
+ break;
+ }
+ },
+ child: Scaffold(
+ appBar: AppBar(
+ leading: IconButton(
+ icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow),
+ onPressed: () {
+ if (_isPlaying) {
+ player.pause();
+ setState(() {
+ _isPlaying = false;
+ });
+ } else {
+ player.resume();
+ setState(() {
+ _isPlaying = true;
+ });
+ }
+ },
),
+ title: Text(widget.stepmaniaFolderPath.split('/').last),
+ actions: [
+ IconButton(
+ icon: Icon(Icons.close),
+ onPressed: () => Navigator.pop(context))
+ ],
+ bottom: PreferredSize(
+ preferredSize: Size(double.infinity, 1.0),
+ child: LinearProgressIndicator(
+ value: (_duration != null &&
+ _position != null &&
+ _position!.inMilliseconds > 0 &&
+ _position!.inMilliseconds < _duration!.inMilliseconds)
+ ? _position!.inMilliseconds / _duration!.inMilliseconds
+ : 0.0,
+ )),
),
- Positioned(
- left: MediaQuery.of(context).size.width / 2 - 50,
- bottom: 50,
- child: Container(
- width: 100,
- height: 100,
- decoration: BoxDecoration(
- shape: BoxShape.circle,
- // color: Colors.blue,
- border: Border.all(color: Colors.black, width: 10)),
+ body: Stack(children: [
+ Arrows(
+ notes: notes,
+ position: _position != null
+ ? _position!.inMilliseconds.toDouble()
+ : 0.0),
+ Positioned(
+ top: 50,
+ width: MediaQuery.of(context).size.width,
+ left: 0,
+ child: Text(
+ "Great!",
+ textScaler: TextScaler.linear(4),
+ textAlign: TextAlign.center,
+ ),
),
- ),
- ]));
+ Positioned(
+ left: MediaQuery.of(context).size.width / 2 - 50,
+ bottom: 50,
+ child: Container(
+ width: 100,
+ height: 100,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ border: Border.all(color: Colors.black, width: 10)),
+ ),
+ ),
+ ])),
+ );
}
@override
@@ -152,18 +252,3 @@ class _LevelState extends State<Level> {
super.dispose();
}
}
-
-class Arrow extends StatelessWidget {
- final double position;
-
- const Arrow({super.key, required this.position});
-
- @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),
- );
- }
-}
diff --git a/lib/simfile.dart b/lib/simfile.dart
new file mode 100644
index 0000000..102c989
--- /dev/null
+++ b/lib/simfile.dart
@@ -0,0 +1,131 @@
+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;
+
+ Map<double, String> beats = {};
+}
+
+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 _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];
+
+ 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);
+ }
+
+ double bpm = bpms.entries.first.value;
+
+ 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;
+ }
+ }
+
+ 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);
+ }
+
+ 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!)) {
+ _parseTag(fieldData);
+ }
+ }
+}