Flutter MIDI Engine
An advanced MIDI synthesizer plugin for Flutter with comprehensive support for SF2 and SF3 soundfont formats.
Features
- Multi-Format Support: Load and play SF2 and SF3 soundfont files
- Multi-Channel MIDI: Full 16-channel MIDI support for complex arrangements
- Program Changes: Switch between 128 different instruments per soundfont
- Audio Effects: Built-in reverb and chorus effects
- Complete MIDI Control:
- Note on/off with velocity
- Volume and pan control per channel
- Pitch bend messages
- Control change messages
- All notes off / reset controllers
- Automatic Audio Routing: Audio follows the active output device — plug in or unplug a wired/Bluetooth headset mid-playback and sound switches over automatically (no manual re-initialization required)
- Cross-Platform: Works on Android, iOS, and Web (Web support is experimental)
Platform Support
| Platform | Status | Technology |
|---|---|---|
| Android | ✅ Fully Supported | MidiDriver (FluidSynth-based) |
| iOS | ✅ Fully Supported | AVFoundation AudioUnit Sampler |
| Web | ⚠️ Experimental | Stub implementation (requires JS library) |
| macOS | 🚧 Planned | Similar to iOS |
| Windows | 🚧 Planned | - |
| Linux | 🚧 Planned | - |
Requirements
| Tool | Minimum version |
|---|---|
| Flutter | 3.44.0 |
| Dart | 3.10 |
| Android | minSdk 24 (Android 7.0) |
| iOS | 13.0 |
Built and validated against Flutter 3.44. The Android module uses Flutter's built-in Kotlin support and builds against both AGP 8 and AGP 9.
Installation
Add this to your package's pubspec.yaml file:
dependencies:
flutter_midi_engine: ^0.1.5
Getting Started
1. Add a Soundfont to Your Project
Place your SF2 or SF3 soundfont file in your assets folder:
flutter:
assets:
- assets/soundfonts/piano.sf2
2. Basic Usage
import 'package:flutter_midi_engine/flutter_midi_engine.dart';
final midiEngine = FlutterMidiEngine();
// Load a soundfont from assets
await midiEngine.loadSoundfontFromAsset('assets/soundfonts/piano.sf2');
// Play middle C
await midiEngine.playNote(note: 60, velocity: 100);
// Wait a bit
await Future.delayed(Duration(seconds: 1));
// Stop the note
await midiEngine.stopNote(note: 60);
3. Advanced Features
Change Instruments
// Change to Church Organ (program 19)
await midiEngine.changeProgram(program: 19);
// Play on different channels with different instruments
await midiEngine.changeProgram(program: 0, channel: 0); // Piano on channel 0
await midiEngine.changeProgram(program: 40, channel: 1); // Violin on channel 1
await midiEngine.playNote(note: 60, channel: 0);
await midiEngine.playNote(note: 67, channel: 1);
Add Audio Effects
// Set reverb (hall effect)
await midiEngine.setReverb(
roomSize: 0.8,
damping: 0.3,
width: 1.0,
level: 0.5,
);
// Set chorus
await midiEngine.setChorus(
voices: 5,
level: 0.7,
speed: 0.5,
depth: 1.0,
);
Volume and Pan Control
// Set volume for channel 0
await midiEngine.setVolume(volume: 100, channel: 0);
// Pan channel 1 to the right
await midiEngine.setPan(pan: 127, channel: 1);
Pitch Bend
// Bend pitch up
await midiEngine.sendPitchBend(value: 4096, channel: 0);
// Reset to center
await midiEngine.sendPitchBend(value: 0, channel: 0);
Control Changes
// Enable sustain pedal
await midiEngine.sendControlChange(controller: 64, value: 127);
// Disable sustain pedal
await midiEngine.sendControlChange(controller: 64, value: 0);
iOS Setup
For iOS, you need to ensure the app can play audio even when the device is muted:
// Call this before loading the soundfont
await midiEngine.unmute();
await midiEngine.loadSoundfontFromAsset('assets/soundfonts/piano.sf2');
Audio Routing (Headset / Bluetooth)
As of v0.1.5 the engine follows the active audio output device automatically. When a wired or Bluetooth headset is connected or disconnected during playback, audio is re-bound to the new route instead of cutting out — in both directions (speaker → headset and headset → speaker). You don't need to re-initialize the engine yourself.
Under the hood:
- Android listens for output-device changes (
AudioDeviceCallback) and restarts the MIDI driver so its audio track re-binds to the current device. - iOS configures the
AVAudioSessionfor playback (including Bluetooth A2DP and AirPlay), observes route-change and interruption notifications, and restarts the audio graph so the sampler follows the new device. It also recovers automatically after interruptions such as phone calls.
Example App
Check out the example directory for a complete piano app demonstrating all features.
MIDI Note Reference
MIDI notes range from 0 to 127, where:
- Middle C (C4) = 60
- A4 (440 Hz) = 69
Formula: MIDI Note = 12 * octave + semitone + 12
General MIDI Program Numbers
Some common instrument program numbers:
| Program | Instrument |
|---|---|
| 0 | Acoustic Grand Piano |
| 4 | Electric Piano 1 |
| 19 | Church Organ |
| 24 | Acoustic Guitar (nylon) |
| 40 | Violin |
| 56 | Trumpet |
| 73 | Flute |
See the full General MIDI specification for all 128 instruments.
Soundfont Resources
Free soundfonts you can use:
API Reference
Core Methods
loadSoundfont(String path)- Load soundfont from file pathloadSoundfontFromAsset(String assetPath)- Load soundfont from assetsunloadSoundfont()- Unload current soundfontplayNote({required int note, int velocity, int channel})- Play MIDI notestopNote({required int note, int velocity, int channel})- Stop MIDI note
Instrument Control
changeProgram({required int program, int channel})- Change instrument
Effects & Mix
setVolume({required int volume, int channel})- Set channel volume (0-127)setPan({required int pan, int channel})- Set stereo pan (0=left, 64=center, 127=right)setReverb({double roomSize, double damping, double width, double level})- Configure reverbsetChorus({int voices, double level, double speed, double depth})- Configure chorus
MIDI Control
sendControlChange({required int controller, required int value, int channel})- Send CC messagesendPitchBend({required int value, int channel})- Send pitch bend (-8192 to 8191)stopAllNotes()- Stop all playing notesresetAllControllers()- Reset all MIDI controllers
iOS-Specific
unmute()- Allow audio playback even when device is muted
Troubleshooting
Android
Issue: No sound on Android
- Make sure you've called
loadSoundfont()with a valid SF2/SF3 file - Check that the soundfont file exists in your assets
- Verify device volume is turned up
iOS
Issue: No sound on iOS
- Call
unmute()before loading soundfont - Test on a real device (simulator has limited MIDI support)
- Ensure SF2 file is properly formatted (SF3 support may vary)
Issue: Soundfont not found
- Verify the file path is correct
- When using assets, make sure the file is listed in
pubspec.yaml
Audio routing
Issue: Sound stops when connecting/disconnecting a headset
- This is handled automatically as of v0.1.5 — upgrade if you are on an older release. Audio re-binds to the active output device on both Android and iOS.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Credits
Built with:
- Android: MidiDriver (FluidSynth)
- iOS: AVFoundation AudioUnit Sampler
- Inspired by: flutter_midi