summaryrefslogtreecommitdiff
path: root/src/mml_composemusic_mcp/semantic_ppmck.py
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-07-12 17:58:19 +0900
committertanaka takashi <isazaka@gmail.com>2026-07-12 17:59:16 +0900
commitbf36a773918959eeb6ffe7dae2fb8b0874438eb4 (patch)
treeda7ff0037ea923fea255d2ed69ed2fb51395d9b9 /src/mml_composemusic_mcp/semantic_ppmck.py
parentf1cd4d7d16f6e75a50bb0c587c0f759f7cf710cd (diff)
feat(ppmck): mckc準拠の構文拡張とIR保持コマンド追加
- @をデューティ比、qをクオンタイズに修正(mckc原本準拠) - ^タイ、Dディチューン、sスイープ、v+/v-相対音量を追加 - @v/@@/@MP/@EP/@EN エンベロープ・LFO系を構文解析してIR保持 - 上記IR保持コマンドは合成未対応を SEMANTIC_UNSUPPORTED_FEATURE で警告 - Design.md / MML_BNF.md を更新 - 全200テスト + ruff チェック通過
Diffstat (limited to 'src/mml_composemusic_mcp/semantic_ppmck.py')
-rw-r--r--src/mml_composemusic_mcp/semantic_ppmck.py348
1 files changed, 317 insertions, 31 deletions
diff --git a/src/mml_composemusic_mcp/semantic_ppmck.py b/src/mml_composemusic_mcp/semantic_ppmck.py
index b4393c0..e2cabf9 100644
--- a/src/mml_composemusic_mcp/semantic_ppmck.py
+++ b/src/mml_composemusic_mcp/semantic_ppmck.py
@@ -1,33 +1,57 @@
"""PPMCK semantic analyzer: AST -> NoteSequence IR."""
-
from .ast_nodes import (
+ DetuneCmdStmt,
+ DutyEnvelopeDefStmt,
+ DutyEnvelopeUseStmt,
DutyStmt,
- EnvelopeDefStmt,
ExtCmdStmt,
GateTimeStmt,
LengthStmt,
+ LfoDefStmt,
+ LfoOffStmt,
+ LfoUseStmt,
+ NoteEnvDefStmt,
+ NoteEnvOffStmt,
+ NoteEnvUseStmt,
NoteStmt,
OctaveStmt,
+ PitchEnvDefStmt,
+ PitchEnvOffStmt,
+ PitchEnvUseStmt,
Program,
+ QuantizeStmt,
+ RelativeVolumeStmt,
RepeatEndStmt,
RepeatStartStmt,
RestStmt,
SweepStmt,
TempoStmt,
+ TieCmdStmt,
TieStmt,
Track,
+ VolumeEnvelopeDefStmt,
+ VolumeEnvelopeUseStmt,
VolumeStmt,
)
from .ir import (
+ DetuneEvent,
+ DutyEnvelopeEvent,
DutyEvent,
ErrorCode,
ErrorDetail,
+ LfoEvent,
+ NoteEnvEvent,
NoteEvent,
NoteSequence,
+ PitchEnvEvent,
+ QuantizeEvent,
+ RelativeVolumeEvent,
RepeatEvent,
RestEvent,
+ SweepEvent,
TempoEvent,
+ VolumeEnvelopeEvent,
VolumeEvent,
)
from .parser_base import clamp, length_value_to_ticks, note_to_midi
@@ -44,9 +68,7 @@ class PpmckSemanticAnalyzer(SemanticAnalyzer):
def analyze(self) -> tuple[NoteSequence, list[ErrorDetail]]:
ns, errors = super().analyze()
if self.loop_start is not None:
- max_tick = max(
- ch.total_ticks for ch in ns.channels.values()
- )
+ max_tick = max(ch.total_ticks for ch in ns.channels.values())
for ch in ns.channels.values():
ch.events.append(
RepeatEvent(
@@ -181,9 +203,9 @@ class PpmckSemanticAnalyzer(SemanticAnalyzer):
code=ErrorCode.SEMANTIC_CHANNEL_MISMATCH,
line=stmt.line,
column=stmt.column,
- message=f"チャンネル '{self.current_channel}' で 'q' は使用できません。",
+ message=f"チャンネル '{self.current_channel}' で '@' は使用できません。",
severity="warning",
- hint="Triangleチャンネルはデューティ比を持ちません。q コマンドを削除してください。",
+ hint="Triangleチャンネルはデューティ比を持ちません。@ コマンドを削除してください。",
context=self._context_line(stmt),
)
return
@@ -192,15 +214,297 @@ class PpmckSemanticAnalyzer(SemanticAnalyzer):
code=ErrorCode.SEMANTIC_VALUE_OUT_OF_RANGE,
line=stmt.line,
column=stmt.column,
- message=f"'q' の値 {stmt.value} は範囲外です。有効範囲: 0〜3。",
+ message=f"'@' の値 {stmt.value} は範囲外です。有効範囲: 0〜3。",
severity="error",
- hint="例: q2 のように指定してください。",
+ hint="例: @2 のように指定してください。",
context=self._context_line(stmt),
)
return
self.duty = stmt.value
self._add_event(DutyEvent(tick_position=self.tick_position, value=stmt.value))
+ def _analyze_quantize(self, stmt: QuantizeStmt) -> None:
+ if not 1 <= stmt.value <= 8:
+ self.ctx.add_error(
+ code=ErrorCode.SEMANTIC_VALUE_OUT_OF_RANGE,
+ line=stmt.line,
+ column=stmt.column,
+ message=f"'q' の値 {stmt.value} は範囲外です。有効範囲: 1〜8。",
+ severity="error",
+ hint="例: q4 のように指定してください。",
+ context=self._context_line(stmt),
+ )
+ return
+ self.gate_time = stmt.value / 8.0
+ self._add_event(
+ QuantizeEvent(tick_position=self.tick_position, value=stmt.value)
+ )
+
+ def _analyze_tie_cmd(self, stmt: TieCmdStmt) -> None:
+ if stmt.target is None:
+ return
+ if isinstance(stmt.target, int):
+ ticks = length_value_to_ticks(stmt.target, 0)
+ self._extend_last_note(ticks)
+ self.tick_position += ticks
+ return
+ if isinstance(stmt.target, NoteStmt):
+ target_ticks = self._ticks_for(stmt.target)
+ self._extend_last_note(target_ticks)
+ self.tick_position += target_ticks
+ return
+
+ def _analyze_detune_cmd(self, stmt: DetuneCmdStmt) -> None:
+ self._add_event(DetuneEvent(tick_position=self.tick_position, value=stmt.value))
+ self.ctx.add_error(
+ code=ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE,
+ line=stmt.line,
+ column=stmt.column,
+ message=f"ディチューン 'D{stmt.value}' はIR保持のみで合成に反映されません。",
+ severity="warning",
+ hint="現時点では無視されます。",
+ context=self._context_line(stmt),
+ )
+
+ def _analyze_relative_volume(self, stmt: RelativeVolumeStmt) -> None:
+ self._add_event(
+ RelativeVolumeEvent(tick_position=self.tick_position, delta=stmt.delta)
+ )
+ sign = "+" if stmt.delta > 0 else "-"
+ self.ctx.add_error(
+ code=ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE,
+ line=stmt.line,
+ column=stmt.column,
+ message=f"相対音量 'v{sign}{abs(stmt.delta)}' はIR保持のみです。",
+ severity="warning",
+ hint="現時点では無視されます。",
+ context=self._context_line(stmt),
+ )
+
+ def _analyze_sweep(self, stmt: SweepStmt) -> None:
+ self._add_event(
+ SweepEvent(
+ tick_position=self.tick_position,
+ speed=stmt.speed,
+ depth=stmt.depth,
+ )
+ )
+ self.ctx.add_error(
+ code=ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE,
+ line=stmt.line,
+ column=stmt.column,
+ message=f"スイープ 's{stmt.speed},{stmt.depth}' はIR保持のみです。",
+ severity="warning",
+ hint="現時点では無視されます。",
+ context=self._context_line(stmt),
+ )
+
+ def _analyze_vol_env_use(self, stmt: VolumeEnvelopeUseStmt) -> None:
+ self._add_event(
+ VolumeEnvelopeEvent(tick_position=self.tick_position, slot=stmt.slot)
+ )
+ self.ctx.add_error(
+ code=ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE,
+ line=stmt.line,
+ column=stmt.column,
+ message=f"音量エンベロープ '@v{stmt.slot}' はIR保持のみです。",
+ severity="warning",
+ hint="現時点では無視されます。",
+ context=self._context_line(stmt),
+ )
+
+ def _analyze_vol_env_def(self, stmt: VolumeEnvelopeDefStmt) -> None:
+ self._add_event(
+ VolumeEnvelopeEvent(
+ tick_position=self.tick_position,
+ slot=stmt.slot,
+ points=stmt.points,
+ loop_points=stmt.loop_points,
+ is_definition=True,
+ )
+ )
+ self.ctx.add_error(
+ code=ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE,
+ line=stmt.line,
+ column=stmt.column,
+ message=f"音量エンベロープ定義 '@v{stmt.slot}={{...}}' はIR保持のみです。",
+ severity="warning",
+ hint="現時点では無視されます。",
+ context=self._context_line(stmt),
+ )
+
+ def _analyze_duty_env_def(self, stmt: DutyEnvelopeDefStmt) -> None:
+ self._add_event(
+ DutyEnvelopeEvent(
+ tick_position=self.tick_position,
+ slot=stmt.slot,
+ points=stmt.points,
+ loop_points=stmt.loop_points,
+ is_definition=True,
+ )
+ )
+ self.ctx.add_error(
+ code=ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE,
+ line=stmt.line,
+ column=stmt.column,
+ message=f"デューティエンベロープ定義 '@{stmt.slot}={{...}}' はIR保持のみです。",
+ severity="warning",
+ hint="現時点では無視されます。",
+ context=self._context_line(stmt),
+ )
+
+ def _analyze_duty_env_use(self, stmt: DutyEnvelopeUseStmt) -> None:
+ self._add_event(
+ DutyEnvelopeEvent(tick_position=self.tick_position, slot=stmt.slot)
+ )
+ self.ctx.add_error(
+ code=ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE,
+ line=stmt.line,
+ column=stmt.column,
+ message=f"デューティエンベロープ使用 '@@{stmt.slot}' はIR保持のみです。",
+ severity="warning",
+ hint="現時点では無視されます。",
+ context=self._context_line(stmt),
+ )
+
+ def _analyze_lfo_def(self, stmt: LfoDefStmt) -> None:
+ self._add_event(
+ LfoEvent(
+ tick_position=self.tick_position,
+ slot=stmt.slot,
+ delay=stmt.delay,
+ speed=stmt.speed,
+ depth=stmt.depth,
+ transition=stmt.transition,
+ is_definition=True,
+ )
+ )
+ self.ctx.add_error(
+ code=ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE,
+ line=stmt.line,
+ column=stmt.column,
+ message=f"LFO定義 '@MP{stmt.slot}={{...}}' はIR保持のみです。",
+ severity="warning",
+ hint="現時点では無視されます。",
+ context=self._context_line(stmt),
+ )
+
+ def _analyze_lfo_use(self, stmt: LfoUseStmt) -> None:
+ self._add_event(LfoEvent(tick_position=self.tick_position, slot=stmt.slot))
+ self.ctx.add_error(
+ code=ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE,
+ line=stmt.line,
+ column=stmt.column,
+ message=f"LFO使用 'MP{stmt.slot}' はIR保持のみです。",
+ severity="warning",
+ hint="現時点では無視されます。",
+ context=self._context_line(stmt),
+ )
+
+ def _analyze_lfo_off(self, stmt: LfoOffStmt) -> None:
+ self._add_event(LfoEvent(tick_position=self.tick_position, slot=0, is_off=True))
+ self.ctx.add_error(
+ code=ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE,
+ line=stmt.line,
+ column=stmt.column,
+ message="LFO解除 'MPOF' はIR保持のみです。",
+ severity="warning",
+ hint="現時点では無視されます。",
+ context=self._context_line(stmt),
+ )
+
+ def _analyze_pitch_env_def(self, stmt: PitchEnvDefStmt) -> None:
+ self._add_event(
+ PitchEnvEvent(
+ tick_position=self.tick_position,
+ slot=stmt.slot,
+ points=stmt.points,
+ loop_points=stmt.loop_points,
+ is_definition=True,
+ )
+ )
+ self.ctx.add_error(
+ code=ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE,
+ line=stmt.line,
+ column=stmt.column,
+ message=f"ピッチエンベロープ定義 '@EP{stmt.slot}={{...}}' はIR保持のみです。",
+ severity="warning",
+ hint="現時点では無視されます。",
+ context=self._context_line(stmt),
+ )
+
+ def _analyze_pitch_env_use(self, stmt: PitchEnvUseStmt) -> None:
+ self._add_event(PitchEnvEvent(tick_position=self.tick_position, slot=stmt.slot))
+ self.ctx.add_error(
+ code=ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE,
+ line=stmt.line,
+ column=stmt.column,
+ message=f"ピッチエンベロープ使用 'EP{stmt.slot}' はIR保持のみです。",
+ severity="warning",
+ hint="現時点では無視されます。",
+ context=self._context_line(stmt),
+ )
+
+ def _analyze_pitch_env_off(self, stmt: PitchEnvOffStmt) -> None:
+ self._add_event(
+ PitchEnvEvent(tick_position=self.tick_position, slot=0, is_off=True)
+ )
+ self.ctx.add_error(
+ code=ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE,
+ line=stmt.line,
+ column=stmt.column,
+ message="ピッチエンベロープ解除 'EPOF' はIR保持のみです。",
+ severity="warning",
+ hint="現時点では無視されます。",
+ context=self._context_line(stmt),
+ )
+
+ def _analyze_note_env_def(self, stmt: NoteEnvDefStmt) -> None:
+ self._add_event(
+ NoteEnvEvent(
+ tick_position=self.tick_position,
+ slot=stmt.slot,
+ points=stmt.points,
+ loop_points=stmt.loop_points,
+ is_definition=True,
+ )
+ )
+ self.ctx.add_error(
+ code=ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE,
+ line=stmt.line,
+ column=stmt.column,
+ message=f"ノートエンベロープ定義 '@EN{stmt.slot}={{...}}' はIR保持のみです。",
+ severity="warning",
+ hint="現時点では無視されます。",
+ context=self._context_line(stmt),
+ )
+
+ def _analyze_note_env_use(self, stmt: NoteEnvUseStmt) -> None:
+ self._add_event(NoteEnvEvent(tick_position=self.tick_position, slot=stmt.slot))
+ self.ctx.add_error(
+ code=ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE,
+ line=stmt.line,
+ column=stmt.column,
+ message=f"ノートエンベロープ使用 'EN{stmt.slot}' はIR保持のみです。",
+ severity="warning",
+ hint="現時点では無視されます。",
+ context=self._context_line(stmt),
+ )
+
+ def _analyze_note_env_off(self, stmt: NoteEnvOffStmt) -> None:
+ self._add_event(
+ NoteEnvEvent(tick_position=self.tick_position, slot=0, is_off=True)
+ )
+ self.ctx.add_error(
+ code=ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE,
+ line=stmt.line,
+ column=stmt.column,
+ message="ノートエンベロープ解除 'ENOF' はIR保持のみです。",
+ severity="warning",
+ hint="現時点では無視されます。",
+ context=self._context_line(stmt),
+ )
+
def _analyze_tempo(self, stmt: TempoStmt) -> None:
if stmt.value < 1:
self.ctx.add_error(
@@ -252,28 +556,10 @@ class PpmckSemanticAnalyzer(SemanticAnalyzer):
self._analyze_stage2(stmt)
def _analyze_stage2(self, stmt) -> None: # type: ignore[no-untyped-def]
- if isinstance(stmt, EnvelopeDefStmt):
- self.ctx.add_error(
- code=ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE,
- line=stmt.line,
- column=stmt.column,
- message="エンベロープ定義 '@v' は第2段階で実装されます。",
- severity="warning",
- hint="現時点では無視されます。",
- context=self._context_line(stmt),
- )
- elif isinstance(stmt, SweepStmt):
- self.ctx.add_error(
- code=ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE,
- line=stmt.line,
- column=stmt.column,
- message="スイープ 's' は第2段階で実装されます。",
- severity="warning",
- hint="現時点では無視されます。",
- context=self._context_line(stmt),
- )
- else:
- super()._analyze_stage2(stmt)
+ # VolumeEnvelopeDefStmt is handled by _analyze_vol_env_def.
+ # SweepStmt is handled by _analyze_sweep.
+ # Remaining stage2 items: count-length notes, noise mode, pyxel ext.
+ super()._analyze_stage2(stmt)
def analyze_ppmck(