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
libsamplerateto 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
EqBandTypeandEqBandConfig. - Added realtime analyzer frame APIs, including stream and pull-style access.
- Added crossfade controls:
setCrossfadeEnabled(bool)andsetCrossfadeDurationMs(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
s24ands32audio 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
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.
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`.