Skip to main content

flutter_sound_processing 1.0.0 copy "flutter_sound_processing: ^1.0.0" to clipboard
flutter_sound_processing: ^1.0.0 copied to clipboard

Calculate MFCCs natively on Android and iOS from samples, 16-bit PCM or audio files (mp3, wav, m4a, flac).

example/lib/main.dart

import 'dart:async';
import 'dart:io';
import 'dart:math' as math;
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_sound/flutter_sound.dart';
import 'package:flutter_sound_processing/flutter_sound_processing.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';

const int bufferSize = 7839;
const int sampleRate = 16000;
const int hopLength = 350;
const int nMels = 40;
const int fftSize = 512;
const int mfcc = 40;

/// 1 s of a 1 kHz sine at [sampleRate]; the shared Android/iOS parity fixture.
const int fixtureLength = 16000;

/// Frames dropped at each end before averaging, so encoder delay/padding does not dominate the means.
const int edgeFrames = 3;

/// Values per `MFCC_DUMP` console line; keeps lines well under the iOS unified-log (1 KiB) and
/// Android logcat (4 KiB) truncation limits.
const int dumpValuesPerLine = 25;

/// Raw channel used only to send arguments that the typed API cannot express.
const debugChannel = MethodChannel('flutter_sound_processing');

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

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final _flutterSoundProcessingPlugin = FlutterSoundProcessing();
  final _mRecorder = FlutterSoundRecorder();
  final pcm = Int16List(bufferSize);
  late StreamSubscription _mRecordingDataSubscription;
  late StreamController<Uint8List> _recordingDataController;
  final pathController = TextEditingController();
  int indexSignal = 0;
  bool running = false;
  int inFlight = 0;
  final checkOutput = <String>[];

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

  @override
  Future<void> dispose() async {
    pathController.dispose();
    await closeRecorder();

    super.dispose();
  }

  Future<void> closeRecorder() async {
    await _mRecordingDataSubscription.cancel();
    await _recordingDataController.close();
    await _mRecorder.closeRecorder();
  }

  Future<void> start() async {
    final status = await Permission.microphone.request();

    if (!status.isGranted) {
      return;
    }

    setState(() {
      running = true;
    });

    _recordingDataController = StreamController<Uint8List>();
    await _mRecorder.openRecorder();

    _mRecordingDataSubscription = _recordingDataController.stream.listen((
      samples,
    ) async {
      final byteData = ByteData.sublistView(samples);

      for (var offset = 0; offset + 1 < samples.length; offset += 2) {
        pcm[indexSignal] = byteData.getInt16(offset, Endian.little);
        indexSignal++;

        if (indexSignal == bufferSize) {
          indexSignal = 0;

          final featureMatrix = await _flutterSoundProcessingPlugin
              .calculateMFCCsFromPcm16(
                samples: Int16List.fromList(pcm),
                fftSize: fftSize,
                hopLength: hopLength,
                nMels: nMels,
                mfcc: mfcc,
                sampleRate: sampleRate,
              );

          debugPrint('${featureMatrix?.toList()}');
        }
      }
    });

    await _mRecorder.startRecorder(
      toStream: _recordingDataController.sink,
      codec: Codec.pcm16,
      numChannels: 1,
      sampleRate: sampleRate,
    );
  }

  void pause() {
    _mRecordingDataSubscription.pause();

    setState(() {
      running = false;
    });
  }

  List<double> sine(int count) => List<double>.generate(
    count,
    (n) => 0.5 * math.sin(2 * math.pi * 1000 * n / sampleRate),
  );

  /// The PCM16 form of [sine]: `round(x * 32767)`, as in the committed wav fixtures.
  Int16List sinePcm16(int count) =>
      Int16List.fromList(sine(count).map((x) => (x * 32767).round()).toList());

  Future<Float64List?> calculate(List<double> input, {int hop = hopLength}) =>
      _flutterSoundProcessingPlugin.calculateMFCCs(
        signals: input,
        sampleRate: sampleRate,
        hopLength: hop,
        nMels: nMels,
        fftSize: fftSize,
        mfcc: mfcc,
      );

  Future<Float64List?> calculateFile(
    String path, {
    int hop = hopLength,
    int coefficients = mfcc,
  }) => _flutterSoundProcessingPlugin.calculateMFCCsFromFile(
    path: path,
    sampleRate: sampleRate,
    hopLength: hop,
    nMels: nMels,
    fftSize: fftSize,
    mfcc: coefficients,
  );

  /// Copies a bundled asset into the temporary directory and returns its absolute path.
  Future<String> copyAsset(String name) async {
    final data = await rootBundle.load('assets/$name');
    final directory = await getTemporaryDirectory();
    final file = File('${directory.path}/$name');

    await file.writeAsBytes(
      data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes),
      flush: true,
    );
    report('copied $name to ${file.path}');

    return file.path;
  }

  /// Writes (once) a [seconds]-long 16 kHz mono PCM16 wav of the 1 kHz sine and returns its path.
  Future<String> generateWav(int seconds) async {
    final directory = await getTemporaryDirectory();
    final file = File('${directory.path}/sine_1khz_16k_${seconds}s.wav');
    final second = Uint8List.view(sinePcm16(sampleRate).buffer);
    final dataLength = second.length * seconds;

    if (await file.exists() && await file.length() == 44 + dataLength) {
      return file.path;
    }

    final header = ByteData(44)
      ..setUint32(0, 0x52494646) // 'RIFF'
      ..setUint32(4, 36 + dataLength, Endian.little)
      ..setUint32(8, 0x57415645) // 'WAVE'
      ..setUint32(12, 0x666d7420) // 'fmt '
      ..setUint32(16, 16, Endian.little)
      ..setUint16(20, 1, Endian.little) // PCM
      ..setUint16(22, 1, Endian.little) // mono
      ..setUint32(24, sampleRate, Endian.little)
      ..setUint32(28, sampleRate * 2, Endian.little)
      ..setUint16(32, 2, Endian.little)
      ..setUint16(34, 16, Endian.little)
      ..setUint32(36, 0x64617461) // 'data'
      ..setUint32(40, dataLength, Endian.little);
    final sink = file.openWrite()..add(header.buffer.asUint8List());

    for (var i = 0; i < seconds; i++) {
      sink.add(second);
    }

    await sink.close();
    report('generated ${seconds}s wav at ${file.path}');

    return file.path;
  }

  String summarize(Float64List? result, {int coefficients = mfcc}) {
    if (result == null) {
      return 'null';
    }

    final first = result.take(5).map((v) => v.toStringAsFixed(6)).join(', ');
    final sum = result.fold<double>(0, (total, v) => total + v);
    final frames = result.length ~/ coefficients;

    return 'length=${result.length} frames=$frames '
        'length%$coefficients=${result.length % coefficients} first5=[$first] '
        'sum=${sum.toStringAsFixed(6)} '
        'means=${interiorMeans(result, coefficients)}';
  }

  /// Per-coefficient means over frames [edgeFrames] … frames − edgeFrames − 1 (layout `data[row + rows * col]`).
  String interiorMeans(Float64List result, int coefficients) {
    final frames = result.length ~/ coefficients;

    if (frames <= 2 * edgeFrames) {
      return 'n/a';
    }

    final means = List<String>.generate(coefficients, (row) {
      var total = 0.0;

      for (var col = edgeFrames; col < frames - edgeFrames; col++) {
        total += result[row + coefficients * col];
      }

      return (total / (frames - 2 * edgeFrames)).toStringAsFixed(4);
    });

    return '[${means.join(', ')}]';
  }

  String maxAbsDiff(Float64List? a, Float64List? b) {
    if (a == null || b == null || a.length != b.length) {
      return 'n/a (lengths ${a?.length} vs ${b?.length})';
    }

    var diff = 0.0;

    for (var i = 0; i < a.length; i++) {
      diff = math.max(diff, (a[i] - b[i]).abs());
    }

    return diff.toStringAsExponential(3);
  }

  /// Prints every value of [result] at full (round-trip) precision on console lines
  /// `MFCC_DUMP run=<id> platform=<os> chunk=<i>/<n> offset=<index> total=<length> values=<v v …>`,
  /// so the dumps of both platforms can be compared positionally by a script.
  void dumpValues(Float64List result) {
    final run = DateTime.now().millisecondsSinceEpoch;
    final chunks = (result.length + dumpValuesPerLine - 1) ~/ dumpValuesPerLine;

    for (var chunk = 0; chunk < chunks; chunk++) {
      final offset = chunk * dumpValuesPerLine;
      final end = math.min(offset + dumpValuesPerLine, result.length);

      debugPrint(
        'MFCC_DUMP run=$run platform=${Platform.operatingSystem} '
        'chunk=$chunk/$chunks offset=$offset total=${result.length} '
        'values=${result.sublist(offset, end).join(' ')}',
      );
    }
  }

  void report(String line) {
    debugPrint(line);

    if (!mounted) {
      return;
    }

    setState(() => checkOutput.insert(0, line));
  }

  Future<void> runCheck(String label, Future<String> Function() check) async {
    setState(() => inFlight++);
    final stopwatch = Stopwatch()..start();
    String line;

    try {
      line = await check();
    } on PlatformException catch (e) {
      line = 'PlatformException(${e.code}, ${e.message}, ${e.details})';
    } catch (e) {
      line = 'error $e';
    }

    if (mounted) {
      setState(() => inFlight--);
    }

    report('$label (${stopwatch.elapsedMilliseconds} ms): $line');
  }

  Widget checkButton(String label, Future<Float64List?> Function() calc) =>
      textCheckButton(label, () async => summarize(await calc()));

  Widget textCheckButton(String label, Future<String> Function() check) =>
      OutlinedButton(
        onPressed: () => runCheck(label, check),
        child: Text(label),
      );

  Widget fileButton(String asset) =>
      checkButton(asset, () async => calculateFile(await copyAsset(asset)));

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
          actions: [
            if (inFlight > 0)
              const Padding(
                padding: EdgeInsets.all(16),
                child: SizedBox.square(
                  dimension: 24,
                  child: CircularProgressIndicator(),
                ),
              ),
          ],
        ),
        body: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            ElevatedButton(
              onPressed: running ? pause : start,
              child: Text(running ? 'Pause' : 'Start'),
            ),
            const SizedBox(height: 24),
            const Text(
              'Fixed-input check: 0.5*sin(2*pi*1000*n/$sampleRate), '
              'n_fft $fftSize, hop $hopLength, n_mels $nMels, mfcc $mfcc',
            ),
            Wrap(
              spacing: 8,
              children: [
                checkButton(
                  'Run fixed-input check',
                  () => calculate(sine(fixtureLength)),
                ),
                textCheckButton('PCM16 fixed input', () async {
                  final doubles = await calculate(sine(fixtureLength));
                  final pcm16 = await _flutterSoundProcessingPlugin
                      .calculateMFCCsFromPcm16(
                        samples: sinePcm16(fixtureLength),
                        sampleRate: sampleRate,
                        hopLength: hopLength,
                        nMels: nMels,
                        fftSize: fftSize,
                        mfcc: mfcc,
                      );

                  return '${summarize(pcm16)} '
                      'maxAbsDiffVsDoubles=${maxAbsDiff(pcm16, doubles)}';
                }),
                textCheckButton('Dump fixed-input values', () async {
                  final result = await calculate(sine(fixtureLength));

                  if (result != null) {
                    dumpValues(result);
                  }

                  return summarize(result);
                }),
                textCheckButton('mfcc 50 > nMels', () async {
                  final result = await _flutterSoundProcessingPlugin
                      .calculateMFCCs(
                        signals: sine(fixtureLength),
                        sampleRate: sampleRate,
                        hopLength: hopLength,
                        nMels: nMels,
                        fftSize: fftSize,
                        mfcc: 50,
                      );

                  return summarize(result, coefficients: 50);
                }),
                checkButton('100 samples', () => calculate(sine(100))),
                checkButton('Empty signal', () => calculate(const [])),
                checkButton(
                  'hopLength 0',
                  () => calculate(sine(fixtureLength), hop: 0),
                ),
                checkButton(
                  'Mistyped n_fft',
                  () =>
                      debugChannel.invokeMethod<Float64List>('calculateMFCCs', {
                        'values': Float64List.fromList(sine(100)),
                        'sample_rate': sampleRate,
                        'mfcc': mfcc,
                        'n_fft': '$fftSize',
                        'n_mels': nMels,
                        'hop_length': hopLength,
                      }),
                ),
                checkButton(
                  'values as List<double>',
                  () =>
                      debugChannel.invokeMethod<Float64List>('calculateMFCCs', {
                        'values': sine(fixtureLength),
                        'sample_rate': sampleRate,
                        'mfcc': mfcc,
                        'n_fft': fftSize,
                        'n_mels': nMels,
                        'hop_length': hopLength,
                      }),
                ),
              ],
            ),
            const SizedBox(height: 24),
            const Text(
              'File checks: assets copied to the temporary directory, '
              'decoded natively, resampled to $sampleRate Hz',
            ),
            Wrap(
              spacing: 8,
              children: [
                for (final asset in [
                  'sine_1khz_16k.wav',
                  'sine_1khz_16k.flac',
                  'sine_1khz_16k.mp3',
                  'sine_1khz_16k.m4a',
                  'sine_1khz_44k1.wav',
                ])
                  fileButton(asset),
                textCheckButton('wav vs PCM16', () async {
                  final file = await calculateFile(
                    await copyAsset('sine_1khz_16k.wav'),
                  );
                  final pcm16 = await _flutterSoundProcessingPlugin
                      .calculateMFCCsFromPcm16(
                        samples: sinePcm16(fixtureLength),
                        sampleRate: sampleRate,
                        hopLength: hopLength,
                        nMels: nMels,
                        fftSize: fftSize,
                        mfcc: mfcc,
                      );

                  return 'maxAbsDiff=${maxAbsDiff(file, pcm16)}';
                }),
                checkButton(
                  'wav as file:// URL',
                  () async => calculateFile(
                    Uri.file(await copyAsset('sine_1khz_16k.wav')).toString(),
                  ),
                ),
                textCheckButton('wav, mfcc 13', () async {
                  final result = await calculateFile(
                    await copyAsset('sine_1khz_16k.wav'),
                    coefficients: 13,
                  );

                  return summarize(result, coefficients: 13);
                }),
              ],
            ),
            const SizedBox(height: 24),
            const Text('File errors and limits'),
            Wrap(
              spacing: 8,
              children: [
                checkButton(
                  'hopLength 0, missing path',
                  () => calculateFile('/does/not/exist.wav', hop: 0),
                ),
                checkButton(
                  'Missing path',
                  () => calculateFile('/does/not/exist.wav'),
                ),
                checkButton(
                  'content:// path',
                  () => calculateFile('content://media/external/audio/1'),
                ),
                fileButton('not_audio.txt'),
                fileButton('garbage.mp3'),
                checkButton(
                  'Generate 11-minute wav',
                  () async => calculateFile(await generateWav(11 * 60)),
                ),
                checkButton(
                  'Generate 9-minute wav',
                  () async => calculateFile(await generateWav(9 * 60)),
                ),
              ],
            ),
            const SizedBox(height: 24),
            TextField(
              controller: pathController,
              decoration: const InputDecoration(
                labelText: 'Absolute path or file:// URL',
              ),
            ),
            Align(
              alignment: Alignment.centerLeft,
              child: checkButton(
                'Run on path',
                () => calculateFile(pathController.text.trim()),
              ),
            ),
            const SizedBox(height: 24),
            for (final line in checkOutput) SelectableText(line),
          ],
        ),
      ),
    );
  }
}
6
likes
150
points
41
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Calculate MFCCs natively on Android and iOS from samples, 16-bit PCM or audio files (mp3, wav, m4a, flac).

Repository (GitHub)
View/report issues

Topics

#audio #mfcc #signal-processing

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on flutter_sound_processing

Packages that implement flutter_sound_processing