From d83a966199671a1cab6b8f56be448662e79f4a46 Mon Sep 17 00:00:00 2001 From: tanaka takashi Date: Sun, 12 Jul 2026 18:50:50 +0900 Subject: feat: 合成機能テンプレートの拡充とドキュメントの更新 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ppmck/pyxel向けに表現力のある作曲テンプレートを追加 - エンベロープ、ビブラート、音程変化の使用例を追加 - テンプレートの検証・IR・音声合成テストを追加 - README、設計書、MCP仕様書を現行実装に合わせて更新 --- README.md | 400 +++++++++++++--------------------- doc/Design.md | 2 +- doc/mcp.md | 2 +- pyproject.toml | 2 +- src/mml_composemusic_mcp/server.py | 4 +- src/mml_composemusic_mcp/templates.py | 9 + tests/test_compose_mml_tool.py | 23 +- tests/test_templates.py | 51 ++++- 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 | d | v+3 c d c2 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 | D | C D 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) -- cgit v1.2.3