# How to Create Custom Audio Editing Experiences in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 20-30 min
- Compatibility: FlutterFlow Pro+ (Custom Code required)
- Last updated: March 2026

## TL;DR

Build audio editing in FlutterFlow at two levels: simple record-and-playback using the record package with a Custom Action, or advanced waveform-display-and-trim using audio_waveforms for visualization plus ffmpeg_kit_flutter for cutting segments. The simple approach adds about 30 lines of Dart. The advanced approach gives you a draggable trim UI with waveform rendering, but adds 20-40MB to your app due to the FFmpeg binary. Choose based on whether you need trimming or just recording.

## Two approaches to audio editing in FlutterFlow

Audio editing ranges from simple record-and-play to full waveform trimming. This tutorial covers both. The simple path uses the record package in a Custom Action — call start(), stop(), and get a file path you can play with just_audio. The advanced path adds the audio_waveforms package for waveform visualization and ffmpeg_kit_flutter for non-destructive audio trimming. You will build a Custom Widget with a waveform display, draggable start/end trim markers, a preview button to hear the trimmed segment, and an export button that runs FFmpeg to produce the trimmed file.

## Before you start

- FlutterFlow Pro plan or higher (Custom Code required)
- Firebase Storage enabled for uploading audio files
- A physical device for testing (microphone required)
- Basic understanding of Custom Actions and Custom Widgets in FlutterFlow

## Step-by-step guide

### 1. Add the record package and create a recording Custom Action

Go to Custom Code → Pubspec Dependencies and add the record package (version ^5.0.4). Create a Custom Action named startAudioRecording. Inside, initialize AudioRecorder, check hasPermission(), then call start() with RecordConfig(encoder: AudioEncoder.aacLc) and an output path from getTemporaryDirectory(). Create a second Custom Action stopAudioRecording that calls stop() and returns the file path as a String. The parent page calls startAudioRecording on mic-button tap, and stopAudioRecording on stop-button tap, storing the returned path in Page State.

```
import 'package:record/record.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';

Future<String> startAudioRecording() async {
  final recorder = AudioRecorder();
  if (!await recorder.hasPermission()) return '';
  final dir = await getTemporaryDirectory();
  final path = '${dir.path}/recording_${DateTime.now().millisecondsSinceEpoch}.m4a';
  await recorder.start(RecordConfig(encoder: AudioEncoder.aacLc), path: path);
  return path;
}

Future<String> stopAudioRecording(AudioRecorder recorder) async {
  final path = await recorder.stop();
  return path ?? '';
}
```

**Expected result:** Tapping the mic button starts recording; tapping stop returns a file path to the recorded .m4a file.

### 2. Add audio_waveforms package and build the waveform Custom Widget

Add audio_waveforms (version ^1.0.5) to Pubspec Dependencies. Create a Custom Widget named AudioWaveformEditor with parameters: audioFilePath (String, required) and onTrimComplete (Action Parameter). In initState(), create a PlayerController and call preparePlayer(path: widget.audioFilePath, shouldExtractWaveform: true). In build(), return AudioFileWaveforms(playerController: _controller, size: Size(width, 100)) which renders the waveform from the file data. The waveform is interactive — tapping seeks to that position.

```
import 'package:audio_waveforms/audio_waveforms.dart';

class AudioWaveformEditor extends StatefulWidget {
  const AudioWaveformEditor({super.key, this.width, this.height, required this.audioFilePath, this.onTrimComplete});
  final double? width;
  final double? height;
  final String audioFilePath;
  final Future Function(String trimmedPath)? onTrimComplete;
  @override
  State<AudioWaveformEditor> createState() => _AudioWaveformEditorState();
}

class _AudioWaveformEditorState extends State<AudioWaveformEditor> {
  late final PlayerController _controller;
  double _trimStart = 0.0; // 0.0 to 1.0
  double _trimEnd = 1.0;

  @override
  void initState() {
    super.initState();
    _controller = PlayerController();
    _controller.preparePlayer(
      path: widget.audioFilePath,
      shouldExtractWaveform: true,
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
```

**Expected result:** The Custom Widget displays the audio file's waveform as an interactive bar chart.

### 3. Add draggable trim handles on the waveform

Wrap the waveform in a Stack. Add two Positioned GestureDetector widgets — one for the left trim handle and one for the right. Each handle is a Container (8px wide, full height, colored accent) that the user drags horizontally. Track positions as _trimStart and _trimEnd (0.0-1.0 fractions of total duration). On drag update, setState with the new fraction calculated from dx / widget width. Draw a semi-transparent overlay outside the trim region to visually indicate the excluded portions. Add a Play Preview button that seeks to _trimStart position and plays until _trimEnd.

**Expected result:** Two draggable handles appear on the waveform. The area outside the handles is dimmed. Preview plays only the selected segment.

### 4. Cut the audio segment with ffmpeg_kit_flutter

Add ffmpeg_kit_flutter (version ^6.0.3) to Pubspec Dependencies. WARNING: this adds 20-40MB to your app binary. Create a Custom Action named trimAudio that takes inputPath, startSeconds, and endSeconds. Run FFmpegKit.execute('-i $inputPath -ss $startSeconds -to $endSeconds -c copy $outputPath'). The -c copy flag does a lossless trim without re-encoding. Return the output path. Wire the Export button on the Custom Widget to call this action with the calculated start/end times (fraction × total duration in seconds).

```
import 'package:ffmpeg_kit_flutter/ffmpeg_kit.dart';
import 'package:path_provider/path_provider.dart';

Future<String> trimAudio(String inputPath, double startSec, double endSec) async {
  final dir = await getTemporaryDirectory();
  final output = '${dir.path}/trimmed_${DateTime.now().millisecondsSinceEpoch}.m4a';
  final cmd = '-i "$inputPath" -ss $startSec -to $endSec -c copy "$output"';
  await FFmpegKit.execute(cmd);
  return output;
}
```

**Expected result:** The trimmed audio file is saved to a temporary path, containing only the segment between the handles.

### 5. Upload the trimmed audio to Firebase Storage

Create a Custom Action named uploadAudio that takes a file path, reads the file, and uploads to Firebase Storage under audio/{userId}/{timestamp}.m4a. Get the download URL and return it. In the parent page, wire the full flow: user records → waveform displays → user drags trim handles → taps Export → trimAudio runs → uploadAudio uploads → save download URL to a Firestore document field. Show a CircularProgressIndicator during the upload with Conditional Visibility.

**Expected result:** The trimmed audio file uploads to Firebase Storage and the download URL is saved in Firestore.

## Complete code example

File: `audio_waveform_editor.dart`

```dart
import 'package:audio_waveforms/audio_waveforms.dart';
import 'package:ffmpeg_kit_flutter/ffmpeg_kit.dart';
import 'package:path_provider/path_provider.dart';

class AudioWaveformEditor extends StatefulWidget {
  const AudioWaveformEditor({
    super.key, this.width, this.height,
    required this.audioFilePath,
    this.onTrimComplete,
  });
  final double? width;
  final double? height;
  final String audioFilePath;
  final Future Function(String trimmedPath)? onTrimComplete;

  @override
  State<AudioWaveformEditor> createState() => _State();
}

class _State extends State<AudioWaveformEditor> {
  late final PlayerController _ctrl;
  double _trimStart = 0.0;
  double _trimEnd = 1.0;
  int _durationMs = 0;
  bool _exporting = false;

  @override
  void initState() {
    super.initState();
    _ctrl = PlayerController();
    _ctrl.preparePlayer(
      path: widget.audioFilePath,
      shouldExtractWaveform: true,
    ).then((_) {
      _durationMs = _ctrl.maxDuration;
      setState(() {});
    });
  }

  Future<void> _preview() async {
    final startMs = (_trimStart * _durationMs).round();
    await _ctrl.seekTo(startMs);
    _ctrl.startPlayer();
    // Stop at trim end after delay
    final playMs = ((_trimEnd - _trimStart) * _durationMs).round();
    Future.delayed(Duration(milliseconds: playMs), () => _ctrl.pausePlayer());
  }

  Future<void> _export() async {
    setState(() => _exporting = true);
    final startSec = (_trimStart * _durationMs / 1000).toStringAsFixed(2);
    final endSec = (_trimEnd * _durationMs / 1000).toStringAsFixed(2);
    final dir = await getTemporaryDirectory();
    final out = '${dir.path}/trim_${DateTime.now().millisecondsSinceEpoch}.m4a';
    await FFmpegKit.execute(
      '-i "${widget.audioFilePath}" -ss $startSec -to $endSec -c copy "$out"',
    );
    setState(() => _exporting = false);
    widget.onTrimComplete?.call(out);
  }

  @override
  void dispose() {
    _ctrl.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final w = widget.width ?? 300;
    return SizedBox(
      width: w, height: widget.height ?? 180,
      child: Column(children: [
        Expanded(
          child: Stack(children: [
            AudioFileWaveforms(playerController: _ctrl, size: Size(w, 100)),
            // Left trim region (dimmed)
            Positioned(left: 0, width: _trimStart * w, top: 0, bottom: 0,
              child: Container(color: Colors.black38)),
            // Right trim region (dimmed)
            Positioned(left: _trimEnd * w, right: 0, top: 0, bottom: 0,
              child: Container(color: Colors.black38)),
            // Left handle
            Positioned(left: _trimStart * w - 4, top: 0, bottom: 0,
              child: GestureDetector(
                onHorizontalDragUpdate: (d) => setState(() {
                  _trimStart = ((_trimStart + d.delta.dx / w)).clamp(0.0, _trimEnd - 0.05);
                }),
                child: Container(width: 8, color: Theme.of(context).primaryColor),
              )),
            // Right handle
            Positioned(left: _trimEnd * w - 4, top: 0, bottom: 0,
              child: GestureDetector(
                onHorizontalDragUpdate: (d) => setState(() {
                  _trimEnd = ((_trimEnd + d.delta.dx / w)).clamp(_trimStart + 0.05, 1.0);
                }),
                child: Container(width: 8, color: Theme.of(context).primaryColor),
              )),
          ]),
        ),
        Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
          IconButton(icon: const Icon(Icons.play_arrow), onPressed: _preview),
          _exporting
            ? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2))
            : IconButton(icon: const Icon(Icons.content_cut), onPressed: _export),
        ]),
      ]),
    );
  }
}
```

## Common mistakes

- **Including ffmpeg_kit_flutter when you only need record and playback** — The FFmpeg binary adds 20-40MB to your app size. If you only need recording and playback without trimming, this bloat is entirely unnecessary. Fix: Use only the record package for recording and just_audio for playback. Only add ffmpeg_kit_flutter if you actually need audio trimming or format conversion.
- **Not disposing the PlayerController when leaving the page** — The waveform player holds decoded audio data in memory. Not disposing leaks memory with every page visit. Fix: Call _controller.dispose() in the widget's dispose() method, just like any other media controller.
- **Trying to trim while the audio is still playing** — FFmpeg cannot read a file that is currently being played by the PlayerController on some platforms, causing a file lock error. Fix: Call _controller.pausePlayer() before starting the FFmpeg trim operation. Resume only after the trim completes.

## Best practices

- Use the record package for simple record/playback — it is lightweight and well-maintained
- Only add FFmpeg if you need trimming, format conversion, or audio merging
- Show waveform extraction progress — preparePlayer with shouldExtractWaveform can take 1-3 seconds for long files
- Clamp trim handles so left never crosses right and minimum segment is 0.5 seconds
- Use -c copy in FFmpeg for lossless trimming — avoids re-encoding and is near-instant
- Compress audio before uploading — use AAC encoder (not WAV) for 5-10x smaller files
- Request microphone permission before recording and handle denial with a settings redirect

## Frequently asked questions

### What audio formats does the record package support?

The record package supports AAC (recommended, small file size), WAV (lossless, large files), and OPUS. Use AAC (AudioEncoder.aacLc) for most cases — it produces files 5-10x smaller than WAV with negligible quality loss.

### How much does FFmpeg add to app size?

The ffmpeg_kit_flutter full package adds 20-40MB to your APK/IPA. Use the min-gpl variant for a smaller binary (10-15MB) if you only need basic audio operations. If app size is critical and you only need trimming, consider the audio_trimmer package as a lighter alternative.

### Can I display a live waveform while recording?

Yes. Use audio_waveforms with RecorderController instead of PlayerController. Call recorderController.record() and the AudioWaveforms widget shows live amplitude bars in real time as the user speaks.

### Does audio recording work on web?

The record package has web support using the browser's MediaRecorder API. However, audio_waveforms does not support web for waveform display. For web, show a simple duration timer instead of a waveform during recording.

### How do I limit recording duration?

Start a Timer when recording begins that calls stopAudioRecording() after your maximum duration (e.g., 60 seconds). Show a countdown in the UI bound to Page State secondsRemaining.

### Can RapidDev help with advanced audio features?

Yes. Multi-track mixing, real-time audio effects, and background recording require complex Custom Code beyond this tutorial. RapidDev can build production-ready audio editing experiences for podcast, music, or education apps.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-create-custom-audio-editing-experiences-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-create-custom-audio-editing-experiences-in-flutterflow
