Pub Version GitHub stars GitHub license

一个易于进行音频剪辑的Flutter插件

功能

  • 可定制的音频剪辑器。
  • 音频播放控制。
  • 检索和存储音频文件。

示例

在Android OnePlus 7上运行的示例应用

Trimmer

用法

easy_audio_trimmer依赖项添加到您的pubspec.yaml文件中

dependencies:
  easy_audio_trimmer: ^1.0.0

Android配置

在Android平台上使用无需额外配置。您已准备就绪!

iOS配置

  • ios/Podfile中将平台版本设置为10

    请参阅FFmpeg发布部分。

    platform :ios, '10'
    

FFmpeg发布

此插件使用了FFmpeg实现的LTS版本

LTS 发行版
Android API 级别 16
Android 相机访问
Android 架构 arm-v7a arm-v7a-neon arm64-v8a x86 x86-64
iOS最低SDK 10
iOS架构 armv7 arm64 i386 x86-64

功能

加载输入音频文件

final Trimmer _trimmer = Trimmer();
await _trimmer.loadAudio(audioFile: widget.file);

保存剪辑后的音频

返回一个字符串,指示保存操作是否成功。

_trimmer.saveTrimmedAudio(
      startValue: _startValue,
      endValue: _endValue,
      audioFileName: DateTime.now().millisecondsSinceEpoch.toString(),
      onSave: (outputPath) {
        setState(() {
          _progressVisibility = false;
        });
        debugPrint('OUTPUT PATH: $outputPath');
      },
    );

音频播放状态

返回音频播放状态。如果为true,则音频正在播放,否则为暂停。

await _trimmer.audioPlaybackControl(
  startValue: _startValue,
  endValue: _endValue,
);

小部件

显示音频剪辑区域

TrimViewer(
  trimmer: _trimmer,
  viewerHeight: 50.0,
  viewerWidth: MediaQuery.of(context).size.width,
  maxAudioLength: const Duration(seconds: 10),
  onChangeStart: (value) => _startValue = value,
  onChangeEnd: (value) => _endValue = value,
  allowAudioSelection: true,
  onChangePlaybackState: (value) =>
      setState(() => _isPlaying = value),
)

示例

在Flutter应用中直接使用此示例之前,请不要忘记将easy_audio_trimmerfile_picker包添加到您的pubspec.yaml文件中。

您可以替换新创建的Flutter项目的main.dart文件的全部内容来试用此示例。

import 'dart:io';
import 'package:example/trimmer_view.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: createMaterialColor(const Color(0xFF332FD0)),
        ),
        home: const FileSelectorWidget());
  }
}

class FileSelectorWidget extends StatefulWidget {
  const FileSelectorWidget({super.key});

  @override
  State<FileSelectorWidget> createState() => _FileSelectorWidgetState();
}

class _FileSelectorWidgetState extends State<FileSelectorWidget> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Audio Trimmer"),
      ),
      body: Center(
        child: ElevatedButton(
            onPressed: () async {
              FilePickerResult? result = await FilePicker.platform.pickFiles(
                type: FileType.audio,
                allowCompression: false,
              );
              if (result != null) {
                File file = File(result.files.single.path!);
                // ignore: use_build_context_synchronously
                Navigator.of(context).push(
                  MaterialPageRoute(builder: (context) {
                    return AudioTrimmerView(file);
                  }),
                );
              }
            },
            child: const Text("Select File")),
      ),
    );
  }
}

class AudioTrimmerView extends StatefulWidget {
  final File file;

  const AudioTrimmerView(this.file, {Key? key}) : super(key: key);
  @override
  State<AudioTrimmerView> createState() => _AudioTrimmerViewState();
}

class _AudioTrimmerViewState extends State<AudioTrimmerView> {
  final Trimmer _trimmer = Trimmer();

  double _startValue = 0.0;
  double _endValue = 0.0;

  bool _isPlaying = false;
  bool _progressVisibility = false;
  bool isLoading = false;

  @override
  void initState() {
    super.initState();

    _loadAudio();
  }

  void _loadAudio() async {
    setState(() {
      isLoading = true;
    });
    await _trimmer.loadAudio(audioFile: widget.file);
    setState(() {
      isLoading = false;
    });
  }

  _saveAudio() {
    setState(() {
      _progressVisibility = true;
    });

    _trimmer.saveTrimmedAudio(
      startValue: _startValue,
      endValue: _endValue,
      audioFileName: DateTime.now().millisecondsSinceEpoch.toString(),
      onSave: (outputPath) {
        setState(() {
          _progressVisibility = false;
        });
        debugPrint('OUTPUT PATH: $outputPath');
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async {
        if (Navigator.of(context).userGestureInProgress) {
          return false;
        } else {
          return true;
        }
      },
      child: Scaffold(
        backgroundColor: Colors.white,
        appBar: AppBar(
          title: const Text("Audio Trimmer"),
        ),
        body: isLoading
            ? const CircularProgressIndicator()
            : Center(
                child: Container(
                  padding: const EdgeInsets.only(bottom: 30.0),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    mainAxisSize: MainAxisSize.max,
                    children: <Widget>[
                      Visibility(
                        visible: _progressVisibility,
                        child: LinearProgressIndicator(
                          backgroundColor:
                              Theme.of(context).primaryColor.withOpacity(0.5),
                        ),
                      ),
                      ElevatedButton(
                        onPressed:
                            _progressVisibility ? null : () => _saveAudio(),
                        child: const Text("SAVE"),
                      ),
                      AudioViewer(trimmer: _trimmer),
                      Center(
                        child: Padding(
                          padding: const EdgeInsets.all(8.0),
                          child: TrimViewer(
                            trimmer: _trimmer,
                            viewerHeight: 100,
                            viewerWidth: MediaQuery.of(context).size.width,
                            durationStyle: DurationStyle.FORMAT_MM_SS,
                            backgroundColor: Theme.of(context).primaryColor,
                            barColor: Colors.white,
                            durationTextStyle: TextStyle(
                                color: Theme.of(context).primaryColor),
                            allowAudioSelection: true,
                            editorProperties: TrimEditorProperties(
                              circleSize: 10,
                              borderPaintColor: Colors.pink,
                              borderWidth: 4,
                              borderRadius: 5,
                              circlePaintColor: Colors.pink.shade800,
                            ),
                            areaProperties:
                                TrimAreaProperties.edgeBlur(blurEdges: true),
                            onChangeStart: (value) => _startValue = value,
                            onChangeEnd: (value) => _endValue = value,
                            onChangePlaybackState: (value) {
                              if (mounted) {
                                setState(() => _isPlaying = value);
                              }
                            },
                          ),
                        ),
                      ),
                      TextButton(
                        child: _isPlaying
                            ? Icon(
                                Icons.pause,
                                size: 80.0,
                                color: Theme.of(context).primaryColor,
                              )
                            : Icon(
                                Icons.play_arrow,
                                size: 80.0,
                                color: Theme.of(context).primaryColor,
                              ),
                        onPressed: () async {
                          bool playbackState =
                              await _trimmer.audioPlaybackControl(
                            startValue: _startValue,
                            endValue: _endValue,
                          );
                          setState(() => _isPlaying = playbackState);
                        },
                      )
                    ],
                  ),
                ),
              ),
      ),
    );
  }
}

故障排除

如果在Android平台上运行,出现minSdkVersion需要是24的错误,或者在iOS平台上出现Podfile平台版本应该是11的错误,请先进入pubspec.lock文件,查看ffmpeg_kit_flutter的版本是否有-LTS后缀。这应该能解决iOS平台的所有问题。

在Android上,如果您仍然遇到相同的问题,请尝试将以下内容添加到<project_directory>/android/app/src/main/AndroidManifest.xml

<manifest xmlns:tools="http://schemas.android.com/tools" ....... >
    <uses-sdk tools:overrideLibrary="com.arthenica.ffmpegkit.flutter, com.arthenica.ffmpegkit" />
</manifest>

赞助商

Agochar Tech LLP https://agochar.com/

许可证

版权所有 (c) 2022 Mayur Pitroda

特此授予任何人出于任何目的获取本软件及相关文档文件(“软件”)的权利,可以不受限制地使用、复制、修改、合并、发布、分发、再许可和/或出售软件的副本,并允许向其提供本软件的人这样做,但需满足以下条件:

以上版权声明和本许可声明应包含在软件的所有副本或实质性部分中。

本软件“按原样”提供,不附带任何形式的保证,明示或暗示,包括但不限于对适销性、特定用途的适用性和非侵权的保证。在任何情况下,作者或版权所有者均不对因使用本软件或与本软件的交易而引起的或与之相关的任何索赔、损害或其他责任负责,无论是在合同、侵权或其他方面。

GitHub

查看 Github