diff --git a/open_wearable/assets/sound_pulse/heart-beat.wav b/open_wearable/assets/sound_pulse/heart-beat.wav new file mode 100644 index 00000000..a86dd7e9 Binary files /dev/null and b/open_wearable/assets/sound_pulse/heart-beat.wav differ diff --git a/open_wearable/assets/sound_pulse/logo.png b/open_wearable/assets/sound_pulse/logo.png new file mode 100644 index 00000000..083cb16e Binary files /dev/null and b/open_wearable/assets/sound_pulse/logo.png differ diff --git a/open_wearable/lib/apps/sound_pulse/assets/logo.png b/open_wearable/lib/apps/sound_pulse/assets/logo.png new file mode 100644 index 00000000..de820445 Binary files /dev/null and b/open_wearable/lib/apps/sound_pulse/assets/logo.png differ diff --git a/open_wearable/lib/apps/sound_pulse/model/sound_player.dart b/open_wearable/lib/apps/sound_pulse/model/sound_player.dart new file mode 100644 index 00000000..6ceed72c --- /dev/null +++ b/open_wearable/lib/apps/sound_pulse/model/sound_player.dart @@ -0,0 +1,169 @@ +import 'dart:async'; +import 'package:audioplayers/audioplayers.dart'; +import 'package:flutter/services.dart'; +import 'package:logger/logger.dart'; + +class SoundPlayer { + final AudioPlayer _audioPlayer1 = AudioPlayer(); + final AudioPlayer _audioPlayer2 = AudioPlayer(); + Timer? _timer; + bool _isPlaying = false; + late String soundAsset; + final StreamController _playbackController = StreamController.broadcast(); + final Logger logger = Logger(); + int _currentPlayerIndex = 0; + double currentIntervalMs = 1000.0; + + Stream get playbackStream => _playbackController.stream; + + SoundPlayer({String? soundAsset}) { + this.soundAsset = soundAsset ?? 'assets/sound_pulse/heart-beat.mp3'; + _audioPlayer1.setVolume(0.0); + _audioPlayer1.setReleaseMode(ReleaseMode.stop); + _audioPlayer2.setVolume(0.0); + _audioPlayer2.setReleaseMode(ReleaseMode.stop); + } + + AudioPlayer _getCurrentPlayer() { + return _currentPlayerIndex == 0 ? _audioPlayer1 : _audioPlayer2; + } + + void _switchPlayer() { + _currentPlayerIndex = 1 - _currentPlayerIndex; + } + + void start(double intervalMs) { + currentIntervalMs = intervalMs; + if (_isPlaying) stop(); + _isPlaying = true; + logger.d("Starting sound player with interval $intervalMs ms"); + _playbackController.add(true); + _timer = Timer.periodic(Duration(milliseconds: intervalMs.toInt()), (timer) { + _playSound(); + }); + } + + void stop() { + _timer?.cancel(); + _timer = null; + _isPlaying = false; + logger.d("Stopping sound player"); + _audioPlayer1.stop(); + _audioPlayer2.stop(); + _playbackController.add(false); + } + + void updateInterval(double intervalMs) { + currentIntervalMs = intervalMs; + if (_isPlaying) { + _timer?.cancel(); + _timer = Timer.periodic(Duration(milliseconds: intervalMs.toInt()), (timer) { + _playSound(); + }); + } + } + + Future playOnce(String asset) async { + try { + logger.d("Playing once: $asset"); + final bytes = await rootBundle.load(asset); + await _audioPlayer1.stop(); + await _audioPlayer1.setVolume(0.0); + await _audioPlayer1.play(BytesSource(bytes.buffer.asUint8List(), mimeType: _getMimeType(asset))); + _fadeIn(_audioPlayer1); + } catch (e) { + logger.e("Error playing once: $e"); + } + } + + Future playCurrentOnce() async { + try { + final bytes = await rootBundle.load(soundAsset); + await _audioPlayer1.stop(); + await _audioPlayer1.setVolume(0.0); + await _audioPlayer1.play(BytesSource(bytes.buffer.asUint8List(), mimeType: _getMimeType(soundAsset))); + _fadeIn(_audioPlayer1); + } catch (e) { + logger.e("Error playing current: $e"); + } + } + + Future _playSound() async { + try { + logger.d("Playing sound: $soundAsset"); + final bytes = await rootBundle.load(soundAsset); + AudioPlayer player = _getCurrentPlayer(); + _switchPlayer(); + await player.stop(); + await player.setVolume(0.0); + await player.play(BytesSource(bytes.buffer.asUint8List(), mimeType: _getMimeType(soundAsset))); + _fadeIn(player); + // Get duration and calculate playback rate + Duration? duration = await player.getDuration(); + double rate = 1.0; + if (duration != null) { + double intervalSec = currentIntervalMs / 1000.0; + double durSec = duration.inMilliseconds / 1000.0; + if (durSec > intervalSec) { + rate = durSec / intervalSec; + } + await player.setPlaybackRate(rate); + } + // Schedule fade out based on rate + int fadeOutMs = (700 / rate).round(); + Timer(Duration(milliseconds: fadeOutMs), () { + _fadeOut(player); + }); + _playbackController.add(true); + // Reset after 200ms + Future.delayed(Duration(milliseconds: 200), () { + _playbackController.add(false); + }); + } catch (e) { + logger.e("Error playing sound: $e"); + } + } + + void _fadeIn(AudioPlayer player) { + _fadeVolume(player, 0.0, 1.0, 100); + } + + void _fadeOut(AudioPlayer player) { + _fadeVolume(player, 1.0, 0.0, 100); + } + + void _fadeVolume(AudioPlayer player, double from, double to, int durationMs) { + const int steps = 10; + double step = (to - from) / steps; + int stepDuration = durationMs ~/ steps; + double current = from; + int count = 0; + Timer.periodic(Duration(milliseconds: stepDuration), (timer) { + current += step; + player.setVolume(current.clamp(0.0, 1.0)); + count++; + if (count >= steps) { + timer.cancel(); + } + }); + } + + bool get isPlaying => _isPlaying; + + void dispose() { + _audioPlayer1.dispose(); + _audioPlayer2.dispose(); + _timer?.cancel(); + _playbackController.close(); + } + + void changeSound(String newSound) { + soundAsset = 'assets/sound_pulse/$newSound'; + } + + String _getMimeType(String asset) { + if (asset.endsWith('.mp3')) return 'audio/mpeg'; + if (asset.endsWith('.wav')) return 'audio/wav'; + return 'audio/mpeg'; // default + } +} diff --git a/open_wearable/lib/apps/sound_pulse/widgets/sound_pulse_page.dart b/open_wearable/lib/apps/sound_pulse/widgets/sound_pulse_page.dart new file mode 100644 index 00000000..f4276496 --- /dev/null +++ b/open_wearable/lib/apps/sound_pulse/widgets/sound_pulse_page.dart @@ -0,0 +1,436 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; +import 'package:open_earable_flutter/open_earable_flutter.dart'; +import 'package:open_wearable/apps/heart_tracker/model/ppg_filter.dart'; +import 'package:open_wearable/apps/sound_pulse/model/sound_player.dart'; +import 'package:open_wearable/view_models/sensor_configuration_provider.dart'; +import 'package:provider/provider.dart'; +import 'dart:io'; +import 'package:share_plus/share_plus.dart'; +import 'package:path_provider/path_provider.dart'; + +enum OffsetMode { absolute, percentual } + +class SoundPulsePage extends StatefulWidget { + final Sensor ppgSensor; + + const SoundPulsePage({super.key, required this.ppgSensor}); + + @override + State createState() => _SoundPulsePageState(); +} + +class _SoundPulsePageState extends State { + late final PpgFilter ppgFilter; + late final SoundPlayer soundPlayer; + double offsetBpm = 0.0; // Offset in BPM + double offsetPercent = 0.0; // Offset in percent + OffsetMode offsetMode = OffsetMode.absolute; + bool isPlaying = false; + bool isStarting = false; + bool isInitialized = false; + bool isSoundPlaying = false; + Stopwatch stopwatch = Stopwatch(); + int durationMinutes = 0; + int durationSeconds = 0; + Timer? _timer; + TextEditingController minController = TextEditingController(text: '0'); + TextEditingController secController = TextEditingController(text: '0'); + String selectedSound = 'heart-beat.wav'; + static const List availableSounds = ['heart-beat.wav']; + double currentBpm = double.nan; + double currentIntervalSeconds = 0.0; + int playCount = 0; + bool pendingUpdate = false; + bool isRecording = false; + List> sessionData = []; + + @override + void initState() { + super.initState(); + soundPlayer = SoundPlayer(soundAsset: 'assets/sound_pulse/$selectedSound'); + soundPlayer.playbackStream.listen((playing) { + if (playing) { + playCount++; + if (pendingUpdate) { + if (isPlaying && !currentBpm.isNaN) { + double effectiveBpm = offsetMode == OffsetMode.absolute ? currentBpm + offsetBpm : currentBpm * (1 + offsetPercent / 100); + if (effectiveBpm > 0) { + double intervalMs = (60 / effectiveBpm) * 1000; + soundPlayer.updateInterval(intervalMs); + currentIntervalSeconds = 60 / effectiveBpm; + } + } + if (isRecording) { + sessionData.add({ + 'timestamp': DateTime.now().toIso8601String(), + 'bpm': currentBpm, + 'interval': currentIntervalSeconds, + }); + } + pendingUpdate = false; + } + } else { + if (playCount % 9 == 0) { + pendingUpdate = true; + } + } + if (mounted) setState(() => isSoundPlaying = playing); + }); + isInitialized = false; + + final sensor = widget.ppgSensor; + + WidgetsBinding.instance.addPostFrameCallback((_) { + SensorConfigurationProvider configProvider = Provider.of(context, listen: false); + SensorConfiguration configuration = sensor.relatedConfigurations.first; + + if (configuration is ConfigurableSensorConfiguration && + configuration.availableOptions.contains(StreamSensorConfigOption())) { + configProvider.addSensorConfigurationOption(configuration, StreamSensorConfigOption()); + } + + List values = configProvider.getSensorConfigurationValues(configuration, distinct: true); + configProvider.addSensorConfiguration(configuration, values.first); + SensorConfigurationValue selectedValue = configProvider.getSelectedConfigurationValue(configuration)!; + configuration.setConfiguration(selectedValue); + + double sampleFreq; + if (selectedValue is SensorFrequencyConfigurationValue) { + sampleFreq = selectedValue.frequencyHz; + } else { + sampleFreq = 25; + } + + setState(() { + ppgFilter = PpgFilter( + inputStream: sensor.sensorStream.asyncMap((data) { + SensorDoubleValue sensorData = data as SensorDoubleValue; + return ( + sensorData.timestamp, + -(sensorData.values[2] + sensorData.values[3]) + ); + }).asBroadcastStream(), + sampleFreq: sampleFreq, + timestampExponent: sensor.timestampExponent, + ); + isInitialized = true; + }); + ppgFilter.heartRateStream.listen((bpm) { + if (mounted) setState(() => currentBpm = bpm); + }); + }); + } + + @override + void dispose() { + soundPlayer.dispose(); + _timer?.cancel(); + minController.dispose(); + secController.dispose(); + super.dispose(); + } + + void _togglePlay(double bpm) async { + if (isPlaying || isStarting) { + soundPlayer.stop(); + _timer?.cancel(); + stopwatch.stop(); + setState(() { + isPlaying = false; + isStarting = false; + }); + if (isRecording && sessionData.isNotEmpty) { + _exportCSV(); + } + } else { + setState(() => isStarting = true); + double effectiveBpm; + if (offsetMode == OffsetMode.absolute) { + effectiveBpm = bpm + offsetBpm; + } else { + effectiveBpm = bpm * (1 + offsetPercent / 100); + } + if (effectiveBpm > 0) { + double intervalMs = (60 / effectiveBpm) * 1000; + await soundPlayer.playCurrentOnce(); + await Future.delayed(Duration(seconds: 1)); // Wait for sound to play + setState(() { + isStarting = false; + isPlaying = true; + }); + currentIntervalSeconds = 60 / effectiveBpm; + stopwatch.reset(); + stopwatch.start(); + playCount = 0; + pendingUpdate = false; + if (isRecording) { + sessionData.clear(); + sessionData.add({ + 'timestamp': DateTime.now().toIso8601String(), + 'bpm': currentBpm, + 'interval': currentIntervalSeconds, + }); + } + _timer = Timer.periodic(Duration(seconds: 1), (timer) { + setState(() {}); + int elapsed = stopwatch.elapsed.inSeconds; + int totalDuration = durationMinutes * 60 + durationSeconds; + if (elapsed >= totalDuration && totalDuration > 0) { + soundPlayer.playOnce('assets/sound_pulse/timer_done.mp3'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Timer done!", style: TextStyle(color: Colors.white)), + backgroundColor: Colors.green, + duration: Duration(seconds: 3), + ), + ); + _togglePlay(bpm); + } + }); + soundPlayer.start(intervalMs); + } else { + setState(() => isStarting = false); + } + } + } + + Future _exportCSV() async { + String csv = 'Timestamp,BPM,Interval\n'; + for (var record in sessionData) { + csv += '${record['timestamp']},${record['bpm']},${record['interval']}\n'; + } + Directory tempDir = await getTemporaryDirectory(); + String filePath = '${tempDir.path}/session_data.csv'; + File file = File(filePath); + await file.writeAsString(csv); + await Share.shareXFiles([XFile(filePath)], text: 'Session Data CSV'); + } + + @override + Widget build(BuildContext context) { + if (!isInitialized) { + return PlatformScaffold( + appBar: PlatformAppBar(title: PlatformText("Sound Pulse")), + body: Center(child: PlatformCircularProgressIndicator()), + ); + } + return PlatformScaffold( + appBar: PlatformAppBar(title: PlatformText("Sound Pulse")), + body: Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: ppgFilterWidget(), + ), + ); + } + + Widget ppgFilterWidget() { + if (!mounted) { + return Center(child: PlatformCircularProgressIndicator()); + } + + int totalDuration = durationMinutes * 60 + durationSeconds; + + return ListView( + children: [ + Padding( + padding: EdgeInsets.all(10), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + PlatformText( + "${currentBpm.isNaN ? "--" : currentBpm.toStringAsFixed(0)} BPM", + style: Theme.of(context).textTheme.titleLarge, + ), + ], + ), + SizedBox(height: 20), + IgnorePointer( + ignoring: isPlaying || isStarting, + child: Opacity( + opacity: (isPlaying || isStarting) ? 0.5 : 1.0, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Radio( + value: OffsetMode.absolute, + groupValue: offsetMode, + onChanged: (value) => setState(() => offsetMode = value!), + ), + PlatformText("Absolute"), + SizedBox(width: 20), + Radio( + value: OffsetMode.percentual, + groupValue: offsetMode, + onChanged: (value) => setState(() => offsetMode = value!), + ), + PlatformText("Percentual"), + ], + ), + SizedBox(height: 20), + if (offsetMode == OffsetMode.absolute) ...[ + PlatformText("Offset (BPM): ${offsetBpm.toInt()}"), + Slider( + value: offsetBpm, + min: -30, + max: 30, + divisions: 60, + onChanged: (value) { + setState(() => offsetBpm = value); + if (isPlaying && !currentBpm.isNaN) { + double effectiveBpm = currentBpm + offsetBpm; + if (effectiveBpm > 0) { + double intervalMs = (60 / effectiveBpm) * 1000; + soundPlayer.updateInterval(intervalMs); + currentIntervalSeconds = 60 / effectiveBpm; + } + } + }, + ), + ] else ...[ + PlatformText("Offset (%): ${offsetPercent.toInt()}"), + Slider( + value: offsetPercent, + min: -50, + max: 50, + divisions: 100, + onChanged: (value) { + setState(() => offsetPercent = value); + if (isPlaying && !currentBpm.isNaN) { + double effectiveBpm = currentBpm * (1 + offsetPercent / 100); + if (effectiveBpm > 0) { + double intervalMs = (60 / effectiveBpm) * 1000; + soundPlayer.updateInterval(intervalMs); + currentIntervalSeconds = 60 / effectiveBpm; + } + } + }, + ), + ], + SizedBox(height: 20), + Card( + child: Padding( + padding: EdgeInsets.all(10), + child: Column( + children: [ + PlatformText("Set Duration"), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: PlatformTextField( + controller: minController, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + enabled: !isPlaying, + onChanged: (v) { + int val = int.tryParse(v) ?? 0; + if (val < 0) val = 0; + if (val > 999) val = 999; // arbitrary max + setState(() => durationMinutes = val); + minController.text = val.toString(); + minController.selection = TextSelection.fromPosition(TextPosition(offset: minController.text.length)); + }, + ), + ), + SizedBox(width: 10), + PlatformText("min"), + SizedBox(width: 20), + Expanded( + child: PlatformTextField( + controller: secController, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + enabled: !isPlaying, + onChanged: (v) { + int val = int.tryParse(v) ?? 0; + if (val < 0) val = 0; + if (val > 59) val = 59; + setState(() => durationSeconds = val); + secController.text = val.toString(); + secController.selection = TextSelection.fromPosition(TextPosition(offset: secController.text.length)); + }, + ), + ), + SizedBox(width: 10), + PlatformText("sec"), + ], + ), + if (totalDuration < 5) ...[ + SizedBox(height: 10), + PlatformText("Duration must be at least 5 seconds.", style: TextStyle(color: Colors.red)), + ], + ], + ), + ), + ), + SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + PlatformText("Sound: "), + DropdownButton( + value: selectedSound, + items: availableSounds.map((s) => DropdownMenuItem(value: s, child: Text(s.split('.').first))).toList(), + onChanged: (v) { + if (v != null) { + setState(() => selectedSound = v); + soundPlayer.changeSound(v); + } + }, + ), + ], + ), + ], + ), + ), + ), + CheckboxListTile( + title: PlatformText("Record Session"), + value: isRecording, + enabled: !isPlaying && !isStarting, + onChanged: (value) { + setState(() => isRecording = value ?? false); + }, + ), + SizedBox(height: 20), + Card( + child: Padding( + padding: EdgeInsets.all(10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + PlatformText("Elapsed Time: ${isPlaying ? '${stopwatch.elapsed.inMinutes.toString().padLeft(2, '0')}:${(stopwatch.elapsed.inSeconds % 60).toString().padLeft(2, '0')}' : '00:00'}${isPlaying ? ' | Interval: ${currentIntervalSeconds.toStringAsFixed(2)}s' : ''}"), + SizedBox(width: 10), + Icon( + Icons.volume_up, + color: isSoundPlaying ? Colors.green : Colors.grey, + ), + ], + ), + ), + ), + SizedBox(height: 20), + PlatformElevatedButton( + material: (_, __) => MaterialElevatedButtonData( + style: ElevatedButton.styleFrom( + backgroundColor: isPlaying ? Colors.red : Colors.green, + foregroundColor: Colors.white, + ), + ), + onPressed: (currentBpm.isNaN || isStarting || totalDuration < 5) ? null : () => _togglePlay(currentBpm), + child: PlatformText(isStarting ? "Starting..." : isPlaying ? "Stop" : "Start"), + ), + ], + ), + ), + ], + ); + } +} diff --git a/open_wearable/lib/apps/widgets/apps_page.dart b/open_wearable/lib/apps/widgets/apps_page.dart index 9400bf94..f79df445 100644 --- a/open_wearable/lib/apps/widgets/apps_page.dart +++ b/open_wearable/lib/apps/widgets/apps_page.dart @@ -6,10 +6,10 @@ import 'package:open_earable_flutter/open_earable_flutter.dart'; import 'package:open_wearable/apps/heart_tracker/widgets/heart_tracker_page.dart'; import 'package:open_wearable/apps/posture_tracker/model/earable_attitude_tracker.dart'; import 'package:open_wearable/apps/posture_tracker/view/posture_tracker_view.dart'; +import 'package:open_wearable/apps/sound_pulse/widgets/sound_pulse_page.dart'; import 'package:open_wearable/apps/widgets/select_earable_view.dart'; import 'package:open_wearable/apps/widgets/app_tile.dart'; - class AppInfo { final String logoPath; final String title; @@ -64,6 +64,30 @@ List _apps = [ }, ), ), + AppInfo( + logoPath: "lib/apps/sound_pulse/assets/logo.png", + title: "Sound Pulse", + description: "Play sounds relative to your heart rate", + widget: SelectEarableView( + startApp: (wearable, _) { + if (wearable.hasCapability()) { + Sensor ppgSensor = wearable.requireCapability().sensors.firstWhere( + (s) => s.sensorName.toLowerCase() == "photoplethysmography".toLowerCase(), + ); + + return SoundPulsePage(ppgSensor: ppgSensor); + } + return PlatformScaffold( + appBar: PlatformAppBar( + title: PlatformText("Sound Pulse"), + ), + body: Center( + child: PlatformText("No PPG Sensor Found"), + ), + ); + }, + ), + ), ]; class AppsPage extends StatelessWidget { diff --git a/open_wearable/linux/flutter/generated_plugin_registrant.cc b/open_wearable/linux/flutter/generated_plugin_registrant.cc index 86be7eb9..f547c379 100644 --- a/open_wearable/linux/flutter/generated_plugin_registrant.cc +++ b/open_wearable/linux/flutter/generated_plugin_registrant.cc @@ -6,11 +6,15 @@ #include "generated_plugin_registrant.h" +#include #include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin"); + audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar); g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); diff --git a/open_wearable/linux/flutter/generated_plugins.cmake b/open_wearable/linux/flutter/generated_plugins.cmake index c842924f..18a2dcb9 100644 --- a/open_wearable/linux/flutter/generated_plugins.cmake +++ b/open_wearable/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + audioplayers_linux file_selector_linux open_file_linux url_launcher_linux diff --git a/open_wearable/macos/Flutter/GeneratedPluginRegistrant.swift b/open_wearable/macos/Flutter/GeneratedPluginRegistrant.swift index a3e457d4..2c78a003 100644 --- a/open_wearable/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/open_wearable/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,22 +5,22 @@ import FlutterMacOS import Foundation +import audioplayers_darwin import file_picker import file_selector_macos import flutter_archive import open_file_mac -import path_provider_foundation import share_plus import shared_preferences_foundation import universal_ble import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterArchivePlugin.register(with: registry.registrar(forPlugin: "FlutterArchivePlugin")) OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin")) - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UniversalBlePlugin.register(with: registry.registrar(forPlugin: "UniversalBlePlugin")) diff --git a/open_wearable/pubspec.lock b/open_wearable/pubspec.lock index 78870ac3..96ed818b 100644 --- a/open_wearable/pubspec.lock +++ b/open_wearable/pubspec.lock @@ -17,6 +17,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + audioplayers: + dependency: "direct main" + description: + name: audioplayers + sha256: "5441fa0ceb8807a5ad701199806510e56afde2b4913d9d17c2f19f2902cf0ae4" + url: "https://pub.dev" + source: hosted + version: "6.5.1" + audioplayers_android: + dependency: transitive + description: + name: audioplayers_android + sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605" + url: "https://pub.dev" + source: hosted + version: "5.2.1" + audioplayers_darwin: + dependency: transitive + description: + name: audioplayers_darwin + sha256: "0811d6924904ca13f9ef90d19081e4a87f7297ddc19fc3d31f60af1aaafee333" + url: "https://pub.dev" + source: hosted + version: "6.3.0" + audioplayers_linux: + dependency: transitive + description: + name: audioplayers_linux + sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001 + url: "https://pub.dev" + source: hosted + version: "4.2.1" + audioplayers_platform_interface: + dependency: transitive + description: + name: audioplayers_platform_interface + sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656" + url: "https://pub.dev" + source: hosted + version: "7.1.1" + audioplayers_web: + dependency: transitive + description: + name: audioplayers_web + sha256: "1c0f17cec68455556775f1e50ca85c40c05c714a99c5eb1d2d57cc17ba5522d7" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + audioplayers_windows: + dependency: transitive + description: + name: audioplayers_windows + sha256: "4048797865105b26d47628e6abb49231ea5de84884160229251f37dfcbe52fd7" + url: "https://pub.dev" + source: hosted + version: "4.2.1" bloc: dependency: transitive description: @@ -45,10 +101,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" clock: dependency: transitive description: @@ -57,6 +113,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" collection: dependency: transitive description: @@ -157,10 +221,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: d974b6ba2606371ac71dd94254beefb6fa81185bde0b59bdc1df09885da85fde + sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343" url: "https://pub.dev" source: hosted - version: "10.3.8" + version: "10.3.10" file_selector: dependency: "direct main" description: @@ -282,10 +346,10 @@ packages: dependency: "direct main" description: name: flutter_platform_widgets - sha256: "22a86564cb6cc0b93637c813ca91b0b1f61c2681a31e0f9d77590c1fa9f12020" + sha256: aa110ef638076831d060047911a62810d02b4695db58e7682b716c4c4eee65bc url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "10.0.1" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -328,14 +392,30 @@ packages: description: flutter source: sdk version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" go_router: dependency: "direct main" description: name: go_router - sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 + sha256: eff94d2a6fc79fa8b811dde79c7549808c2346037ee107a1121b4a644c745f2a url: "https://pub.dev" source: hosted - version: "14.8.1" + version: "17.0.1" + hooks: + dependency: transitive + description: + name: hooks + sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7" + url: "https://pub.dev" + source: hosted + version: "1.0.0" http: dependency: transitive description: @@ -420,18 +500,18 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" mcumgr_flutter: dependency: "direct main" description: @@ -456,6 +536,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" + url: "https://pub.dev" + source: hosted + version: "0.17.4" nested: dependency: transitive description: @@ -464,6 +552,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "7fd0c4d8ac8980011753b9bdaed2bf15111365924cdeeeaeb596214ea2b03537" + url: "https://pub.dev" + source: hosted + version: "9.2.4" open_earable_flutter: dependency: "direct main" description: @@ -572,10 +668,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "2.6.0" path_provider_linux: dependency: transitive description: @@ -732,10 +828,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc" + sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f url: "https://pub.dev" source: hosted - version: "2.4.18" + version: "2.4.20" shared_preferences_foundation: dependency: transitive description: @@ -813,6 +909,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" term_glyph: dependency: transitive description: @@ -825,10 +929,10 @@ packages: dependency: transitive description: name: test_api - sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.7" tuple: dependency: transitive description: @@ -905,10 +1009,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" url_launcher_windows: dependency: transitive description: @@ -945,10 +1049,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc + sha256: "201e876b5d52753626af64b6359cd13ac6011b80728731428fd34bc840f71c9b" url: "https://pub.dev" source: hosted - version: "1.1.19" + version: "1.1.20" vector_math: dependency: transitive description: @@ -997,6 +1101,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: - dart: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + dart: ">=3.10.3 <4.0.0" + flutter: ">=3.38.4" diff --git a/open_wearable/pubspec.yaml b/open_wearable/pubspec.yaml index 4f1365d1..4f9d1cc8 100644 --- a/open_wearable/pubspec.yaml +++ b/open_wearable/pubspec.yaml @@ -36,7 +36,7 @@ dependencies: cupertino_icons: ^1.0.8 open_file: ^3.3.2 open_earable_flutter: ^2.3.0 - flutter_platform_widgets: ^9.0.0 + flutter_platform_widgets: ^10.0.1 provider: ^6.1.2 logger: ^2.5.0 community_charts_flutter: ^1.0.4 @@ -53,7 +53,8 @@ dependencies: flutter_archive: ^6.0.3 shared_preferences: ^2.5.3 url_launcher: ^6.3.2 - go_router: ^14.6.2 + go_router: ^17.0.1 + audioplayers: ^6.0.0 dev_dependencies: flutter_test: @@ -81,6 +82,8 @@ flutter: assets: - lib/apps/posture_tracker/assets/ - lib/apps/heart_tracker/assets/ + - lib/apps/sound_pulse/assets/ + - assets/sound_pulse/ # An image asset can refer to one or more resolution-specific "variants", seeq # https://flutter.dev/to/resolution-aware-images diff --git a/open_wearable/windows/flutter/generated_plugin_registrant.cc b/open_wearable/windows/flutter/generated_plugin_registrant.cc index 55f09317..37245d29 100644 --- a/open_wearable/windows/flutter/generated_plugin_registrant.cc +++ b/open_wearable/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -13,6 +14,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AudioplayersWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); PermissionHandlerWindowsPluginRegisterWithRegistrar( diff --git a/open_wearable/windows/flutter/generated_plugins.cmake b/open_wearable/windows/flutter/generated_plugins.cmake index 279f13bf..154f7830 100644 --- a/open_wearable/windows/flutter/generated_plugins.cmake +++ b/open_wearable/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + audioplayers_windows file_selector_windows permission_handler_windows share_plus