欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

本文基于flutter3.27.5开发

一、flutter_sound 库概述

音频录制与播放是移动应用的核心功能之一,用于语音消息、音乐播放器、录音笔记、语音通话等场景。在 Flutter for OpenHarmony 应用开发中,flutter_sound 是一个功能全面的音频处理插件,提供了完整的跨平台音频录制和播放能力。

flutter_sound 库特点

flutter_sound 库基于 Flutter 平台接口实现,提供了以下核心特性:

完整播放功能:支持从文件、URL、Asset、数据流等多种来源播放音频,支持暂停、恢复、停止、跳转等操作。

完整录制功能:支持录制到文件或数据流,支持多种音频编码格式,支持暂停和恢复录制。

多种编码格式:支持 AAC、MP3、Opus、Vorbis、FLAC、PCM、AMR 等多种音频编码格式。

播放控制:支持音量调节、播放速度控制、进度监听、播放完成回调等。

录制控制:支持采样率、声道数、比特率配置,支持分贝级别监听。

流式处理:支持从数据流播放和录制到数据流,适合实时音频处理场景。

编码格式支持对比

编码格式 Android iOS OpenHarmony
AAC-ADTS
AAC-MP4
MP3
Opus-OGG
Vorbis-OGG
FLAC
PCM16
PCM16-WAV
AMR-NB ✅ (API 18+)
AMR-WB ✅ (API 18+)

使用场景:语音消息、音乐播放器、录音笔记、语音备忘录、播客应用、语音通话、实时音频处理等。


二、安装与配置

2.1 添加依赖

在项目的 pubspec.yaml 文件中添加 flutter_sound 依赖:

dependencies:
  flutter_sound:
    git:
      url: https://atomgit.com/openharmony-sig/flutter_sound.git
      path: flutter_sound

然后执行以下命令获取依赖:

flutter pub get

2.2 权限配置

在 OpenHarmony 项目中,需要配置麦克风权限才能进行录音。打开 ohos/entry/src/main/module.json5 文件,添加以下权限:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "$string:reason_microphone",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

三、核心 API 详解

3.1 FlutterSoundPlayer 播放器

FlutterSoundPlayer 是音频播放的核心类,提供完整的播放控制功能。

class FlutterSoundPlayer {
  PlayerState get playerState;
  bool get isPlaying;
  bool get isPaused;
  bool get isStopped;
  Stream<PlaybackDisposition>? get onProgress;
  StreamSink<Food>? get foodSink;

  Future<FlutterSoundPlayer?> openPlayer();
  Future<void> closePlayer();
  Future<Duration> startPlayer({
    String? fromURI,
    Codec codec = Codec.aacADTS,
    TWhenFinished? whenFinished,
  });
  Future<void> stopPlayer();
  Future<void> pausePlayer();
  Future<void> resumePlayer();
  Future<void> seekToPlayer(int milliSeconds);
  Future<void> setVolume(double volume);
  Future<void> setSpeed(double speed);
  Future<void> setSubscriptionDuration(Duration duration);
}

3.2 FlutterSoundRecorder 录音器

FlutterSoundRecorder 是音频录制的核心类,提供完整的录制控制功能。

class FlutterSoundRecorder {
  RecorderState get recorderState;
  bool get isRecording;
  bool get isPaused;
  bool get isStopped;
  Stream<RecordingDisposition>? get onProgress;

  Future<FlutterSoundRecorder?> openRecorder();
  Future<void> closeRecorder();
  Future<void> startRecorder({
    String? toFile,
    Codec codec = Codec.aacADTS,
    int sampleRate = 44100,
    int numChannels = 2,
    int bitRate = 128000,
    AudioSource? audioSource,
  });
  Future<String> stopRecorder();
  Future<void> pauseRecorder();
  Future<void> resumeRecorder();
  Future<bool> isEncoderSupported(Codec codec);
  Future<void> setSubscriptionDuration(Duration duration);
}

3.3 PlayerState 播放状态

enum PlayerState {
  isStopped,
  isPlaying,
  isPaused,
}

3.4 RecorderState 录制状态

enum RecorderState {
  isStopped,
  isPaused,
  isRecording,
}

3.5 Codec 编码格式

enum Codec {
  defaultCodec,
  aacADTS,
  opusOGG,
  opusCAF,
  mp3,
  vorbisOGG,
  pcm16,
  pcm16WAV,
  pcm16AIFF,
  pcm16CAF,
  flac,
  aacMP4,
  amrNB,
  amrWB,
  pcm8,
  pcmFloat32,
  pcmWebM,
  opusWebM,
  vorbisWebM,
}

3.6 AudioSource 音频源

enum AudioSource {
  defaultSource,
  microphone,
  voiceDownlink,
  camCorder,
  remote_submix,
  unprocessed,
  voice_call,
  voice_communication,
  voice_performance,
  voice_recognition,
  voiceUpLink,
  bluetoothHFP,
  headsetMic,
  lineIn,
}

四、播放器 API 详解

4.1 openPlayer 方法

打开播放器会话,必须在播放前调用。

Future<FlutterSoundPlayer?> openPlayer()

返回值:返回播放器实例。

使用示例

FlutterSoundPlayer player = FlutterSoundPlayer();
await player.openPlayer();

4.2 closePlayer 方法

关闭播放器会话,释放资源。

Future<void> closePlayer()

使用示例

await player.closePlayer();
player = null;

4.3 startPlayer 方法

开始播放音频。

Future<Duration> startPlayer({
  String? fromURI,
  Uint8List? fromDataBuffer,
  Codec codec = Codec.aacADTS,
  TWhenFinished? whenFinished,
  int? numChannels,
  int? sampleRate,
})

参数说明

参数 类型 说明
fromURI String? 音频文件路径或 URL
fromDataBuffer Uint8List? 音频数据缓冲区
codec Codec 音频编码格式,默认 aacADTS
whenFinished TWhenFinished? 播放完成回调
numChannels int? 声道数(PCM 格式需要)
sampleRate int? 采样率(PCM 格式需要)

返回值:返回音频总时长。

使用示例

await player.startPlayer(
  fromURI: 'https://example.com/audio.mp3',
  codec: Codec.mp3,
  whenFinished: () {
    print('播放完成');
  },
);

4.4 stopPlayer 方法

停止播放。

Future<void> stopPlayer()

使用示例

await player.stopPlayer();

4.5 pausePlayer 方法

暂停播放。

Future<void> pausePlayer()

使用示例

await player.pausePlayer();

4.6 resumePlayer 方法

恢复播放。

Future<void> resumePlayer()

使用示例

await player.resumePlayer();

4.7 seekToPlayer 方法

跳转到指定位置。

Future<void> seekToPlayer(Duration position)

参数说明

参数 类型 说明
position Duration 目标播放位置

使用示例

await player.seekToPlayer(Duration(milliseconds: 30000));
await player.seekToPlayer(Duration(seconds: 30));

4.8 setVolume 方法

设置音量。

Future<void> setVolume(double volume)

参数说明

参数 类型 说明
volume double 音量值,范围 0.0 ~ 1.0

使用示例

await player.setVolume(0.5);

4.9 setSpeed 方法

设置播放速度。

Future<void> setSpeed(double speed)

参数说明

参数 类型 说明
speed double 播放速度倍率

使用示例

await player.setSpeed(1.5);
await player.setSpeed(0.5);

4.10 onProgress 属性

播放进度流,用于监听播放进度。

Stream<PlaybackDisposition>? get onProgress

PlaybackDisposition 结构

字段 类型 说明
position Duration 当前播放位置
duration Duration 音频总时长

使用示例

player.onProgress!.listen((e) {
  print('进度: ${e.position} / ${e.duration}');
});

五、录音器 API 详解

5.1 openRecorder 方法

打开录音器会话,必须在录音前调用。

Future<FlutterSoundRecorder?> openRecorder()

使用示例

FlutterSoundRecorder recorder = FlutterSoundRecorder();
await recorder.openRecorder();

5.2 closeRecorder 方法

关闭录音器会话,释放资源。

Future<void> closeRecorder()

使用示例

await recorder.closeRecorder();
recorder = null;

5.3 startRecorder 方法

开始录音。

Future<void> startRecorder({
  String? toFile,
  StreamSink<Food>? toStream,
  Codec codec = Codec.aacADTS,
  int sampleRate = 44100,
  int numChannels = 2,
  int bitRate = 128000,
  AudioSource? audioSource,
})

参数说明

参数 类型 说明
toFile String? 录音文件路径
toStream StreamSink? 录音数据流
codec Codec 音频编码格式
sampleRate int 采样率,默认 44100
numChannels int 声道数,默认 2
bitRate int 比特率,默认 128000
audioSource AudioSource? 音频源,默认 microphone

使用示例

await recorder.startRecorder(
  toFile: 'recording.m4a',
  codec: Codec.aacMP4,
  sampleRate: 44100,
  numChannels: 1,
  bitRate: 128000,
  audioSource: AudioSource.microphone,
);

5.4 stopRecorder 方法

停止录音。

Future<String> stopRecorder()

返回值:返回录音文件路径。

使用示例

String path = await recorder.stopRecorder();
print('录音保存到: $path');

5.5 pauseRecorder 方法

暂停录音。

Future<void> pauseRecorder()

使用示例

await recorder.pauseRecorder();

5.6 resumeRecorder 方法

恢复录音。

Future<void> resumeRecorder()

使用示例

await recorder.resumeRecorder();

5.7 isEncoderSupported 方法

检查编码格式是否支持。

Future<bool> isEncoderSupported(Codec codec)

使用示例

bool supported = await recorder.isEncoderSupported(Codec.mp3);
print('MP3 编码支持: $supported');

5.8 onProgress 属性

录音进度流,用于监听录音进度和分贝级别。

Stream<RecordingDisposition>? get onProgress

RecordingDisposition 结构

字段 类型 说明
duration Duration 已录制时长
dbPeakLevel double 分贝级别

使用示例

recorder.onProgress!.listen((e) {
  print('时长: ${e.duration}, 分贝: ${e.dbPeakLevel}');
});

六、OpenHarmony 平台实现原理

6.1 原生 API 映射

Flutter API OpenHarmony API
播放器 media.AVPlayer / audio.AudioRenderer
录音器 media.AVRecorder / audio.AudioCapturer
文件播放 AVPlayer.url = “fd://” + fd
网络播放 AVPlayer.url = “http://…”
文件录制 AVRecorder.prepare(config)

6.2 播放器实现

OpenHarmony 使用 AVPlayer 播放普通音频格式,使用 AudioRenderer 播放 PCM 数据:

export class FlutterAVPlayer implements IPlayer {
  private avPlayer: media.AVPlayer | null = null;

  async startPlayer(codec: t_CODEC, path: string, dataBuffer: Uint8Array, 
                    numChannels: number, sampleRate: number): Promise<boolean> {
    this.avPlayer = await media.createAVPlayer();
    this.setAVPlayerCallback();
    
    if(path.startsWith("http")) {
      this.avPlayer.url = path;
    } else {
      this.audioFile = fs.openSync(path, fs.OpenMode.READ_ONLY);
      this.avPlayer.url = "fd://" + this.audioFile?.fd;
    }
    return true;
  }

  private setAVPlayerCallback(): void {
    this.avPlayer?.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
      switch (state) {
        case "initialized":
          this.avPlayer?.prepare();
          break;
        case "prepared":
          await this.avPlayer?.play();
          break;
        case "completed":
          this.callback?.audioPlayerDidFinishPlaying(true);
          break;
        case "playing":
          this.callback?.startPlayerCompleted(true, this.avPlayer!.duration);
          break;
      }
    });
  }
}

6.3 录音器实现

OpenHarmony 使用 AVRecorder 录制音频,使用 AudioCapturer 录制 PCM 数据:

export class FlutterAVRecorder implements IRecorder {
  private avRecorder: media.AVRecorder | null = null;

  async startRecord(numChannels: number, sampleRate: number, bitRate: number,
    codec: t_CODEC, path: string, audioSource: number): Promise<void> {
    
    let audioCodec = media.CodecMimeType.AUDIO_AAC;
    let fileFormat = media.ContainerFormatType.CFT_MPEG_4A;
    
    if (codec === t_CODEC.mp3) {
      audioCodec = media.CodecMimeType.AUDIO_MP3;
      fileFormat = media.ContainerFormatType.CFT_MP3;
    }

    this.avProfile = {
      audioBitrate: bitRate,
      audioChannels: numChannels,
      audioSampleRate: sampleRate,
      audioCodec: audioCodec,
      fileFormat: fileFormat
    };

    this.audioFile = fs.openSync(path, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
    this.avConfig = {
      audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
      profile: this.avProfile,
      url: "fd://" + this.audioFile.fd.toString()
    };

    this.avRecorder = await media.createAVRecorder();
    await this.avRecorder?.prepare(this.avConfig);
    await this.avRecorder?.start();
  }
}

6.4 编码格式映射

Codec OpenHarmony CodecMimeType ContainerFormatType
aacADTS AUDIO_AAC CFT_MPEG_4A
aacMP4 AUDIO_AAC CFT_MPEG_4
mp3 AUDIO_MP3 CFT_MP3
amrNB AUDIO_AMR_NB CFT_AMR
amrWB AUDIO_AMR_WB CFT_AMR

七、MethodChannel 通信协议

7.1 播放器 Channel 名称

const MethodChannel _channel = MethodChannel('com.dooboolab.flutter_sound_player');

7.2 录音器 Channel 名称

const MethodChannel _channel = MethodChannel('com.dooboolab.flutter_sound_recorder');

7.3 播放器方法列表

方法 参数 返回值
openPlayer - int
closePlayer - void
startPlayer path, codec, fromDataBuffer, … Duration
stopPlayer - void
pausePlayer - void
resumePlayer - void
seekToPlayer millis void
setVolume volume void
setSpeed speed void
setSubscriptionDuration duration void

7.4 录音器方法列表

方法 参数 返回值
openRecorder - int
closeRecorder - void
startRecorder path, codec, sampleRate, … void
stopRecorder - String
pauseRecorder - void
resumeRecorder - void
isEncoderSupported codec bool
setSubscriptionDuration duration void

八、实战案例

8.1 完整音频录制与播放示例

在这里插入图片描述

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_sound/flutter_sound.dart';
import 'package:permission_handler/permission_handler.dart';

void main() {
  runApp(const MaterialApp(home: AudioRecorderPage()));
}

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

  
  State<StatefulWidget> createState() => _AudioRecorderPageState();
}

class _AudioRecorderPageState extends State<AudioRecorderPage> {
  FlutterSoundPlayer? _player = FlutterSoundPlayer();
  FlutterSoundRecorder? _recorder = FlutterSoundRecorder();
  bool _isPlayerInited = false;
  bool _isRecorderInited = false;
  bool _isPlaybackReady = false;

  String _recordPath = 'recording.m4a';
  Codec _codec = Codec.aacMP4;

  Duration _duration = Duration.zero;
  Duration _position = Duration.zero;
  double _volume = 1.0;
  double _speed = 1.0;

  StreamSubscription? _playerSubscription;
  StreamSubscription? _recorderSubscription;

  
  void initState() {
    super.initState();
    _init();
  }

  Future<void> _init() async {
    await _player!.openPlayer();
    _isPlayerInited = true;

    await _recorder!.openRecorder();
    _isRecorderInited = true;

    bool supported = await _recorder!.isEncoderSupported(_codec);
    if (!supported) {
      _codec = Codec.aacADTS;
      _recordPath = 'recording.aac';
    }

    await _recorder!.setSubscriptionDuration(const Duration(milliseconds: 100));
    await _player!.setSubscriptionDuration(const Duration(milliseconds: 100));

    _recorderSubscription = _recorder!.onProgress!.listen((e) {
      setState(() {
        _duration = e.duration;
      });
    });

    _playerSubscription = _player!.onProgress!.listen((e) {
      setState(() {
        _position = e.position;
        _duration = e.duration;
      });
    });

    setState(() {});
  }

  
  void dispose() {
    _playerSubscription?.cancel();
    _recorderSubscription?.cancel();
    _player!.closePlayer();
    _player = null;
    _recorder!.closeRecorder();
    _recorder = null;
    super.dispose();
  }

  Future<void> _startRecorder() async {
    await _recorder!.startRecorder(
      toFile: _recordPath,
      codec: _codec,
      sampleRate: 44100,
      numChannels: 1,
      bitRate: 128000,
    );
    setState(() {});
  }

  Future<void> _stopRecorder() async {
    await _recorder!.stopRecorder();
    _isPlaybackReady = true;
    setState(() {});
  }

  Future<void> _startPlayer() async {
    await _player!.startPlayer(
      fromURI: _recordPath,
      codec: _codec,
      whenFinished: () {
        setState(() {});
      },
    );
    setState(() {});
  }

  Future<void> _stopPlayer() async {
    await _player!.stopPlayer();
    setState(() {});
  }

  Future<void> _pausePlayer() async {
    await _player!.pausePlayer();
    setState(() {});
  }

  Future<void> _resumePlayer() async {
    await _player!.resumePlayer();
    setState(() {});
  }

  String _formatDuration(Duration d) {
    String twoDigits(int n) => n.toString().padLeft(2, '0');
    String minutes = twoDigits(d.inMinutes.remainder(60));
    String seconds = twoDigits(d.inSeconds.remainder(60));
    return '$minutes:$seconds';
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('音频录制与播放')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            _buildRecorderSection(),
            const SizedBox(height: 24),
            _buildPlayerSection(),
            const SizedBox(height: 24),
            _buildControlsSection(),
          ],
        ),
      ),
    );
  }

  Widget _buildRecorderSection() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                const Icon(Icons.mic, size: 24),
                const SizedBox(width: 8),
                const Text('录音器', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                const Spacer(),
                Container(
                  padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
                  decoration: BoxDecoration(
                    color: _recorder!.isRecording ? Colors.red : Colors.grey,
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Text(
                    _recorder!.isRecording ? '录音中' : '已停止',
                    style: const TextStyle(color: Colors.white),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 16),
            Row(
              children: [
                Expanded(
                  child: ElevatedButton.icon(
                    onPressed: _isRecorderInited && _recorder!.isStopped
                        ? _startRecorder
                        : null,
                    icon: const Icon(Icons.fiber_manual_record),
                    label: const Text('开始录音'),
                    style: ElevatedButton.styleFrom(
                      backgroundColor: Colors.red,
                      foregroundColor: Colors.white,
                    ),
                  ),
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: ElevatedButton.icon(
                    onPressed: _recorder!.isRecording ? _stopRecorder : null,
                    icon: const Icon(Icons.stop),
                    label: const Text('停止录音'),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 12),
            Text('时长: ${_formatDuration(_duration)}'),
          ],
        ),
      ),
    );
  }

  Widget _buildPlayerSection() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                const Icon(Icons.play_circle, size: 24),
                const SizedBox(width: 8),
                const Text('播放器', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                const Spacer(),
                Container(
                  padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
                  decoration: BoxDecoration(
                    color: _player!.isPlaying ? Colors.green : Colors.grey,
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Text(
                    _player!.isPlaying
                        ? '播放中'
                        : _player!.isPaused
                            ? '已暂停'
                            : '已停止',
                    style: const TextStyle(color: Colors.white),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 16),
            Row(
              children: [
                Expanded(
                  child: ElevatedButton.icon(
                    onPressed: _isPlaybackReady && _player!.isStopped
                        ? _startPlayer
                        : null,
                    icon: const Icon(Icons.play_arrow),
                    label: const Text('播放'),
                    style: ElevatedButton.styleFrom(
                      backgroundColor: Colors.green,
                      foregroundColor: Colors.white,
                    ),
                  ),
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: ElevatedButton.icon(
                    onPressed: _player!.isPlaying ? _pausePlayer : null,
                    icon: const Icon(Icons.pause),
                    label: const Text('暂停'),
                  ),
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: ElevatedButton.icon(
                    onPressed: _player!.isPaused ? _resumePlayer : null,
                    icon: const Icon(Icons.play_arrow),
                    label: const Text('恢复'),
                  ),
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: ElevatedButton.icon(
                    onPressed: !_player!.isStopped ? _stopPlayer : null,
                    icon: const Icon(Icons.stop),
                    label: const Text('停止'),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 12),
            Row(
              children: [
                Text(_formatDuration(_position)),
                Expanded(
                  child: Slider(
                    value: _duration.inMilliseconds > 0
                        ? _position.inMilliseconds.toDouble()
                        : 0,
                    max: _duration.inMilliseconds.toDouble(),
                    onChanged: (value) async {
                      await _player!.seekToPlayer(Duration(milliseconds: value.toInt()));
                    },
                  ),
                ),
                Text(_formatDuration(_duration)),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildControlsSection() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text('播放控制', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
            const SizedBox(height: 16),
            Row(
              children: [
                const Icon(Icons.volume_up, size: 20),
                const SizedBox(width: 8),
                const Text('音量:'),
                Expanded(
                  child: Slider(
                    value: _volume,
                    min: 0.0,
                    max: 1.0,
                    onChanged: (value) async {
                      await _player!.setVolume(value);
                      setState(() {
                        _volume = value;
                      });
                    },
                  ),
                ),
                Text('${(_volume * 100).toStringAsFixed(0)}%'),
              ],
            ),
            const SizedBox(height: 8),
            Row(
              children: [
                const Icon(Icons.speed, size: 20),
                const SizedBox(width: 8),
                const Text('速度:'),
                Expanded(
                  child: Slider(
                    value: _speed,
                    min: 0.25,
                    max: 2.0,
                    divisions: 7,
                    onChanged: (value) async {
                      await _player!.setSpeed(value);
                      setState(() {
                        _speed = value;
                      });
                    },
                  ),
                ),
                Text('${_speed}x'),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

代码要点说明

功能模块 实现方式
播放器初始化 openPlayer() 打开会话
录音器初始化 openRecorder() 打开会话
权限请求 Permission.microphone.request()
开始录音 startRecorder(toFile: ...)
停止录音 stopRecorder()
开始播放 startPlayer(fromURI: ...)
暂停播放 pausePlayer()
恢复播放 resumePlayer()
停止播放 stopPlayer()
进度监听 onProgress!.listen()
音量控制 setVolume(double)
速度控制 setSpeed(double)
资源释放 closePlayer() / closeRecorder()

九、常见问题与解决方案

9.1 麦克风权限问题

问题:录音时提示权限被拒绝。

解决方案

确保在 module.json5 中正确配置权限,并在录音前请求权限:

var status = await Permission.microphone.request();
if (status != PermissionStatus.granted) {
  throw Exception('麦克风权限未授权');
}

9.2 编码格式不支持

问题:某些编码格式在特定设备上不支持。

解决方案

使用 isEncoderSupported 检查编码支持情况:

bool supported = await recorder.isEncoderSupported(Codec.mp3);
if (!supported) {
  _codec = Codec.aacMP4;
}

9.3 播放进度不准确

问题:播放进度更新不及时或不准确。

解决方案

设置订阅间隔以获得更频繁的更新:

await player.setSubscriptionDuration(const Duration(milliseconds: 100));

9.4 录音文件无法播放

问题:录制的文件无法播放或播放异常。

解决方案

  1. 确保文件扩展名与编码格式匹配
  2. 检查采样率和声道数配置是否正确
  3. 确保录音完成后再进行播放
const List<String> ext = [
  '', '.aac', '.opus', '.caf', '.mp3', '.ogg', '.pcm', '.wav',
  '.aiff', '.caf', '.flac', '.mp4', '.amr', '.amr', '.pcm', '.pcm',
];

9.5 多个播放器冲突

问题:同时播放多个音频时出现问题。

解决方案

每个播放器需要独立的实例:

FlutterSoundPlayer player1 = FlutterSoundPlayer();
FlutterSoundPlayer player2 = FlutterSoundPlayer();
await player1.openPlayer();
await player2.openPlayer();

十、与其他库对比

特性 flutter_sound audioplayers just_audio
音频播放
音频录制
多种编码格式 有限 有限
流式播放
流式录制
播放速度控制 有限
音量控制
进度监听
OpenHarmony 支持
维护状态 活跃开发 活跃开发 活跃开发

建议

  • 需要录音功能:使用 flutter_sound
  • 只需要播放功能:可考虑 just_audioaudioplayers
  • 需要流式处理:使用 flutter_sound

十一、总结

flutter_sound 库为 Flutter for OpenHarmony 提供了完整的音频录制和播放能力,支持多种编码格式和丰富的控制选项,适合各种音频处理场景。

核心要点

  1. 使用 FlutterSoundPlayer 进行音频播放
  2. 使用 FlutterSoundRecorder 进行音频录制
  3. 播放器和录音器都需要先打开会话再使用
  4. 使用完毕后必须关闭会话释放资源
  5. 录音前需要请求麦克风权限
  6. 使用 onProgress 监听播放/录制进度
  7. 使用 isEncoderSupported 检查编码格式支持

最佳实践

  • initState 中初始化播放器和录音器
  • dispose 中关闭播放器和录音器
  • 录音前检查并请求麦克风权限
  • 使用 whenFinished 回调处理播放完成事件
  • 设置合适的订阅间隔以获得流畅的进度更新
  • 根据平台支持情况选择合适的编码格式
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐