flutter_realtime_voice_ai 0.1.1
flutter_realtime_voice_ai: ^0.1.1 copied to clipboard
A Flutter package for streaming voice recording, and audio playback with focus on real-time voice interactions.
example/lib/main.dart
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_realtime_voice_ai/flutter_realtime_voice_ai.dart';
void main() {
runApp(const VoiceAIExampleApp());
}
class VoiceAIExampleApp extends StatelessWidget {
const VoiceAIExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Realtime Voice AI Example',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const LocalVoicePage(),
);
}
}
class LocalVoicePage extends StatefulWidget {
const LocalVoicePage({super.key});
@override
State<LocalVoicePage> createState() => _LocalVoicePageState();
}
class _LocalVoicePageState extends State<LocalVoicePage> {
late VoiceRecorderService _recorderService;
late VoicePlayerService _playerService;
// State variables
VoiceRecorderState _recorderState = VoiceRecorderState.idle;
VoicePlayerState _playerState = VoicePlayerState.idle;
bool _isInitialized = false;
String _statusMessage = 'Not initialized';
// Recorded audio data
final List<Uint8List> _recordedAudioChunks = [];
bool _isRecording = false;
@override
void initState() {
super.initState();
_initializeServices();
}
Future<void> _initializeServices() async {
try {
// Request microphone permission
final micStatus = await Permission.microphone.request();
if (!micStatus.isGranted) {
setState(() {
_statusMessage = 'Microphone permission denied';
});
return;
}
// Initialize services
_recorderService = VoiceRecorderService();
_playerService = VoicePlayerService();
// Setup state listeners
_setupStateListeners();
setState(() {
_isInitialized = true;
_statusMessage = 'Ready to record';
});
} catch (e) {
setState(() {
_statusMessage = 'Initialization failed: $e';
});
}
}
void _setupStateListeners() {
_recorderService.stateStream.listen((state) {
setState(() {
_recorderState = state;
_updateStatusMessage();
});
});
_playerService.stateStream.listen((state) {
setState(() {
_playerState = state;
_updateStatusMessage();
});
});
}
void _updateStatusMessage() {
String message = '';
if (_recorderState == VoiceRecorderState.recording) {
message = 'Recording...';
} else if (_playerState == VoicePlayerState.playing) {
message = 'Playing...';
} else if (_playerState == VoicePlayerState.buffering) {
message = 'Buffering...';
} else if (_playerState == VoicePlayerState.paused) {
message = 'Paused';
} else {
message = 'Ready';
}
if (_recordedAudioChunks.isNotEmpty) {
message += ' (${_recordedAudioChunks.length} chunks)';
}
setState(() {
_statusMessage = message;
});
}
Future<void> _startRecording() async {
try {
_recordedAudioChunks.clear();
_isRecording = true;
await _recorderService.startRecording(
config: StreamConfig.lowLatency(),
onAudioData: (data) {
if (_isRecording) {
_recordedAudioChunks.add(data);
}
},
);
} catch (e) {
_showErrorDialog('Failed to start recording: $e');
}
}
Future<void> _stopRecording() async {
try {
_isRecording = false;
await _recorderService.stopRecording();
} catch (e) {
_showErrorDialog('Failed to stop recording: $e');
}
}
Future<void> _playRecordedAudio() async {
if (_recordedAudioChunks.isEmpty) {
_showErrorDialog('No recorded audio to play');
return;
}
try {
// Start buffering
_playerService.startBuffering('local-recording');
// Add all recorded chunks
for (final chunk in _recordedAudioChunks) {
_playerService.addAudioData(chunk);
}
// Finalize and play
await _playerService.finalizeBufferAndPlay();
} catch (e) {
_showErrorDialog('Failed to play audio: $e');
}
}
Future<void> _stopPlayback() async {
try {
await _playerService.stopPlayback();
} catch (e) {
_showErrorDialog('Failed to stop playback: $e');
}
}
Future<void> _pausePlayback() async {
try {
await _playerService.pausePlayback();
} catch (e) {
_showErrorDialog('Failed to pause playback: $e');
}
}
Future<void> _resumePlayback() async {
try {
await _playerService.resumePlayback();
} catch (e) {
_showErrorDialog('Failed to resume playback: $e');
}
}
void _clearRecording() {
setState(() {
_recordedAudioChunks.clear();
_updateStatusMessage();
});
}
void _showErrorDialog(String message) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Error'),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Local Voice Recording & Playback'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Status display
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Status: $_statusMessage',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text('Recorder: ${_recorderState.name}'),
Text('Player: ${_playerState.name}'),
],
),
),
),
const SizedBox(height: 24),
// Recording controls
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Recording Controls',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _isInitialized && !_isRecording
? _startRecording
: null,
icon: const Icon(Icons.mic),
label: const Text('Start Recording'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton.icon(
onPressed: _isRecording ? _stopRecording : null,
icon: const Icon(Icons.stop),
label: const Text('Stop Recording'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey,
foregroundColor: Colors.white,
),
),
),
],
),
],
),
),
),
const SizedBox(height: 16),
// Playback controls
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Playback Controls',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _recordedAudioChunks.isNotEmpty &&
_playerState != VoicePlayerState.playing
? _playRecordedAudio
: null,
icon: const Icon(Icons.play_arrow),
label: const Text('Play Recording'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: _playerState == VoicePlayerState.playing
? _pausePlayback
: _playerState == VoicePlayerState.paused
? _resumePlayback
: null,
icon: Icon(_playerState == VoicePlayerState.paused
? Icons.play_arrow
: Icons.pause),
label: Text(_playerState == VoicePlayerState.paused
? 'Resume'
: 'Pause'),
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed:
_playerState == VoicePlayerState.playing ||
_playerState == VoicePlayerState.paused
? _stopPlayback
: null,
icon: const Icon(Icons.stop),
label: const Text('Stop Playback'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: _recordedAudioChunks.isNotEmpty
? _clearRecording
: null,
icon: const Icon(Icons.clear),
label: const Text('Clear Recording'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
),
),
),
],
),
],
),
),
),
const Spacer(),
// Instructions
Card(
color: Colors.blue.shade50,
child: const Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Instructions:',
style: TextStyle(fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text('1. Click "Start Recording" to begin recording audio'),
Text('2. Click "Stop Recording" when finished'),
Text(
'3. Click "Play Recording" to playback what you recorded'),
Text('4. Use Pause/Resume to control playback'),
Text('5. Use "Clear Recording" to start over'),
],
),
),
),
],
),
),
);
}
@override
void dispose() {
if (_isInitialized) {
_recorderService.dispose();
_playerService.dispose();
}
super.dispose();
}
}