diff options
| author | tanaka takashi <isazaka@gmail.com> | 2026-07-11 19:02:14 +0900 |
|---|---|---|
| committer | tanaka takashi <isazaka@gmail.com> | 2026-07-11 19:02:14 +0900 |
| commit | ca35520338057512a71acd42bb116ffbb82cc192 (patch) | |
| tree | 9faa2428aeb71b62497f21ceec93b99ef7f2a5ac /src/mml_composemusic_mcp/parser_base.py | |
| parent | 7dca1616b8c891f5e140c7267323d1fcdde6741a (diff) | |
feat: implement Famicom-style MML composer MCP server
- Add FastMCP server with compose_mml tool
- Support ppmck and pyxel MML syntax
- Add lexer and parsers for both formats
- Add NES APU-style synthesizer (pulse/triangle/noise) to WAV
- Add MML templates (basic/melody/chord/drum/empty) for both modes
- Add tests for lexer, parser, and server
- Support stdio, http, sse, streamable-http transports
- Update README, pyproject.toml, and .gitignore
Diffstat (limited to 'src/mml_composemusic_mcp/parser_base.py')
| -rw-r--r-- | src/mml_composemusic_mcp/parser_base.py | 92 |
1 files changed, 92 insertions, 0 deletions
diff --git a/src/mml_composemusic_mcp/parser_base.py b/src/mml_composemusic_mcp/parser_base.py new file mode 100644 index 0000000..0427a4c --- /dev/null +++ b/src/mml_composemusic_mcp/parser_base.py @@ -0,0 +1,92 @@ +"""Base parser utilities shared between ppmck and pyxel parsers.""" + +from dataclasses import dataclass + +from .ir import ErrorCode, ErrorDetail +from .lexer import Token, TokenType + + +@dataclass +class ParserContext: + tokens: list[Token] + pos: int = 0 + errors: list[ErrorDetail] = None # type: ignore[assignment] + + def __post_init__(self) -> None: + if self.errors is None: + self.errors = [] + + def peek(self, offset: int = 0) -> Token: + idx = self.pos + offset + if idx >= len(self.tokens): + return self.tokens[-1] + return self.tokens[idx] + + def advance(self) -> Token: + token = self.peek() + self.pos += 1 + return token + + def match(self, *types: TokenType) -> Token | None: + token = self.peek() + if token.type in types: + return self.advance() + return None + + def expect(self, *types: TokenType) -> Token | None: + return self.match(*types) + + def add_error( + self, + code: ErrorCode, + line: int, + column: int, + message: str, + severity: str, + hint: str = "", + context: str = "", + ) -> None: + self.errors.append( + ErrorDetail( + code=code, + line=line, + column=column, + message=message, + severity=severity, + hint=hint, + context=context, + ) + ) + + +class ParserError(Exception): + pass + + +def length_value_to_ticks(length: int, dots: int, ticks_per_quarter: int = 192) -> int: + base = ticks_per_quarter * 4 // length + total = base + add = base + for _ in range(dots): + add //= 2 + total += add + return total + + +NOTE_OFFSET = { + "c": 0, + "d": 2, + "e": 4, + "f": 5, + "g": 7, + "a": 9, + "b": 11, +} + + +def note_to_midi(note: str, octave: int, accidental: int = 0) -> int: + return (octave + 1) * 12 + NOTE_OFFSET[note] + accidental + + +def clamp(value: int, min_val: int, max_val: int) -> int: + return max(min_val, min(value, max_val)) |
