Flutter MIDI控制插件flutter_midi_pro的使用

发布于 1周前 作者 ionicwang 来自 Flutter

Flutter MIDI控制插件flutter_midi_pro的使用

简介

flutter_midi_pro 插件提供了在Flutter应用程序中加载SoundFont(.sf2)文件和播放MIDI音符的功能。该插件在Android上使用fluidsynth,在iOS和macOS上使用AVFoundation来播放MIDI音符。目前,该插件兼容Android、iOS和macOS平台,未来将添加对Windows、Linux和Web的支持。

pub package GitHub stars GitHub issues

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。

Melih Hakan Pektas

感谢您为flutter_piano_pro做出的贡献!

许可证

本项目采用MIT许可证。详见LICENSE文件。


更多关于Flutter MIDI控制插件flutter_midi_pro的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于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();
  }
}

代码解释:

  1. 依赖引入:在pubspec.yaml文件中添加flutter_midi_pro依赖。
  2. 权限请求:在initState方法中,初始化FlutterMidiPro实例并请求MIDI访问权限。
  3. 设备列表获取:如果有权限,获取MIDI设备列表并更新UI。
  4. 设备连接:通过点击设备名称,调用_connectToDevice方法连接到指定的MIDI设备。
  5. 消息发送:一旦设备连接成功,可以调用_sendMidiMessage方法发送MIDI消息。
  6. 资源释放:在dispose方法中关闭FlutterMidiPro实例以释放资源。

这个示例展示了基本的MIDI设备访问和控制流程。实际应用中,你可能需要根据具体需求进行更多的错误处理和功能扩展。

回到顶部