flutter_sequencer

这个Flutter插件允许您设置采样器乐器,并创建在这些乐器上播放音符的多轨序列。您可以为序列指定循环范围,并安排音量自动化。

它在Android和iOS上都使用了AudioKit的核心采样器引擎,这允许您通过加载一些采样并指定它们的音符来创建一个乐器。如果您播放了一个没有采样对应的音符,它将通过音高调整您的其他采样来填补空白。它还支持在两个平台上播放SF2(SoundFont)文件,并且在iOS上,您可以加载任何AudioUnit乐器。

示例应用程序是一个鼓机。理论上,您也可以使用此插件制作一个完整的基于采样的DAW。您也可以将其用于游戏音效,甚至生成动态游戏配乐。

flutter_sequencer

如何使用

创建序列

final sequence = Sequence(tempo: 120.0, endBeat: 8.0);

创建序列时,您需要设置节奏和结束节拍。

创建乐器

final instruments = [
  Sf2Instrument(path: "assets/sf2/TR-808.sf2", isAsset: true),
  SfzInstrument(path: "assets/sfz/SplendidGrandPiano.sfz", isAsset: true),
  SamplerInstrument(
    id: "80's FM Bass",
    sampleDescriptors: [
      SampleDescriptor(filename: "assets/wav/D3.wav", isAsset: true, noteNumber: 62),
      SampleDescriptor(filename: "assets/wav/F3.wav", isAsset: true, noteNumber: 65),
      SampleDescriptor(filename: "assets/wav/G#3.wav", isAsset: true, noteNumber: 68),
    ]
  ),
];

一个乐器可以用于创建一条或多条音轨。
有四种乐器

  1. SamplerInstrument,用于在AudioKitSampler中手动加载SampleDescriptors列表,而无需SFZ文件
    • 在iOS和Android上,它将由 AudioKit Sampler 播放
    • SampleDescriptor可以指向.wav或.wv(WavPack)文件。我推荐尽可能使用.wv,
      因为它支持无损压缩。使用ffmpeg可以轻松地将音频文件转换为WavPack
      格式。
    • 您只需要指定样本的路径、它是否是资源,
      以及它对应的音符编号。
    • 可选地,您可以为每个样本设置一个音符范围或一个力度范围。您可以这样创建
      "力度层",其中较低的力度触发的样本与较高的力度不同。
  2. SfzInstrument,用于加载.sfz SFZ文件。
    • 这也将由AudioKit采样器播放。
    • 仅支持少数SFZ opcodes。请查看sfz_parser.dart以了解哪些opcodes
      被识别。
    • 这不是一个功能齐全的SFZ播放器。SFZ只是提供了一种方便的格式将采样
      加载到采样器中。
  3. Sf2Instrument,用于加载.sf2 SoundFont文件。
    • 在iOS上,它将由内置的Apple MIDI合成器AudioUnit播放
    • 在Android上,它将由 tinysoundfont 播放
  4. AudioUnitInstrument,用于加载AudioUnit
    • 这仅在iOS上有效

对于SF2或SFZ乐器,将isAsset: true传递给Flutter资源目录中的路径。
您应该为“工厂预设”声音使用资源。要从文件系统加载用户提供或下载的声音
,请传递isAsset: false

可选:保持引擎运行

GlobalState().setKeepEngineRunning(true);

即使所有序列都暂停,这也会使音频引擎保持运行。如果需要
在序列暂停时触发声音,请将其设置为true。否则请不要这样做,因为它会增加
能耗。

创建音轨

sequence.createTracks(instruments).then((tracks) {
  setState(() {
    this.tracks = tracks;
    ...
  });
});

createTracks返回Future<List<Track>>。您可能希望将它完成的值存储在
您的widget的状态中。

在音轨上安排事件

track.addNote(noteNumber: 60, velocity: 0.7, startBeat: 0.0, durationBeats: 2.0);

这将把中央C(MIDI音符编号60)添加到序列中,从节拍0开始,并在
2拍后停止。

track.addVolumeChange(volume: 0.75, beat: 2.0);

这将安排一个音量变化。它可以用于音量自动化。请注意,音轨音量
是线性的,而不是对数的。您可能需要使用对数刻度并转换为
线性。

控制播放

sequence.play();
sequence.pause();
sequence.stop();

启动、暂停或停止序列。

sequence.setBeat(double beat);

以节拍为单位设置序列中的播放位置。

sequence.setEndBeat(double beat);

以节拍为单位设置序列的长度。

sequence.setTempo(120.0);

以每分钟节拍数设置节奏。

sequence.setLoop(double loopStartBeat, double loopEndBeat);

启用循环。

sequence.unsetLoop();

禁用循环。

获取序列的实时信息

您可以使用SingleTickerProviderStateMixin在每一帧上调用这些方法。

sequence.getPosition(); // double

获取序列的播放位置(以节拍为单位)。

sequence.getIsPlaying(); // bool

获取序列是否正在播放。如果已到达末尾,它可能已停止。

track.getVolume();

获取音轨的音量。VolumeEvent在播放过程中可能已更改。

实时播放

track.startNoteNow(noteNumber: 60, velocity: 0.75);

立即向音轨发送MIDI Note On消息。

track.stopNoteNow(noteNumber: 60);

立即向音轨发送MIDI Note Off消息。

track.changeVolumeNow(volume: 0.5);

立即更改音轨的音量。请注意,这是线性增益,而不是对数。

工作原理

Android和iOS后端启动它们各自的音频引擎。iOS引擎将一个AudioUnit
添加到每个音轨,然后将其连接到Mixer AudioUnit。Android引擎则需要
手动完成所有渲染。它们都共享一个“BaseScheduler”,可以在
ios目录中找到。

后端不了解序列、序列中的位置、循环状态,甚至时间
(以秒或节拍为单位)。它只维护一个音轨映射,并在音频引擎
渲染完适当数量的帧时触发这些音轨上的事件。BaseScheduler
为每个音轨提供一个Buffer,其中包含其计划的事件。Buffer被认为是
线程安全的(一个读取器和一个写入器)且实时安全的(即它不会分配内存,
因此可以在音频渲染线程上使用)。

Sequence存在于Dart前端。Sequence包含Tracks。每个Track由
后端的一个Buffer支持。当您向音轨添加音符或音量更改时,它会根据
节奏和采样率,在适当的帧上将事件安排到Buffer中。

Buffer可能不够大以容纳所有事件。此外,当启用循环时,事件
将无限发生,因此Buffer永远不够大。为了解决这个问题,前端
将定期“填充”每个音轨的Buffer。

开发说明

请注意,Android构建使用了几个第三方库。Gradle构建将从GitHub
下载它们到android/third_party目录。

iOS构建依赖于AudioKit库。它将由CocoaPods下载。

要在Mac OS上构建C++测试,请进入cpp_test目录,然后运行

cmake .
make

然后,要运行测试,

./build/sequencer_test

我没有在Windows或Linux上尝试过,但它应该可以在没有太多更改的情况下工作。

待办事项

欢迎PR!如果您在项目中使用了此插件,请考虑通过修复错误或处理
这些待办事项来做出贡献。

难度:简单

  • (重要)启用设置AudioKit Sampler的包络参数
  • 将position_frame_t更改为64位整数以避免溢出错误
  • 使常量可配置(TOP_OFF_PERIOD_MS和LEAD_FRAMES)
  • 支持联合插件

难度:中等

  • 开始使用Dart空安全
  • 支持节奏自动化
  • 支持音高弯音和自定义调音
  • MIDI Out乐器
    • 创建一个本身不发声,只将MIDI
      事件发送到指定MIDI输出端口的乐器。
  • 重构
    • GlobalState、Sequence和Track应该有清晰的职责划分
    • Engine、Sequencer、Mixer、Scheduler也是如此
    • “Track index”和“Track ID”被混用,“Track ID”应该在所有地方使用
    • 使不同文件(Plugin/Engine/Scheduler)的名称和组织保持一致
  • 支持MacOS
    • 大部分iOS代码应该可以被重用
  • 添加更多C++测试
    • 特别是对于BaseScheduler。
  • 添加Dart测试
    • 测试循环边缘情况
  • 确保没有内存泄漏或并发问题

难度:困难

  • 通用C++乐器
    • 创建一个可以在iOS和Android上使用的乐器。AudioKit Sampler就是这样工作的,
      但这个库将其用于AudioKit的AudioUnit包装器。难点在于创建一个
      C++ IInstrument的通用AudioUnit包装器。
  • 完整的SFZ支持
    • 这可能需要 sfizz
      • 在Android和iOS上构建sfizz及其依赖项可能会很困难。理想情况下,它
        应该具有更模块化的构建——此项目不需要其对AU/LV/VST、
        音频输出和音频文件解码的支持。
  • 支持Windows
    • Android目录中的一些代码可能会被重用
  • 录制音频输出
  • 支持React Native?
    • 可以使用dart2js

难度:非常困难

  • (重要)音频图
    • 创建音轨和效果的图,其中任何输出都可以连接到任何输入。
    • 到那时,大部分音频引擎都可以跨平台。在iOS上,它可以
      被包装在一个AudioUnit中,并且外部AudioUnit乐器和效果可以通过
      输入和输出总线与其连接。
    • LabSound 似乎是需要使用的库
      • 它基于WebAudio,所以也可以有一个Web后端
    • 可用于添加一些关键效果,如混响、延迟和压缩
    • 支持音频输入、将输出写入文件以及其他DAW功能
  • 支持Web

GitHub

https://github.com/mikeperri/flutter_sequencer