Sautiflow

High-fidelity, cross-platform Dart/Flutter audio engine for audiophiles. Powered by miniaudio and native C++ FFI.

Zero Latency

Native C++ binding ensures instant playback and response times.

Gapless Playlist

Seamless transitions between tracks with advanced queue management.

Pro FX Chain

Built-in EQ suite, reverb, delay, low/high/band-pass, notch, peak EQ, and shelf filters.

Cross-Platform

Support for Windows, Linux, Android, iOS, and macOS.

Pro Pipeline State

Query true internal flow from native input formats to hardware negotiated device outputs.

Audio Formats & Quality

Sautiflow is engineered with audiophile-grade fidelity in mind. It handles different audio formats natively within its C++ engine to provide the best possible acoustic reproduction:

  • Lossy Formats (MP3, AAC, OGG etc.): These formats are inherently compressed. Upon playback, Sautiflow upscales/upsamples the decoded bitstream internally (using 32-bit floating point math) ensuring that DSP operations—like EQ, reverb, and spatial width—have plenty of headroom and do not clip or introduce artificial artifacts typical of low-resolution math.
  • Lossless Formats (FLAC, WAV, ALAC, etc.): These are mapped and processed raw. Sautiflow does not dynamically upscale them to guess lost data (since nothing is lost). It maintains their pristine bit-perfect original depth into the DSP chain. If the file's sample rate doesn't match your device's native hardware rate (e.g., streaming an 88.2kHz FLAC file on a 48kHz output device), Sautiflow employs high-fidelity resampling transparently using libsamplerate to match your hardware, avoiding quality loss while retaining original bit-depth fidelity.

Changes Made So Far

Quick implementation summary of the latest work in this package:

  • Integrated libsamplerate (Secret Rabbit Code) for high-fidelity audio resampling (SRC Sinc Best Quality, Medium, Fastest, ZOH, Linear).
  • Added mixed multiband FX chain API with per-band types via EqBandType and EqBandConfig.
  • Added realtime analyzer frame APIs, including stream and pull-style access.
  • Added crossfade controls: setCrossfadeEnabled(bool) and setCrossfadeDurationMs(int).
  • Added real-time injectible custom filters: LPF1, HPF1, Biquad which tap directly into the audio engine node graph.
  • Added standalone object-oriented filter APIs (LPF1, LPF2, Biquad, Resampler, etc) that work independently from the main player.
  • Added s24 and s32 audio formats, with dithering options.
  • Updated sample-rate behavior so setOutputSampleRate(0) uses native device rate mode.
  • Updated example app to test: mixed EQ mode, analyzer visualization, output format/rate/channels, and crossfade controls.
  • Updated docs with implementation snippets for mixed multiband FX, realtime analyzer, crossfade, and output sample-rate native mode.

Installation

Add sautiflow to your pubspec.yaml:

flutter pub add sautiflow

Or manually:

dependencies:
    sautiflow: ^0.2.0
Note: On Android, network streaming support depends on the build configuration. See Platform Specifics.

Quick Start

Initialize the player, set a playlist, and start playing.

import 'package:sautiflow/sautiflow.dart';

void main() async {
  final player = MiniAudioPlayer();
  
  // 1. Initialize the engine
  player.init();

  // 2. Define playlist
  final playlist = [
    AudioSource.network('https://example.com/track1.mp3'),
    AudioSource.file('/path/to/local/track2.mp3'),
  ];

  // 3. Load and play
  player.setAudioSources(playlist);
  player.play();

  // 4. Control playback
  await Future.delayed(Duration(seconds: 5));
  player.seekToNext();
}

Initialization

The MiniAudioPlayer is the main entry point.

init

bool init({int sampleRate, int channels, bool enableSystemAudio})

Initializes the underlying C++ audio engine. Must be called before any other operation.

Parameter Type Description
sampleRate int Output sample rate (default: 48000)
channels int Output channels (default: 2)
enableSystemAudio bool Integrate with OS media controls (Android/iOS)

dispose

void dispose()

Releases all native resources and stops the audio engine. Always call this when the player is no longer needed.

Playback Controls

play / pause / stop

bool play() / bool pause() / bool stop()

Basic transport controls. Returns true if the operation was successful.

seek

bool seek(Duration position, {int? index})

Seeks to a specific position. If index is provided, it jumps to that track in the playlist first.

seekToNext / seekToPrevious

bool seekToNext() / bool seekToPrevious()

Skips to the next or previous track in the playlist.

setVolume / setGain

void setVolume(double volume)

Sets the master volume gain. 1.0 is unity gain.

Playlist Management

setAudioSources

bool setAudioSources(List<AudioSource> sources, ...)

Replaces the current playlist with a new list of sources.

player.setAudioSources([
  AudioSource.network('https://stream.url/audio.mp3'),
  AudioSource.file('assets/music.mp3')
]);

Playlist Mutation

  • addAudioSource(AudioSource source): Append a track.
  • insertAudioSource(int index, AudioSource source): Insert at position.
  • removeAudioSourceAt(int index): Remove track.
  • moveAudioSource(int from, int to): Reorder playback.
  • clearAudioSources(): Clear playlist.

Modes

  • setLoopMode(LoopMode mode): off, all, one.
  • setShuffleModeEnabled(bool enabled): Toggles shuffle.
  • reshuffle(): Re-randomizes the shuffle order.

Player Status

You can listen to the player status stream for real-time UI updates.

StreamBuilder<PlayerStatus>(
  stream: player.statusStream,
  builder: (context, snapshot) {
    final status = snapshot.data;
    if (status == null) return CircularProgressIndicator();
    
    return Column(
      children: [
        Text("Time: ${status.positionSeconds} / ${status.durationSeconds}"),
        LinearProgressIndicator(
          value: status.positionSeconds / status.durationSeconds
        ),
      ],
    );
  },
);

Equalizer (EQ)

Sautiflow includes a 3-band parametric EQ and a multiband equalizer.

3-Band EQ

player.setEqEnabled(true);
player.setEq(low: 1.2, mid: 0.8, high: 1.1);

Multiband EQ

Initialize with custom frequencies.

player.initMultibandEq([32, 64, 125, 250, 500, 1000, 2000, 4000, 8000, 16000]);
player.setMultibandEqEnabled(true);
player.setMultibandEqBandGain(0, 2.0); // Boost 32Hz by 2dB

Mixed Multiband FX (Peak + Shelf + Bandpass + Notch)

Build a single, ordered chain where each band can use a different filter type.

final bands = <EqBandConfig>[
  EqBandConfig(type: EqBandType.lowshelf, frequencyHz: 120, gainDb: 4.0, slope: 1.0),
  EqBandConfig(type: EqBandType.peak, frequencyHz: 1000, gainDb: 2.0, q: 1.2),
  EqBandConfig(type: EqBandType.notch, frequencyHz: 60, q: 10.0),
  EqBandConfig(type: EqBandType.bandpass, frequencyHz: 2500, q: 0.9),
  EqBandConfig(type: EqBandType.highshelf, frequencyHz: 9000, gainDb: 2.5, slope: 1.0),
];

// Initialize and enable the mixed chain.
player.initMultibandFx(bands, enabled: true);

// Update all bands later (for dynamic user-added bands in UI).
player.setMultibandFxBands(bands);

// Toggle or clear.
player.setMultibandFxEnabled(false);
player.clearMultibandFx();

Realtime Analyzer Frames + fl_chart

Expose frame values as a stream and plug into any pub chart library (example below uses fl_chart).

// pubspec.yaml (app)
// fl_chart: ^0.69.0

player.configureAnalyzer(frameSize: 1024);
player.setAnalyzerEnabled(true);

StreamSubscription? sub;
List<double> bins = List.filled(96, 0.0);

sub = player.analyzerStream.listen((frame) {
    // Convert raw frame samples to chart bins.
    const targetBins = 96;
    final out = List<double>.filled(targetBins, 0.0);
    for (var i = 0; i < targetBins; i++) {
        final from = (i * frame.length / targetBins).floor();
        final to = ((i + 1) * frame.length / targetBins).ceil();
        var sum = 0.0;
        var count = 0;
        for (var j = from; j < to && j < frame.length; j++) {
            sum += frame[j].abs();
            count++;
        }
        out[i] = count > 0 ? sum / count : 0.0;
    }
    bins = out;
    // setState(() {});
});

// Pull API also exists:
final latest = player.getLatestAnalyzerFrame();

Reverb

Add spatial depth to your audio.

player.setReverbEnabled(true);
player.setReverb(
  mix: 0.3,       // Wet/Dry mix (0.0 - 1.0)
  feedback: 0.5,  // Decay tail
  delayMs: 50.0   // Pre-delay
);

Delay (Echo)

player.setDelay(
  enabled: true,
  mix: 0.4,
  feedback: 0.6,
  delayMs: 250.0 // Quarter second echo
);

Stereo Stage Widening

Enhance the spatial image of your audio without losing punch or mono-compatibility. Uses a hybrid Mid/Side (M/S) Matrix and Haas effect delay strategy.

player.setStereoWiden(
  enabled: true,
  width: 2.0,      // > 1.0 expands the side channels (wide)
  delayMs: 15.0    // ~15-30ms creates psychoacoustic width via the Haas effect
);

Filters & Advanced EQ

Sautiflow provides a suite of professional filters for sound design and mixing.

// High-pass (Remove rumble)
player.setHighpass(enabled: true, cutoffHz: 120.0);

// Low-pass (Lo-fi effect)
player.setLowpass(enabled: true, cutoffHz: 3000.0);

// Band-pass (Telephone effect / Focus)
player.setBandpass(
  enabled: true,
  cutoffHz: 1000.0,
  q: 1.0 // Q-factor (width)
);

// Notch Filter (Remove specific frequency)
player.setNotch(
  enabled: true,
  frequencyHz: 60.0, // Remove 60Hz hum
  q: 10.0
);

// Peak EQ (Parametric Boost/Cut)
player.setPeakEq(
  enabled: true,
  frequencyHz: 2500.0,
  gainDb: 3.0,
  q: 1.0
);

// Low Shelf (Bass Boost)
player.setLowshelf(
  enabled: true,
  frequencyHz: 100.0,
  gainDb: 4.0,
  slope: 1.0
);

// High Shelf (Treble Boost)
player.setHighshelf(
  enabled: true,
  frequencyHz: 10000.0,
  gainDb: 2.0,
  slope: 1.0
);

// Custom Real-Time Filter Injection 
player.setCustomLpf1(enabled: true, cutoffHz: 500.0);
player.setCustomHpf1(enabled: true, cutoffHz: 120.0);
player.setCustomBiquad(
  enabled: true,
  b0: 1.0, b1: 0.0, b2: 0.0, // Feedforward coefficients
  a0: 1.0, a1: 0.0, a2: 0.0, // Feedback coefficients
);

Standalone Filters & Resampling

Advanced audio manipulation independent of the main player instance.

Filter Classes (LPF1, HPF2, Biquad, etc)

Apply standalone filters to arbitrary audio buffers, bypassing the internal player completely.

final ffi = MiniaudioFiltersFFI();
// Initialize a 1-pole Low-pass filter
final lpf = MiniaudioLpf1(ffi, AudioFormat.f32, 2, 48000, 1000.0);

// Process custom frames from pointer
// bool success = lpf.process(outFrames, inFrames, 1024);

// Always dispose the native pointers when done
lpf.dispose();

Resampler

Sample rate conversion with controllable algorithms and dithering.

final ffi = MiniaudioFiltersFFI();
final resampler = MiniaudioResampler(
  ffi,
  AudioFormat.f32,  // including new formats: s24, s32
  2,                // channels
  44100,            // input rate
  48000,            // output rate
  algorithm: ResampleAlgorithm.linear,
  ditherMode: DitherMode.none,
);

// process(inFrames, inCount, outFrames, outCount)
// resampler.setRate(48000, 96000);

resampler.dispose();

Crossfade

Enable transition fades between tracks and control duration in milliseconds.

// Enable track transition fade.
player.setCrossfadeEnabled(true);

// Duration is clamped in native layer to 0..10000 ms.
player.setCrossfadeDurationMs(300);

// Disable if you want hard cuts.
player.setCrossfadeEnabled(false);

Output Format

Configure the audio engine output format.


// Set to Floating Point 32-bit (highest quality)
player.setOutputFormat(AudioFormat.f32);

// Set Sample Rate to 44.1kHz
player.setOutputSampleRate(44100);

// Use native device sample rate mode
player.setOutputSampleRate(0);

// Set to Mono
player.setOutputChannels(1);

// Configure Global Native Engine Resampler
// 0 = Linear, 1 = Custom
player.setEngineResampleAlgorithm(0);

// Configure Output Dithering Mode (e.g for 16-bit PCM output)
// 0 = None, 1 = Rectangle, 2 = Triangle
player.setEngineDitherMode(2);
          

Stream Pushing

For custom streaming implementations or raw data handling.

pushStream

Future<void> pushStream({required String url})

Manually fetches chunks from a URL and pushes them to the engine. Useful for environments where native networking is restricted.

Android Full Setup

For a production-ready app with background playback, lockscreen controls, and notifications, follow this checklist.

1. Dependencies

Ensure your pubspec.yaml includes:

dependencies:
    sautiflow: ^0.2.0
  audio_session: ^0.1.21
  audio_service: ^0.18.18

2. AndroidManifest.xml

Add the following permissions and components to android/app/src/main/AndroidManifest.xml.

Critical: You must replace the default FlutterActivity with AudioServiceActivity or Ensure your MainActivity extends it.
<manifest ...>
    <!-- Required Permissions -->
    <uses-permission android:name="android.permission.WAKE_LOCK"/>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
    <!-- Android 13+ Notifications -->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

    <application ...>
        
        <!-- Main Activity (Must use AudioServiceActivity) -->
        <activity
            android:name="com.ryanheise.audioservice.AudioServiceActivity"
            android:exported="true"
            android:launchMode="singleTask">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

        <!-- Audio Service -->
        <service
            android:name="com.ryanheise.audioservice.AudioService"
            android:foregroundServiceType="mediaPlayback"
            android:exported="true">
            <intent-filter>
                <action android:name="android.media.browse.MediaBrowserService" />
            </intent-filter>
        </service>

        <!-- Media Button Receiver -->
        <receiver
            android:name="com.ryanheise.audioservice.MediaButtonReceiver"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MEDIA_BUTTON" />
            </intent-filter>
        </receiver>

    </application>
</manifest>

3. Dart Initialization

Initialize the audio session and system integration in your main() function.

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 1. Configure Audio Session
  final session = await AudioSession.instance;
  await session.configure(const AudioSessionConfiguration.music());

  // 2. Initialize Player with System Audio
  final player = MiniAudioPlayer();
  await player.init(enableSystemAudio: true);

  runApp(const MyApp());
}

4. Update Metadata & State

For the notification to show correct info, update the now playing info whenever the track changes.

await player.updateNowPlaying(
  title: "Song Title",
  artist: "Artist Name",
  album: "Album Name",
  duration: Duration(minutes: 3, seconds: 45),
  // id, artwork can also be added if supported
);

Platform Specifics

Android Network Streaming

Native properties can be overridden in your `android/gradle.properties`:

MINIAUDIODART_ENABLE_CURL=ON
MINIAUDIODART_CURL_INCLUDE_DIR=/path/to/curl/include
MINIAUDIODART_CURL_LIBRARY=/path/to/libcurl.so

iOS / macOS

Linked statically. Ensure `ffiPlugin` is enabled in your `pubspec.yaml`.