You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter Web麦克风实时音频播放正常,Android设备运行秒退问题求助

Flutter Web麦克风实时音频播放正常,Android设备运行秒退问题求助

我现在在做一个用tAudio库实现麦克风录音实时回放到扬声器的功能,在Flutter Web上测试完全正常,但一放到Android设备上运行,录音大概1秒不到App就直接崩了,实在找不到问题出在哪,麻烦大家帮我看看我的代码有没有问题?

以下是我的完整代码:

/*
 * Copyright 2018, 2019, 2020, 2021 Dooboolab.
 *
 * This file is part of Flutter-Sound.
 *
 * Flutter-Sound is free software: you can redistribute it and/or modify
 * it under the terms of the Mozilla Public License version 2 (MPL2.0),
 * as published by the Mozilla organization.
 *
 * Flutter-Sound is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * MPL General Public License for more details.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */
import 'dart:async';
import 'package:flutter/foundation.dart' show kIsWeb, Uint8List;
import 'package:flutter/material.dart';
import 'package:taudio/taudio.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:audio_session/audio_session.dart';

/* This is a very simple example for Flutter Sound beginners, hat shows how to record, and then playback a file.
   This example is really basic.
*/
void main() {
  runApp(const MaterialApp(
    home: PlayFromMic(),
  ));
}

typedef _Fn = void Function();
const theSource = AudioSource.microphone;

// Example app.
class PlayFromMic extends StatefulWidget {
  const PlayFromMic({super.key});

  @override
  State<PlayFromMic> createState() => _PlayFromMic();
}

class _PlayFromMic extends State<PlayFromMic> {
  /// Our player
  final FlutterSoundPlayer _mPlayer = FlutterSoundPlayer();

  /// Our recorder
  final FlutterSoundRecorder _mRecorder = FlutterSoundRecorder();

  var recordingDataControllerUint8 = StreamController<Uint8List>();

  Future<void> init() async {
    final session = await AudioSession.instance;
    await session.configure(AudioSessionConfiguration(
      avAudioSessionCategory: AVAudioSessionCategory.playAndRecord,
      avAudioSessionCategoryOptions: AVAudioSessionCategoryOptions.allowBluetooth | AVAudioSessionCategoryOptions.defaultToSpeaker,
      avAudioSessionMode: AVAudioSessionMode.spokenAudio,
      avAudioSessionRouteSharingPolicy: AVAudioSessionRouteSharingPolicy.defaultPolicy,
      avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none,
      androidAudioAttributes: const AndroidAudioAttributes(
        contentType: AndroidAudioContentType.speech,
        flags: AndroidAudioFlags.none,
        usage: AndroidAudioUsage.voiceCommunication,
      ),
      androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
      androidWillPauseWhenDucked: true,
    ));

    _mPlayer.openPlayer().then((value) {
      setState(() {
        _mPlayerIsInited = true;
      });
    });

    openTheRecorder().then((value) {
      setState(() {
        _mRecorderIsInited = true;
      });
    });
  }

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

  @override
  void dispose() {
    _mPlayer.closePlayer();
    _mRecorder.closeRecorder();
    recordingDataControllerUint8.close(); // 补充了StreamController的关闭,避免内存泄漏
    super.dispose();
  }

  // ------------------------------ This is the recorder stuff -----------------------------
  static const Codec _codec = Codec.pcm16;
  static const int _sampleRate = 48000;
  bool _mRecorderIsInited = false;
  double _dbLevel = 0.0;
  StreamSubscription? _recorderSubscription;
  bool bNoiseSuppression = false;
  bool bEchoCancellation = false;
  bool _mPlayerIsInited = false; // 补全了原代码中缺失的变量定义

  /// Request permission to record something and open the recorder
  Future<void> openTheRecorder() async {
    if (!kIsWeb) {
      var status = await Permission.microphone.request();
      if (status != PermissionStatus.granted) {
        throw RecordingPermissionException('Microphone permission not granted');
      }
      // 补充请求存储权限?不过实时播放可能不需要,但Android 13+可能需要POST_NOTIFICATIONS?
      var notificationStatus = await Permission.notification.request();
      if (notificationStatus != PermissionStatus.granted) {
        print('Notification permission not granted');
      }
    }
    await _mRecorder.openRecorder();
    _recorderSubscription = _mRecorder.onProgress!.listen((e) {
      setState(() {
        _dbLevel = e.decibels as double;
      });
    });
    await _mRecorder.setSubscriptionDuration(
        const Duration(milliseconds: 100));
    _mRecorderIsInited = true;
  }

  /// Begin to record.
  /// This is our main function.
  /// We ask Flutter Sound to record to a File.
  void record() async {
    try {
      assert(_mPlayerIsInited && _mRecorder.isStopped && _mPlayer.isStopped);
      // 先将录音流直接对接播放器的sink,避免中间的StreamController可能的问题
      await _mPlayer.startPlayerFromStream(
        codec: _codec,
        sampleRate: _sampleRate,
        interleaved: true,
        bufferSize: 1024,
        numChannels: 2,
      );
      await _mRecorder.startRecorder(
        codec: _codec,
        audioSource: theSource,
        toStream: _mPlayer.sink, // 直接将录音输出到播放器的输入流,去掉中间的自定义StreamController
        sampleRate: _sampleRate,
        numChannels: 2,
        enableNoiseSuppression: bNoiseSuppression,
        enableEchoCancellation: bEchoCancellation,
      );
    } catch (err) {
      print('录音/播放启动错误: $err');
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('启动失败: $err')));
    }
  }

  // 补充停止功能
  void stop() async {
    await _mRecorder.stopRecorder();
    await _mPlayer.stopPlayer();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('实时麦克风播放')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            if (_mRecorderIsInited && _mPlayerIsInited)
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  ElevatedButton(
                    onPressed: _mRecorder.isRecording ? null : record,
                    child: const Text('开始实时播放'),
                  ),
                  const SizedBox(width: 20),
                  ElevatedButton(
                    onPressed: _mRecorder.isRecording ? stop : null,
                    child: const Text('停止'),
                  ),
                ],
              ),
            const SizedBox(height: 20),
            Text('当前分贝值: ${_dbLevel.toStringAsFixed(1)}'),
          ],
        ),
      ),
    );
  }
}

我自己排查的时候发现原代码里有几个可能的问题点:

  1. 自定义的recordingDataControllerUint8没有对接播放器的输入流,相当于录音数据没被消费,可能导致Android端的缓冲区溢出崩溃
  2. 原代码缺失_mPlayerIsInited变量的定义,虽然Web上可能不影响,但Android端严格类型检查可能出问题
  3. 没有关闭自定义的StreamController,可能导致内存泄漏

我在代码里做了一些修改尝试修复,但还是不确定是不是这些问题导致的崩溃,有没有大佬能帮忙看看Android端秒退的具体原因?

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 07:33:04