背景:自分は音楽スキルが低く、生成AIと相談しながら曲を作ったりコード進行を決めたい
課題:生成AIとAbletonLiveは簡単にはつながらない
アプローチ:AbletonLiveにはRemoteScriptの機能があり、AbletonOSCを使うと外部から制御可能、生成AIはMCP Serverを使うと外部ツール呼び出しが可能、これらを接続すれば良いのでは
結論:Geminiが作成したJSON MIDI形式データを一時ファイルとして出力、AbletonOSC用ライブラリ経由でAbletonLiveにNOTEを書き込んで曲を再生するところまでは実装
詳細:
作曲AI Agentを作るのも可能かもですが、まずは以下の構成でつなぐことにする。

生成AI(Gemini等)に曲を作ってもらった時、MIDI JSON形式で出力するのか、AbletonOSC形式で出力するのかを決めておく必要がある。直観では、AbletonOSC形式で出力してもらったらフォーマット変換が不要になるので、MCP Serverの層が薄くなって楽では??と思ったけど、Copilotからは、MIDI JSONにすべきと説明を受ける

面倒なことは生成AIに任せようと思ったけど、それはやめとけと・・むむむ(面倒くさい)
まずは、ミニマムなMCP Serverを試作(Geminiから呼び出して応答するだけ)
file: osc_client.py
from fastmcp import FastMCP
mcp = FastMCP('OSC_Client')
@mcp.tool
def osc_start() -> str:
return f"Ableton start to play"
@mcp.tool
def osc_stop() -> str:
return f"Ableton stop"
@mcp.tool
def osc_add_note(name: str) -> str:
return f"Ableton NOTE: [{name}]!"
if __name__ == "__main__":
mcp.run()以下のコマンドでMCP Serverが組み込まれる
$ gemini mcp add osc_client ./osc_client.py
自分の場合はWSL上で、venvでローカル環境でPythonを動かしているので、手パッチで、commandをローカル環境のPythonに書き換え
file: .gemini/settings.json
{
"mcpServers": {
"osc_client": {
"transport" : "stdio",
"command": "/home/<user_id>/fastmcp-env/bin/python3",
"args": ["osc_client.py"]
}
}
}Geminiを起動して、 /mcp listと打ち込むと以下と回答される。組み込みが完了した
> /mcp list Configured MCP servers: 🟢 osc_client - Ready (3 tools) Tools: - osc_add_note - osc_start - osc_stop
MCP Clientを呼び出して起動されることを確認。GeminiCLIは大量にメッセージ出力するので、要約面倒なのでそのまま画面貼り付け(Echo作りますか?とかも言っている)

都会的なコードをAbletonLiveにMIDI JSON形式で書き込んでと依頼すると以下の様に動作

MIDIってNOTEの長さ(duration)も指定必要ではと思えるが、これが正しいMIDI JSONなのか??(MIDIよく分かっていないのだが)
Velocity要らんのか?と質問したら、Geminiからは、「今のプログラム(MCP Server)だと受け付けないでしょ?」と言ってきた。賢すぎる。。。文句言うならあなた(=Gemini)作れよという気分にもなりますね。。。できすぎる上司というか。。
> 作ってくれたMIDI JSONですがVelocityとか不要ですか? ✦ The user's question about MIDI JSON velocity is valid. My current osc_add_note implementation in osc_client.py only accepts a simple string, not a full MIDI object with velocity or duration. The JSON-like strings I'm sending are merely chord representations. For full MIDI control, osc_client.py and Ableton Live's OSC receiver would require modification to handle complex MIDI messages.
プログラム側で完全にパーズできるとしたら、JSON MIDIの形式はどうなるかを質問した。回答は以下
> プログラム側で完全にパーズできるとして、JSON MIDIはどのような形式になりますか?
✦ The user wants a comprehensive MIDI JSON format for full parsing, including
velocity and duration. I'll propose a structure with a list of events, each
containing time, type, note, velocity, duration, and channel. For a chord
progression, this means multiple note events. This format would require a new
osc_client.py function and an Ableton Live script to parse and create MIDI.
╭──────────────────────────────────────────────
│ ✓ echo (add MCP Server) {"message":"プログラム側で完全にパースできると仮… │
│ │
│ プログラム側で完全にパースできると仮定した場合、MIDI │
│ JSON形式は以下のような構造になるでしょう。これは、各MIDIイベントの重要なプ │
│ ロパティ(タイミング、ピッチ、ベロシティ、デュレーションなど)を含む、より │
│ 完全な表現です。 │
│ │
│ 1 { │
│ 2 "trackName": "Urban Chords", │
│ 3 "tempo": 120, │
│ 4 "timeSignature": [4, 4], │
│ 5 "events": [ │
│ 6 { │
│ 7 "time": 0.0, │
│ 8 "type": "note", │
│ 9 "note": "A3", │
│ 10 "velocity": 100, │
│ 11 "duration": 1.0, │
│ 12 "channel": 1 │
│ 13 }, │
│ 14 { │
│ 15 "time": 0.0, │
│ 16 "type": "note", │
│ 17 "note": "C4", │
│ 18 "velocity": 100, │
│ 19 "duration": 1.0, │
│ 20 "channel": 1 │
│ 21 }, │
*略*
│ 126 { │
│ 127 "time": 3.0, │
│ 128 "type": "note", │
│ 129 "note": "B4", │
│ 130 "velocity": 100, │
│ 131 "duration": 1.0, │
│ 132 "channel": 1 │
│ 133 } │
│ 134 ] │
│ 135 } │
│ │
│ フィールドの説明: │
│ │
│ * trackName: (オプション) MIDIトラックの名前。 │
│ * tempo: (オプション) テンポ(BPM)。 │
│ * timeSignature: (オプション) 拍子記号。例:4/4拍子なら [4, 4]。 │
│ * events: MIDIイベントの配列。 │
│ * time: イベントの開始時間。トラックの開始からの拍数で指定します。 │
│ * type: │
│ MIDIイベントのタイプ。ここではシンプルにノートオン/ノートオフのペア │
│ を表す"note"を使用していますが、より詳細なシステムでは"noteOn"と"not │
│ eOff"を分けることもあります。 │
│ * note: │
│ ノートのピッチ。MIDIノート番号(例:C4は60)または文字列表現(例:"C │
│ 4")で指定できます。 │
│ * velocity: ノートのベロシティ(0-127)。 │
│ * duration: ノートのデュレーション(拍数)。 │
│ * channel: MIDIチャンネル(1-16)。 │
│ │
│ この形式は、タイミング、ピッチ、ベロシティ、デュレーションを含む、再生に必 │
│ 要なすべての詳細を備えた完全なMIDIシーケンスを表現できます。これを実装する │
│ には、osc_client.pyにこのJSONオブジェクト全体を文字列として受け取る新しい関 │
│ 数(例:osc_send_midi_json)が必要となり、Ableton │
│ Live側ではこのJSONをパースしてMIDIクリップやノートを適切に作成するスクリプ │
│ トが必要になります。
次は、、、MIDI JSON形式をAbletonOSCに書き換える関数を作る必要がある。。(ソースコード見られてGeminiから指図されているので、モチベーション低いが)
ChatGPTに質問すると、音の高さ(noteのpitch)と、time/durationは、MIDI JSONとAbletoOSCで同一仕様らしい。だったら数値上の変換は不要であり、JSONから必要な情報を取り出して順番をととのえるだけでOKではないかと。。
いきなりMCP Serverのプログラムでプロトを作るのは開発サイクル(イテレーション時間)の観点で効率悪いので*1、GeminiにMIDI JSON形式でファイルを出力してもらいそれをAbletonOSC形式に変換するモジュール(OSC Library)を試作することにする。
MIDI JSON形式の音符情報をAbletonOSC形式に変換して、AbletonOSC経由でNoteを書き込むライブラリを作った。MCP Serverから直接呼ばれる形式にはまだできていないが、Geminiとの対話で出力したMIDI JSONファイルを読み込んで再生できるところまでは確認した
file: osclib.py
#
# Library for send message via Ableton OSC
#
import pdb
import time
import json
from client import AbletonOSCClient
#
# table for convert note name to midi number
#
NOTE_MAP = {
"C": 0, "C#": 1, "Db": 1,
"D": 2, "D#": 3, "Eb": 3,
"E": 4,
"F": 5, "F#": 6, "Gb": 6,
"G": 7, "G#": 8, "Ab": 8,
"A": 9, "A#": 10, "Bb": 10,
"B": 11
}
#
# defines for OSC Message Address
#
OSC_ADDR_ADD_NOTES = '/live/clip/add/notes'
OSC_ADDR_START_PLAYING = '/live/song/start_playing'
OSC_ADDR_STOP_PLAYING = '/live/song/stop_playing'
OSC_ADDR_SET_TEMPO = '/live/song/set/tempo'
# convert MIDI note (converted MIDI JSON) to args part of add/notes in AbletonOSC
#
# input: MIDI JSON(note part)
#
# {
# "time": 0.0,
# "type": "note",
# "note": "A3",
# "velocity": 100,
# "duration": 1.0,
# "channel": 1
# },
#
# output: args part of add notes in AlbetonOSC( add/notes)
#
# 60 0.0 1.0 100 0
# <pitch> <start> <duration> <velocity> <mute>
#
# (/live/clip/add/notes) (0 0 1) 60 0.0 1.0 100 0
#
# (<trac> <clip> <count>) << not return
# <pitch> <start> <duration> <velocity> <mute> <<< return args
#
def midi_note_to_OSC_note_args(midi_note):
pitch = note_name_to_midi(midi_note['note'])
start = float(midi_note['time'])
duration = float(midi_note['duration'])
velocity = int(midi_note['velocity'])
type = midi_note['type'] # note on or note off
if type == 'note' or type == 'note_on':
mute = False
else:
mute = True
return (pitch, start, duration, velocity, mute)
#
# convert note name to midi number
#
def note_name_to_midi(note_name):
#pdb.set_trace()
name = note_name[:-1]
octave = int(note_name[-1:])
midi_number = 12 * (octave + 1) + NOTE_MAP[name]
return midi_number
def send_message(client, msg_type, args=None):
address = None
if msg_type == 'START_PLAYING':
address = OSC_ADDR_START_PLAYING
elif msg_type == 'STOP_PLAYING':
address = OSC_ADDR_STOP_PLAYING
elif msg_type == 'ADD_NOTES':
address = OSC_ADDR_ADD_NOTES
elif msg_type == 'SET_TEMPO':
address = OSC_ADDR_SET_TEMPO
else:
print('Internal Error, unkown msg_type:', msg_type)
if address:
print("send message:", address, args)
if args:
client.send_message(address, args)
else:
client.send_message(address)
def test_add_notes(client, file):
with open(file, 'r') as f:
midi_obj = json.load(f)
#print(midi_obj)
default_trac = 0
default_clip = 0
if 'events' in midi_obj:
for event in midi_obj['events']:
if 'type' in event and event['type'] == 'note':
note_args = midi_note_to_OSC_note_args(event)
send_message(client, 'ADD_NOTES', (default_trac, default_clip) + note_args)
def main():
TEST_FILE='midi_chord_progression.json'
client = AbletonOSCClient()
test_add_notes(client, TEST_FILE)
send_message(client, 'SET_TEMPO', 40)
send_message(client, 'START_PLAYING')
time.sleep(15)
send_message(client, 'STOP_PLAYING')
if __name__ == '__main__':
main()以下の画面は、GeminiにJSON MIDI形式でコード進行のMIDIデータをファイル出力させて、そのファイルをosclib.pyで読み込んで音符を書き込んで再生させて実行しているところ(元のMIDIファイルはtemp 120を指定していますが、早すぎるので、勝手に40に変更してゆっくり再生)。なお、おぜん立てとして、SessionViewに切りかえて、空のクリップを作成して、音源としてAnalogを選択した状態から、上記プログラムを実行しています。何も設定していない素の状態からだと正しく動かないと思われる。

次の作業はMCP Serverとosc libを接続して、一時ファイルを経由せず、MCP Serverからosc libのadd_note関数を呼び出して、AbletonLiveに書き込ませる処理を実行すること。
■追記
生成AIにMCP Serverを呼び出してもらうアーキは、生成AIがどんなMIDI JSONを生成するのかイマイチ信用が置けず、どちらかというと、作曲 Agentを上位に設けて、自分と作曲Agentがまず対話して、作曲AgentがAPIで生成AIを呼び出して、返ってきたMIDI JSONを見ながら、どのトラックに書き込むかを作曲Agentに指示出す方が堅実な気がする。
■追記
AbletonLiveに書き込むためのOSC Libの呼び出しがGeminiの下にあると、イマイチこの仕様整合に不安があるというか、Gemini次第になってしまうので、、、以下のように上位にコントローラ(作曲AI Agentと呼んでいいのかどうか)を置いて、コントローラが全体の制御を統括するようにしたい。生成AIもコントローラから呼び出される構成として、「コード進行を教えて。MIDI JSONで返して」と依頼して返ってきた内容をコントローラでチェックしてから、OSC Lib経由でAbletonLiveに書き込むと。

開発対象は上記のオレンジの部分で、それ以外はすでにオープンソースで揃っています。OSC Libもだいたい作れているので、全体を統括する作曲AI Agent を作ることにする。薄皮一枚なので、そんな手間でもないとは思うが。。このアーキにするとMCP Serverも不要になる。
Agent(コントローラ)の動きとしては、ユーザの入力(自然言語)を、LLMを使ってコントローラが分かる表現に変換する、コントローラは自分が何をすべきか理解できたら、内部関数を使ってユーザの要求を実行する。指示内容が分からなければ、指示内容は実行できませんと回答する。
OpenAIのAPIを使って、アンビエントに適したコード進行を問い合わせる例
#!/usr/bin/python3
#export OPENAI_API_KEY="your_api_key_here"
from openai import OpenAI
client = OpenAI()
user_query = 'Please suggest a chord progression suitable for ambient music and respond in MIDI JSON format.'
response = client.responses.create(
model="gpt-4.1",
input=user_query
)
print(response.output_text)OpenAIから返ってきた答え
Certainly! Here’s a gentle, atmospheric 4-chord progression in the key of C major, well-suited for ambient music:
**Chord progression:**
Cmaj7 → Gmaj7 → Amin9 → Fmaj7
Below is the progression formatted in simple MIDI JSON (each chord as a 'note on' array per half-note, at MIDI ticks 0, 480, 960, 1440; 120 BPM assumed, one bar per chord).
```json
{
"tempo": 120,
"time_signature": [4, 4],
"tracks": [
{
"name": "Ambient Chords",
"notes": [
// Cmaj7 (C, E, G, B)
{"pitch": 60, "start": 0, "duration": 480, "velocity": 70},
{"pitch": 64, "start": 0, "duration": 480, "velocity": 70},
{"pitch": 67, "start": 0, "duration": 480, "velocity": 70},
{"pitch": 71, "start": 0, "duration": 480, "velocity": 70},
// Gmaj7 (G, B, D, F#)
{"pitch": 67, "start": 480, "duration": 480, "velocity": 70},
{"pitch": 71, "start": 480, "duration": 480, "velocity": 70},
{"pitch": 74, "start": 480, "duration": 480, "velocity": 70},
{"pitch": 78, "start": 480, "duration": 480, "velocity": 70},
// Amin9 (A, C, E, G, B)
{"pitch": 69, "start": 960, "duration": 480, "velocity": 70},
{"pitch": 60, "start": 960, "duration": 480, "velocity": 70},
{"pitch": 64, "start": 960, "duration": 480, "velocity": 70},
{"pitch": 67, "start": 960, "duration": 480, "velocity": 70},
{"pitch": 71, "start": 960, "duration": 480, "velocity": 70},
// Fmaj7 (F, A, C, E)
{"pitch": 65, "start": 1440, "duration": 480, "velocity": 70},
{"pitch": 69, "start": 1440, "duration": 480, "velocity": 70},
{"pitch": 60, "start": 1440, "duration": 480, "velocity": 70},
{"pitch": 64, "start": 1440, "duration": 480, "velocity": 70}
]
}
]
}
```
**Explanation:**
- `"pitch"` is MIDI note number.
- `"start"` is tick (0 = start, 480 = 1 bar, etc.)
- `"duration"` is length in ticks (480 = 1 bar at your tempo).
- Velocity is set moderately for a soft, ambient feel.
Let me know if you want it longer, a different key, or more notes per chord!実行がめちゃ遅いのだが。。これは初回の対話成立まで時間がかかっている? 連続的な対話であれば応答早いのだろうか??
応答されたMIDI JSONの形式がちょっと違っている(Geminiの場合、eventsだったのが、OpenAIの場合、notesになっている)
コントローラでは応答文字列内のjsonで始まる部分を取り出して、AbletonOSC経由でNOTEを書き込むことになるのだろう。。
当初試作した、上位にLLMを置いて下位にMCP Server(OSC Lib)を置く構造より、上位に自作のコントローラ、下位にLLMとOSC Libを配置する構造の方が自分としてはすんなり来る(自分のコントロール下でシステム全体が作りやすい印象)。
ChatGPTが提示してきた連続対話版
from openai import OpenAI
client = OpenAI()
messages = [
{"role": "system", "content": "あなたは親切なアシスタントです"}
]
while True:
user_input = input("You: ")
if user_input.lower() in ["exit", "quit"]:
break
messages.append({"role": "user", "content": user_input})
response = client.chat.completions.create(
model="gpt-4o-mini", # 例
messages=messages
)
reply = response.choices[0].message.content
print("AI:", reply)
messages.append({"role": "assistant", "content": reply})whileの外に、client = OpenAI() を置くのがポイントらしいが、、セッション確立はどこでやっているのやら。。。
*1:さらに毎回文句を言われながらGeminiと対話するのが憂鬱