Play Music on Ruby - Picorubyで作るMIDIオーケストレーションツール -

439 Views

May 31, 26

スライド概要

2026/05/31 関ケ原Ruby会議01 発表資料

profile-image

日常と情報科学関連の話題を中心に話してます。

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

Picoruby MIDI Toshio Maki(@Kirika_K2) 音 玉 源牧雅楽頭ミュージ郎俊男

2.

Omotesando.rb SaaS 山 歯 己 子 Roppongi.rb 身 自 @Kirika_K2 Ruby kaja Web New!

3.

MIDoRI RubyKaigi2026 Smart HR HR Music Arena Pixiv Showcase Ruby Illuminations 自 人 日 ANDPAD

4.

MIDoRI MIDoRI MIDI on Ruby Interpreter M5Stack CoreS3 / Tab5 で動く、MIDI プロセッサ。 PicoRuby でMIDoRIに接続された MIDI 機器を 自由に演奏 / 変換 / 中継 できる。

5.

MIDoRI 入力 処理 あらゆる MIDI 機器を接続し、入力をフックできる MIDI メッセージを生成・加工し、任意の MIDI 機器に出力で きる 設定 設定ファイルを Ruby で書ける 電子楽器が連携するシーンで、様々な用途に使える汎用 MIDI デバイス を目指している。

6.

実演 1 先づ、音を鳴らし申す

7.

デモのスクリプト MIDoRI本体 YAMAHA Reface CP MIDoRI 本体 3 オクターブのMIDIキーボード M5Stack Tab5 USB-MIDI LUMI Keys MIDoRI (M5stack Core5) YAMAHA SEQTRAK グルーブボックス MIDI-DIN 入力ルーティング + リズムシーケンスを MIDoRI が担当 PA SEQTRAK

8.

SEQTRAK Ch.1 Ch.2 Ch.3 琴 和太鼓 三味線 Ch.4 Ch.5 Ch.6 Ch.7 Ch.8 尺八 Ch.9 琴 Ch.10 Ch.11 サンプラー 2024年発売のグルーブボックス。リズム、シンセサイザー、FM、サンプラーを搭載 和太鼓、三味線、琴、尺八など、和楽器も搭載

9.

音色 和太鼓+三味線 和太鼓+三味線+琴+尺八 担当パートなど 和太鼓 … SEQTRAK 内蔵シーケンサ 三味線 … MIDoRIのシーケンサで再生 MIDIキーボードの下1オクターブで琴を演奏 MIDIキーボードの上2オクターブで尺八を演奏 和太鼓+三味線+琴 SEQTRAKの内蔵シーケンサのみ 和太鼓+三味線+琴+尺八 MIDIキーボードの上2オクターブで尺八を演奏

10.

MIDI 主なメッセージ 中身 用途 Note On 鍵盤番号 + 強さ 「ドを強さ 100 で押した」 90 3C 64 Note Off 鍵盤番号 「ドを離した」 80 3C 00 Control Change 番号 + 値 音量・モジュレーション等 B0 07 6E Pitch Bend 上下の量 ベンド E0 00 40 MIDI Clock 1 バイト テンポ同期 (24 発/拍) ● 1 メッセージは 2〜3 バイトが大半。 ● 1 本の MIDI ケーブルに 16 チャンネル流せる ※ 例は ch1 / 60 (中央C) を中心とした典型値。16 進数表記。 実際のバイト列 F8

11.

MIDoRI MIDI USB-MIDI MIDI-DIN TRS-MIDI USB-MIDI MIDoRI (CoreS3 / Tab5) (BLE-MIDI) MIDI-DIN TRS-MIDI (BLE-MIDI) 入出力のルーティング・加工を担当

12.

MIDI::Device 物理層 (Transport) USB-MIDI MIDI-DIN / TRS-MIDI どの機器でも同じ API Ruby 層 (Abstraction) MIDI::Device 統一インターフェース # どの物理層でも device = MIDI::Device.new( MIDIDevices.USB_MIDI ) device.note_on(0, 60, 100) device.note_off(0, 60) BLE-MIDI (未実装) Transport 層の違いを Ruby から意識せずに MIDI::Device 1 つで扱える

13.

MML 1 MIDI UI.pad ON / OFF type: :toggle ON player.start / OFF MIDI clock SEQTRAK tick player.tick(t) SEQTRAK 用 MIDI.start! MIDI clock 文 4 # Pad 1: 三味線 MML の出力 ON/OFF トグル Player MML 長 3 Sequence 文 2 UI.log("Creating MML sequences...") shamisen = MIDI::MML::Sequence.new(shamisen_mml, channel: 1) player = MIDI::MML::Player.new(seqtrak, shamisen) 音 音 MML + UI.pad player.stop # ON → player.start (SEQTRAK の小節グリッドに同期した位置から再生) # OFF → player.stop (鳴っている音は all_notes_off で停止) UI.pad(1, label: "Shamisen", color: :green, type: :toggle) do | state| if state player.start UI.log("Shamisen: ON") else player.stop UI.log("Shamisen: OFF") end end MIDI.start!(bpm: 140, subdivisions: 24, output: seqtrak) do |t| player.tick(t) end

14.

MML Music Macro Language # ===== MML Sequences ===== melody_mml = <<~MML o4 l4 ccgg aag2 ffee ddc2 ggff eed2 ggff eed2 ccgg aag2 ffee ddc2 MML # ===== 音色設定 ===== device.program_change(10, channel: 0) # Melody: Music Box(キラキラした音色) # ===== シーケンス生成 ===== melody_seq = MIDI::MML::Sequence.new(melody_mml, channel: 0, velocity: 110) player = MIDI::MML::Player.new(device, melody_seq, loop: true) # ===== 再生ループ ===== MIDI.bpm_loop(100, output: device, subdivisions: 24) do |clock| player.tick(clock) end 足 SequenceでMMLをイベント情報に変換し、Playerに渡したclock値で発生させるMIDIメッセージを決定

15.

MIDI (1 note < 64 ) note ≥ 64 if e[:velocity] == 0 pair = $active_notes.delete(e[:note]) seqtrak.note_off(pair[1], 0, channel: pair[0]) if pair next end SPLIT_NOTE = 64 ch7 keys_input.on(:note_on) do |e| # velocity 0 は note_off と同義 in_note = e[:note] if in_note < SPLIT_NOTE out_ch = LOWER_CH raw = in_note else out_ch = UPPER_CH raw = in_note + UPPER_OFFSET end # 量子化 ON なら A# ペンタトニックに丸め、OFF なら素通し ch8 out_note = $quantize_enabled ? quantize_to_pentatonic_minor(raw) : raw note_on ch/note $active_notes note_off if out_note >= 0 && out_note <= 127 seqtrak.note_on(out_note, e[:velocity], channel: out_ch) $active_notes[in_note] = [out_ch, out_note] end end 力 止 八 keys_input.on(:note_off) do |e| pair = $active_notes.delete(e[:note]) seqtrak.note_off(pair[1], 0, channel: pair[0]) if pair end

16.

(root = A#) A# B C = C# D# ON / OFF 音 八 音 子 D E F F# G G# A # ============================================================================= # A# マイナーペンタトニック量子化 # ============================================================================= # マイナーペンタトニックの度数 (root, b3, 4, 5, b7, +1oct) PENTATONIC_MINOR_DEGREES = [0, 3, 5, 7, 10, 12] SCALE_ROOT_PC = 10 # A# (0=C, 10=A#) [0, 3, 5, 7, 10] Pad 2 音 用 赤 A# # 任意の note を最も近い A# マイナーペンタトニック音に量子化 ON def quantize_to_pentatonic_minor(note) return note if note < 0 || note > 127 rel = note - SCALE_ROOT_PC pc = rel % 12 base = rel - pc best = 0 best_dist = 13 PENTATONIC_MINOR_DEGREES.each do |s| d = (pc - s).abs if d < best_dist best_dist = d best = s end end base + best + SCALE_ROOT_PC end

17.
[beta]
UI.pad

Ruby

三味線シーケンス、ペンタトニック量子化のオン・オフに使用

# Pad 1: 三味線 MML の出力 ON/OFF トグル
# ON

→ player.start (SEQTRAK の小節グリッドに同期した位置から再生)

# OFF → player.stop

(鳴っている音は all_notes_off で停止)

UI.pad(1, label: "Shamisen", color: :green, type: :toggle) do |state|
if state
player.start
UI.log("Shamisen: ON")
else
player.stop
UI.log("Shamisen: OFF")
end
end
# Pad 2: ペンタトニック量子化の ON/OFF トグル (初期 ON)
# トグルパッドは起動時 OFF 表示で初期状態を設定できないため、押下ごとに
# 自前フラグを反転し、ラベルで実状態を示す (押すたびに必ず切り替わる)。
UI.pad(2, label: "Quant ON", color: :cyan, type: :toggle) do |state|
$quantize_enabled = !$quantize_enabled
UI.pad_label(2, $quantize_enabled ? "Quant ON" : "Quant OFF")
UI.log("Quantize: #{$quantize_enabled ? 'ON' : 'OFF'}")
end

行

タッチパッドを定義 → 叩かれたら登録した任意の Ruby ブロックを実行。

18.

開発の話

19.

2023 Keebkaigi @ 松本 PRK_Firmware を拡張してMIDIメッセージが使えるようにしたものをLT発表 2023〜 MIDI IN/OUT 両対応の自分用MIDIデバイスが欲しい 構想はあったが、RP2040 はCPU・メモリ共にスペック不足 / ESP32 への PicoRuby 移植が必要 / 入力 IF も自作が 必要など、完成までの道のりが険しかった 2025 RubyKaigi2025 で picoruby-esp32 の発表 🎯 ESP32 移植問題が解決。さらに M5Stack CoreS3 が要件を満たす機器として入手可能に → MIDoRI 開発開始

20.

M5Stack CoreS3 M5Stack Tab5 メインターゲット 高スペック版 PSRAM PSRAM 8 MB 32 MB UI UI 2.0インチ タッチパネル LCD 5インチ タッチパネル LCD ESP32-S3 + PSRAM 8 MB PicoRuby + USB Host が同居可能。 ESP32-P4 + PSRAM 32 MB。リソースに余裕あり。 LCD / SD カード / インターフェースが揃う。 USB Host 機能内蔵でCoreS3より、更に拡張性が高い。

21.

FreeRTOSベースのアプリケーション実装の上にPicorubyを含む複数タスクを実装 ESP32-S3 / ESP32-P4 (2 コア) Core 0 リアルタイム I/O メインタスク (C) USB MIDI Class Driver (C) アプリ初期化 / 画面描画 / タッチ処理 / USB ホストイベント処理 デバイス開閉 / 転送 callback Core 1 ロジック / スクリプト Supervisor Task (C) PicoRuby Task (Ruby) VM 生成 / 破棄 ● SD カードのユーザスクリプト実行 (main_task.rb) ● UI.pad / UI.log 等で UI に状態を渡す ● MIDI.bpm_loop中にコンテキストスイッチを発生させて、他のタスクと並行で動かす MIDI Input Task (C) 受信バイトをパース → enqueue FreeRTOS queue esp_timer … 高分解能タイマ MIDI Clock Timer Note Scheduler Timer 24 PPQ で送出 1 ms 周期で note_off制御

22.

Ruby C 「何をどう鳴らすか」は Ruby で書きたい。 データの入出力などリアルタイム性の高い部分は C/C++ で書きたい。 C Ruby • メインタスク (UI 描画 / タッチ) • MIDIメッセージの変換・演奏ロジック • USB MIDI Class Driver • MIDIイベントのフック • MIDI Input Task (受信パース) • UI イベントのフック • MIDI Clock / Note Scheduler Timer • Supervisor Task (VM lifecycle) C 側で拾ったイベントを FreeRTOS キューで送って、Rubyオブジェクトに変換

23.

問題: 普通の loopだと他のタスクに切り替わらず、フリーズする 解決策: ループの裏側でタスク切換えを行うようにした # ❌ CPU を握りっぱなし # ✅ MIDoRIのループ loop do device.note_on(0, 60, 100) sleep 1 device.note_off(0, 60) end # ループ中、他のタスクに切り替わらない MIDI.bpm_loop(120, output: device) do device.note_on(0, 60, 100) MIDI.sleep_ms(100) device.note_off(0, 60) end # ループの裏側でコンテキストスイッチを発生 工 MIDI.sleep_ms(Cバインディング)を呼び出して、コンテキストスイッチを発生させる必要がある

24.

Supervisor VM VM 新規生成 スクリプト load PSRAM 2 MB を割当 SD カードから選択したRuby 実行 スクリプトをload スクリプトごとに 1 VM PSRAM 2 MB を割当て エラーも Supervisor がハンドリング VM 内例外で本体は落ちない。例外をハンドリングして UI に戻る、エラーはログに記録する VM ごと破棄

25.

1 2 3 4 5 picoruby-midi の upstream マージ MIDoRI 以外でも MIDI が簡単に使えるように BLE-MIDI 対応 無線 MIDI キーボード対応等 r2p2 対応 実験的スクリプトを試しやすく USB-MIDI Device 側でも動かす MIDoRI 自身が PC/iPad から MIDI デバイスに見えるように UI 機能拡張 スライダ / XY パッド等のコンポーネントを揃え、Ruby DSL で自分仕様のデバイスを定義できるように

26.

picoruby-midi picoruby-midi-mml picoruby-midi プロトコル / パーサ / Clock Transport interface picoruby-usb_midi_host picoruby-uart_midi picoruby-sam2695 MIDoRI 内部の C / Ruby コードをFreeRTOS 依存を抽象化して Picoruby本体にマージ。

27.

picoruby-midi Phase 1 Phase 2 Phase 3 Phase 4 内部リファクタ API 整理 mruby バインディングの追加 スコープ整理 OS-free コア抽出 MML 分離 Phase 5 Phase 6 Phase 7 Phase 8 gem rename + USB host stack 取り込み README / sig/*.rbs / example 整備 ホストビルドのパーサ単体テス ト upstream 提出

28.

MIDoRI Phase 1 ✓ 完了 Phase 2 ✓ 完了 内部リファクタ API 整理 OS-free コア抽出 MIDI.start! / on_bpm_change追 Phase 3 ✓ 完了 mruby バインディングの追加 Phase 4 ✓ 完了 スコープ整理 MML 分離 加 Phase 5 ✓ 完了 gem rename + USB host stack 取り込み (main/usb_midi_host.c が ~70 行に) Phase 6 ✓ 完了 README / sig/*.rbs / example 整備 Phase 7 ✓ 完了 ホストビルドのパーサ単体テス ト Phase 8 upstream 提出 (残作業) 残作業

29.

実演2 picoruby-midi を MIDoRI 以外で動かす ATOM Matrix (素の ESP32 / PSRAM 無し)

30.

MIDoRI (CoreS3) ATOM Matrix PSRAM 8 MB 無し ホスト USB MIDI Host UART (GPIO 26) 音源 外部 SEQTRAK SAM2695 ファーム MIDoRI R2P2-ESP32 + picoruby-midi MIDoRI 上で動くものと、同じ picoruby-midi gem が ATOM Matrix でも走る

31.

MML Music Macro Language require "midi" require "midi-mml" require "sam2695" sam = SAM2695.new(26,32) device = MIDI::Device.new(sam) # ===== MML Sequences ===== melody_mml = <<~MML o4 l4 ccgg aag2 ffee ddc2 ggff eed2 ggff eed2 ccgg aag2 ffee ddc2 MML # ===== 音色設定 ===== device.program_change(10, channel: 0) # Melody: Music Box(キラキラした音色) # ===== シーケンス生成 ===== melody_seq = MIDI::MML::Sequence.new(melody_mml, channel: 0, velocity: 110) player = MIDI::MML::Player.new(device, melody_seq, loop: true) # ===== 再生ループ ===== MIDI.bpm_loop(100, output: device, subdivisions: 24) do |clock| player.tick(clock) end

32.

✓ 動くは動く … 同じ Ruby ファイルが PSRAM 無しの素の ESP32 でも走る △ メモリ的にはギリギリ … picoruby-midi 自体をもっと省メモリにする必要がある これから • picoruby-midi の省メモリ化 PR提出前に、やれるだけやっておく • FreeRTOS 抽象の RP2040 / RP2350 への移植 今回は ESP32 系のみ。PRマージ後に他ボードへ広げる

33.

ご清聴ありがとうございました github.com/kirikak2/midori 興味持っていただいた方は、是非この後お声がけください。スターもらえるとすごく嬉しいです。