当前位置: 首页 > ds >正文

flutter开发(二)检测媒体中的静音

要分析媒体中的静音,需要分成几步:

  1. 提取媒体中的音频;
  2. 转化音频为裸pcm数据;
  3. 分析pcm数据,输出静音结果。

引入 ffmpeg

我们可以使用ffmpeg来提取媒体中的音频。

在flutter中使用ffmpeg,可以使用ffmpeg_kit_flutter_new模块。

flutter add ffmpeg_kit_flutter_new

也可以在pubspec.yaml中添加:

dependencies:  flutter:  sdk: flutter  ……ffmpeg_kit_flutter_new: ^3.1.0

然后执行flutter pub get

之后就可以使用FFMpegKit等类来实现功能了。

如以下代码,就可以提取出a.mp4视频文件中的音频到a.mp3:

import 'dart:async';  
import 'dart:io';  import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart';  
import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit_config.dart'; final command = '-y -i a.mp4 a.mp3';  try {final session = await FFmpegKit.execute(command);  final returnCode = await session.getReturnCode();  if (!ReturnCode.isSuccess(returnCode)) {    final logs = await session.getLogsAsString();  _logger.warning('FFmpeg execution failed for a.mp4. Return code: $returnCode\n$logs');    }} catch (e, s) {  _logger.severe('Error during audio processing for $filePath', e, s);    }

管道

以上代码,虽然可以把媒体中的音频提取出来,但是要保存到文件中。对于嵌入式设备来说,频繁地IO对设备不够友好。

我们可以使用管道:ffmpeg执行写入管道,另外一个异步方法读取管道的数据,进行解析。

ffmpeg使用管道的方法非常简单,只要使用FfMpegConfig.registerPipe()获取一个管道的路径,然后以普通路径的方式在FfmpegKit.execute()中调用就行了。

如:

    // 注册管道final pipePath = await FFmpegKitConfig.registerNewFFmpegPipe();  if (pipePath == null) {  _logger.severe('Failed to create an FFmpeg pipe.');  return null;  }  // 使用s16le转码为16位小端序整数// 单通道// 拼接管道文件到末尾final command = '-y -i a.mp4 -f s16le -ac 1 $pipePath';  try {// 这里的onData是另外一个异步方法,后文会使用。  final processingFuture = onData(File(pipePath).openRead(), duration);  final session = await FFmpegKit.execute(command);  final returnCode = await session.getReturnCode();  if (ReturnCode.isSuccess(returnCode)) {  return await processingFuture;  } else {final logs = await session.getLogsAsString();  _logger.warning(  'FFmpeg execution failed for a.mp4. Return code: $returnCode\n$logs');}  } catch (e, s) {  _logger.severe('Error during audio processing for $filePath', e, s);} finally {// 最后释放管道FFmpegKitConfig.closeFFmpegPipe(pipePath);  }  }

检测静音

我们使用ffmpeg提取出媒体中的音频数据,数据格式为每秒16000个采样点,每个采样点一个16位整数,就可以通过声音的分贝计算公式,来判断每个采样点是否是静音了。

    // 静音分贝阈值暂时设为30分贝// 采样点分贝数低于这个数值的则为静音final silenceThreshold = -30;// 静音时间阈值设为2秒// 低于2秒的静音忽略final minSilenceDuration = Duration(seconds: 2);// 计算分贝数对应的16位采样点值final linearThreshold = pow(10, silenceThreshold / 20) * 32767;// 计算静音时间对应的采样数final minSilenceSamples =(minSilenceDuration.inMilliseconds / 1000.0 * 16000).round();// 根据时长计算总采样点数final totalSamples =(audioDuration.inMilliseconds / 1000.0 * options.sampleRate).round();int samplesProcessed = 0;int silenceStartSample = -1;await for (var chunk in pcmStream) {final samples = Int16List.view(Uint8List.fromList(chunk).buffer);for (int i = 0; i < samples.length; i++) {final currentSampleIndex = samplesProcessed + i;final isSilent = samples[i].abs() < linearThreshold;// 如果当前采样点是静音,而且前面的不是静音,表示静音开始if (isSilent && silenceStartSample == -1) {silenceStartSample = currentSampleIndex;// 如果当前采样点不是静音,而且前面的是静音,则表示一个静音段} else if (!isSilent && silenceStartSample != -1) {final silentSamples = currentSampleIndex - silenceStartSample;// 如果静音段的节点数超过时长阈值,则记录一个静音if (silentSamples >= minSilenceSamples) {final startTime =_calculateTime(silenceStartSample, totalSamples, audioDuration);final endTime =_calculateTime(currentSampleIndex, totalSamples, audioDuration);silenceSegments.add((startTime, endTime));}silenceStartSample = -1;}}samplesProcessed += samples.length;}

样例代码

我们把以上代码总结起来,写成两个服务。

  • 一个FFmpegProcessingService,用于通过给定的参数调用ffmpeg。
import 'dart:async';  
import 'dart:io';  import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart';  
import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit_config.dart';  
import 'package:ffmpeg_kit_flutter_new/return_code.dart';  
import 'package:logging/logging.dart';  class FFmpegPcmConversionOptions {  final int sampleRate;  final String format;  final int channels;  FFmpegPcmConversionOptions({  required this.sampleRate,  required this.format,  this.channels = 1,  });  String toArgs() {  return '-f $format -ar $sampleRate -ac $channels';  }  
}  class FFmpegProcessingService {  final _logger = Logger('FFmpegProcessingService');  Future<T?> processAudio<T>({  required String filePath,  required Duration duration,  required FFmpegPcmConversionOptions options,  required Future<T> Function(  Stream<List<int>> pcmStream, Duration audioDuration)  onData,  }) async {  final pipePath = await FFmpegKitConfig.registerNewFFmpegPipe();  if (pipePath == null) {  _logger.severe('Failed to create an FFmpeg pipe.');  return null;  }  final command = '-y -i "$filePath" ${options.toArgs()} $pipePath';  try {  final processingFuture = onData(File(pipePath).openRead(), duration);  final session = await FFmpegKit.execute(command);  final returnCode = await session.getReturnCode();  if (ReturnCode.isSuccess(returnCode)) {  return await processingFuture;  } else {  final logs = await session.getLogsAsString();  _logger.warning(  'FFmpeg execution failed for $filePath. Return code: $returnCode\n$logs');  return null;  }  } catch (e, s) {  _logger.severe('Error during audio processing for $filePath', e, s);  return null;  } finally {  FFmpegKitConfig.closeFFmpegPipe(pipePath);  }  }  
}
  • 一个SilenceDetectionService,用于调用FFmpegProcessingService,注册异步回调方法,生成静音片段列表。
import 'dart:async';  
import 'dart:math';  
import 'dart:typed_data';  import 'package:example/app/services/ffmpeg_processing_service.dart';
import 'package:logging/logging.dart';class SilenceDetectionService {final _logger = Logger('SilenceDetectionService');final VoiceDetectionService _voiceDetectionService = VoiceDetectionService();final FFmpegProcessingService _ffmpegProcessingService =FFmpegProcessingService();Future<List<(Duration, Duration)>> findSilenceSegments(String filePath,Duration duration, {required double silenceThreshold,required Duration minSilenceDuration,}) async {try {final options =FFmpegPcmConversionOptions(sampleRate: 16000, format: 's16le');final result = await _ffmpegProcessingService.processAudio(filePath: filePath,duration: duration,options: options,onData: (pcmStream, audioDuration) => _performSilenceDetection(pcmStream: pcmStream,audioDuration: audioDuration,options: options,silenceThreshold: silenceThreshold,minSilenceDuration: minSilenceDuration,),);return result ?? [];} catch (e, s) {_logger.severe('Failed to analyze audio file for silence: $filePath', e, s);return [];}}Future<List<(Duration, Duration)>> _performSilenceDetection({required Stream<List<int>> pcmStream,required Duration audioDuration,required FFmpegPcmConversionOptions options,required double silenceThreshold,required Duration minSilenceDuration,}) async {final silenceSegments = <(Duration, Duration)>[];final linearThreshold = pow(10, silenceThreshold / 20) * 32767;final minSilenceSamples =(minSilenceDuration.inMilliseconds / 1000.0 * options.sampleRate).round();final totalSamples =(audioDuration.inMilliseconds / 1000.0 * options.sampleRate).round();int samplesProcessed = 0;int silenceStartSample = -1;await for (var chunk in pcmStream) {final samples = Int16List.view(Uint8List.fromList(chunk).buffer);for (int i = 0; i < samples.length; i++) {final currentSampleIndex = samplesProcessed + i;final isSilent = samples[i].abs() < linearThreshold;if (isSilent && silenceStartSample == -1) {silenceStartSample = currentSampleIndex;} else if (!isSilent && silenceStartSample != -1) {final silentSamples = currentSampleIndex - silenceStartSample;if (silentSamples >= minSilenceSamples) {final startTime =_calculateTime(silenceStartSample, totalSamples, audioDuration);final endTime =_calculateTime(currentSampleIndex, totalSamples, audioDuration);silenceSegments.add((startTime, endTime));}silenceStartSample = -1;}}samplesProcessed += samples.length;}if (silenceStartSample != -1) {final silentSamples = totalSamples - silenceStartSample;if (silentSamples >= minSilenceSamples) {final startTime =_calculateTime(silenceStartSample, totalSamples, audioDuration);silenceSegments.add((startTime, audioDuration));}}_logger.info('Silence analysis complete, found ${silenceSegments.length} segments.');return silenceSegments;}// 通过采样点计算时间Duration _calculateTime(int sampleIndex, int totalSamples, Duration audioDuration) {if (totalSamples == 0) return Duration.zero;final ratio = sampleIndex / totalSamples;return Duration(milliseconds: (audioDuration.inMilliseconds * ratio).round());}
}
http://www.xdnf.cn/news/18034.html

相关文章:

  • Day59--图论--47. 参加科学大会(卡码网),94. 城市间货物运输 I(卡码网)
  • 【DDIA】第二部分:分布式数据
  • 应用层协议——HTTP
  • 抽奖程序web程序
  • JavaScript 基础实战:DOM 操作、数据类型与常见需求实现
  • 项目管理工具
  • NPM 、 NPX
  • 清除 pnpm 缓存,解决不同源安装依赖包失败的问题
  • electron之win/mac通知免打扰
  • 【R语言】R 语言中 gsub 与正则表达式详解(含 POSIX 与 Perl 风格实例)
  • 汽车电子:现代汽车的智能核心
  • [激光原理与应用-287]:理论 - 波动光学 - 电磁波既能承载能量,又能承载信息?
  • 【软件设计模式】前置知识类图、七大原则(精简笔记版)
  • Spark 运行流程核心组件(二)任务调度
  • EN/IEC 55015 照明设备的电磁兼容标准安全
  • Docker Compose部署Clickhouse最新版
  • 【LINUX网络】HTTP协议基本结构、搭建自己的HTTP简单服务器
  • 为什么游戏会出现“卡顿”:`clock.tick()` v.s. `clock.get_fps()`
  • 【uni-app】根据角色/身份切换显示不同的 自定义 tabbar
  • 线性代数 · 直观理解矩阵 | 空间变换 / 特征值 / 特征向量
  • CERT/CC警告:新型HTTP/2漏洞“MadeYouReset“恐致全球服务器遭DDoS攻击瘫痪
  • 机械加工元件——工业精密制造的璀璨明珠
  • Day14: Flask太空站搭建指南:从零到超光速的Web开发之旅
  • git clone https://gh.llkk.cc/
  • C++从入门到实战(十九)C++ vector容器及其常用接口
  • 电子电路学习日记
  • qt项目中解决关闭弹窗后执行主界面的信号槽时闪退问题
  • MySql——聚簇索引(主键索引)和非聚簇索索引(非主键索引)引区别(即聚集索引和非聚集索引区别)
  • Java 学习笔记(基础篇2)
  • Docker build创建镜像命令入门教程