From 1843a74b9d32162ec27c6b9cf4671e0d7f315dc0 Mon Sep 17 00:00:00 2001 From: bobbykumar706584 Date: Mon, 1 Jul 2024 14:17:55 +0530 Subject: [PATCH 1/2] audio recorder --- .../obs/obs_demo/android/app/build.gradle | 5 + .../android/app/src/main/AndroidManifest.xml | 1 + obs_flutter/obs/obs_demo/lib/editortext.dart | 422 +++++++++++++----- obs_flutter/obs/obs_demo/lib/main.dart | 19 +- obs_flutter/obs/obs_demo/pubspec.lock | 8 + obs_flutter/obs/obs_demo/pubspec.yaml | 1 + 6 files changed, 336 insertions(+), 120 deletions(-) diff --git a/obs_flutter/obs/obs_demo/android/app/build.gradle b/obs_flutter/obs/obs_demo/android/app/build.gradle index c230f7c..353b070 100644 --- a/obs_flutter/obs/obs_demo/android/app/build.gradle +++ b/obs_flutter/obs/obs_demo/android/app/build.gradle @@ -28,6 +28,11 @@ android { compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion + configurations { + implementation.exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8' + } + + compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 diff --git a/obs_flutter/obs/obs_demo/android/app/src/main/AndroidManifest.xml b/obs_flutter/obs/obs_demo/android/app/src/main/AndroidManifest.xml index ceb8ef8..282f6a1 100644 --- a/obs_flutter/obs/obs_demo/android/app/src/main/AndroidManifest.xml +++ b/obs_flutter/obs/obs_demo/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ + { final TextEditingController _controller = TextEditingController(); String _errorMessage = ""; String _textFieldValue = ""; + + late final RecorderController recorderController; + late final PlayerController playerController; + + String? _recordedFilePath; + String? _audioFilePath; + Future fetchStoryText() async { final jsonString = await rootBundle.loadString('assets/OBSTextData.json'); setState(() { @@ -80,6 +87,131 @@ class _EditorTextLayoutState extends State { super.initState(); fetchStoryText(); fetchJson(); + _initialiseControllers(); + } + + void _initialiseControllers() async { + recorderController = RecorderController() + ..androidEncoder = AndroidEncoder.aac + ..androidOutputFormat = AndroidOutputFormat.mpeg4 + ..iosEncoder = IosEncoder.kAudioFormatMPEG4AAC + ..sampleRate = 16000; + + playerController = PlayerController(); + + // Prepare path for recording and audio playback + await _preparePaths(); + } + + Future _preparePaths() async { + Directory directory = await getApplicationDocumentsDirectory(); + int storyId = storyDatas[storyIndex]['storyId']; + int paraId = storyDatas[storyIndex]['story'][paraIndex]['id']; + + _recordedFilePath = '${directory.path}/OBS_${storyId}_$paraId.wav'; + _audioFilePath = '${directory.path}/OBS_${storyId}_$paraId.wav'; + } + + void _startRecording() async { + int storyId = storyDatas[storyIndex]['storyId']; + int paraId = storyDatas[storyIndex]['story'][paraIndex]['id']; + + _recordedFilePath = + '${(await getApplicationDocumentsDirectory()).path}/OBS_${storyId}_$paraId.wav'; + + if (story['story'][paraIndex]['audio'] == null) { + try { + await recorderController.record(path: _recordedFilePath!); + } catch (e) { + print('Error recording: $e'); + } + } else { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: Text('Audio already recorded'), + content: Text('Do you want to re-record the audio?'), + actions: [ + TextButton( + child: Text('Yes'), + onPressed: () async { + Navigator.of(context).pop(); + await _startNewRecording(); + }, + ), + TextButton( + child: Text('No'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ), + ); + } + } + + Future _startNewRecording() async { + try { + if (_recordedFilePath != null) { + final file = File(_recordedFilePath!); + if (await file.exists()) { + await file.delete(); + print('Deleted old recorded file: $_recordedFilePath'); + } + } + + final path = await recorderController.record(path: _recordedFilePath!); + setState(() { + story['story'][paraIndex]['audio'] = _recordedFilePath; + }); + } catch (e) { + print('Error starting new recording: $e'); + } + } + + void _stopRecording() async { + try { + final path = await recorderController.stop(); + setState(() { + _recordedFilePath = path; + story['story'][paraIndex]['audio'] = _recordedFilePath; + }); + writeJsonToFile(story); + } catch (e) { + print('Error stopping recording: $e'); + } + } + + Future _startPlayback() async { + if (_audioFilePath != null) { + try { + await playerController.preparePlayer( + path: _audioFilePath!, + shouldExtractWaveform: true, + ); + await playerController.startPlayer(); + } catch (e) { + print('Error starting playback: $e'); + } + } else { + print('Audio file path is null. Cannot start playback.'); + } + } + + void _stopPlayback() async { + try { + await playerController.stopPlayer(); + } catch (e) { + print('Error stopping playback: $e'); + } + } + + @override + void dispose() { + super.dispose(); + recorderController.dispose(); + playerController.dispose(); } @override @@ -136,119 +268,185 @@ class _EditorTextLayoutState extends State { ), Expanded( child: SingleChildScrollView( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - storyIndex != 0 - ? IconButton( - icon: const Icon(Icons.skip_previous), - iconSize: 35, - onPressed: () { - setState(() { - storyIndex = - storyIndex > 0 ? storyIndex - 1 : 0; - paraIndex = 0; - }); - fetchJson(); - }, - ) - : IconButton( - icon: Icon(Icons.skip_previous), - iconSize: 35, - color: Color.fromARGB(66, 168, 163, 163) - .withOpacity(0.5), - onPressed: () {}, - ), - paraIndex != 0 - ? IconButton( - icon: Icon(Icons.arrow_left_sharp), - iconSize: 35, - onPressed: () { - setState(() { - paraIndex = - paraIndex > 0 ? paraIndex - 1 : 0; - }); - _controller.text = - story['story'][paraIndex]['text']; - }, - ) - : IconButton( - icon: Icon(Icons.arrow_left_sharp), - iconSize: 35, - color: Color.fromARGB(66, 168, 163, 163) - .withOpacity(0.5), - onPressed: () {}, - ), - Text(storyDatas[storyIndex]['storyId'].toString()), - const Text(":"), - Text(storyDatas[storyIndex]['story'][paraIndex]['id'] - .toString()), - paraIndex != storyDatas[storyIndex]['story'].length - 1 - ? IconButton( - icon: Icon(Icons.arrow_right_sharp), - iconSize: 35, - onPressed: () { - setState(() { - paraIndex = paraIndex + 1; - }); - _controller.text = - story['story'][paraIndex]['text']; - }, - ) - : IconButton( - icon: Icon(Icons.arrow_right_sharp), - iconSize: 35, - color: Colors.black26.withOpacity(0.5), - onPressed: () {}, - ), - storyIndex != storyDatas.length - 1 - ? IconButton( - icon: Icon(Icons.skip_next), - iconSize: 35, - onPressed: () { - setState(() { - storyIndex = storyIndex + 1; - paraIndex = 0; - }); - fetchJson(); - }, - ) - : IconButton( - icon: Icon(Icons.skip_next), - iconSize: 35, - color: Color.fromARGB(66, 168, 163, 163) - .withOpacity(0.5), - onPressed: () {}, - ), - ], - ), - Padding( - padding: const EdgeInsets.all(4.0), - child: SizedBox( - height: 200, - child: TextField( - controller: _controller, - onChanged: (value) { - setState(() { - _textFieldValue = value; - }); - saveData(_textFieldValue); - }, - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: 'Enter your text', - errorText: - _errorMessage.isNotEmpty ? _errorMessage : null, - ), - maxLines: - 30, // Increases the height to accommodate up to 30 lines + child: Column(children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + storyIndex != 0 + ? IconButton( + icon: const Icon(Icons.skip_previous), + iconSize: 35, + onPressed: () { + setState(() { + storyIndex = + storyIndex > 0 ? storyIndex - 1 : 0; + paraIndex = 0; + }); + fetchJson(); + }, + ) + : IconButton( + icon: Icon(Icons.skip_previous), + iconSize: 35, + color: Color.fromARGB(66, 168, 163, 163) + .withOpacity(0.5), + onPressed: () {}, + ), + paraIndex != 0 + ? IconButton( + icon: Icon(Icons.arrow_left_sharp), + iconSize: 35, + onPressed: () { + setState(() { + paraIndex = paraIndex > 0 ? paraIndex - 1 : 0; + }); + _controller.text = + story['story'][paraIndex]['text']; + }, + ) + : IconButton( + icon: Icon(Icons.arrow_left_sharp), + iconSize: 35, + color: Color.fromARGB(66, 168, 163, 163) + .withOpacity(0.5), + onPressed: () {}, + ), + Text(storyDatas[storyIndex]['storyId'].toString()), + const Text(":"), + Text(storyDatas[storyIndex]['story'][paraIndex]['id'] + .toString()), + paraIndex != storyDatas[storyIndex]['story'].length - 1 + ? IconButton( + icon: Icon(Icons.arrow_right_sharp), + iconSize: 35, + onPressed: () { + setState(() { + paraIndex = paraIndex + 1; + }); + _controller.text = + story['story'][paraIndex]['text']; + }, + ) + : IconButton( + icon: Icon(Icons.arrow_right_sharp), + iconSize: 35, + color: Colors.black26.withOpacity(0.5), + onPressed: () {}, + ), + storyIndex != storyDatas.length - 1 + ? IconButton( + icon: Icon(Icons.skip_next), + iconSize: 35, + onPressed: () { + setState(() { + storyIndex = storyIndex + 1; + paraIndex = 0; + }); + fetchJson(); + }, + ) + : IconButton( + icon: Icon(Icons.skip_next), + iconSize: 35, + color: Color.fromARGB(66, 168, 163, 163) + .withOpacity(0.5), + onPressed: () {}, + ), + ], + ), + Padding( + padding: const EdgeInsets.all(4.0), + child: SizedBox( + height: 200, + child: TextField( + controller: _controller, + onChanged: (value) { + setState(() { + _textFieldValue = value; + }); + saveData(_textFieldValue); + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: 'Enter your text', + errorText: + _errorMessage.isNotEmpty ? _errorMessage : null, ), + maxLines: + 30, // Increases the height to accommodate up to 30 lines ), ), - ], - ), + ), + _recordedFilePath != null + ? AudioWaveforms( + enableGesture: true, + size: Size( + MediaQuery.of(context).size.width / 1.07, 80), + recorderController: recorderController, + waveStyle: WaveStyle( + waveColor: Colors.white, + extendWaveform: true, + showMiddleLine: true, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.0), + color: Color.fromARGB(255, 114, 106, 136), + ), + ) + : SizedBox.shrink(), + _audioFilePath != null + ? AudioFileWaveforms( + size: Size(MediaQuery.of(context).size.width, 10), + playerController: playerController, + playerWaveStyle: const PlayerWaveStyle( + scaleFactor: 0.8, + fixedWaveColor: Colors.white30, + liveWaveColor: Colors.white, + waveCap: StrokeCap.butt, + ), + ) + : SizedBox.shrink(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: Icon(Icons.mic), + iconSize: 50, + onPressed: _startRecording, + ), + IconButton( + icon: Icon(Icons.stop), + iconSize: 50, + onPressed: _stopRecording, + ), + SizedBox(width: 20), + // IconButton( + // icon: Icon(Icons.play_arrow), + // iconSize: 50, + // onPressed: () async { + // if (playerController.playerState == + // PlayerState.playing) { + // _stopPlayback(); + // } else { + // await _startPlayback(); + // } + // }, + // ), + IconButton( + icon: Icon(Icons.play_arrow), + iconSize: 50, + onPressed: _startPlayback, + ), + IconButton( + icon: Icon(Icons.stop_circle), + iconSize: 50, + onPressed: _stopPlayback, + ), + // if (playerController.playerState == PlayerState.playing) + // const Text('Playing...'), + ], + ), + ]), ), ), ], @@ -259,7 +457,9 @@ class _EditorTextLayoutState extends State { void saveData(String value) async { story['story'][paraIndex]['text'] = value; + story['story'][paraIndex]['audio'] = _recordedFilePath; writeJsonToFile(story); + print('Data saved: $value'); // You can perform saving operations here, like storing to a database, file, etc. } diff --git a/obs_flutter/obs/obs_demo/lib/main.dart b/obs_flutter/obs/obs_demo/lib/main.dart index 8dc40ee..1e28d7c 100644 --- a/obs_flutter/obs/obs_demo/lib/main.dart +++ b/obs_flutter/obs/obs_demo/lib/main.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:obs_demo/CreateUserPage.dart'; import 'package:obs_demo/screen/bottomNavi.dart'; +import 'package:obs_demo/screen/dashboard.dart'; void main() { runApp(const MyApp()); @@ -12,14 +13,14 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - debugShowCheckedModeBanner: false, - title: 'Flutter Demo', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - useMaterial3: true, - ), - home: CreateUserPage() // Navigate to UserProfilePage directly - // home: BottomNavigationBarExample(), - ); + debugShowCheckedModeBanner: false, + title: 'Flutter Demo', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + // home: CreateUserPage() // Navigate to UserProfilePage directly + home: Dashboard(), + ); } } diff --git a/obs_flutter/obs/obs_demo/pubspec.lock b/obs_flutter/obs/obs_demo/pubspec.lock index ca691a4..375080b 100644 --- a/obs_flutter/obs/obs_demo/pubspec.lock +++ b/obs_flutter/obs/obs_demo/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + audio_waveforms: + dependency: "direct main" + description: + name: audio_waveforms + sha256: e1425956cfa07bf8cfa02826bb509db252553964484ac516ba6e2c65b64e018e + url: "https://pub.dev" + source: hosted + version: "1.0.5" boolean_selector: dependency: transitive description: diff --git a/obs_flutter/obs/obs_demo/pubspec.yaml b/obs_flutter/obs/obs_demo/pubspec.yaml index 58a9112..d921dde 100644 --- a/obs_flutter/obs/obs_demo/pubspec.yaml +++ b/obs_flutter/obs/obs_demo/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: http: ^0.13.3 path_provider: ^2.0.2 + audio_waveforms: ^1.0.5 dev_dependencies: flutter_test: From c2bca7a3afa8ccf89f0968e3e2f2817d404b5936 Mon Sep 17 00:00:00 2001 From: bobbykumar706584 Date: Wed, 3 Jul 2024 11:04:51 +0530 Subject: [PATCH 2/2] audio bubble --- .../obs/obs_demo/lib/audio_bubble.dart | 90 +++++++++ obs_flutter/obs/obs_demo/lib/editortext.dart | 179 +++++++++++------- 2 files changed, 203 insertions(+), 66 deletions(-) create mode 100644 obs_flutter/obs/obs_demo/lib/audio_bubble.dart diff --git a/obs_flutter/obs/obs_demo/lib/audio_bubble.dart b/obs_flutter/obs/obs_demo/lib/audio_bubble.dart new file mode 100644 index 0000000..c31c15e --- /dev/null +++ b/obs_flutter/obs/obs_demo/lib/audio_bubble.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:audio_waveforms/audio_waveforms.dart'; + +class AudioBubble extends StatelessWidget { + final String? recordedFilePath; + final RecorderController? recorderController; + final PlayerController? playerController; + final int currentPositionInSeconds; + + const AudioBubble({ + Key? key, + this.recordedFilePath, + this.recorderController, + this.playerController, + required this.currentPositionInSeconds, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(10), + margin: const EdgeInsets.all(10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: const Color.fromARGB(255, 92, 91, 95), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + if (recordedFilePath != null && recorderController != null) + Column( + children: [ + AudioWaveforms( + enableGesture: true, + size: Size(MediaQuery.of(context).size.width / 1.08, 80), + recorderController: recorderController!, + waveStyle: WaveStyle( + waveColor: Colors.white, + extendWaveform: true, + showDurationLabel: true), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.0), + color: const Color(0xFF1E1B26), + ), + padding: const EdgeInsets.only(left: 18), + margin: const EdgeInsets.symmetric(horizontal: 15), + ), + SizedBox(height: 8), + // Text( + // _formatDuration(Duration(seconds: currentPositionInSeconds)), + // style: TextStyle(color: Colors.white), + // ), + ], + ), + if (recordedFilePath != null && playerController != null) + Column( + children: [ + AudioFileWaveforms( + size: Size(MediaQuery.of(context).size.width, 80), + enableSeekGesture: true, + waveformType: WaveformType.long, + playerController: playerController!, + playerWaveStyle: const PlayerWaveStyle( + scaleFactor: 0.8, + fixedWaveColor: Colors.white54, + liveWaveColor: Colors.blueAccent, + // spacing: 6, + waveCap: StrokeCap.butt, + ), + ), + SizedBox(height: 8), + // Text( + // _formatDuration(Duration(seconds: currentPositionInSeconds)), + // style: TextStyle(color: Colors.white), + // ), + ], + ), + ], + ), + ); + } + + String _formatDuration(Duration duration) { + String twoDigits(int n) => n.toString().padLeft(2, "0"); + String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60)); + String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60)); + return "$twoDigitMinutes:$twoDigitSeconds"; + } +} diff --git a/obs_flutter/obs/obs_demo/lib/editortext.dart b/obs_flutter/obs/obs_demo/lib/editortext.dart index d17bee2..c3876d7 100644 --- a/obs_flutter/obs/obs_demo/lib/editortext.dart +++ b/obs_flutter/obs/obs_demo/lib/editortext.dart @@ -1,7 +1,9 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'dart:convert'; +import 'dart:async'; import 'package:flutter/services.dart'; +import 'package:obs_demo/audio_bubble.dart'; import 'package:path_provider/path_provider.dart'; import 'package:audio_waveforms/audio_waveforms.dart'; @@ -23,12 +25,14 @@ class _EditorTextLayoutState extends State { final TextEditingController _controller = TextEditingController(); String _errorMessage = ""; String _textFieldValue = ""; + bool isRecording = false; + bool isPlaying = false; + int currentPositionInSeconds = 0; late final RecorderController recorderController; late final PlayerController playerController; String? _recordedFilePath; - String? _audioFilePath; Future fetchStoryText() async { final jsonString = await rootBundle.loadString('assets/OBSTextData.json'); @@ -88,6 +92,7 @@ class _EditorTextLayoutState extends State { fetchStoryText(); fetchJson(); _initialiseControllers(); + _preparePaths(); } void _initialiseControllers() async { @@ -109,7 +114,20 @@ class _EditorTextLayoutState extends State { int paraId = storyDatas[storyIndex]['story'][paraIndex]['id']; _recordedFilePath = '${directory.path}/OBS_${storyId}_$paraId.wav'; - _audioFilePath = '${directory.path}/OBS_${storyId}_$paraId.wav'; + + if (_recordedFilePath != null) { + final file = File(_recordedFilePath!); + if (await file.exists()) { + await playerController.preparePlayer( + path: _recordedFilePath!, + shouldExtractWaveform: true, + ); + } else { + print('File does not exist: $_recordedFilePath'); + } + } else { + print('Recorded file path is null.'); + } } void _startRecording() async { @@ -121,6 +139,9 @@ class _EditorTextLayoutState extends State { if (story['story'][paraIndex]['audio'] == null) { try { + setState(() { + isRecording = true; // Start recording, update state + }); await recorderController.record(path: _recordedFilePath!); } catch (e) { print('Error recording: $e'); @@ -153,6 +174,9 @@ class _EditorTextLayoutState extends State { Future _startNewRecording() async { try { + setState(() { + isRecording = true; // Start new recording, update state + }); if (_recordedFilePath != null) { final file = File(_recordedFilePath!); if (await file.exists()) { @@ -176,6 +200,7 @@ class _EditorTextLayoutState extends State { setState(() { _recordedFilePath = path; story['story'][paraIndex]['audio'] = _recordedFilePath; + isRecording = false; }); writeJsonToFile(story); } catch (e) { @@ -183,16 +208,49 @@ class _EditorTextLayoutState extends State { } } - Future _startPlayback() async { - if (_audioFilePath != null) { - try { - await playerController.preparePlayer( - path: _audioFilePath!, - shouldExtractWaveform: true, - ); - await playerController.startPlayer(); - } catch (e) { - print('Error starting playback: $e'); + void _startPlayback() async { + String? playbackPath = _recordedFilePath; + + if (playbackPath != null) { + final file = File(playbackPath); + if (await file.exists()) { + try { + await playerController.preparePlayer( + path: playbackPath, + shouldExtractWaveform: true, + ); + + // Start playback + await playerController.startPlayer(); + + // Update playback state + setState(() { + isPlaying = true; + }); + + playerController.onPlayerStateChanged.listen((state) { + if (state == PlayerState.stopped) { + _stopPlayback(); // Stop playback when it completes + } + }); + // Manually track playback position using a Timer + Timer? timer; + timer = Timer.periodic(Duration(seconds: 1), (timer) { + setState(() { + currentPositionInSeconds++; + }); + + // Example: Stop playback after 10 seconds (adjust as needed) + if (currentPositionInSeconds >= 2) { + _stopPlayback(); + timer?.cancel(); + } + }); + } catch (e) { + print('Error starting playback: $e'); + } + } else { + print('Audio file does not exist at path: $playbackPath'); } } else { print('Audio file path is null. Cannot start playback.'); @@ -202,6 +260,10 @@ class _EditorTextLayoutState extends State { void _stopPlayback() async { try { await playerController.stopPlayer(); + setState(() { + isPlaying = false; + currentPositionInSeconds = 0; // Reset position on stop + }); } catch (e) { print('Error stopping playback: $e'); } @@ -325,6 +387,12 @@ class _EditorTextLayoutState extends State { }); _controller.text = story['story'][paraIndex]['text']; + _recordedFilePath = + story['story'][paraIndex]['audio']; + _preparePaths(); // Prepare audio path for playback + if (_recordedFilePath == null) { + _startRecording(); // Start recording if audio path is null + } }, ) : IconButton( @@ -377,71 +445,46 @@ class _EditorTextLayoutState extends State { ), ), ), - _recordedFilePath != null - ? AudioWaveforms( - enableGesture: true, - size: Size( - MediaQuery.of(context).size.width / 1.07, 80), - recorderController: recorderController, - waveStyle: WaveStyle( - waveColor: Colors.white, - extendWaveform: true, - showMiddleLine: true, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.0), - color: Color.fromARGB(255, 114, 106, 136), - ), - ) - : SizedBox.shrink(), - _audioFilePath != null - ? AudioFileWaveforms( - size: Size(MediaQuery.of(context).size.width, 10), - playerController: playerController, - playerWaveStyle: const PlayerWaveStyle( - scaleFactor: 0.8, - fixedWaveColor: Colors.white30, - liveWaveColor: Colors.white, - waveCap: StrokeCap.butt, - ), - ) - : SizedBox.shrink(), + AudioBubble( + recordedFilePath: _recordedFilePath, + recorderController: recorderController, + playerController: playerController, + currentPositionInSeconds: currentPositionInSeconds, + ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( icon: Icon(Icons.mic), iconSize: 50, - onPressed: _startRecording, + onPressed: isRecording ? null : _startRecording, ), IconButton( icon: Icon(Icons.stop), iconSize: 50, - onPressed: _stopRecording, + onPressed: isRecording ? _stopRecording : null, ), SizedBox(width: 20), - // IconButton( - // icon: Icon(Icons.play_arrow), - // iconSize: 50, - // onPressed: () async { - // if (playerController.playerState == - // PlayerState.playing) { - // _stopPlayback(); - // } else { - // await _startPlayback(); - // } - // }, - // ), - IconButton( - icon: Icon(Icons.play_arrow), - iconSize: 50, - onPressed: _startPlayback, - ), - IconButton( - icon: Icon(Icons.stop_circle), - iconSize: 50, - onPressed: _stopPlayback, - ), + if (!isPlaying) + IconButton( + icon: Icon(Icons.play_arrow), + iconSize: 50, + onPressed: (_recordedFilePath != null && + _recordedFilePath!.isNotEmpty) + ? _startPlayback + : null, + color: (_recordedFilePath != null && + _recordedFilePath!.isNotEmpty) + ? Colors.green + : Colors.grey, + disabledColor: Colors.grey, + ), + if (isPlaying) + IconButton( + icon: Icon(Icons.stop_circle), + iconSize: 50, + onPressed: _stopPlayback, + ), // if (playerController.playerState == PlayerState.playing) // const Text('Playing...'), ], @@ -457,7 +500,11 @@ class _EditorTextLayoutState extends State { void saveData(String value) async { story['story'][paraIndex]['text'] = value; - story['story'][paraIndex]['audio'] = _recordedFilePath; + if (_recordedFilePath != null) { + story['story'][paraIndex]['audio'] = _recordedFilePath; + } + + // Save updated data to JSON file writeJsonToFile(story); print('Data saved: $value');