diff options
Diffstat (limited to 'lib/utils')
-rw-r--r-- | lib/utils/esense_input.dart | 166 | ||||
-rw-r--r-- | lib/utils/simfile.dart | 151 |
2 files changed, 317 insertions, 0 deletions
diff --git a/lib/utils/esense_input.dart b/lib/utils/esense_input.dart new file mode 100644 index 0000000..d909c0d --- /dev/null +++ b/lib/utils/esense_input.dart @@ -0,0 +1,166 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:esense_flutter/esense.dart'; +import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:sense_the_rhythm/models/arrow_direction.dart'; +import 'package:sense_the_rhythm/models/input_direction.dart'; + +class ESenseInput { + static final instance = ESenseInput._(); + + ESenseManager eSenseManager = ESenseManager('unknown'); + ValueNotifier<String> deviceStatus = ValueNotifier(''); + StreamSubscription? subscription; + + String eSenseDeviceName = ''; + bool connected = false; + bool sampling = false; + + int sampleRate = 20; + + InputDirection inputDirection = InputDirection(); + int x = 0; + int y = 0; + int z = 0; + + ESenseInput._() { + _listenToESense(); + } + + Future<void> _askForPermissions() async { + if (!Platform.isAndroid && !Platform.isIOS) return; + if (!(await Permission.bluetoothScan.request().isGranted && + await Permission.bluetoothConnect.request().isGranted && + await Permission.bluetooth.request().isGranted)) { + print( + 'WARNING - no permission to use Bluetooth granted. Cannot access eSense device.'); + } + // for some strange reason, Android requires permission to location for Bluetooth to work.....? + if (Platform.isAndroid) { + if (!(await Permission.locationWhenInUse.request().isGranted)) { + print( + 'WARNING - no permission to access location granted. Cannot access eSense device.'); + } + } + } + + void _listenToESense() { + // if you want to get the connection events when connecting, + // set up the listener BEFORE connecting... + eSenseManager.connectionEvents.listen((event) { + print('CONNECTION event: $event'); + + // when we're connected to the eSense device, we can start listening to events from it + // if (event.type == ConnectionType.connected) _listenToESenseEvents(); + + connected = false; + switch (event.type) { + case ConnectionType.connected: + deviceStatus.value = 'connected'; + connected = true; + _startListenToSensorEvents(); + break; + case ConnectionType.unknown: + deviceStatus.value = 'unknown'; + break; + case ConnectionType.disconnected: + deviceStatus.value = 'disconnected'; + sampling = false; + _pauseListenToSensorEvents(); + break; + case ConnectionType.device_found: + deviceStatus.value = 'device_found'; + break; + case ConnectionType.device_not_found: + deviceStatus.value = 'device_not_found'; + break; + } + }); + } + + Stream<ButtonEventChanged> buttonEvents() { + return eSenseManager.eSenseEvents + .where((event) => event.runtimeType == ButtonEventChanged) + .cast(); + } + + void _startListenToSensorEvents() async { + // // any changes to the sampling frequency must be done BEFORE listening to sensor events + print('setting sampling frequency...'); + bool successs = await eSenseManager.setSamplingRate(sampleRate); + if (successs) { + print('setSamplingRate success'); + } else { + print('setSamplingRate fail'); + } + + // subscribe to sensor event from the eSense device + subscription = eSenseManager.sensorEvents.listen((event) { + // print('SENSOR event: $event'); + if (event.gyro != null) { + _parseGyroData(event.gyro!); + } + }); + sampling = true; + } + + void _pauseListenToSensorEvents() async { + subscription?.cancel(); + sampling = false; + } + + void _parseGyroData(List<int> data) { + // Float value in deg/s = Gyro value / Gyro scale factor + // The default configuration is +- 500deg/s for the gyroscope. + x = (x + (15 * data[0] ~/ (500 * sampleRate))) % 360; + y = (y + (15 * data[1] ~/ (500 * sampleRate))) % 360; + z = (z + (15 * data[2] ~/ (500 * sampleRate))) % 360; + print('$x, $y, $z'); + // print('${(z.toDouble() / 500.0 * (1.0 / sampleRate.toDouble())) * 7.5}'); + // print('${z.toDouble() / 500.0 * (1.0 / 10.0)}'); + } + + void resetAngles() { + inputDirection.reset(); + x = 0; + y = 0; + z = 0; + } + + InputDirection getInputDirection(ArrowDirection expect) { + inputDirection.up = z > 270 && z < 340; + inputDirection.down = z > 40 && z < 180; + inputDirection.left = y > 40 && y < 180; + inputDirection.right = y > 270 && y < 340; + + if (expect == ArrowDirection.up && inputDirection.up || + expect == ArrowDirection.down && inputDirection.down) { + y = 0; + print("ehit"); + } + if (expect == ArrowDirection.left && inputDirection.left || + expect == ArrowDirection.right && inputDirection.right) { + z = 0; + print("ehit"); + } + + return inputDirection; + } + + Future<void> connectToESense(String deviceName) async { + if (!connected) { + await _askForPermissions(); + print('Trying to connect to eSense device namend \'$deviceName\''); + eSenseDeviceName = deviceName; + eSenseManager.deviceName = deviceName; + connected = await eSenseManager.connect(); + print( + 'Trying to connect to eSense device namend \'${eSenseManager.deviceName}\''); + + deviceStatus.value = connected ? 'connecting...' : 'connection failed'; + print(deviceStatus.value); + } + } +} diff --git a/lib/utils/simfile.dart b/lib/utils/simfile.dart new file mode 100644 index 0000000..0af734f --- /dev/null +++ b/lib/utils/simfile.dart @@ -0,0 +1,151 @@ +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 directoryPath; + String? simfilePath; + String? audioPath; + String? bannerPath; + String? lines; + + // tags of simfile + Map<String, String> tags = {}; + + Chart? chartSimplest; + + Map<double, double> bpms = {}; + double offset = 0; + + Simfile(this.directoryPath); + + 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() { + simfilePath = Directory(directoryPath) + .listSync() + .firstWhere((entity) => entity.path.endsWith('.sm'), + orElse: () => File('')) + .path; + + audioPath = Directory(directoryPath) + .listSync() + .firstWhere((entity) => entity.path.endsWith('.ogg'), + orElse: () => File('')) + .path; + + bannerPath = Directory(directoryPath) + .listSync() + .firstWhere((file) => file.path.toLowerCase().endsWith('banner.png'), + orElse: () => File('')) + .path; + + lines = File(simfilePath!).readAsStringSync(); + + RegExp commentsRegExp = RegExp(r'//.*$'); + lines = lines?.replaceAll(commentsRegExp, ''); + RegExp fieldDataRegExp = RegExp(r'#([^;]+):([^;]*);'); + + for (final fieldData in fieldDataRegExp.allMatches(lines!)) { + _parseTag(fieldData); + } + } +} |