mic_stream_recorder 1.1.2 copy "mic_stream_recorder: ^1.1.2" to clipboard
mic_stream_recorder: ^1.1.2 copied to clipboard

A Flutter plugin for recording audio from the microphone with real-time amplitude monitoring. Supports both iOS and Android platforms with configurable recording settings and built-in playback functionality.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:mic_stream_recorder/mic_stream_recorder.dart';
import 'package:path_provider/path_provider.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Mic Stream Recorder Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const RecordingPage(),
    );
  }
}

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

  @override
  State<RecordingPage> createState() => _RecordingPageState();
}

class _RecordingPageState extends State<RecordingPage> {
  final MicStreamRecorder _recorder = MicStreamRecorder();
  final TextEditingController _fileNameController = TextEditingController();

  bool _isRecording = false;
  bool _isPlaying = false;
  double _currentAmplitude = 0.0;
  String? _lastRecordingPath;
  List<String> _recordingFiles = [];
  RangeValues _amplitudeRange = const RangeValues(0.0, 100.0);

  @override
  void initState() {
    super.initState();
    _loadRecordingFiles();
    _setupAmplitudeListener();
  }

  void _setupAmplitudeListener() {
    _recorder.amplitudeStream.listen((rawAmplitude) {
      if (mounted && _isRecording) {
        // Post-process the raw 0.0-1.0 amplitude to custom range
        final normalizedAmplitude = _normalizeAmplitude(rawAmplitude);
        setState(() => _currentAmplitude = normalizedAmplitude);
      }
    });
  }

  /// Post-process raw amplitude (0.0-1.0) to custom range
  double _normalizeAmplitude(double rawAmplitude) {
    final minPercent = _amplitudeRange.start / 100.0;
    final maxPercent = _amplitudeRange.end / 100.0;

    // Apply custom range normalization
    return minPercent + (rawAmplitude * (maxPercent - minPercent));
  }

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

  // Core functionality methods
  Future<void> _loadRecordingFiles() async {
    try {
      final directory = await getApplicationDocumentsDirectory();
      final files = directory
          .listSync()
          .where((file) => file.path.endsWith('.m4a'))
          .map((file) => file.path)
          .toList();
      setState(() => _recordingFiles = files);
    } catch (e) {
      _showMessage('Failed to load files: $e', isError: true);
    }
  }

  Future<void> _startRecording() async {
    if (_isRecording) return;
    try {
      String? customPath;
      if (_fileNameController.text.isNotEmpty) {
        final directory = await getApplicationDocumentsDirectory();
        customPath = '${directory.path}/${_fileNameController.text}.m4a';
        _showMessage('Recording: ${_fileNameController.text}.m4a');
      }
      await _recorder.startRecording(customPath);
      setState(() => _isRecording = true);
    } catch (e) {
      _showMessage('Failed to start recording: $e', isError: true);
    }
  }

  Future<void> _stopRecording() async {
    if (!_isRecording) return;
    try {
      final recordingPath = await _recorder.stopRecording();
      setState(() {
        _isRecording = false;
        _lastRecordingPath = recordingPath;
        _currentAmplitude = 0.0;
      });
      _fileNameController.clear();
      await _loadRecordingFiles();
      _showMessage('Recording saved');
    } catch (e) {
      _showMessage('Failed to stop recording: $e', isError: true);
    }
  }

  Future<void> _playRecording(String filePath) async {
    try {
      await _recorder.playRecording(filePath);
      setState(() => _isPlaying = true);
      _checkPlayingStatus();
    } catch (e) {
      _showMessage('Failed to play: $e', isError: true);
    }
  }

  Future<void> _pausePlayback() async {
    try {
      await _recorder.pausePlayback();
      setState(() => _isPlaying = false);
    } catch (e) {
      _showMessage('Failed to pause: $e', isError: true);
    }
  }

  Future<void> _stopPlayback() async {
    try {
      await _recorder.stopPlayback();
      setState(() => _isPlaying = false);
    } catch (e) {
      _showMessage('Failed to stop: $e', isError: true);
    }
  }

  void _checkPlayingStatus() async {
    while (_isPlaying && mounted) {
      await Future.delayed(const Duration(milliseconds: 500));
      if (mounted) {
        final isPlaying = await _recorder.isPlaying();
        if (!isPlaying && _isPlaying) {
          setState(() => _isPlaying = false);
          break;
        }
      }
    }
  }

  // UI helper methods
  void _showMessage(String message, {bool isError = false}) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(message),
        backgroundColor: isError ? Colors.red : Colors.green,
        duration: const Duration(seconds: 1),
      ),
    );
  }

  Color _getAmplitudeColor(double amplitude) {
    final progress = _getAmplitudeProgress(amplitude);
    if (progress < 0.3) return Colors.green;
    if (progress < 0.7) return Colors.orange;
    return Colors.red;
  }

  double _getAmplitudeProgress(double amplitude) {
    final minPercent = _amplitudeRange.start / 100.0;
    final maxPercent = _amplitudeRange.end / 100.0;
    if (maxPercent == minPercent) return 0.0;
    final clampedAmplitude = amplitude.clamp(minPercent, maxPercent);
    return ((clampedAmplitude - minPercent) / (maxPercent - minPercent))
        .clamp(0.0, 1.0);
  }

  void _setAmplitudeRange(double start, double end) {
    setState(() => _amplitudeRange = RangeValues(start, end));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Mic Stream Recorder'),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            FileNameInputCard(
              controller: _fileNameController,
              isRecording: _isRecording,
            ),
            const SizedBox(height: 16),
            AmplitudeRangeCard(
              amplitudeRange: _amplitudeRange,
              isRecording: _isRecording,
              onRangeChanged: (values) =>
                  setState(() => _amplitudeRange = values),
              onPresetSelected: _setAmplitudeRange,
            ),
            const SizedBox(height: 16),
            RecordingControlsCard(
              isRecording: _isRecording,
              currentAmplitude: _currentAmplitude,
              amplitudeProgress: _getAmplitudeProgress(_currentAmplitude),
              amplitudeColor: _getAmplitudeColor(_currentAmplitude),
              onStartRecording: _startRecording,
              onStopRecording: _stopRecording,
            ),
            const SizedBox(height: 16),
            if (_lastRecordingPath != null)
              PlaybackControlsCard(
                isPlaying: _isPlaying,
                onPlay: () => _playRecording(_lastRecordingPath!),
                onPause: _pausePlayback,
                onStop: _stopPlayback,
              ),
            const SizedBox(height: 16),
            RecordingFilesCard(
              recordingFiles: _recordingFiles,
              isPlaying: _isPlaying,
              onRefresh: _loadRecordingFiles,
              onPlayFile: _playRecording,
            ),
            const SizedBox(height: 16),
          ],
        ),
      ),
    );
  }
}

// Reusable card wrapper widget
class CardWrapper extends StatelessWidget {
  final String title;
  final Widget child;
  final Widget? trailingItem;

  const CardWrapper({
    super.key,
    required this.title,
    required this.child,
    this.trailingItem,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Text(
                  title,
                  style: Theme.of(context).textTheme.titleMedium,
                ),
                const Spacer(),
                if (trailingItem != null) trailingItem!,
              ],
            ),
            const SizedBox(height: 16),
            child,
          ],
        ),
      ),
    );
  }
}

// File name input widget
class FileNameInputCard extends StatelessWidget {
  final TextEditingController controller;
  final bool isRecording;

  const FileNameInputCard({
    super.key,
    required this.controller,
    required this.isRecording,
  });

  @override
  Widget build(BuildContext context) {
    return CardWrapper(
      title: 'Custom Recording Name (Optional)',
      child: TextField(
        controller: controller,
        enabled: !isRecording,
        decoration: const InputDecoration(
          hintText: 'Enter filename (without extension)',
          border: OutlineInputBorder(),
          suffixText: '.m4a',
        ),
      ),
    );
  }
}

// Recording controls widget
class RecordingControlsCard extends StatelessWidget {
  final bool isRecording;
  final double currentAmplitude;
  final double amplitudeProgress;
  final Color amplitudeColor;
  final VoidCallback onStartRecording;
  final VoidCallback onStopRecording;

  const RecordingControlsCard({
    super.key,
    required this.isRecording,
    required this.currentAmplitude,
    required this.amplitudeProgress,
    required this.amplitudeColor,
    required this.onStartRecording,
    required this.onStopRecording,
  });

  @override
  Widget build(BuildContext context) {
    return CardWrapper(
      title: 'Recording Controls',
      child: Column(
        children: [
          if (isRecording) ...[
            Text(
              'Audio Level: ${(currentAmplitude * 100).toStringAsFixed(1)}%',
              style: Theme.of(context).textTheme.bodyMedium,
            ),
            const SizedBox(height: 8),
            LinearProgressIndicator(
              value: amplitudeProgress,
              backgroundColor: Colors.grey[300],
              valueColor: AlwaysStoppedAnimation<Color>(amplitudeColor),
              minHeight: 8,
            ),
            const SizedBox(height: 16),
          ],
          SizedBox(
            width: double.infinity,
            child: ElevatedButton.icon(
              onPressed: isRecording ? onStopRecording : onStartRecording,
              icon: Icon(isRecording ? Icons.stop : Icons.mic),
              label: Text(isRecording ? 'Stop Recording' : 'Start Recording'),
              style: ElevatedButton.styleFrom(
                backgroundColor: isRecording ? Colors.red : Colors.blue,
                foregroundColor: Colors.white,
                padding: const EdgeInsets.symmetric(vertical: 12),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

// Playback controls widget
class PlaybackControlsCard extends StatelessWidget {
  final bool isPlaying;
  final VoidCallback onPlay;
  final VoidCallback onPause;
  final VoidCallback onStop;

  const PlaybackControlsCard({
    super.key,
    required this.isPlaying,
    required this.onPlay,
    required this.onPause,
    required this.onStop,
  });

  @override
  Widget build(BuildContext context) {
    return CardWrapper(
      title: 'Last Recording Playback',
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          PlaybackButton(
            icon: Icons.play_arrow,
            label: 'Play',
            onPressed: isPlaying ? null : onPlay,
          ),
          PlaybackButton(
            icon: Icons.pause,
            label: 'Pause',
            onPressed: isPlaying ? onPause : null,
          ),
          PlaybackButton(
            icon: Icons.stop,
            label: 'Stop',
            onPressed: isPlaying ? onStop : null,
          ),
        ],
      ),
    );
  }
}

// Playback button widget
class PlaybackButton extends StatelessWidget {
  final IconData icon;
  final String label;
  final VoidCallback? onPressed;

  const PlaybackButton({
    super.key,
    required this.icon,
    required this.label,
    required this.onPressed,
  });

  @override
  Widget build(BuildContext context) {
    return ElevatedButton.icon(
      onPressed: onPressed,
      icon: Icon(icon),
      label: Text(label),
    );
  }
}

// Recording files list widget
class RecordingFilesCard extends StatelessWidget {
  final List<String> recordingFiles;
  final bool isPlaying;
  final VoidCallback onRefresh;
  final Function(String) onPlayFile;

  const RecordingFilesCard({
    super.key,
    required this.recordingFiles,
    required this.isPlaying,
    required this.onRefresh,
    required this.onPlayFile,
  });

  @override
  Widget build(BuildContext context) {
    return CardWrapper(
      title: 'Recorded Files',
      trailingItem: IconButton(
        onPressed: onRefresh,
        icon: const Icon(Icons.refresh),
      ),
      child: recordingFiles.isEmpty
          ? const Center(child: Text('No recordings found'))
          : ListView.builder(
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              itemCount: recordingFiles.length,
              itemBuilder: (context, index) {
                final filePath = recordingFiles[index];
                final fileName = filePath.split('/').last;
                return ListTile(
                  leading: const Icon(Icons.audiotrack),
                  title: Text(fileName),
                  trailing: IconButton(
                    onPressed: isPlaying ? null : () => onPlayFile(filePath),
                    icon: const Icon(Icons.play_arrow),
                  ),
                );
              },
            ),
    );
  }
}

// Amplitude range card - demonstrates post-processing of raw amplitude values
class AmplitudeRangeCard extends StatelessWidget {
  final RangeValues amplitudeRange;
  final bool isRecording;
  final ValueChanged<RangeValues> onRangeChanged;
  final Function(double, double) onPresetSelected;

  const AmplitudeRangeCard({
    super.key,
    required this.amplitudeRange,
    required this.isRecording,
    required this.onRangeChanged,
    required this.onPresetSelected,
  });

  @override
  Widget build(BuildContext context) {
    return CardWrapper(
      title: 'Amplitude Range (Post-Processing Demo)',
      child: Column(
        children: [
          Text(
            'Plugin returns raw 0.0-1.0 values.\nExample shows post-processing to ${amplitudeRange.start.toStringAsFixed(0)}%-${amplitudeRange.end.toStringAsFixed(0)}%',
            style: Theme.of(context).textTheme.bodySmall,
            textAlign: TextAlign.center,
          ),
          const SizedBox(height: 12),
          RangeSlider(
            values: amplitudeRange,
            min: 0.0,
            max: 100.0,
            divisions: 100,
            labels: RangeLabels(
              '${amplitudeRange.start.toStringAsFixed(0)}%',
              '${amplitudeRange.end.toStringAsFixed(0)}%',
            ),
            onChanged: isRecording ? null : onRangeChanged,
          ),
          const SizedBox(height: 8),
          Wrap(
            spacing: 8,
            children: [
              PresetButton(
                label: 'Full 0-100%',
                isEnabled: !isRecording,
                onPressed: () => onPresetSelected(0.0, 100.0),
              ),
              PresetButton(
                label: 'Mid 20-80%',
                isEnabled: !isRecording,
                onPressed: () => onPresetSelected(20.0, 80.0),
              ),
              PresetButton(
                label: 'Focus 30-70%',
                isEnabled: !isRecording,
                onPressed: () => onPresetSelected(30.0, 70.0),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

// Preset button widget
class PresetButton extends StatelessWidget {
  final String label;
  final bool isEnabled;
  final VoidCallback onPressed;

  const PresetButton({
    super.key,
    required this.label,
    required this.isEnabled,
    required this.onPressed,
  });

  @override
  Widget build(BuildContext context) {
    return TextButton(
      onPressed: isEnabled ? onPressed : null,
      child: Text(label),
    );
  }
}
5
likes
150
points
14.1k
downloads

Publisher

verified publisheryasinarik.com

Weekly Downloads

A Flutter plugin for recording audio from the microphone with real-time amplitude monitoring. Supports both iOS and Android platforms with configurable recording settings and built-in playback functionality.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on mic_stream_recorder