audio_trimmer_plus 0.0.2
audio_trimmer_plus: ^0.0.2 copied to clipboard
A customizable Flutter widget for audio trimming UI with waveform visualization, trim handles, and playback controls.
ðĩ Audio Trimmer Plus #
Beautiful, customizable audio trimming widgets for Flutter
Create stunning audio editing interfaces with waveform visualization, playback control, and frame-accurate trimming
Installation âĒ Quick Start âĒ Features âĒ Examples âĒ API Reference
ðļ Screenshots #
âĻ Features #
- ðĩ Complete Audio Editor - All-in-one widget with file picker, waveform display, and trim controls
- âïļ Frame-Accurate Trimming - Precise audio trimming with FFmpeg integration
- ðŪ Playback Control - Built-in play/pause with automatic end position stopping
- ð Waveform Visualization - Automatic waveform extraction with customizable appearance
- ð File Management - Built-in file import with validation support
- ðĻ Fully Customizable - Configure colors, sizes, borders, and more
- ðŊ Easy to Use - Simple API with sensible defaults
- ð Animated Progress - Smooth progress animations during playback
- ðž Format Support - WAV, MP3, M4A, and other audio formats
- ðą Responsive - Adapts to different screen sizes
- ⥠Performance Optimized - Smooth scrolling and rendering
- ðŠķ Lightweight - Minimal dependencies
ðĶ Installation #
Add this to your package's pubspec.yaml file:
dependencies:
audio_trimmer_plus: ^0.0.1
audio_waveforms: ^2.0.2 # Required for waveform visualization
ffmpeg_kit_flutter_new: ^4.1.0 # Required for audio trimming
file_picker: ^8.1.6 # Required for file selection
Or run:
flutter pub add audio_trimmer_plus
flutter pub add audio_waveforms
flutter pub add ffmpeg_kit_flutter_new
flutter pub add file_picker
ð Quick Start #
1. Import the Package #
import 'package:audio_trimmer_plus/audio_trimmer_plus.dart';
import 'package:file_picker/file_picker.dart';
2. Create a Controller #
final _trimmerController = AudioTrimmerController();
3. Build the UI #
AudioTrimWidget(
onTap: () async {
// Pick and import audio file
final result = await FilePicker.platform.pickFiles(type: FileType.audio);
if (result?.files.single.path != null) {
await _trimmerController.importFile(result!.files.single.path!);
}
},
trimmerController: _trimmerController,
onTrimRangeChanged: (startMs, endMs) {
print('Trim range: $startMs - $endMs ms');
},
)
4. Dispose When Done #
@override
void dispose() {
_trimmerController.dispose();
super.dispose();
}
That's it! Your audio trimmer is ready. ð
ð Examples #
Basic Audio Trimmer with Playback #
import 'package:flutter/material.dart';
import 'package:audio_trimmer_plus/audio_trimmer_plus.dart';
import 'package:audio_waveforms/audio_waveforms.dart';
import 'package:file_picker/file_picker.dart';
class AudioTrimmerDemo extends StatefulWidget {
const AudioTrimmerDemo({super.key});
@override
State<AudioTrimmerDemo> createState() => _AudioTrimmerDemoState();
}
class _AudioTrimmerDemoState extends State<AudioTrimmerDemo> {
final _trimmerController = AudioTrimmerController();
bool _isPlaying = false;
int? _startMs;
int? _endMs;
Future<void> _pickAudio() async {
final result = await FilePicker.platform.pickFiles(
type: FileType.audio,
allowMultiple: false,
);
if (result?.files.single.path != null) {
await _trimmerController.importFile(result!.files.single.path!);
}
}
@override
void dispose() {
_trimmerController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Audio Trimmer')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Container(
height: 200,
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(12),
),
child: AudioTrimWidget(
onTap: _pickAudio,
trimmerController: _trimmerController,
onPlayerStateChanged: (state) {
setState(() {
_isPlaying = state == PlayerState.playing;
});
},
onTrimRangeChanged: (startMs, endMs) {
setState(() {
_startMs = startMs;
_endMs = endMs;
});
},
),
),
const SizedBox(height: 16),
if (_startMs != null && _endMs != null)
Text(
'Range: ${(_startMs! / 1000).toStringAsFixed(2)}s - ${(_endMs! / 1000).toStringAsFixed(2)}s',
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
if (_isPlaying) {
_trimmerController.pause();
} else {
_trimmerController.play();
}
},
icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow),
label: Text(_isPlaying ? 'Pause' : 'Play'),
),
],
),
),
);
}
}
Custom Styling #
Create a beautifully styled audio trimmer:
AudioTrimWidget(
onTap: _pickAudio,
trimmerController: _trimmerController,
config: const AudioTrimmerConfig(
borderColor: Colors.deepPurple,
borderWidth: 3.0,
borderRadius: 12.0,
tintColor: Color(0x20673AB7),
handleColor: Colors.deepPurple,
handleWidth: 10.0,
progressColor: Color(0x80673AB7),
),
fixedWaveColor: Colors.deepPurple.withOpacity(0.2),
scaleFactor: 500.0,
waveSpacing: 4.0,
onTrimRangeChanged: (startMs, endMs) {
print('Trim: $startMs - $endMs ms');
},
)
Complete Example with Trimming #
Full implementation with file import, playback, and trimming:
class AudioEditorPage extends StatefulWidget {
const AudioEditorPage({super.key});
@override
State<AudioEditorPage> createState() => _AudioEditorPageState();
}
class _AudioEditorPageState extends State<AudioEditorPage> {
final _trimmerController = AudioTrimmerController();
bool _isPlaying = false;
bool _isTrimming = false;
int? _startMs;
int? _endMs;
Future<void> _pickAudio() async {
final result = await FilePicker.platform.pickFiles(
type: FileType.audio,
allowMultiple: false,
);
if (result?.files.single.path != null) {
await _trimmerController.importFile(result!.files.single.path!);
}
}
Future<void> _trimAudio() async {
if (_startMs == null || _endMs == null) return;
setState(() => _isTrimming = true);
try {
// Trim the audio using the current trim range
final outputPath = await _trimmerController.trim();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Audio trimmed successfully!'),
backgroundColor: Colors.green,
),
);
// Load the trimmed file back for preview
await _trimmerController.importFile(outputPath);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: $e'),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() => _isTrimming = false);
}
}
}
@override
void dispose() {
_trimmerController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Audio Editor')),
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
height: 200,
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(12),
),
child: AudioTrimWidget(
onTap: _pickAudio,
trimmerController: _trimmerController,
onPlayerStateChanged: (state) {
setState(() {
_isPlaying = state == PlayerState.playing;
});
},
config: const AudioTrimmerConfig(
borderColor: Colors.deepPurple,
handleColor: Colors.deepPurple,
progressColor: Color(0x80673AB7),
),
fixedWaveColor: Colors.deepPurple.withOpacity(0.2),
scaleFactor: 500.0,
waveSpacing: 4.0,
onTrimRangeChanged: (startMs, endMs) {
setState(() {
_startMs = startMs;
_endMs = endMs;
});
},
),
),
const SizedBox(height: 24),
// Trim range info
if (_startMs != null && _endMs != null)
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.deepPurple[50],
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
const Text(
'Trim Range',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'${(_startMs! / 1000).toStringAsFixed(2)}s - ${(_endMs! / 1000).toStringAsFixed(2)}s',
style: const TextStyle(fontSize: 18),
),
Text(
'Duration: ${((_endMs! - _startMs!) / 1000).toStringAsFixed(2)}s',
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
),
),
const SizedBox(height: 16),
// Play button
ElevatedButton.icon(
onPressed: () {
if (_isPlaying) {
_trimmerController.pause();
} else {
_trimmerController.play();
}
},
icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow),
label: Text(_isPlaying ? 'Pause' : 'Play'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepPurple,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
const SizedBox(height: 8),
// Trim button
if (_startMs != null && _endMs != null)
ElevatedButton.icon(
onPressed: _isTrimming ? null : _trimAudio,
icon: _isTrimming
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Icon(Icons.content_cut),
label: Text(_isTrimming ? 'Trimming...' : 'Trim Audio'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
],
),
),
);
}
}
ð API Reference #
AudioTrimWidget #
The main widget for audio trimming with built-in waveform display and playback control.
AudioTrimWidget({
required VoidCallback? onTap,
required AudioTrimmerController trimmerController,
Function(int startMs, int endMs)? onTrimRangeChanged,
Function(PlayerState)? onPlayerStateChanged,
AudioTrimmerConfig? config,
double trimWindowMs = 15000.0,
double waveformHeight = 80.0,
double scaleFactor = 75.0,
double waveSpacing = 6.0,
Color? fixedWaveColor,
bool autoExtractWaveform = true,
})
| Property | Type | Required | Description |
|---|---|---|---|
onTap |
VoidCallback? |
No | Callback when user taps the widget (for file picking) |
trimmerController |
AudioTrimmerController |
â Yes | Controller for playback and file operations |
onTrimRangeChanged |
Function(int, int)? |
No | Called when trim range changes (startMs, endMs) |
onPlayerStateChanged |
Function(PlayerState)? |
No | Called when player state changes |
config |
AudioTrimmerConfig? |
No | Configuration for trim overlay appearance |
trimWindowMs |
double |
No | Duration of trim window in milliseconds (default: 15000) |
waveformHeight |
double |
No | Height of waveform display (default: 80.0) |
scaleFactor |
double |
No | Waveform amplitude scale factor (default: 75.0) |
waveSpacing |
double |
No | Spacing between waveform bars (default: 6.0) |
fixedWaveColor |
Color? |
No | Color of the waveform |
autoExtractWaveform |
bool |
No | Automatically extract waveform (default: true) |
AudioTrimmerController #
Controller for managing file operations, playback, and trimming.
final controller = AudioTrimmerController();
// File operations
await controller.importFile('/path/to/audio.mp3');
// Playback control
controller.play();
controller.pause();
// Trimming
final outputPath = await controller.trim();
final customPath = await controller.trim(outputPath: '/custom/path.mp3');
// Get current state
final path = controller.currentFilePath;
final start = controller.startMs;
final end = controller.endMs;
// Clean up
controller.dispose();
| Method | Returns | Description |
|---|---|---|
importFile(String) |
Future<void> |
Import and validate an audio file |
play() |
void |
Start playback of trimmed region |
pause() |
void |
Pause playback |
trim({String? outputPath}) |
Future<String> |
Trim audio file, returns output path |
updateTrimRange(int, int) |
void |
Update trim range (called automatically by widget) |
dispose() |
void |
Dispose and clean up resources |
| Property | Type | Description |
|---|---|---|
currentFilePath |
String? |
Currently loaded audio file path |
startMs |
int |
Start time of trim range in milliseconds |
endMs |
int |
End time of trim range in milliseconds |
AudioTrimmerConfig #
Configuration object for customizing the trim overlay appearance.
| Property | Type | Default | Description |
|---|---|---|---|
borderColor |
Color |
Color(0xFFFF0080) |
Border color around the overlay |
borderWidth |
double |
3.0 |
Width of the border stroke |
borderRadius |
double |
10.0 |
Border radius for rounded corners |
tintColor |
Color |
Color(0x1AFF0080) |
Tint color overlay on trim region |
handleColor |
Color |
Color(0xFFFF0080) |
Color of the trim handles |
handleWidth |
double |
8.0 |
Width of each trim handle |
progressColor |
Color |
Color(0x80FF0080) |
Color of the progress fill |
ðĄ Best Practices #
â Do's #
- â
Always dispose
AudioTrimmerControllerwhen done to prevent memory leaks - â
Use
importFile()to validate audio files before loading - â
Monitor player state changes via
onPlayerStateChanged - â Handle trim range changes to show duration information
- â Provide visual feedback during trimming operations
- â Test on different screen sizes and audio file formats
- â
Use appropriate
trimWindowMsfor your use case (shorter for precision, longer for overview)
â Don'ts #
- â Don't forget to add required dependencies (audio_waveforms, ffmpeg_kit_flutter, file_picker)
- â Don't forget to dispose the controller - use it in the dispose() method
- â Don't make trim handles too small (< 6px) - they become hard to interact with
- â Don't forget to handle audio file permissions on mobile platforms
- â Don't block the UI during long trimming operations - show loading indicators
- â Don't use extreme
scaleFactorvalues that distort the waveform
ðą Use Cases #
- ðĩ Audio editing applications
- ðïļ Podcast editing and trimming
- ð Audio messaging with trim-before-send
- ðŽ Video editor audio tracks
- ðą Voice recorder apps
- ðķ Music production tools
- ð Call recording editors
- ð Educational audio content creation
ðĪ Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
ð License #
This project is licensed under the MIT License - see the LICENSE file for details.
ð Links #
- Repository: github.com/youseflabs-k/audio_trimmer
- Issues: github.com/youseflabs-k/audio_trimmer/issues
- pub.dev: pub.dev/packages/audio_trimmer_plus
- Example App: example/
Made with âĪïļ by Ahmed Yousef
If you find this package helpful, please consider giving it a âïļ!