Flutter MIDI控制插件flutter_midi_pro的使用
Flutter MIDI控制插件flutter_midi_pro的使用
简介
flutter_midi_pro
插件提供了在Flutter应用程序中加载SoundFont(.sf2)文件和播放MIDI音符的功能。该插件在Android上使用fluidsynth,在iOS和macOS上使用AVFoundation来播放MIDI音符。目前,该插件兼容Android、iOS和macOS平台,未来将添加对Windows、Linux和Web的支持。
Android注意事项
在Android上,Fluidsynth库需要使用CMake构建。特别是对于flutter_midi_pro
,必须安装CMake版本3.22.1。有关如何安装CMake,请参考此评论:CMake Installation。
安装
要使用此插件,可以通过终端或pubspec.yaml
文件添加flutter_midi_pro
:
flutter pub add flutter_midi_pro
使用方法
导入flutter_midi_pro.dart
并使用MidiPro
类访问插件的功能。MidiPro
类是一个单例类,因此可以在应用程序中使用同一个实例。
import 'package:flutter_midi_pro/flutter_midi_pro.dart';
加载SoundFont文件
使用loadSoundfont
函数加载SoundFont文件。该函数返回一个整数值,表示SoundFont ID。可以使用此ID从SoundFont文件中加载乐器并播放MIDI音符。
final soundfontId = await MidiPro().loadSoundfont(path: 'path/to/soundfont.sf2', bank: 0, program: 0);
选择乐器
使用selectInstrument
函数选择指定bank和program的乐器。
await MidiPro().selectInstrument(sfId: soundfontId, channel: 0, bank: 0, program: 0);
播放MIDI音符
使用playMidiNote
函数播放具有给定MIDI值和速度的音符。MIDI值是您想要播放的音符的MIDI编号(0-127),速度是音符的音量(0-127)。
midiPro.playNote(sfId: soundfontId, channel: 0, key: 60, velocity: 127);
停止MIDI音符
使用stopMidiNote
函数停止具有给定MIDI编号的音符。
midiPro.stopNote(sfId: soundfontId, channel: 0, key: 60);
示例代码
以下是如何使用flutter_midi_pro
插件结合flutter_piano_pro
播放钢琴的示例:
import 'package:flutter/material.dart';
import 'package:flutter_midi_pro/flutter_midi_pro.dart';
import 'package:flutter_piano_pro/flutter_piano_pro.dart';
import 'package:flutter_piano_pro/note_model.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final MidiPro midiPro = MidiPro();
final ValueNotifier<Map<int, String>> loadedSoundfonts = ValueNotifier<Map<int, String>>({});
final ValueNotifier<int?> selectedSfId = ValueNotifier<int?>(null);
final instrumentIndex = ValueNotifier<int>(0);
final bankIndex = ValueNotifier<int>(0);
final channelIndex = ValueNotifier<int>(0);
final volume = ValueNotifier<int>(127);
Map<int, NoteModel> pointerAndNote = {};
Future<int> loadSoundfont(String path, int bank, int program) async {
if (loadedSoundfonts.value.containsValue(path)) {
print('Soundfont file: $path already loaded. Returning ID.');
return loadedSoundfonts.value.entries.firstWhere((element) => element.value == path).key;
}
final int sfId = await midiPro.loadSoundfont(path: path, bank: bank, program: program);
loadedSoundfonts.value = {sfId: path, ...loadedSoundfonts.value};
print('Loaded soundfont file: $path with ID: $sfId');
return sfId;
}
Future<void> selectInstrument({
required int sfId,
required int program,
int channel = 0,
int bank = 0,
}) async {
int? sfIdValue = sfId;
if (!loadedSoundfonts.value.containsKey(sfId)) {
sfIdValue = loadedSoundfonts.value.keys.first;
} else {
selectedSfId.value = sfId;
}
print('Selected soundfont file: $sfIdValue');
await midiPro.selectInstrument(sfId: sfIdValue, channel: channel, bank: bank, program: program);
}
Future<void> playNote({
required int key,
required int velocity,
int channel = 0,
int sfId = 1,
}) async {
int? sfIdValue = sfId;
if (!loadedSoundfonts.value.containsKey(sfId)) {
sfIdValue = loadedSoundfonts.value.keys.first;
}
await midiPro.playNote(channel: channel, key: key, velocity: velocity, sfId: sfIdValue);
}
Future<void> stopNote({
required int key,
int channel = 0,
int sfId = 1,
}) async {
int? sfIdValue = sfId;
if (!loadedSoundfonts.value.containsKey(sfId)) {
sfIdValue = loadedSoundfonts.value.keys.first;
}
await midiPro.stopNote(channel: channel, key: key, sfId: sfIdValue);
}
Future<void> unloadSoundfont(int sfId) async {
await midiPro.unloadSoundfont(sfId);
loadedSoundfonts.value = {
for (final entry in loadedSoundfonts.value.entries)
if (entry.key != sfId) entry.key: entry.value
};
if (selectedSfId.value == sfId) selectedSfId.value = null;
}
final sf2Paths = ['assets/TimGM6mb.sf2', 'assets/SalC5Light2.sf2'];
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: Colors.amber,
brightness: Brightness.dark,
),
home: Scaffold(
appBar: AppBar(
title: const Text('Flutter Midi Pro Example'),
),
body: SingleChildScrollView(
child: Column(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const SizedBox(height: 10),
FittedBox(
fit: BoxFit.scaleDown,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
sf2Paths.length,
(index) => ElevatedButton(
onPressed: () => loadSoundfont(sf2Paths[index], bankIndex.value, instrumentIndex.value),
child: Text('Load Soundfont ${sf2Paths[index]}'),
),
),
),
),
const SizedBox(height: 10),
ValueListenableBuilder(
valueListenable: loadedSoundfonts,
builder: (context, value, child) {
if (value.isEmpty) {
return const Text('No soundfont file loaded');
}
return Column(
children: [
const Text('Loaded Soundfont files:'),
for (final entry in value.entries)
ListTile(
title: Text(entry.value),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
ValueListenableBuilder(
valueListenable: selectedSfId,
builder: (context, selectedSfIdValue, child) {
return ElevatedButton(
onPressed: selectedSfIdValue == entry.key
? null
: () => selectedSfId.value = entry.key,
child: Text(selectedSfIdValue == entry.key ? 'Selected' : 'Select'),
);
},
),
ElevatedButton(
onPressed: () => unloadSoundfont(entry.key),
child: const Text('Unload'),
),
],
),
)
],
);
},
),
ValueListenableBuilder(
valueListenable: selectedSfId,
builder: (context, selectedSfIdValue, child) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ValueListenableBuilder(
valueListenable: bankIndex,
builder: (context, bankIndexValue, child) {
return DropdownButton<int>(
value: bankIndexValue,
items: [
for (int i = 0; i < 128; i++)
DropdownMenuItem<int>(
value: i,
child: Text('Bank $i', style: const TextStyle(fontSize: 13)),
)
],
onChanged: (int? value) {
if (value != null) {
bankIndex.value = value;
}
},
);
},
),
ValueListenableBuilder(
valueListenable: instrumentIndex,
builder: (context, channelValue, child) {
return DropdownButton<int>(
value: channelValue,
items: [
for (int i = 0; i < 128; i++)
DropdownMenuItem<int>(
value: i,
child: Text('Instrument $i', style: const TextStyle(fontSize: 13)),
)
],
onChanged: (int? value) {
if (value != null) {
instrumentIndex.value = value;
}
},
);
},
),
ValueListenableBuilder(
valueListenable: channelIndex,
builder: (context, channelIndexValue, child) {
return DropdownButton<int>(
value: channelIndexValue,
items: [
for (int i = 0; i < 16; i++)
DropdownMenuItem<int>(
value: i,
child: Text('Channel $i', style: const TextStyle(fontSize: 13)),
)
],
onChanged: (int? value) {
if (value != null) {
channelIndex.value = value;
}
},
);
},
),
],
),
ValueListenableBuilder(
valueListenable: bankIndex,
builder: (context, bankIndexValue, child) {
return ValueListenableBuilder(
valueListenable: channelIndex,
builder: (context, channelIndexValue, child) {
return ValueListenableBuilder(
valueListenable: instrumentIndex,
builder: (context, instrumentIndexValue, child) {
return ElevatedButton(
onPressed: selectedSfIdValue != null
? () => selectInstrument(
sfId: selectedSfIdValue,
program: instrumentIndexValue,
bank: bankIndexValue,
channel: channelIndexValue,
)
: null,
child: Text(
'Load Instrument $instrumentIndexValue on Bank $bankIndexValue to Channel $channelIndexValue'),
);
},
);
},
);
},
),
Padding(
padding: const EdgeInsets.all(18),
child: ValueListenableBuilder(
valueListenable: volume,
child: const Text('Volume: '),
builder: (context, value, child) {
return Row(
children: [
child!,
Expanded(
child: Slider(
value: value.toDouble(),
min: 0,
max: 127,
onChanged: selectedSfIdValue != null
? (value) => volume.value = value.toInt()
: null,
),
),
const SizedBox(width: 10),
Text('${volume.value}'),
],
);
},
),
),
Padding(
padding: const EdgeInsets.all(8),
child: ElevatedButton(
onPressed: !(selectedSfIdValue != null)
? null
: () => unloadSoundfont(loadedSoundfonts.value.keys.first),
child: const Text('Unload Soundfont file'),
),
),
Stack(
children: [
PianoPro(
noteCount: 15,
onTapDown: (NoteModel? note, int tapId) {
if (note == null) return;
pointerAndNote[tapId] = note;
playNote(
key: note.midiNoteNumber,
velocity: volume.value,
channel: channelIndex.value,
sfId: selectedSfIdValue!,
);
debugPrint(
'DOWN: note= ${note.name + note.octave.toString() + (note.isFlat ? "♭" : '')}, tapId= $tapId');
},
onTapUpdate: (NoteModel? note, int tapId) {
if (note == null) return;
if (pointerAndNote[tapId] == note) return;
stopNote(
key: pointerAndNote[tapId]!.midiNoteNumber,
channel: channelIndex.value,
sfId: selectedSfIdValue!,
);
pointerAndNote[tapId] = note;
playNote(
channel: channelIndex.value,
key: note.midiNoteNumber,
velocity: volume.value,
sfId: selectedSfIdValue,
);
debugPrint(
'UPDATE: note= ${note.name + note.octave.toString() + (note.isFlat ? "♭" : '')}, tapId= $tapId');
},
onTapUp: (int tapId) {
stopNote(
key: pointerAndNote[tapId]!.midiNoteNumber,
channel: channelIndex.value,
sfId: selectedSfIdValue!,
);
pointerAndNote.remove(tapId);
debugPrint('UP: tapId= $tapId');
},
),
if (selectedSfIdValue == null)
Positioned.fill(
child: Container(
color: Colors.black.withOpacity(0.5),
child: const Center(
child: Text(
'Load Soundfont file\nMust be called before other methods',
textAlign: TextAlign.center,
),
),
),
)
],
)
],
);
},
),
],
),
],
),
),
),
);
}
}
贡献
欢迎贡献!请随时提交PR或打开问题。
待办事项
- ❌ 添加对Web、Windows和Linux的支持。
- ❌ 添加对通道功能(MIDI Channels)的支持。
- ❌ 添加控制器支持。
- ❌ 添加对MIDI文件的支持。
联系方式
如果有任何问题或建议,请联系包维护者 Melih Hakan Pektas,通过电子邮件或GitHub。
感谢您为flutter_piano_pro
做出的贡献!
许可证
本项目采用MIT许可证。详见LICENSE文件。
更多关于Flutter MIDI控制插件flutter_midi_pro的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter MIDI控制插件flutter_midi_pro的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,以下是一个关于如何使用Flutter MIDI控制插件flutter_midi_pro
的代码示例。这个示例展示了如何初始化插件、列出MIDI设备、打开MIDI设备以及发送MIDI消息。
首先,确保你已经在pubspec.yaml
文件中添加了flutter_midi_pro
依赖:
dependencies:
flutter:
sdk: flutter
flutter_midi_pro: ^最新版本号 # 替换为实际的最新版本号
然后,运行flutter pub get
来安装依赖。
接下来是示例代码:
import 'package:flutter/material.dart';
import 'package:flutter_midi_pro/flutter_midi_pro.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
FlutterMidiPro? _midiPro;
List<MidiDeviceInfo>? _midiDevices;
MidiDeviceConnection? _connectedDevice;
@override
void initState() {
super.initState();
_initMidiPro();
}
Future<void> _initMidiPro() async {
_midiPro = FlutterMidiPro();
// 请求MIDI访问权限
bool hasPermission = await _midiPro!.requestMidiAccess();
if (hasPermission) {
// 获取MIDI设备列表
_midiDevices = await _midiPro!.getMidiDevices();
setState(() {});
} else {
// 处理权限被拒绝的情况
print("MIDI访问权限被拒绝");
}
}
Future<void> _connectToDevice(MidiDeviceInfo device) async {
_connectedDevice = await _midiPro!.openMidiDevice(device.id);
if (_connectedDevice != null) {
print("已连接到设备: ${device.name}");
// 发送MIDI消息示例
_sendMidiMessage([0x90, 60, 127]); // Note On, middle C, velocity 127
} else {
print("无法连接到设备: ${device.name}");
}
}
Future<void> _sendMidiMessage(List<int> message) async {
if (_connectedDevice != null) {
await _connectedDevice!.sendMidiMessage(message);
} else {
print("没有连接的设备");
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Flutter MIDI Pro Example'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: _midiDevices == null
? Center(child: CircularProgressIndicator())
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('MIDI设备列表:', style: TextStyle(fontSize: 20)),
SizedBox(height: 16),
..._midiDevices!.map((device) {
return GestureDetector(
onTap: () => _connectToDevice(device),
child: Text(
device.name,
style: TextStyle(decoration: TextDecoration.underline),
),
);
}).toList(),
],
),
),
),
);
}
@override
void dispose() {
_midiPro?.close();
super.dispose();
}
}
代码解释:
- 依赖引入:在
pubspec.yaml
文件中添加flutter_midi_pro
依赖。 - 权限请求:在
initState
方法中,初始化FlutterMidiPro
实例并请求MIDI访问权限。 - 设备列表获取:如果有权限,获取MIDI设备列表并更新UI。
- 设备连接:通过点击设备名称,调用
_connectToDevice
方法连接到指定的MIDI设备。 - 消息发送:一旦设备连接成功,可以调用
_sendMidiMessage
方法发送MIDI消息。 - 资源释放:在
dispose
方法中关闭FlutterMidiPro
实例以释放资源。
这个示例展示了基本的MIDI设备访问和控制流程。实际应用中,你可能需要根据具体需求进行更多的错误处理和功能扩展。