summaryrefslogtreecommitdiff
path: root/src/mml_composemusic_mcp/semantic_pyxel.py
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-07-12 18:28:50 +0900
committertanaka takashi <isazaka@gmail.com>2026-07-12 18:28:50 +0900
commit3d06bee12a26bc55cd73b1494e4d008a8098f6f5 (patch)
treedd13a8f3fcd9d6dbf74d6406bfaa8894895af0ce /src/mml_composemusic_mcp/semantic_pyxel.py
parentbf36a773918959eeb6ffe7dae2fb8b0874438eb4 (diff)
feat: MML拡張合成とNTSC 2A03準拠のIR v2音源を実装
- エンベロープ、LFO、ビブラート、グライドのグローバル定義表を追加 - ppmckおよびPyxelの拡張コマンドを音声合成へ反映 - 2A03のPulseデューティ列、Triangle DAC、Noise LFSR、Sweepを実装 - APUタイマーに基づく周波数量子化と非線形ミキサを追加 - 途中テンポ変更、拡張コマンドの検証、トラック外定義に対応 - APU規格、拡張合成、IR v2のテストを追加 - プロジェクト独自のppmck拡張仕様と対応範囲を文書化
Diffstat (limited to 'src/mml_composemusic_mcp/semantic_pyxel.py')
-rw-r--r--src/mml_composemusic_mcp/semantic_pyxel.py69
1 files changed, 59 insertions, 10 deletions
diff --git a/src/mml_composemusic_mcp/semantic_pyxel.py b/src/mml_composemusic_mcp/semantic_pyxel.py
index 7bf94ba..74a6c29 100644
--- a/src/mml_composemusic_mcp/semantic_pyxel.py
+++ b/src/mml_composemusic_mcp/semantic_pyxel.py
@@ -54,10 +54,36 @@ class RepeatFrame:
class PyxelSemanticAnalyzer(SemanticAnalyzer):
def __init__(self, source: str, program: Program) -> None:
super().__init__(source, program)
+ self.note_sequence.mode = "pyxel"
self.transpose = 0
self.detune_cents = 0.0
self.repeat_stack: list[RepeatFrame] = []
+ def analyze(self) -> tuple[NoteSequence, list[ErrorDetail]]:
+ ns, errors = super().analyze()
+ references = (
+ (EnvelopeEvent, "envelopes"),
+ (VibratoEvent, "vibratos"),
+ (GlideEvent, "glides"),
+ )
+ for channel in ns.channels.values():
+ for event in channel.events:
+ for event_type, table_name in references:
+ if (
+ isinstance(event, event_type)
+ and event.slot != 0
+ and event.slot not in ns.definitions[table_name]
+ ):
+ self.ctx.add_error(
+ ErrorCode.SEMANTIC_UNDEFINED_REFERENCE,
+ 0,
+ 0,
+ f"未定義の@{table_name}スロット {event.slot} です。",
+ "error",
+ "パラメータ付きで定義してから選択してください。",
+ )
+ return ns, errors
+
def _reset_state(self, track: Track) -> None:
self.octave = 4
self.default_length = 4
@@ -256,7 +282,8 @@ class PyxelSemanticAnalyzer(SemanticAnalyzer):
context=self._context_line(stmt),
)
return
- self.note_sequence.bpm = stmt.value
+ if self.tick_position == 0:
+ self.note_sequence.bpm = stmt.value
self._add_event(TempoEvent(tick_position=self.tick_position, bpm=stmt.value))
def _analyze_transpose(self, stmt: TransposeStmt) -> None:
@@ -358,7 +385,33 @@ class PyxelSemanticAnalyzer(SemanticAnalyzer):
self._add_event(ev)
return
if stmt.params:
+ table_name = {
+ "ENV": "envelopes",
+ "VIB": "vibratos",
+ "GLI": "glides",
+ }[stmt.cmd]
+ if stmt.slot in self.note_sequence.definitions[table_name]:
+ self.ctx.add_error(
+ ErrorCode.SEMANTIC_DUPLICATE_DEFINITION,
+ stmt.line,
+ stmt.column,
+ f"@{stmt.cmd}スロット {stmt.slot} は既に定義されています。",
+ "error",
+ "別のスロット番号を使用してください。",
+ self._context_line(stmt),
+ )
+ return
if stmt.cmd == "ENV":
+ if len(stmt.params) % 2:
+ self.ctx.add_error(
+ ErrorCode.SEMANTIC_VALUE_OUT_OF_RANGE,
+ stmt.line,
+ stmt.column,
+ "@ENVのパラメータはtarget,durationの組で指定してください。",
+ "error",
+ context=self._context_line(stmt),
+ )
+ return
points = []
for i in range(1, len(stmt.params), 2):
duration = stmt.params[i]
@@ -373,6 +426,9 @@ class PyxelSemanticAnalyzer(SemanticAnalyzer):
slot=stmt.slot,
points=points,
)
+ self.note_sequence.definitions["envelopes"][stmt.slot] = {
+ "points": points
+ }
elif stmt.cmd == "VIB":
if len(stmt.params) >= 3:
params = {
@@ -387,6 +443,7 @@ class PyxelSemanticAnalyzer(SemanticAnalyzer):
slot=stmt.slot,
params=params,
)
+ self.note_sequence.definitions["vibratos"][stmt.slot] = params
else:
if len(stmt.params) >= 2:
params = {
@@ -400,6 +457,7 @@ class PyxelSemanticAnalyzer(SemanticAnalyzer):
slot=stmt.slot,
params=params,
)
+ self.note_sequence.definitions["glides"][stmt.slot] = params
self._add_event(ev)
else:
if stmt.cmd == "ENV":
@@ -409,15 +467,6 @@ class PyxelSemanticAnalyzer(SemanticAnalyzer):
else:
ev = GlideEvent(tick_position=self.tick_position, slot=stmt.slot)
self._add_event(ev)
- self.ctx.add_error(
- code=ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE,
- line=stmt.line,
- column=stmt.column,
- message=f"@{stmt.cmd} は第1段階では未サポート、無視されます。",
- severity="warning",
- hint="合成には反映されませんが、IRとして保持されます。",
- context=self._context_line(stmt),
- )
def analyze_pyxel(