Flutter音频序列控制插件flutter_sequencer的使用

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

Flutter音频序列控制插件flutter_sequencer的使用

1. 插件简介

flutter_sequencer 是一个Flutter插件,允许你设置采样乐器并创建多轨音符序列。你可以为序列指定循环范围,并安排音量自动化。该插件在Android和iOS上使用 sfizz SFZ播放器,SFZ格式可以用于创建高质量的基于样本的乐器并进行减法合成。这些乐器可以通过Flutter UI进行调制参数控制。插件还支持在两个平台上播放SF2(SoundFont)文件,在iOS上可以加载任何AudioUnit乐器。

2. 使用方法

2.1 创建序列
final sequence = Sequence(tempo: 120.0, endBeat: 8.0);

你需要在创建序列时设置节拍速度(tempo)和结束拍号(endBeat)。

2.2 创建乐器
final instruments = [
  Sf2Instrument(path: "assets/sf2/TR-808.sf2", isAsset: true),
  SfzInstrument(
    path: "assets/sfz/GMPiano.sfz",
    isAsset: true,
    tuningPath: "assets/sfz/meanquar.scl",
  ),
  RuntimeSfzInstrument(
    id: "Sampled Synth",
    sampleRoot: "assets/wav",
    isAsset: true,
    sfz: Sfz(
      groups: [
        SfzGroup(
          regions: [
            SfzRegion(sample: "D3.wav", noteNumber: 62),
            SfzRegion(sample: "F3.wav", noteNumber: 65),
            SfzRegion(sample: "Gsharp3.wav", noteNumber: 68),
          ],
        ),
      ],
    ),
  ),
  RuntimeSfzInstrument(
    id: "Generated Synth",
    // 这个SFZ不使用任何样本文件,所以只需将sampleRoot设置为"/"作为占位符。
    sampleRoot: "/",
    isAsset: false,
    // 基于Unison Oscillator示例
    sfz: Sfz(
      groups: [
        SfzGroup(
          regions: [
            SfzRegion(
              sample: "*saw",
              otherOpcodes: {
                "oscillator_multi": "5",
                "oscillator_detune": "50",
              }
            )
          ]
        )
      ]
    )
  ),
];

乐器有四种类型:

  1. SfzInstrument:用于加载 .sfz 文件及其引用的样本文件。
  2. RuntimeSfzInstrument:用于在运行时构建SFZ。
  3. Sf2Instrument:用于加载 .sf2 SoundFont文件。
  4. AudioUnitInstrument:仅在iOS上有效,用于加载AudioUnit。

对于SF2或SFZ乐器,如果要从Flutter资产目录加载路径,请传递 isAsset: true。如果你想从文件系统加载用户提供的或下载的声音,请传递 isAsset: false

2.3 重要注意事项
  • 在Android上,使用 isAsset: true 加载的SFZ文件和样本将被提取到应用文件目录中,因为 sfizz 无法直接从Android资产读取。这意味着它们会在设备上存在两份——压缩形式存在于APK中,未压缩形式存在于文件目录中。因此,我只建议在样本较小的情况下使用 isAsset: true。如果你想要包含高质量的音色库,应用程序应在运行时下载它们。
  • 在任一平台上,Flutter资产路径会被URL编码。例如,如果你在资产文件夹中放置了一个名为 “Piano G#5.wav” 的文件,它最终会被称为 “Piano%20G%235.wav”。因此,如果你打包了一个SFZ文件作为资产,确保你删除了样本文件名中的任何特殊字符和空格,或者更新SFZ文件以引用URL编码的样本路径。我建议删除所有空格和特殊字符。
2.4 可选:保持引擎运行
GlobalState().setKeepEngineRunning(true);

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

2.5 创建轨道
sequence.createTracks(instruments).then((tracks) {
  setState(() {
    this.tracks = tracks;
    ...
  });
});

createTracks 返回一个 Future<List<Track>>。你可能希望将它完成后的值存储在小部件的状态中。

2.6 在轨道上调度事件
track.addNote(noteNumber: 60, velocity: 0.7, startBeat: 0.0, durationBeats: 2.0);

这将在第0拍处添加中间C(MIDI音符编号60),并在2拍后停止。

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

这将安排一个音量变化。它可以用于音量自动化。注意,轨道音量是在线性尺度上,而不是对数尺度上。你可能想要使用对数尺度并转换为线性。

track.addMidiCC(ccNumber: 127, ccValue: 127, beat: 2.0);

这将安排一个MIDI CC事件。这些可以用于更改音色库中的参数。例如,在SFZ中,你可以使用 cutoff_cc1cutoff 操作码来定义一个滤波器,其中截止频率可以通过MIDI CC事件更改。

track.addMidiPitchBend(value: 1.0, beat: 2.0);

这将安排一个MIDI弯音事件。值可以从 -1.0 到 1.0。请注意,该值不是半音数。音色库定义了当弯音值设置为 -1.0 和 1.0 时,音高变化多少个音分。例如,在SFZ中,你可以使用 bend_downbend_up 操作码来定义弯音范围。

2.7 控制播放
sequence.play();
sequence.pause();
sequence.stop();

开始、暂停或停止序列。

sequence.setBeat(double beat);

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

sequence.setEndBeat(double beat);

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

sequence.setTempo(120.0);

设置每分钟的节拍数(BPM)。

sequence.setLoop(double loopStartBeat, double loopEndBeat);
sequence.unsetLoop();

启用或禁用循环。

2.8 获取实时信息

你可以使用 SingleTickerProviderStateMixin 在每一帧上调用以下方法:

sequence.getPosition(); // 获取序列的播放位置,以拍为单位
sequence.getIsPlaying(); // 获取序列是否正在播放
track.getVolume(); // 获取轨道的音量
2.9 实时播放
track.startNoteNow(noteNumber: 60, velocity: 0.75);
track.stopNoteNow(noteNumber: 60);
track.changeVolumeNow(volume: 0.5);

立即发送MIDI音符开、关消息或更改轨道音量。注意,这是线性增益,而不是对数增益。

3. 完整示例Demo

以下是一个完整的示例代码,展示了如何使用 flutter_sequencer 创建一个简单的鼓机应用。

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_sequencer/global_state.dart';
import 'package:flutter_sequencer/models/sfz.dart';
import 'package:flutter_sequencer/models/instrument.dart';
import 'package:flutter_sequencer/sequence.dart';
import 'package:flutter_sequencer/track.dart';

const INITIAL_TEMPO = 120.0;
const INITIAL_STEP_COUNT = 8;
const INITIAL_IS_LOOPING = true;

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  [@override](/user/override)
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  final sequence = Sequence(tempo: INITIAL_TEMPO, endBeat: INITIAL_STEP_COUNT.toDouble());
  Map<int, StepSequencerState?> trackStepSequencerStates = {};
  List<Track> tracks = [];
  Map<int, double> trackVolumes = {};
  Track? selectedTrack;
  late Ticker ticker;
  double tempo = INITIAL_TEMPO;
  int stepCount = INITIAL_STEP_COUNT;
  double position = 0.0;
  bool isPlaying = false;
  bool isLooping = INITIAL_IS_LOOPING;

  [@override](/user/override)
  void initState() {
    super.initState();

    GlobalState().setKeepEngineRunning(true);

    final instruments = [
      Sf2Instrument(path: "assets/sf2/TR-808.sf2", isAsset: true),
      SfzInstrument(
        path: "assets/sfz/GMPiano.sfz",
        isAsset: true,
        tuningPath: "assets/sfz/meanquar.scl",
      ),
      RuntimeSfzInstrument(
          id: "Sampled Synth",
          sampleRoot: "assets/wav",
          isAsset: true,
          sfz: Sfz(groups: [
            SfzGroup(regions: [
              SfzRegion(sample: "D3.wav", key: 62),
              SfzRegion(sample: "F3.wav", key: 65),
              SfzRegion(sample: "Gsharp3.wav", key: 68),
            ])
          ])),
      RuntimeSfzInstrument(
          id: "Generated Synth",
          sampleRoot: "/",
          isAsset: false,
          sfz: Sfz(groups: [
            SfzGroup(regions: [
              SfzRegion(sample: "*saw", otherOpcodes: {
                "oscillator_multi": "5",
                "oscillator_detune": "50",
              })
            ])
          ])),
    ];

    sequence.createTracks(instruments).then((tracks) {
      this.tracks = tracks;
      tracks.forEach((track) {
        trackVolumes[track.id] = 0.0;
        trackStepSequencerStates[track.id] = StepSequencerState();
      });

      setState(() {
        this.selectedTrack = tracks[0];
      });
    });

    ticker = this.createTicker((Duration elapsed) {
      setState(() {
        tempo = sequence.getTempo();
        position = sequence.getBeat();
        isPlaying = sequence.getIsPlaying();

        tracks.forEach((track) {
          trackVolumes[track.id] = track.getVolume();
        });
      });
    });
    ticker.start();
  }

  handleTogglePlayPause() {
    if (isPlaying) {
      sequence.pause();
    } else {
      sequence.play();
    }
  }

  handleStop() {
    sequence.stop();
  }

  handleSetLoop(bool nextIsLooping) {
    if (nextIsLooping) {
      sequence.setLoop(0, stepCount.toDouble());
    } else {
      sequence.unsetLoop();
    }

    setState(() {
      isLooping = nextIsLooping;
    });
  }

  handleToggleLoop() {
    final nextIsLooping = !isLooping;

    handleSetLoop(nextIsLooping);
  }

  handleStepCountChange(int nextStepCount) {
    if (nextStepCount < 1) return;

    sequence.setEndBeat(nextStepCount.toDouble());

    if (isLooping) {
      final nextLoopEndBeat = nextStepCount.toDouble();

      sequence.setLoop(0, nextLoopEndBeat);
    }

    setState(() {
      stepCount = nextStepCount;
      tracks.forEach((track) => syncTrack(track));
    });
  }

  handleTempoChange(double nextTempo) {
    if (nextTempo <= 0) return;
    sequence.setTempo(nextTempo);
  }

  handleTrackChange(Track? nextTrack) {
    setState(() {
      selectedTrack = nextTrack;
    });
  }

  handleVolumeChange(double nextVolume) {
    if (selectedTrack != null) {
      selectedTrack!.changeVolumeNow(volume: nextVolume);
    }
  }

  handleVelocitiesChange(int trackId, int step, int noteNumber, double velocity) {
    final track = tracks.firstWhere((track) => track.id == trackId);

    trackStepSequencerStates[trackId]!.setVelocity(step, noteNumber, velocity);

    syncTrack(track);
  }

  syncTrack(track) {
    track.clearEvents();
    trackStepSequencerStates[track.id]!
        .iterateEvents((step, noteNumber, velocity) {
      if (step < stepCount) {
        track.addNote(
            noteNumber: noteNumber,
            velocity: velocity,
            startBeat: step.toDouble(),
            durationBeats: 1.0);
      }
    });
    track.syncBuffer();
  }

  loadProjectState(ProjectState projectState) {
    handleStop();

    trackStepSequencerStates[tracks[0].id] = projectState.drumState;
    trackStepSequencerStates[tracks[1].id] = projectState.pianoState;
    trackStepSequencerStates[tracks[2].id] = projectState.bassState;
    trackStepSequencerStates[tracks[3].id] = projectState.synthState;

    handleStepCountChange(projectState.stepCount);
    handleTempoChange(projectState.tempo);
    handleSetLoop(projectState.isLooping);

    tracks.forEach(syncTrack);
  }

  handleReset() {
    loadProjectState(ProjectState.empty());
  }

  handleLoadDemo() {
    loadProjectState(ProjectState.demo());
  }

  Widget _getMainView() {
    if (selectedTrack == null) return Text('Loading...');

    final isDrumTrackSelected = selectedTrack == tracks[0];

    return Center(
      child: Column(children: [
        Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
          Transport(
            isPlaying: isPlaying,
            isLooping: isLooping,
            onTogglePlayPause: handleTogglePlayPause,
            onStop: handleStop,
            onToggleLoop: handleToggleLoop,
          ),
          PositionView(position: position),
        ]),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            StepCountSelector(
                stepCount: stepCount, onChange: handleStepCountChange),
            TempoSelector(
              selectedTempo: tempo,
              handleChange: handleTempoChange,
            ),
          ],
        ),
        TrackSelector(
          tracks: tracks,
          selectedTrack: selectedTrack,
          handleChange: handleTrackChange,
        ),
        Row(mainAxisAlignment: MainAxisAlignment.center, children: [
          MaterialButton(
            child: Text('Reset'),
            onPressed: handleReset,
          ),
          MaterialButton(
            child: Text('Load Demo'),
            onPressed: handleLoadDemo,
          ),
        ]),
        DrumMachineWidget(
          track: selectedTrack!,
          stepCount: stepCount,
          currentStep: position.floor(),
          rowLabels: isDrumTrackSelected ? ROW_LABELS_DRUMS : ROW_LABELS_PIANO,
          columnPitches:
              isDrumTrackSelected ? ROW_PITCHES_DRUMS : ROW_PITCHES_PIANO,
          volume: trackVolumes[selectedTrack!.id] ?? 0.0,
          stepSequencerState: trackStepSequencerStates[selectedTrack!.id],
          handleVolumeChange: handleVolumeChange,
          handleVelocitiesChange: handleVelocitiesChange,
        ),
      ]),
    );
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
          colorScheme: ColorScheme.dark(),
          textTheme:
              Theme.of(context).textTheme.apply(bodyColor: Colors.white)),
      home: Scaffold(
        appBar: AppBar(title: const Text('Drum machine example')),
        body: _getMainView(),
      ),
    );
  }
}

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

1 回复

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


当然,关于如何在Flutter项目中使用flutter_sequencer插件来控制音频序列,以下是一个基本的代码示例,展示了如何设置和使用该插件。

首先,确保你已经在pubspec.yaml文件中添加了flutter_sequencer依赖:

dependencies:
  flutter:
    sdk: flutter
  flutter_sequencer: ^最新版本号  # 请替换为当前最新版本号

然后,运行flutter pub get来安装依赖。

接下来,在你的Flutter项目中,你可以按照以下步骤来设置和使用flutter_sequencer

1. 导入必要的包

在你的Dart文件中,首先导入flutter_sequencer包:

import 'package:flutter/material.dart';
import 'package:flutter_sequencer/flutter_sequencer.dart';

2. 创建并配置FlutterSequencer

在你的主页面或相关页面中,配置并显示FlutterSequencer

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Sequencer Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SequencerDemoPage(),
    );
  }
}

class SequencerDemoPage extends StatefulWidget {
  @override
  _SequencerDemoPageState createState() => _SequencerDemoPageState();
}

class _SequencerDemoPageState extends State<SequencerDemoPage> {
  late FlutterSequencerController _controller;

  @override
  void initState() {
    super.initState();
    // 初始化FlutterSequencerController
    _controller = FlutterSequencerController(
      sequenceLength: 16,  // 序列长度
      ticksPerBeat: 4,     // 每拍的ticks数
      bpm: 120,            // 每分钟节拍数
    );

    // 添加一些音符到序列中作为示例
    _addNotesToSequence();
  }

  void _addNotesToSequence() {
    // 在序列的第4个位置添加一个音符
    _controller.addNote(4, 60, 100); // 位置4, MIDI音符60(C4), 力度100
    // 在序列的第8个位置添加一个音符
    _controller.addNote(8, 62, 80);  // 位置8, MIDI音符62(D4), 力度80
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Sequencer Demo'),
      ),
      body: Center(
        child: FlutterSequencer(
          controller: _controller,
          builder: (context, state) {
            return Padding(
              padding: const EdgeInsets.all(8.0),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Text('BPM: ${state.bpm.toString()}'),
                  SizedBox(height: 16),
                  Expanded(
                    child: SequencerWidget(
                      controller: state,
                      noteColor: Colors.blue,
                      backgroundColor: Colors.grey[200]!,
                      onNotePressed: (position) {
                        // 当音符被点击时触发
                        print('Note at position $position pressed');
                      },
                    ),
                  ),
                ],
              ),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // 播放/暂停序列
          _controller.togglePlayPause();
        },
        tooltip: 'Play/Pause',
        child: Icon(_controller.isPlaying ? Icons.pause : Icons.play_arrow),
      ),
    );
  }

  @override
  void dispose() {
    // 释放资源
    _controller.dispose();
    super.dispose();
  }
}

3. 运行你的应用

确保你的设备或模拟器已经连接,然后运行你的Flutter应用。你应该能够看到一个简单的音频序列控制器,其中包含了播放/暂停按钮和可视化序列的部件。点击序列中的音符会在控制台中打印出相应的位置信息。

这个示例展示了如何初始化FlutterSequencerController,如何向序列中添加音符,以及如何显示和控制音频序列。根据实际需求,你可以进一步自定义和扩展这个示例。

回到顶部