summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-07-12 18:50:50 +0900
committertanaka takashi <isazaka@gmail.com>2026-07-12 18:50:50 +0900
commitd83a966199671a1cab6b8f56be448662e79f4a46 (patch)
tree88f1ee34230e5f60411bf988a251b00c3c9fe8ad
parent3d06bee12a26bc55cd73b1494e4d008a8098f6f5 (diff)
feat: 合成機能テンプレートの拡充とドキュメントの更新
- ppmck/pyxel向けに表現力のある作曲テンプレートを追加 - エンベロープ、ビブラート、音程変化の使用例を追加 - テンプレートの検証・IR・音声合成テストを追加 - README、設計書、MCP仕様書を現行実装に合わせて更新
-rw-r--r--README.md400
-rw-r--r--doc/Design.md2
-rw-r--r--doc/mcp.md2
-rw-r--r--pyproject.toml2
-rw-r--r--src/mml_composemusic_mcp/server.py4
-rw-r--r--src/mml_composemusic_mcp/templates.py9
-rw-r--r--tests/test_compose_mml_tool.py23
-rw-r--r--tests/test_templates.py51
8 files changed, 231 insertions, 262 deletions
diff --git a/README.md b/README.md
index 2935a21..7733139 100644
--- a/README.md
+++ b/README.md
@@ -1,359 +1,253 @@
# mml-composemusic-mcp
-レトロチップ音源(2A03 APU)風のMMLをLLMに作曲させるためのMCPサーバです。
+LLMにMMLを書かせ、レトロゲーム機風の音楽をWAVとして生成するMCPサーバです。
-`compose_mml` という1つのツールで、MMLの**作曲(WAV生成)**、**構文検証**、**テンプレート生成**を行えます。
+Pulse 2ch、Triangle 1ch、Noise 1chの構成を持つ簡易2A03 APU風シンセサイザーを内蔵しています。MCPクライアントから単一の`compose_mml`ツールを呼び出すことで、作曲、検証、作例テンプレートの取得まで行えます。
----
+> [!IMPORTANT]
+> `ppmck`と`pyxel`は、このプロジェクトが受理するMML構文モードの名前です。オリジナルのPPMCK/mckcやPyxelとの完全な互換性は保証しません。
-## 目次
+## 主な機能
-1. [機能概要](#機能概要)
-2. [動作環境](#動作環境)
-3. [インストール](#インストール)
-4. [MCPクライアントへの登録](#mcpクライアントへの登録)
-5. [ツールの使い方](#ツールの使い方)
-6. [MML形式](#mml形式)
-7. [テスト・開発](#テスト開発)
-8. [ディレクトリ構成](#ディレクトリ構成)
-9. [ライセンス](#ライセンス)
+- MMLを解析し、モノラル16-bit PCMのWAVファイルを生成
+- 構文・値域・チャンネル適合性を合成前に検証
+- Pulse1、Pulse2、Triangle、Noiseの4チャンネルに対応
+- ppmck風の小文字MMLと、Pyxel風の大文字MMLに対応
+- 音量・音色エンベロープ、ビブラート、LFO、グライド、スイープなどを合成へ反映
+- LLMがコピーして改変できる8種類の作例テンプレートを収録
+- stdio、HTTP、SSE、Streamable HTTPトランスポートに対応
----
+## 必要環境
-## 機能概要
-
-| 機能 | 説明 |
-|---|---|
-| `compose` | MMLを解析し、レトロAPU風の矩形波/三角波/ノイズでWAVファイルを生成する |
-| `validate` | MMLの構文を検証し、エラー・警告・チャンネル概要を返す |
-| `template` | `ppmck` / `pyxel` 用のテンプレートMMLを生成する |
-
-### 対応MML形式
-
-| モード | 特徴 |
-|---|---|
-| `ppmck` | PPMCKインスパイアのクラシックAPU準拠形式。小文字コマンド、`A`/`B`/`T`/`N` トラック |
-| `pyxel` | Pyxel MML準拠形式。大文字コマンド、`0:`〜`3:` トラック、リピート・ゲートタイム対応 |
-
----
-
-## 動作環境
-
-- Python 3.14 以上
+- Python 3.14以上
- [uv](https://docs.astral.sh/uv/)(推奨)
-- 依存パッケージ: `fastmcp`, `numpy`
-
----
-## インストール
+## セットアップ
-### 1. リポジトリをクローンまたは配置
-
-```bash
-cd C:\\path\\to\\mml-composemusic-mcp
-```
-
-### 2. 依存関係をインストール
-
-`uv` を使う場合:
+リポジトリを取得し、プロジェクトのディレクトリで依存関係をインストールします。
```bash
uv sync
-```
-
-`pip` を使う場合:
-
-```bash
-pip install -e .
-```
-
-### 3. 起動確認
-
-```bash
uv run mml-composemusic-mcp --help
```
-以下のように表示されればOKです。
+pipを使う場合は、仮想環境内で次のようにインストールできます。
+```bash
+python -m pip install -e .
+mml-composemusic-mcp --help
```
-usage: mml-composemusic-mcp [-h] [--output-dir OUTPUT_DIR]
- [--transport {stdio,http,sse,streamable-http}] [--host HOST] [--port PORT]
-```
-
----
## MCPクライアントへの登録
-### Claude Desktop の場合
-
-1. 設定ファイルを開きます。
-
- | OS | パス |
- |---|---|
- | Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
- | macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
-
-2. `mcpServers` に以下を追加します。
-
- ```json
- {
- "mcpServers": {
- "mml-composemusic": {
- "command": "uv",
- "args": [
- "run",
- "--project",
- "C:\\path\\to\\mml-composemusic-mcp",
- "mml-composemusic-mcp",
- "--output-dir",
- "C:\\path\\to\\mml-composemusic-mcp\\data"
- ]
- }
- }
- }
- ```
-
-3. Claude Desktop を再起動します。
-
-### 設定ファイル例
-
-[`doc/mcp-client-config-example.json`](doc/mcp-client-config-example.json) に設定例があります。必要に応じてコピーしてください。
-
-### トランスポートの選択
-
-| トランスポート | 用途 | 設定方法 |
-|---|---|---|
-| `stdio` | 標準的なMCPクライアント接続(デフォルト) | `command`/`args` で指定 |
-| `http` | HTTPエンドポイント | `--transport http` で起動後、`url` で接続 |
-| `sse` | Server-Sent Events | `--transport sse` で起動後、`url` で接続 |
-| `streamable-http` | Streamable HTTP | `--transport streamable-http` で起動 |
-
-#### SSEで使う場合の例
-
-```bash
-uv run mml-composemusic-mcp --transport sse --port 8080 --output-dir ./data
-```
+標準入出力で起動する設定例です。`C:\path\to\mml-composemusic-mcp`と出力先を実際の絶対パスへ置き換えてください。
```json
{
"mcpServers": {
"mml-composemusic": {
- "url": "http://127.0.0.1:8080/sse"
+ "command": "uv",
+ "args": [
+ "run",
+ "--project",
+ "C:\\path\\to\\mml-composemusic-mcp",
+ "mml-composemusic-mcp",
+ "--output-dir",
+ "C:\\path\\to\\mml-composemusic-mcp\\data"
+ ]
}
}
}
```
-### 注意点
+`--output-dir`を省略した場合は、サーバの作業ディレクトリにある`./data`へ出力します。MCPクライアントによって作業ディレクトリが異なるため、通常は絶対パスの指定を推奨します。
-- `--output-dir` は相対パスでも動きますが、MCPクライアントの作業ディレクトリが不定なため、**絶対パスを推奨**します。
-- 初回起動時は `uv` が依存関係を解決するため、少し時間がかかることがあります。
+HTTP系トランスポートで手動起動する場合は、次のオプションを利用できます。
+
+```bash
+uv run mml-composemusic-mcp \
+ --transport streamable-http \
+ --host 127.0.0.1 \
+ --port 8080 \
+ --output-dir ./data
+```
----
+利用可能な`--transport`は`stdio`(デフォルト)、`http`、`sse`、`streamable-http`です。HTTP側の接続URLは、使用するFastMCPとMCPクライアントのトランスポート設定に合わせてください。
-## ツールの使い方
+## LLMからの使い方
-### ツール名
+公開されるツールは`compose_mml`の1つです。基本的には、テンプレートを取得して編集し、検証してから合成します。
-`compose_mml`
+1. `action: "template"`で作例を取得する
+2. 作りたい曲に合わせてLLMがMMLを書き換える
+3. `action: "validate"`でエラーと警告を確認する
+4. 問題がなければ`action: "compose"`でWAVを生成する
-### 引数
+MCPクライアントでは、たとえば次のように依頼できます。
-| 引数名 | 型 | 必須 | デフォルト | 説明 |
+> pyxelモードのvibrato_leadテンプレートを参考に、テンポ150の明るい8小節のボス戦BGMを作ってください。まずvalidateし、問題を直してからcomposeしてください。
+
+### `compose_mml`の引数
+
+| 引数 | 型 | 使用するaction | デフォルト | 説明 |
|---|---|---|---|---|
-| `action` | string | yes | — | `compose`, `validate`, `template` のいずれか |
-| `mml` | string | compose/validate時 | `""` | MMLソース文字列 |
-| `mode` | string | compose/validate時 | `""` | `ppmck` または `pyxel` |
-| `template` | string | template時 | `"basic"` | `basic`, `melody`, `chord`, `drum`, `empty` |
-| `sample_rate` | integer | no | `44100` | 出力WAVのサンプリングレート(Hz) |
-| `normalize` | boolean | no | `true` | 出力振幅を正規化するか |
+| `action` | string | すべて | 必須 | `template`、`validate`、`compose` |
+| `mode` | string | すべて | `""` | `ppmck`または`pyxel` |
+| `mml` | string | validate/compose | `""` | 検証・合成するMML |
+| `template` | string | template | `"basic"` | 取得するテンプレート名 |
+| `sample_rate` | integer | compose | `44100` | WAVのサンプリング周波数 |
+| `normalize` | boolean | compose | `true` | 合成後に振幅を正規化するか |
+
+未知の`mode`をtemplateで指定した場合は`ppmck`、未知のテンプレート名を指定した場合は`basic`へフォールバックします。validate/composeでは`mml`と`mode`が必須です。
-### action=`compose` — WAVを生成
+### テンプレートを取得する
```json
{
- "action": "compose",
- "mml": "0: T120 L8 O4 V100 @1\n C D E F | G A B >C",
+ "action": "template",
"mode": "pyxel",
- "sample_rate": 44100,
- "normalize": true
+ "template": "vibrato_lead"
}
```
-#### 戻り値
-
-```json
-{
- "success": true,
- "wav_path": "./data/output_20260711_120000_123.wav",
- "duration_sec": 2.0,
- "note_sequence": { ... },
- "validation": {
- "errors": [],
- "warnings": []
- }
-}
-```
+| テンプレート | 用途 |
+|---|---|
+| `basic` | メロディ、伴奏、ベース、リズムの基本4ch構成 |
+| `melody` | Pulse1の主旋律を中心にした構成 |
+| `chord` | Pulseのコード伴奏とTriangleベースを中心にした構成 |
+| `drum` | Noiseのビートを中心にした構成 |
+| `empty` | 各チャンネルを休符だけにした最小構成 |
+| `expressive_lead` | 音量・音色エンベロープを使うリード |
+| `vibrato_lead` | ビブラート、ピッチ変化、デチューンを使うリード |
+| `pitch_motion` | アルペジオ、スイープ、グライドを使う効果的な音程変化 |
-- `success` が `false` の場合、`wav_path` は `null` になります。
-- エラーがある場合は `validation.errors` に詳細なエラー情報が入ります。
-- **WAVファイル名は生成時刻ベースの `output_YYYYMMDD_HHMMSS_mmm.wav` 形式で出力されます。** 毎回異なるファイル名になるため、複数回の `compose` 実行でファイルが上書きされることはありません。
+テンプレートは両モードに用意され、同じ目的を各モード固有のコマンドで表現します。
-### action=`validate` — 構文チェック
+### MMLを検証する
```json
{
"action": "validate",
- "mml": "A t120 l8 o4 v15 q2\n c d e f",
- "mode": "ppmck"
+ "mode": "ppmck",
+ "mml": "A t120 l8 o4 v15 q7 @2\n c d e f | g a b >c"
}
```
-#### 戻り値
+`valid`、`errors`、`warnings`に加え、解析済みの`note_sequence`とチャンネルごとの`channel_summary`を返します。エラーには行・列、原因、修正ヒントが含まれます。
-```json
-{
- "valid": true,
- "errors": [],
- "warnings": [],
- "note_sequence": { ... },
- "channel_summary": [
- {
- "channel": "Pulse1",
- "note_count": 4,
- "octave_range": [4, 5],
- "duration_ticks": 768
- }
- ]
-}
-```
-
-### action=`template` — テンプレート生成
+### WAVを生成する
```json
{
- "action": "template",
+ "action": "compose",
"mode": "pyxel",
- "template": "basic"
+ "mml": "0: T120 L8 O4 V100 Q90 @1\n C D E F | G A B >C",
+ "sample_rate": 44100,
+ "normalize": true
}
```
-#### 戻り値
+成功時は`success: true`、`wav_path`、`duration_sec`、解析済みの`note_sequence`を返します。ファイル名は`output_YYYYMMDD_HHMMSS_mmm.wav`形式で、指定した出力ディレクトリへ保存されます。
-```json
-{
- "mml": "0: T120 L8 O4 V100 @1\n C D E F | ...",
- "description": "基本的な4ch構成(メロディ+和音+ベース+リズム)"
-}
-```
+`wav_path`はMCPサーバが動作しているマシン上のローカルパスです。リモートのHTTPサーバとして運用する場合、このツール自体はWAVのダウンロード配信を行いません。
-### テンプレート種別
+## MMLの書き方
-| テンプレート | 内容 |
-|---|---|
-| `basic` | 基本的な4ch構成(メロディ+和音+ベース+リズム) |
-| `melody` | メロディ重視(Pulse1主旋律、他は伴奏最小限) |
-| `chord` | コード伴奏重視(Pulse2で和音、Triangleでベース) |
-| `drum` | リズム重視(Noise中心のビートパターン) |
-| `empty` | 各チャンネルのヘッダーのみ(空のテンプレート) |
+### チャンネル対応
----
+| 音源 | ppmck | pyxel | 特徴 |
+|---|---|---|---|
+| Pulse1 | `A` | `0:` | デューティ比を変更できる矩形波 |
+| Pulse2 | `B` | `1:` | デューティ比を変更できる矩形波 |
+| Triangle | `T` | `2:` | 主にベース向けの三角波 |
+| Noise | `N` | `3:` | ドラムや効果音向けのノイズ |
+| Loop | `L` | — | ppmckのループトラック |
-## MML形式
+ppmckは音符と基本コマンドに小文字、pyxelは大文字を使います。
-### ppmck 形式の例
+### ppmckモード
-```
+```mml
#TITLE "My Song"
#COMPOSER "LLM"
-A t150 l8 o4 v15 q2
- c d e f | g a b > c
+A t150 l8 o4 v15 q7 @2
+ c d e f | g a b >c
-B l8 o3 v12 q1
- c r g r c r g r
+B l8 o3 v11 @1
+ c r g r | c r g r
-T l4 o3 v7
- c2 c2 g2 g2
+T l4 o2 v7
+ c2 g2 | a2 f2
N l8 v10
- r c r c r c r c
+ c r c c | c r c r
```
-### pyxel 形式の例
+主な拡張機能:
-```
-0: T150 L8 O4 V100 @1
- C D E F G A B >C
+- `D`: セント単位のデチューン
+- `s`: Pulseハードウェアスイープ
+- `v+` / `v-`: 相対音量
+- `@v` / `@@`: 音量・デューティエンベロープ
+- `@MP` / `MP` / `MPOF`: LFOの定義・適用・解除
+- `@EP` / `EP` / `EPOF`: ピッチエンベロープ
+- `@EN` / `EN` / `ENOF`: 高速アルペジオ向けノートエンベロープ
+- `^`: タイ、`&`: スラー
+
+### pyxelモード
+
+```mml
+0: T150 L8 O4 V110 Q90 @1
+ C D E F | G A B >C
1: L8 O3 V80 @2
- E G B R E G B R
+ C R G R | C R G R
-2: L4 O3 V60
- C2 G2 E2 C2
+2: L4 O2 V60
+ C2 G2 | A2 F2
3: L8 V80
- C R C R C R C R
+ C R C C | C R C R
```
-### 詳細仕様
+主な拡張機能:
+
+- `K`: 半音単位のトランスポーズ
+- `Y`: セント単位のデチューン
+- `@ENV`: 音量エンベロープ
+- `@VIB`: ビブラート
+- `@GLI`: グライド
+- `[...]N`: 回数付きリピート
+- `&`: スラー
-詳細なMMLコマンド仕様、IR構造、エラーコードは [`doc/Design.md`](doc/Design.md) を参照してください。
+値域や正確な文法は[MML構文規則](doc/MML_BNF.md)、MCPの入出力は[MCPツール仕様](doc/mcp.md)、内部構造は[設計書](doc/Design.md)を参照してください。
----
+## 現在の制約
-## テスト・開発
+- DPCM、NSF出力、GUI、Webプレイヤーはありません。
+- 実機や既存ドライバの音を厳密に再現するエミュレーターではありません。
+- Pulse専用コマンドをTriangleやNoiseへ指定すると、エラーまたは警告になります。
+- ppmckの区間リピートなど、設計上予約されていても未実装の構文があります。
+- pyxelの無限リピートは安全のため有限回で打ち切られ、警告が返る場合があります。
-### テスト実行
+## 開発
```bash
+# 全テスト
uv run pytest
-```
-
-### リント・フォーマット
-```bash
+# lint
uv run ruff check .
-uv run ruff format .
-```
-### 手動でサーバを起動
+# format
+uv run ruff format .
-```bash
+# stdioサーバを手動起動
uv run mml-composemusic-mcp --output-dir ./data
```
-または:
-
-```bash
-uv run python -m mml_composemusic_mcp.server --output-dir ./data
-```
-
----
-
-## ディレクトリ構成
-
-```
-.
-├── doc/ # 設計書・設定例
-│ ├── Design.md # 統合設計書
-│ ├── mcp.md # MCPツールスキーマ
-│ └── mcp-client-config-example.json # MCPクライアント設定例
-├── src/mml_composemusic_mcp/ # ソースコード
-│ ├── server.py # MCPサーバ
-│ ├── lexer.py # MML字句解析
-│ ├── parser_ppmck.py # ppmckパーサ
-│ ├── parser_pyxel.py # pyxelパーサ
-│ ├── parser_base.py # パーサ共通処理
-│ ├── ir.py # 中間表現・エラー型
-│ ├── synthesizer.py # APU風合成・WAV出力
-│ └── templates.py # テンプレート
-├── tests/ # テスト
-├── README.md # このファイル
-└── pyproject.toml # プロジェクト設定
-```
-
----
+受理するMML文法を変更する場合は、実装・テストと合わせて`doc/MML_BNF.md`を更新してください。
## ライセンス
diff --git a/doc/Design.md b/doc/Design.md
index eab87aa..92f8dbc 100644
--- a/doc/Design.md
+++ b/doc/Design.md
@@ -129,7 +129,7 @@ MML文字列
| `action` | string | yes | — | 動作モード: `"compose"` / `"validate"` / `"template"` |
| `mml` | string | compose/validate時 | `""` | MML文字列 |
| `mode` | string | compose/validate時 | `""` | `"ppmck"` / `"pyxel"` |
-| `template` | string | template時 | `"basic"` | テンプレート種別: `"basic"` / `"melody"` / `"chord"` / `"drum"` / `"empty"` |
+| `template` | string | template時 | `"basic"` | テンプレート種別: `"basic"` / `"melody"` / `"chord"` / `"drum"` / `"empty"` / `"expressive_lead"` / `"vibrato_lead"` / `"pitch_motion"` |
| `sample_rate` | int | no | `44100` | 出力サンプリング周波数(Hz) |
| `normalize` | bool | no | `true` | 出力振幅の正規化 |
diff --git a/doc/mcp.md b/doc/mcp.md
index ecdda8e..c6cc682 100644
--- a/doc/mcp.md
+++ b/doc/mcp.md
@@ -25,7 +25,7 @@ MML文字列を解析し、以下のいずれかの処理を実行します。
| `action` | string | yes | - | `compose`, `validate`, `template` のいずれか |
| `mml` | string | compose/validate時 | `""` | MMLソース文字列 |
| `mode` | string | compose/validate時 | `""` | `ppmck` または `pyxel` |
-| `template` | string | template時 | `"basic"` | `basic`, `melody`, `chord`, `drum`, `empty` |
+| `template` | string | template時 | `"basic"` | `basic`, `melody`, `chord`, `drum`, `empty`, `expressive_lead`, `vibrato_lead`, `pitch_motion` |
| `sample_rate` | integer | no | `44100` | 出力WAVのサンプリングレート(Hz) |
| `normalize` | boolean | no | `true` | 出力振幅を正規化するか |
diff --git a/pyproject.toml b/pyproject.toml
index 65de706..9eb1ab2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "mml-composemusic-mcp"
-version = "0.1.2"
+version = "0.2.0"
description = "MCP server for LLM-driven retro chiptune-style MML composition"
readme = "README.md"
requires-python = ">=3.14"
diff --git a/src/mml_composemusic_mcp/server.py b/src/mml_composemusic_mcp/server.py
index 06520a6..45bae68 100644
--- a/src/mml_composemusic_mcp/server.py
+++ b/src/mml_composemusic_mcp/server.py
@@ -13,7 +13,7 @@ from .parser_pyxel import parse_pyxel
from .semantic_ppmck import analyze_ppmck
from .semantic_pyxel import analyze_pyxel
from .synthesizer import build_channel_summary, synthesize, write_wav
-from .templates import get_template
+from .templates import TEMPLATES, get_template
mcp = FastMCP("mml-composemusic-mcp")
OUTPUT_DIR = Path("./data")
@@ -88,7 +88,7 @@ def compose_mml(
if action == "template":
if mode not in ("ppmck", "pyxel"):
mode = "ppmck"
- if template not in ("basic", "melody", "chord", "drum", "empty"):
+ if template not in TEMPLATES[mode]:
template = "basic"
mml_text, description = get_template(mode, template)
return {"mml": mml_text, "description": description}
diff --git a/src/mml_composemusic_mcp/templates.py b/src/mml_composemusic_mcp/templates.py
index 9872e44..4d71568 100644
--- a/src/mml_composemusic_mcp/templates.py
+++ b/src/mml_composemusic_mcp/templates.py
@@ -7,6 +7,9 @@ TEMPLATES = {
"chord": '#TITLE "Chord Backing"\n#COMPOSER "LLM"\n\nA t120 l4 o4 v12 @2\n c e g c | f a > c f | g b > d g | c e g c\n\nB l4 o4 v12 @1\n e g b e | f a > c f | d g b d | e g b e\n\nT l2 o3 v7\n c2 | f2 | g2 | c2\n\nN l8 v8\n r c r c | r c r c | r c r c | r c r c\n',
"drum": '#TITLE "Drum Beat"\n#COMPOSER "LLM"\n\nA t120 l8 o4 v15 @2\n c r e r | g r e r | c r e r | g r c r\n\nB l8 o3 v12 @1\n e r g r | e r g r | e r g r | e r g r\n\nT l4 o2 v7\n c2 c2 | c2 c2 | c2 c2 | c2 c2\n\nN l16 v10\n c r c r c r c r | c r c r c r c r\n c r c r c r c r | c r c r c r c r\n',
"empty": '#TITLE "Empty Template"\n#COMPOSER "LLM"\n\nA t120 l4 o4 v15 @2\n r1\n\nB l4 o3 v12 @1\n r1\n\nT l4 o3 v7\n r1\n\nN l4 v10\n r1\n',
+ "expressive_lead": '#TITLE "Expressive Lead"\n#COMPOSER "LLM"\n\n; Define reusable volume and duty envelopes before the tracks.\n@v1={15,12,9|7,9}\n@0={2,1|2,3}\n\nA t132 l8 o5 v15 q7 @v1 @@0\n c e g >c | <b g e c | v-3 d f a >d | v+3 <c4 r4\n\nB l8 o4 v10 @1\n c r g r | e r g r | d r a r | e4 r4\n\nT l4 o3 v7\n c2 e2 | d2 c2\n\nN l8 v9\n c r c c | c r c r | c r c c | c r c r\n',
+ "vibrato_lead": '#TITLE "Vibrato Lead"\n#COMPOSER "LLM"\n\n; MP adds cyclic vibrato; EP adds a one-shot upward pitch shape.\n@MP1={12,16,18,0}\n@EP1={40,20,0|10,0}\n\nA t116 l8 o5 v15 @2 D-6 MP1\n c4 e4 | g2 MPOF | EP1 a4 g4 | EPOF D0 e2\n\nB l8 o4 v10 @1 D6\n c e g e | c e g e | f a >c <a | g b >d <b\n\nT l4 o3 v7\n c2 g2 | f2 g2\n\nN l8 v8\n c r c r | c r c r | c r c r | c r c r\n',
+ "pitch_motion": '#TITLE "Pitch Motion"\n#COMPOSER "LLM"\n\n; EN creates a rapid arpeggio; sweep bends Pulse pitch over time.\n@EN1={0,4,7|12,7,4}\n\nA t144 l8 o4 v15 @2 EN1\n c2 g2 | ENOF s2,3 >c2 <g2 | s0,-2 c1\n\nB l8 o3 v11 @1\n c c g g | a a g4 | f f e e | d d c4\n\nT l4 o2 v7\n c2 g2 | a2 f2 | c1\n\nN l8 v10\n c r c c | c r c r | c r c c | c c c r\n',
},
"pyxel": {
"basic": "0: T120 L8 O4 V100 @1\n C D E F | G A B >C\n\n1: L8 O3 V80 @2\n E G B R | E G B R\n\n2: L4 O3 V60\n C2 G2 E2 C2\n\n3: L8 V80\n C R C R | C R C R\n",
@@ -14,6 +17,9 @@ TEMPLATES = {
"chord": "0: T120 L4 O4 V90 @1\n C E G C | F A >C F | G B >D G | C E G C\n\n1: L4 O4 V90 @2\n E G B E | F A >C F | D G B D | E G B E\n\n2: L2 O3 V60\n C2 | F2 | G2 | C2\n\n3: L8 V70\n R C R C | R C R C | R C R C | R C R C\n",
"drum": "0: T120 L8 O4 V100 @1\n C R E R | G R E R | C R E R | G R C R\n\n1: L8 O3 V80 @2\n E R G R | E R G R | E R G R | E R G R\n\n2: L4 O2 V60\n C2 C2 | C2 C2 | C2 C2 | C2 C2\n\n3: L16 V80\n C R C R C R C R | C R C R C R C R\n C R C R C R C R | C R C R C R C R\n",
"empty": "0: T120 L4 O4 V100 @1\n R1\n\n1: L4 O3 V80 @2\n R1\n\n2: L4 O3 V60\n R1\n\n3: L4 V80\n R1\n",
+ "expressive_lead": "0: T132 L8 O5 V110 Q90 @1 @ENV1 110 24 80 24 45 48\n C E G >C | <B G E C | @ENV0 D F A >D | <C4 R4\n\n1: L8 O4 V75 @2\n C R G R | E R G R | D R A R | E4 R4\n\n2: L4 O3 V60\n C2 E2 | D2 C2\n\n3: L8 V75\n C R C C | C R C R | C R C C | C R C R\n",
+ "vibrato_lead": "0: T116 L8 O5 V110 @1 Y-6 @VIB1 12 16 18\n C4 E4 | G2 @VIB0 | @VIB1 A4 G4 | @VIB0 Y0 E2\n\n1: L8 O4 V75 @2 Y6\n C E G E | C E G E | F A >C <A | G B >D <B\n\n2: L4 O3 V60\n C2 G2 | F2 G2\n\n3: L8 V70\n C R C R | C R C R | C R C R | C R C R\n",
+ "pitch_motion": "0: T144 L8 O4 V110 @2 @GLI1 700 48\n C2 G2 | @GLI0 K12 C2 K0 G2 | @GLI1 C1\n\n1: L8 O3 V80 @1\n C C G G | A A G4 | F F E E | D D C4\n\n2: L4 O2 V60\n C2 G2 | A2 F2 | C1\n\n3: L8 V80\n C R C C | C R C R | C R C C | C C C R\n",
},
}
@@ -23,6 +29,9 @@ DESCRIPTIONS = {
"chord": "コード伴奏重視(Pulse2で和音、Triangleでベース)",
"drum": "リズム重視(Noise中心のビートパターン)",
"empty": "各チャンネルのヘッダーのみ(空のテンプレート)",
+ "expressive_lead": "音量・音色エンベロープで表情を付けるリード",
+ "vibrato_lead": "ビブラート・ピッチ変化・デチューンを使うリード",
+ "pitch_motion": "アルペジオ・スイープ・グライドによる大きな音程変化",
}
diff --git a/tests/test_compose_mml_tool.py b/tests/test_compose_mml_tool.py
index 3d56115..be4735e 100644
--- a/tests/test_compose_mml_tool.py
+++ b/tests/test_compose_mml_tool.py
@@ -65,7 +65,16 @@ def test_validate_pyxel(compose_mml):
def test_template_all(compose_mml):
for mode in ("ppmck", "pyxel"):
- for template in ("basic", "melody", "chord", "drum", "empty"):
+ for template in (
+ "basic",
+ "melody",
+ "chord",
+ "drum",
+ "empty",
+ "expressive_lead",
+ "vibrato_lead",
+ "pitch_motion",
+ ):
result = compose_mml(action="template", mode=mode, template=template)
assert "mml" in result
assert "description" in result
@@ -77,6 +86,18 @@ def test_template_all(compose_mml):
)
+def test_synthesis_templates_compose(compose_mml, tmp_output_dir):
+ for mode in ("ppmck", "pyxel"):
+ for template in ("expressive_lead", "vibrato_lead", "pitch_motion"):
+ generated = compose_mml(action="template", mode=mode, template=template)
+ result = compose_mml(action="compose", mml=generated["mml"], mode=mode)
+ assert result["success"] is True, (
+ f"{mode}/{template} synthesis failed: {result['validation']}"
+ )
+ assert result["duration_sec"] > 0
+ assert Path(result["wav_path"]).exists()
+
+
def test_template_invalid_fallback(compose_mml):
result = compose_mml(action="template", mode="pyxel", template="unknown")
assert result["description"] == "基本的な4ch構成(メロディ+和音+ベース+リズム)"
diff --git a/tests/test_templates.py b/tests/test_templates.py
index c6837b0..5fcf893 100644
--- a/tests/test_templates.py
+++ b/tests/test_templates.py
@@ -5,17 +5,28 @@ import pytest
from mml_composemusic_mcp.server import compose_mml
from mml_composemusic_mcp.templates import DESCRIPTIONS, TEMPLATES, get_template
+TEMPLATE_NAMES = (
+ "basic",
+ "melody",
+ "chord",
+ "drum",
+ "empty",
+ "expressive_lead",
+ "vibrato_lead",
+ "pitch_motion",
+)
+
def test_all_templates_exist():
for mode in ("ppmck", "pyxel"):
- for name in ("basic", "melody", "chord", "drum", "empty"):
+ for name in TEMPLATE_NAMES:
assert mode in TEMPLATES
assert name in TEMPLATES[mode]
assert TEMPLATES[mode][name]
def test_all_descriptions_exist():
- for name in ("basic", "melody", "chord", "drum", "empty"):
+ for name in TEMPLATE_NAMES:
assert name in DESCRIPTIONS
assert DESCRIPTIONS[name]
@@ -51,7 +62,7 @@ def test_get_template_empty():
@pytest.mark.parametrize("mode", ["ppmck", "pyxel"])
-@pytest.mark.parametrize("template", ["basic", "melody", "chord", "drum", "empty"])
+@pytest.mark.parametrize("template", TEMPLATE_NAMES)
def test_template_validates(mode, template):
mml, _ = get_template(mode, template)
result = compose_mml(action="validate", mml=mml, mode=mode)
@@ -81,3 +92,37 @@ def test_template_invalid_name_falls_back():
result = compose_mml(action="template", mode="ppmck", template="nonexistent")
assert result["mml"]
assert result["description"]
+
+
+@pytest.mark.parametrize(
+ ("mode", "template", "event_types"),
+ [
+ ("ppmck", "expressive_lead", {"vol_envelope", "duty_envelope", "rel_volume"}),
+ ("pyxel", "expressive_lead", {"envelope"}),
+ ("ppmck", "vibrato_lead", {"lfo", "pitch_envelope", "detune"}),
+ ("pyxel", "vibrato_lead", {"vibrato"}),
+ ("ppmck", "pitch_motion", {"note_envelope", "sweep"}),
+ ("pyxel", "pitch_motion", {"glide"}),
+ ],
+)
+def test_synthesis_templates_contain_expected_events(mode, template, event_types):
+ mml, _ = get_template(mode, template)
+ result = compose_mml(action="validate", mml=mml, mode=mode)
+ actual_types = {
+ event["type"]
+ for channel in result["note_sequence"]["channels"].values()
+ for event in channel["events"]
+ }
+ assert event_types <= actual_types
+
+
+def test_pyxel_vibrato_template_applies_detune_to_notes():
+ mml, _ = get_template("pyxel", "vibrato_lead")
+ result = compose_mml(action="validate", mml=mml, mode="pyxel")
+ notes = [
+ event
+ for channel in result["note_sequence"]["channels"].values()
+ for event in channel["events"]
+ if event["type"] == "note"
+ ]
+ assert any(note["detune_cents"] != 0 for note in notes)