fft_recorder_ui 1.0.4
fft_recorder_ui: ^1.0.4 copied to clipboard
audio recorder + FFT bar visualizer Flutter package. Built on top of flutter_recorder.
example/lib/main.dart
// The following Flutter application demonstrates a complete FFT recorder example.
// This example encompasses audio recording, FFT data visualization, and simple playback of the latest recorded file.
// Each critical part of the code is explained in detail below:
import 'dart:async'; // Provides support for asynchronous programming, required for streams and async/await.
import 'package:audioplayers/audioplayers.dart'; // Used for audio playback functionality.
import 'package:flutter/material.dart'; // Flutter Material UI components.
import 'package:fft_recorder_ui/fft_recorder_ui.dart'; // Provides FFT recording and visualization tools.
import 'package:path_provider/path_provider.dart'; // To locate paths in the mobile filesystem.
/// Entry point of the Flutter application.
/// Runs the [RecorderExampleApp] root widget.
void main() {
runApp(const RecorderExampleApp());
}
/// The root widget of the application which sets up the main theme and home page.
///
/// [RecorderExampleApp] is a stateless widget for simple configuration of MaterialApp.
/// It defines the application's name, disables the debug banner, sets a theme, and assigns the main page.
class RecorderExampleApp extends StatelessWidget {
const RecorderExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'FFT Recorder UI Example',
debugShowCheckedModeBanner: false, // Hides the debug banner in non-debug builds.
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
), // Sets color palette from an indigo seed color.
useMaterial3: true, // Enables Material 3 UI elements.
),
home: const RecorderExamplePage(), // Specifies [RecorderExamplePage] as the landing page.
);
}
}
/// The main interactive page showing FFT recording controls and a simple visualizer.
///
/// [RecorderExamplePage] is a stateful widget in order to:
/// 1. Track UI state (such as the last recorded file, current FFT data, button enable/disable states).
/// 2. Manage long-lived resources such as the FFT controller and AudioPlayer.
class RecorderExamplePage extends StatefulWidget {
const RecorderExamplePage({super.key});
@override
State<RecorderExamplePage> createState() => _RecorderExamplePageState();
}
class _RecorderExamplePageState extends State<RecorderExamplePage> {
// The controller encapsulating recording, playback, and FFT data streaming logic.
late final FftRecorderController _controller;
// Subscription to FFT data updates, used to display real-time FFT bar visualizations.
StreamSubscription<List<double>>? _fftSubscription;
// Holds the latest FFT data sample for visual display.
List<double> _fftData = [];
// Used for audio file playback of recorded WAV files.
late final AudioPlayer _player;
// Tracks the most recently saved recording file path.
String? _lastSavedPath;
/// Initializes the audio recorder controller, requests microphone permissions, and
/// sets up FFT data stream listening and audio playback.
@override
void initState() {
super.initState();
// Initialize the recorder controller.
_controller = FftRecorderController();
// Request microphone permission from the user.
_controller.requestMicPermission();
// Initialize the audio player for playback.
_player = AudioPlayer();
// Listen to the FFT data stream from the controller; update [_fftData] on new data.
_fftSubscription = _controller.fftStream.listen((data) {
if (!mounted) return; // Prevent updates after widget disposal.
setState(() {
_fftData = data;
});
});
}
/// Disposes of resources when the widget is removed from the widget tree.
/// Cancels subscriptions and disposes controllers and players to avoid memory leaks.
@override
void dispose() {
_fftSubscription?.cancel(); // Cancel FFT data stream.
_player.dispose(); // Release audio player resources.
_controller.dispose(); // Release FFT recorder resources.
super.dispose();
}
/// Starts a new audio recording session, saves the output file to the app's documents directory,
/// and updates UI state to remember the location of the recorded file.
Future<void> _startRecording() async {
// Get the directory for application documents.
final dir = await getApplicationDocumentsDirectory();
// Build a new unique file name based on timestamp to prevent overwrites.
final filePath = '${dir.path}/recording_${DateTime.now().millisecondsSinceEpoch}.wav';
// Start the recording, saving WAV audio to [filePath].
await _controller.startRecording(filePath: filePath);
// Update the state so that the last saved path is now [filePath].
setState(() => _lastSavedPath = filePath);
}
/// Plays back the audio file using the [AudioPlayer].
/// Stops any current playback before starting the new one.
///
/// [path]: The file system path to the WAV file to play.
Future<void> _playRecording(String path) async {
await _player.stop(); // Stop any ongoing playback.
await _player.play(DeviceFileSource(path)); // Play the new file.
}
/// Builds the main user interface:
/// - Provides buttons to control recording (start, stop, pause, resume) and audio playback.
/// - Displays the most recent saved file path.
/// - Contains an FFT bar visualizer showing real-time frequency data.
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: const Text('Recorder + Bar Visualizer')),
// Padding ensures content isn't flush with the screen edges.
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// The Wrap widget arranges action buttons gracefully, allowing them to wrap to new lines if space is insufficient.
Wrap(
spacing: 8,
runSpacing: 8,
children: [
// Play button: enabled only if a recording exists.
ElevatedButton(
onPressed: _lastSavedPath == null
? null
: () {
_playRecording(_lastSavedPath!);
},
child: const Text('재생'), // "Play" in Korean
),
// Start Recording button: enabled only if not currently recording.
ElevatedButton(
onPressed: _controller.recordingStatus.value == RecordingStatus.recording
? null
: _startRecording,
child: const Text('녹음 시작'), // "Start Recording" in Korean
),
// Pause button: enabled only if currently recording.
ElevatedButton(
onPressed: _controller.recordingStatus.value == RecordingStatus.recording
? _controller.pauseRecording
: null,
child: const Text('일시정지'), // "Pause" in Korean
),
// Resume button: enabled only if currently paused.
ElevatedButton(
onPressed: _controller.recordingStatus.value == RecordingStatus.paused
? _controller.resumeRecording
: null,
child: const Text('재개'), // "Resume" in Korean
),
// Stop button: enabled if recording is not idle.
ElevatedButton(
onPressed: _controller.recordingStatus.value == RecordingStatus.idle
? null
: () {
// Stop the recording and update the last saved path if a new recording was created.
final path = _controller.stopRecording();
setState(() {
_lastSavedPath = path ?? _lastSavedPath;
});
// After stopping, if a path exists, play the recorded audio right away.
final playPath = path ?? _lastSavedPath;
if (playPath != null) {
_playRecording(playPath);
}
},
child: const Text('정지'), // "Stop" in Korean
),
],
),
const SizedBox(height: 12),
// Displays the path of the last saved recording.
if (_lastSavedPath != null)
Align(
alignment: Alignment.centerLeft,
child: Text('Saved: $_lastSavedPath', style: theme.textTheme.bodySmall),
),
const SizedBox(height: 16),
// A container for the FFT data bar visualizer.
// Its appearance is styled to stand out, with rounded corners and a dark background.
Container(
width: 170,
height: 48,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.grey.shade900,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(12),
// The [BarVisualizer] displays the most recent FFT data as a sequence of vertical bars.
// Customizable parameters control the number, width, spacing, and color of the bars.
// If no data exists, a placeholder message is shown.
child: BarVisualizer(
data: _fftData, // The amplitude values to visualize.
barColor: Colors.white,
barCount: 9,
barWidth: 5.33,
maxHeight: 48,
spacing: 8,
emptyText: 'FFT 데이터 대기 중', // "Waiting for FFT data" in Korean
),
),
const SizedBox(height: 16),
],
),
),
);
}
}