コンテンツにスキップ

MIDI (MIDI::Device / FmrbMidi)

2.0 で追加。Family mruby は鳴らさせる側、つまり MIDI を送る制御機として振る舞います。 音を出すのは、本体の APU 音源でも、GROVE 端子につないだ外部音源でもかまいません。 どちらも同じ MIDI::Device の後ろにいるので、同じアプリのコードで両方を鳴らせます。

両機種で使えます。

device = FmrbMidi.device(self)     # 内蔵 APU
device.note_on(1, 60, 100)         # チャンネル、音番号、強さ
device.note_off(1, 60)

この層は Midori の MIDI gem から取り込んだもので、 MIDI::Device の API はその gem が定めているものです。

音の出口は 2 つ

内蔵 APU

def on_create
  @device = FmrbMidi.device(self)
end

def on_update
  FmrbMidi.tick        # 予約された note off を処理する。毎回呼びます
end

音はファミコン風 APU のチャンネルに割り当てられます。矩形波 2、三角波 1、雑音 1 の 4 声なので、和音を鳴らすとすぐ埋まります。どの MIDI チャンネルをどの声に載せるかは 割り当て表で決まります。

GROVE 端子から外部の音源へ

device = FmrbMidi.serial_device(tx: 53)      # 素のシリアル MIDI 出力
device = FmrbMidi.sam2695_device(tx: 53)     # M5Stack Unit MIDI。初期化まで済ませて返します

端子が開けないときは (I2C で既に使われている場合など) どちらも nil を返すので、 使う前に確認してください。

@device = FmrbMidi.sam2695_device(tx: 53)
if @device.nil?
  Log.warn("no MIDI port")
  @device = FmrbMidi.device(self)   # 内蔵 APU に切り替える
end

シリアル MIDI は MIDI 規格どおり 31250 bps です。ピンは基板によって違います。

機種 MIDI 送信に使う GROVE のピン
Modern (Tab5) GPIO 53
Retro (narya-board) GPIO 47 (GROVE 2)
tx = FmrbConst::BOARD == "tab5" ? 53 : 47

外部音源としては M5Stack Unit MIDI (SAM2695) を基準にしています。GM 音源で、GROVE ケーブル 1 本でつながります (GROVE 2 から 5V を供給できます)。

M5Stack Unit MIDI で SMF を鳴らしている Tab5

上の写真は、SMF プレイヤーのアプリから GROVE 経由で Unit MIDI を鳴らし、モジュールの 音声出力をスピーカーにつないだところです。

標準 MIDI ファイルを鳴らす

player = FmrbMidi::SmfPlayer.new(@device)
player.load("/usr/share/sounds/midi/song.mid")
player.play

def on_update
  player.tick
  FmrbMidi.tick
end

終わったかどうかは player.playing?、途中で止めるのは player.stop です (鳴りっぱなしの 音は消してくれます)。

同梱の曲は /usr/share/sounds/midi にあります。SMF Player (/app/tool/smf_player.app.rb) はファイルを選んで鳴らせる完成品なので、読むのにも使うのにも 向いています。

MML

ファイルではなく文字列で書いた曲を鳴らす場合です。

@device = FmrbMidi.device(self)
@player = FmrbMidi::MmlPlayer.new(@device)
@player.bpm = 120
@player.load_string("o4 l8 crdrerfrgrarbr>cr")
@player.play

複数の文字列を渡すと、それらが 1 つの曲に合わさって同時に鳴ります。

演奏の間隔はアプリの更新処理から取っているのではありません。演奏側は「何マイクロ秒に 出す」という印をつけた命令を C の層に渡し、時計がその時刻に VM を通さず送り出します。 だからアプリが忙しくても拍が崩れません。

MML のデモ (/app/demo/mml.app.rb) は、同じ曲を内蔵 APU と外部音源のどちらでも 鳴らせて、動作中に切り替えられます。

この MML は BASIC の MML とは別物です

FMRuby BASIC には Family BASIC 互換の PLAY 文があります。これは別の実装で、書き方も 同じではありません。BASIC と MicroPython を参照してください。

出ているものを確かめる

開発時には、リポジトリの tools/fmrb_midi_monitor.rb がシリアル端子から出るバイトを 到着時刻つきで表示します。

note on ch1 C4 vel=100 [90 3C 64]

到着時刻が分かるので、聴いて判断するより正確に拍や音符の間隔を測れます。パソコン側の ソフト音源に流して、機材なしで実際に鳴らすこともできます。

負荷について

シリアル経路では、1 音送るのにメモリを一切確保しません。だから演奏中に GC が割り込んで 曲が止まることがありません。自分のコードでも同じにしてください。必要な文字列や配列は、 音を出す繰り返しの中ではなく、読み込み時に一度だけ作ります。

関連