├── images └── 2025-03-10_06-24-03.png ├── requirements.txt ├── __init__.py ├── pyproject.toml ├── .github └── workflows │ └── publish_action.yml ├── LICENSE ├── config.py ├── README-CN.md ├── README.md ├── .gitignore ├── utils.py ├── NotaGenNode.py └── abc2xml.py /images/2025-03-10_06-24-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/billwuhao/ComfyUI_NotaGen/HEAD/images/2025-03-10_06-24-03.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | wandb>=0.17.2 2 | abctoolkit>=0.0.6 3 | samplings>=0.1.7 4 | pyparsing>=3.2.1 5 | transformers>=4.40.0 6 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from .NotaGenNode import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS 2 | 3 | __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "notagen-mw" 3 | description = "Symbolic Music Generation, NotaGen node for ComfyUI." 4 | version = "2.2.11" 5 | license = {file = "LICENSE"} 6 | 7 | [project.urls] 8 | Repository = "https://github.com/billwuhao/ComfyUI_NotaGen" 9 | # Used by Comfy Registry https://comfyregistry.org 10 | 11 | [tool.comfy] 12 | PublisherId = "mw" 13 | DisplayName = "ComfyUI_NotaGen" 14 | Icon = "" 15 | -------------------------------------------------------------------------------- /.github/workflows/publish_action.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Comfy registry 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | paths: 9 | - "pyproject.toml" 10 | 11 | jobs: 12 | publish-node: 13 | name: Publish Custom Node to registry 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out code 17 | uses: actions/checkout@v4 18 | - name: Publish Custom Node 19 | uses: Comfy-Org/publish-node-action@main 20 | with: 21 | ## Add your own personal access token to your Github Repository secrets and reference it here. 22 | personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 MW 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # Configurations for model 2 | nota_lx = { 3 | "PATCH_STREAM": True, # Stream training / inference 4 | "PATCH_SIZE": 16, # Patch Size 5 | "PATCH_LENGTH": 1024, # Patch Length 6 | "CHAR_NUM_LAYERS": 6, # Number of layers in the decoder 7 | "PATCH_NUM_LAYERS": 20, # Number of layers in the encoder 8 | "HIDDEN_SIZE": 1280, # Hidden Size 9 | "PATCH_SAMPLING_BATCH_SIZE": 0 # Batch size for patch during training, 0 for full conaudio 10 | } 11 | 12 | nota_small = { 13 | "PATCH_STREAM": True, # Stream training / inference 14 | "PATCH_SIZE": 16, # Patch Size 15 | "PATCH_LENGTH": 2048, # Patch Length 16 | "CHAR_NUM_LAYERS": 3, # Number of layers in the decoder 17 | "PATCH_NUM_LAYERS": 12, # Number of layers in the encoder 18 | "HIDDEN_SIZE": 768, # Hidden Size 19 | "PATCH_SAMPLING_BATCH_SIZE": 0 # Batch size for patch during training, 0 for full conaudio 20 | } 21 | 22 | nota_medium = { 23 | "PATCH_STREAM": True, # Stream training / inference 24 | "PATCH_SIZE": 16, # Patch Size 25 | "PATCH_LENGTH": 2048, # Patch Length 26 | "CHAR_NUM_LAYERS": 3, # Number of layers in the decoder 27 | "PATCH_NUM_LAYERS": 16, # Number of layers in the encoder 28 | "HIDDEN_SIZE": 1024, # Hidden Size 29 | "PATCH_SAMPLING_BATCH_SIZE": 0 # Batch size for patch during training, 0 for full conaudio 30 | } -------------------------------------------------------------------------------- /README-CN.md: -------------------------------------------------------------------------------- 1 | [中文](README-CN.md) | [English](README.md) 2 | 3 | # 符号音乐生成. NotaGen 的 ComfyUI 节点. 4 | 5 | https://github.com/user-attachments/assets/0671657f-e66b-4000-a0aa-48520f15b782 6 | 7 | ![image](https://github.com/billwuhao/ComfyUI_NotaGen/blob/master/images/2025-03-10_06-24-03.png) 8 | 9 | 10 | ## 📣 更新 11 | 12 | [2025-04-09]⚒️: 不再需要输入 MuseScore4 或 mscore 以及 python 路径, 只需要将 MuseScore4 或 mscore 安装目录例如 `C:\Program Files\MuseScore 4\bin` 添加到系统 path 环境变量即可. 如果仍然找不到 MuseScore4 或 mscore, 请启动 ComfyUI 时, 用 ComfyUI 官方启动脚本例如 `run_nvidia_gpu.bat`. 13 | 14 | [2025-03-21]⚒️: 增加更多可调参数, 更自由畅玩. 可选是否卸载模型. 15 | 16 | [2025-03-15]⚒️: 支持 Linux Ubuntu/Debian 系列, 以及服务器, 其他未测试. 17 | 18 | 本地 Linux 电脑, 安装 `musescore` 等: 19 | ``` 20 | sudo apt update 21 | sudo apt install musescore 22 | sudo apt install libxcb-xinerama0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-xkb1 libxkbcommon-x11-0 23 | ``` 24 | 25 | 服务器, 安装虚拟显示器 Xvfb, 其他操作同上: 26 | ``` 27 | sudo apt update 28 | sudo apt install xvfb 29 | ``` 30 | 31 | [2025-03-13]⚒️: 32 | 33 | - 生成 `.abc` 自动转 `.xml`, `.mp3`, `.png` 格式, 可以听生成的音乐, 同时可以看曲谱啦🎵🎵🎵 34 | 35 | - 支持自定义 prompt, 格式必须保持 `||` 的格式, `period`, `composer`, `instrumentation` 的顺序不能乱, 而且以 `|` 分割. 36 | 37 | ## 安装 38 | 39 | ``` 40 | cd ComfyUI/custom_nodes 41 | git clone https://github.com/billwuhao/ComfyUI_NotaGen.git 42 | cd ComfyUI_NotaGen 43 | pip install -r requirements.txt 44 | 45 | # python_embeded 46 | ./python_embeded/python.exe -m pip install -r requirements.txt 47 | ``` 48 | 49 | ## 模型下载 50 | 51 | 将模型下载放到 `ComfyUI\models\TTS\NotaGen` 下, 并按要求重命名: 52 | 53 | [NotaGen-X](https://huggingface.co/ElectricAlexis/NotaGen/blob/main/weights_notagenx_p_size_16_p_length_1024_p_layers_20_h_size_1280.pth) → `notagenx.pth` 54 | [NotaGen-small](https://huggingface.co/ElectricAlexis/NotaGen/blob/main/weights_notagen_pretrain_p_size_16_p_length_2048_p_layers_12_c_layers_3_h_size_768_lr_0.0002_batch_8.pth) → `notagen_small.pth` 55 | [NotaGen-medium](https://huggingface.co/ElectricAlexis/NotaGen/blob/main/weights_notagen_pretrain_p_size_16_p_length_2048_p_layers_16_c_layers_3_h_size_1024_lr_0.0001_batch_4.pth) → `notagen_medium.pth` 56 | [NotaGen-large](https://huggingface.co/ElectricAlexis/NotaGen/blob/main/weights_notagen_pretrain_p_size_16_p_length_1024_p_layers_20_c_layers_6_h_size_1280_lr_0.0001_batch_4.pth) → `notagen_large.pth` 57 | 58 | 59 | https://github.com/user-attachments/assets/229139bd-1065-4539-bcfa-b0c245259f6d 60 | 61 | ## 鸣谢 62 | 63 | [NotaGen](https://github.com/ElectricAlexis/NotaGen) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [中文](README-CN.md) | [English](README.md) 2 | 3 | # Symbolic Music Generation, NotaGen node for ComfyUI. 4 | 5 | https://github.com/user-attachments/assets/0671657f-e66b-4000-a0aa-48520f15b782 6 | 7 | ![image](https://github.com/billwuhao/ComfyUI_NotaGen/blob/master/images/2025-03-10_06-24-03.png) 8 | 9 | ## 📣 Updates 10 | 11 | [2025-04-09]⚒️: It is no longer necessary to input MuseScore4 or mscore and the Python path. You only need to add the MuseScore4 or mscore installation directory (e.g., `C:\Program Files\MuseScore 4\bin`) to the system path environment variable. If you still cannot find MuseScore4 or mscore, launch Comfyui using the official startup script, such as run_nvidia_gpu.bat. 12 | 13 | [2025-03-21] ⚒️: Added more tunable parameters for more creative freedom. Optional model unloading. 14 | 15 | [2025-03-15]⚒️: Supports Linux Ubuntu/Debian series, as well as servers, others untested, as well as servers. 16 | 17 | For local Linux computers, install `musescore` etc.: 18 | ``` 19 | sudo apt update 20 | sudo apt install musescore 21 | sudo apt install libxcb-xinerama0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-xkb1 libxkbcommon-x11-0 22 | ``` 23 | 24 | For servers, install the virtual display Xvfb, other operations are the same as above: 25 | ``` 26 | sudo apt update 27 | sudo apt install xvfb 28 | ``` 29 | 30 | [2025-03-13]⚒️: 31 | 32 | - Automatically convert generated `.abc` to `.xml`, `.mp3`, and `.png` formats. Now you can listen to the generated music and see the sheet music too! 🎵🎵🎵 33 | 34 | - Supports custom prompts. The format must be maintained as `||`, with the order of `period`, `composer`, and `instrumentation` strictly enforced and separated by `|`. 35 | 36 | ## Installation 37 | 38 | ``` 39 | cd ComfyUI/custom_nodes 40 | git clone https://github.com/billwuhao/ComfyUI_NotaGen.git 41 | cd ComfyUI_NotaGen 42 | pip install -r requirements.txt 43 | 44 | # python_embeded 45 | ./python_embeded/python.exe -m pip install -r requirements.txt 46 | ``` 47 | 48 | ## Model Download 49 | 50 | Download the model to `ComfyUI\models\TTS\NotaGen` and rename it as required: 51 | 52 | [NotaGen-X](https://huggingface.co/ElectricAlexis/NotaGen/blob/main/weights_notagenx_p_size_16_p_length_1024_p_layers_20_h_size_1280.pth) → `notagenx.pth` 53 | [NotaGen-small](https://huggingface.co/ElectricAlexis/NotaGen/blob/main/weights_notagen_pretrain_p_size_16_p_length_2048_p_layers_12_c_layers_3_h_size_768_lr_0.0002_batch_8.pth) → `notagen_small.pth` 54 | [NotaGen-medium](https://huggingface.co/ElectricAlexis/NotaGen/blob/main/weights_notagen_pretrain_p_size_16_p_length_2048_p_layers_16_c_layers_3_h_size_1024_lr_0.0001_batch_4.pth) → `notagen_medium.pth` 55 | [NotaGen-large](https://huggingface.co/ElectricAlexis/NotaGen/blob/main/weights_notagen_pretrain_p_size_16_p_length_1024_p_layers_20_c_layers_6_h_size_1280_lr_0.0001_batch_4.pth) → `notagen_large.pth` 56 | 57 | 58 | https://github.com/user-attachments/assets/229139bd-1065-4539-bcfa-b0c245259f6d 59 | 60 | 61 | ## Acknowledgments 62 | 63 | [NotaGen](https://github.com/ElectricAlexis/NotaGen) 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import random 3 | import bisect 4 | import numpy as np 5 | import re 6 | from .config import nota_lx, nota_small, nota_medium 7 | 8 | from transformers import GPT2Model, GPT2LMHeadModel, PreTrainedModel 9 | from samplings import top_p_sampling, top_k_sampling, temperature_sampling 10 | # from tokenizers import Tokenizer 11 | 12 | 13 | class Patchilizer: 14 | def __init__(self, model): 15 | if model == "notagenx.pth" or model == "notagen_large.pth": 16 | cf = nota_lx 17 | elif model == "notagen_small.pth": 18 | cf = nota_small 19 | elif model == "notagen_medium.pth": 20 | cf = nota_medium 21 | 22 | self.stream = cf["PATCH_STREAM"] 23 | self.patch_size = cf["PATCH_SIZE"] 24 | self.patch_length = cf["PATCH_LENGTH"] 25 | # self.char_num_layers = cf["CHAR_NUM_LAYERS"] 26 | # self.patch_num_layers = cf["PATCH_NUM_LAYERS"] 27 | # self.hidden_size = cf["HIDDEN_SIZE"] 28 | # self.psbs = cf["PATCH_SAMPLING_BATCH_SIZE"] 29 | self.delimiters = ["|:", "::", ":|", "[|", "||", "|]", "|"] 30 | self.regexPattern = '(' + '|'.join(map(re.escape, self.delimiters)) + ')' 31 | self.bos_token_id = 1 32 | self.eos_token_id = 2 33 | self.special_token_id = 0 34 | 35 | def split_bars(self, body_lines): 36 | """ 37 | Split a body of music into individual bars. 38 | """ 39 | new_bars = [] 40 | try: 41 | for line in body_lines: 42 | line_bars = re.split(self.regexPattern, line) 43 | line_bars = list(filter(None, line_bars)) 44 | new_line_bars = [] 45 | 46 | if len(line_bars) == 1: 47 | new_line_bars = line_bars 48 | else: 49 | if line_bars[0] in self.delimiters: 50 | new_line_bars = [line_bars[i] + line_bars[i + 1] for i in range(0, len(line_bars), 2)] 51 | else: 52 | new_line_bars = [line_bars[0]] + [line_bars[i] + line_bars[i + 1] for i in range(1, len(line_bars), 2)] 53 | if 'V' not in new_line_bars[-1]: 54 | new_line_bars[-2] += new_line_bars[-1] # 吸收最后一个 小节线+\n 的组合 55 | new_line_bars = new_line_bars[:-1] 56 | new_bars += new_line_bars 57 | except: 58 | pass 59 | 60 | return new_bars 61 | 62 | def split_patches(self, abc_text, generate_last=False): 63 | if not generate_last and len(abc_text) % self.patch_size != 0: 64 | abc_text += chr(self.eos_token_id) 65 | patches = [abc_text[i : i + self.patch_size] for i in range(0, len(abc_text), self.patch_size)] 66 | return patches 67 | 68 | def patch2chars(self, patch): 69 | """ 70 | Convert a patch into a bar. 71 | """ 72 | bytes = '' 73 | for idx in patch: 74 | if idx == self.eos_token_id: 75 | break 76 | if idx < self.eos_token_id: 77 | pass 78 | bytes += chr(idx) 79 | return bytes 80 | 81 | 82 | def patchilize_metadata(self, metadata_lines): 83 | 84 | metadata_patches = [] 85 | for line in metadata_lines: 86 | metadata_patches += self.split_patches(line) 87 | 88 | return metadata_patches 89 | 90 | def patchilize_tunebody(self, tunebody_lines, encode_mode='train'): 91 | 92 | tunebody_patches = [] 93 | bars = self.split_bars(tunebody_lines) 94 | if encode_mode == 'train': 95 | for bar in bars: 96 | tunebody_patches += self.split_patches(bar) 97 | elif encode_mode == 'generate': 98 | for bar in bars[:-1]: 99 | tunebody_patches += self.split_patches(bar) 100 | tunebody_patches += self.split_patches(bars[-1], generate_last=True) 101 | 102 | return tunebody_patches 103 | 104 | def encode_train(self, abc_text, add_special_patches=True, cut=True): 105 | lines = abc_text.split('\n') 106 | lines = list(filter(None, lines)) 107 | lines = [line + '\n' for line in lines] 108 | 109 | tunebody_index = -1 110 | for i, line in enumerate(lines): 111 | if '[V:' in line: 112 | tunebody_index = i 113 | break 114 | 115 | metadata_lines = lines[ : tunebody_index] 116 | tunebody_lines = lines[tunebody_index : ] 117 | 118 | if self.stream: 119 | tunebody_lines = ['[r:' + str(line_index) + '/' + str(len(tunebody_lines) - line_index - 1) + ']' + line for line_index, line in 120 | enumerate(tunebody_lines)] 121 | 122 | metadata_patches = self.patchilize_metadata(metadata_lines) 123 | tunebody_patches = self.patchilize_tunebody(tunebody_lines, encode_mode='train') 124 | 125 | if add_special_patches: 126 | bos_patch = chr(self.bos_token_id) * (self.patch_size - 1) + chr(self.eos_token_id) 127 | eos_patch = chr(self.bos_token_id) + chr(self.eos_token_id) * (self.patch_size - 1) 128 | 129 | metadata_patches = [bos_patch] + metadata_patches 130 | tunebody_patches = tunebody_patches + [eos_patch] 131 | 132 | if self.stream: 133 | if len(metadata_patches) + len(tunebody_patches) > self.patch_length: 134 | available_cut_indexes = [0] + [index + 1 for index, patch in enumerate(tunebody_patches) if '\n' in patch] 135 | line_index_for_cut_index = list(range(len(available_cut_indexes))) 136 | end_index = len(metadata_patches) + len(tunebody_patches) - self.patch_length 137 | biggest_index = bisect.bisect_left(available_cut_indexes, end_index) 138 | available_cut_indexes = available_cut_indexes[:biggest_index + 1] 139 | 140 | if len(available_cut_indexes) == 1: 141 | choices = ['head'] 142 | elif len(available_cut_indexes) == 2: 143 | choices = ['head', 'tail'] 144 | else: 145 | choices = ['head', 'tail', 'middle'] 146 | choice = random.choice(choices) 147 | if choice == 'head': 148 | patches = metadata_patches + tunebody_patches[0:] 149 | else: 150 | if choice == 'tail': 151 | cut_index = len(available_cut_indexes) - 1 152 | else: 153 | cut_index = random.choice(range(1, len(available_cut_indexes) - 1)) 154 | 155 | line_index = line_index_for_cut_index[cut_index] 156 | stream_tunebody_lines = tunebody_lines[line_index : ] 157 | 158 | stream_tunebody_patches = self.patchilize_tunebody(stream_tunebody_lines, encode_mode='train') 159 | if add_special_patches: 160 | stream_tunebody_patches = stream_tunebody_patches + [eos_patch] 161 | patches = metadata_patches + stream_tunebody_patches 162 | else: 163 | patches = metadata_patches + tunebody_patches 164 | else: 165 | patches = metadata_patches + tunebody_patches 166 | 167 | if cut: 168 | patches = patches[ : self.patch_length] 169 | else: 170 | pass 171 | 172 | # encode to ids 173 | id_patches = [] 174 | for patch in patches: 175 | id_patch = [ord(c) for c in patch] + [self.special_token_id] * (self.patch_size - len(patch)) 176 | id_patches.append(id_patch) 177 | 178 | return id_patches 179 | 180 | def encode_generate(self, abc_code, add_special_patches=True): 181 | lines = abc_code.split('\n') 182 | lines = list(filter(None, lines)) 183 | 184 | tunebody_index = None 185 | for i, line in enumerate(lines): 186 | if line.startswith('[V:') or line.startswith('[r:'): 187 | tunebody_index = i 188 | break 189 | 190 | metadata_lines = lines[ : tunebody_index] 191 | tunebody_lines = lines[tunebody_index : ] 192 | 193 | metadata_lines = [line + '\n' for line in metadata_lines] 194 | if self.stream: 195 | if not abc_code.endswith('\n'): 196 | tunebody_lines = [tunebody_lines[i] + '\n' for i in range(len(tunebody_lines) - 1)] + [tunebody_lines[-1]] 197 | else: 198 | tunebody_lines = [tunebody_lines[i] + '\n' for i in range(len(tunebody_lines))] 199 | else: 200 | tunebody_lines = [line + '\n' for line in tunebody_lines] 201 | 202 | metadata_patches = self.patchilize_metadata(metadata_lines) 203 | tunebody_patches = self.patchilize_tunebody(tunebody_lines, encode_mode='generate') 204 | 205 | if add_special_patches: 206 | bos_patch = chr(self.bos_token_id) * (self.patch_size - 1) + chr(self.eos_token_id) 207 | 208 | metadata_patches = [bos_patch] + metadata_patches 209 | 210 | patches = metadata_patches + tunebody_patches 211 | patches = patches[ : self.patch_length] 212 | 213 | # encode to ids 214 | id_patches = [] 215 | for patch in patches: 216 | if len(patch) < self.patch_size and patch[-1] != chr(self.eos_token_id): 217 | id_patch = [ord(c) for c in patch] 218 | else: 219 | id_patch = [ord(c) for c in patch] + [self.special_token_id] * (self.patch_size - len(patch)) 220 | id_patches.append(id_patch) 221 | 222 | return id_patches 223 | 224 | def decode(self, patches): 225 | """ 226 | Decode patches into music. 227 | """ 228 | return ''.join(self.patch2chars(patch) for patch in patches) 229 | 230 | 231 | class PatchLevelDecoder(PreTrainedModel): 232 | """ 233 | A Patch-level Decoder model for generating patch features in an auto-regressive manner. 234 | It inherits PreTrainedModel from transformers. 235 | """ 236 | def __init__(self, config, model): 237 | if model == "notagenx.pth" or model == "notagen_large.pth": 238 | cf = nota_lx 239 | elif model == "notagen_small.pth": 240 | cf = nota_small 241 | elif model == "notagen_medium.pth": 242 | cf = nota_medium 243 | 244 | self.patch_size = cf["PATCH_SIZE"] 245 | 246 | super().__init__(config) 247 | self.patch_embedding = torch.nn.Linear(self.patch_size * 128, config.n_embd) 248 | torch.nn.init.normal_(self.patch_embedding.weight, std=0.02) 249 | self.base = GPT2Model(config) 250 | 251 | def forward(self, 252 | patches: torch.Tensor, 253 | masks=None) -> torch.Tensor: 254 | """ 255 | The forward pass of the patch-level decoder model. 256 | :param patches: the patches to be encoded 257 | :param masks: the masks for the patches 258 | :return: the encoded patches 259 | """ 260 | patches = torch.nn.functional.one_hot(patches, num_classes=128).to(self.dtype) 261 | patches = patches.reshape(len(patches), -1, self.patch_size * (128)) 262 | patches = self.patch_embedding(patches.to(self.device)) 263 | 264 | if masks==None: 265 | return self.base(inputs_embeds=patches) 266 | else: 267 | return self.base(inputs_embeds=patches, 268 | attention_mask=masks) 269 | 270 | 271 | class CharLevelDecoder(PreTrainedModel): 272 | """ 273 | A Char-level Decoder model for generating the chars within each patch in an auto-regressive manner 274 | based on the encoded patch features. It inherits PreTrainedModel from transformers. 275 | """ 276 | def __init__(self, config, model): 277 | super().__init__(config) 278 | self.special_token_id = 0 279 | self.bos_token_id = 1 280 | 281 | self.base = GPT2LMHeadModel(config) 282 | 283 | if model == "notagenx.pth" or model == "notagen_large.pth": 284 | cf = nota_lx 285 | elif model == "notagen_small.pth": 286 | cf = nota_small 287 | elif model == "notagen_medium.pth": 288 | cf = nota_medium 289 | 290 | self.psbs = cf["PATCH_SAMPLING_BATCH_SIZE"] 291 | 292 | def forward(self, 293 | encoded_patches: torch.Tensor, 294 | target_patches: torch.Tensor): 295 | """ 296 | The forward pass of the char-level decoder model. 297 | :param encoded_patches: the encoded patches 298 | :param target_patches: the target patches 299 | :return: the output of the model 300 | """ 301 | # preparing the labels for model training 302 | target_patches = torch.cat((torch.ones_like(target_patches[:,0:1])*self.bos_token_id, target_patches), dim=1) 303 | # print('target_patches shape:', target_patches.shape) 304 | 305 | target_masks = target_patches == self.special_token_id 306 | labels = target_patches.clone().masked_fill_(target_masks, -100) 307 | 308 | # masking the labels for model training 309 | target_masks = torch.ones_like(labels) 310 | target_masks = target_masks.masked_fill_(labels == -100, 0) 311 | 312 | # select patches 313 | if self.psbs != 0 and self.psbs < target_patches.shape[0]: 314 | indices = list(range(len(target_patches))) 315 | random.shuffle(indices) 316 | selected_indices = sorted(indices[:self.psbs]) 317 | 318 | target_patches = target_patches[selected_indices,:] 319 | target_masks = target_masks[selected_indices,:] 320 | encoded_patches = encoded_patches[selected_indices,:] 321 | 322 | # get input embeddings 323 | inputs_embeds = torch.nn.functional.embedding(target_patches, self.base.transformer.wte.weight) 324 | 325 | # concatenate the encoded patches with the input embeddings 326 | inputs_embeds = torch.cat((encoded_patches.unsqueeze(1), inputs_embeds[:,1:,:]), dim=1) 327 | 328 | output = self.base(inputs_embeds=inputs_embeds, 329 | attention_mask=target_masks, 330 | labels=labels) 331 | # output_hidden_states=True=True) 332 | 333 | return output 334 | 335 | def generate(self, 336 | encoded_patch: torch.Tensor, # [hidden_size] 337 | tokens: torch.Tensor): # [1] 338 | """ 339 | The generate function for generating a patch based on the encoded patch and already generated tokens. 340 | :param encoded_patch: the encoded patch 341 | :param tokens: already generated tokens in the patch 342 | :return: the probability distribution of next token 343 | """ 344 | encoded_patch = encoded_patch.reshape(1, 1, -1) # [1, 1, hidden_size] 345 | tokens = tokens.reshape(1, -1) 346 | 347 | # Get input embeddings 348 | tokens = torch.nn.functional.embedding(tokens, self.base.transformer.wte.weight) 349 | 350 | # Concatenate the encoded patch with the input embeddings 351 | tokens = torch.cat((encoded_patch, tokens[:,1:,:]), dim=1) 352 | 353 | # Get output from model 354 | outputs = self.base(inputs_embeds=tokens) 355 | 356 | # Get probabilities of next token 357 | probs = torch.nn.functional.softmax(outputs.logits.squeeze(0)[-1], dim=-1) 358 | 359 | return probs 360 | 361 | def safe_normalize_probs(probs): 362 | epsilon = 1e-12 363 | probs = np.array(probs, dtype=np.float64) 364 | probs = np.where(np.isnan(probs) | (probs < 0), 0, probs) 365 | probs = probs + epsilon 366 | s = probs.sum() 367 | if s > 0: 368 | probs = probs / s 369 | else: 370 | probs = np.zeros_like(probs) 371 | probs[0] = 1.0 372 | return probs 373 | 374 | class NotaGenLMHeadModel(PreTrainedModel): 375 | """ 376 | NotaGen is a language model with a hierarchical structure. 377 | It includes a patch-level decoder and a char-level decoder. 378 | The patch-level decoder is used to generate patch features in an auto-regressive manner. 379 | The char-level decoder is used to generate the chars within each patch in an auto-regressive manner. 380 | It inherits PreTrainedModel from transformers. 381 | """ 382 | def __init__(self, encoder_config, decoder_config, model): 383 | super().__init__(encoder_config) 384 | self.special_token_id = 0 385 | self.bos_token_id = 1 386 | self.eos_token_id = 2 387 | self.patch_level_decoder = PatchLevelDecoder(encoder_config, model) 388 | self.char_level_decoder = CharLevelDecoder(decoder_config, model) 389 | 390 | if model == "notagenx.pth" or model == "notagen_large.pth": 391 | cf = nota_lx 392 | elif model == "notagen_small.pth": 393 | cf = nota_small 394 | elif model == "notagen_medium.pth": 395 | cf = nota_medium 396 | 397 | self.patch_size = cf["PATCH_SIZE"] 398 | 399 | def forward(self, 400 | patches: torch.Tensor, 401 | masks: torch.Tensor): 402 | """ 403 | The forward pass of the bGPT model. 404 | :param patches: the patches to be encoded 405 | :param masks: the masks for the patches 406 | :return: the decoded patches 407 | """ 408 | patches = patches.reshape(len(patches), -1, self.patch_size) 409 | encoded_patches = self.patch_level_decoder(patches, masks)["last_hidden_state"] 410 | 411 | left_shift_masks = masks * (masks.flip(1).cumsum(1).flip(1) > 1) 412 | masks[:, 0] = 0 413 | 414 | encoded_patches = encoded_patches[left_shift_masks == 1] 415 | patches = patches[masks == 1] 416 | 417 | return self.char_level_decoder(encoded_patches, patches) 418 | 419 | def generate(self, 420 | patches: torch.Tensor, 421 | top_k=0, 422 | top_p=1, 423 | temperature=1.0): 424 | """ 425 | The generate function for generating patches based on patches. 426 | :param patches: the patches to be encoded 427 | :param top_k: the top k for sampling 428 | :param top_p: the top p for sampling 429 | :param temperature: the temperature for sampling 430 | :return: the generated patches 431 | """ 432 | if patches.shape[-1] % self.patch_size != 0: 433 | tokens = patches[:,:,-(patches.shape[-1]%self.patch_size):].squeeze(0, 1) 434 | tokens = torch.cat((torch.tensor([self.bos_token_id], device=self.device), tokens), dim=-1) 435 | patches = patches[:,:,:-(patches.shape[-1]%self.patch_size)] 436 | else: 437 | tokens = torch.tensor([self.bos_token_id], device=self.device) 438 | 439 | patches = patches.reshape(len(patches), -1, self.patch_size) # [bs, seq, patch_size] 440 | encoded_patches = self.patch_level_decoder(patches)["last_hidden_state"] # [bs, seq, hidden_size] 441 | generated_patch = [] 442 | 443 | while True: 444 | prob = self.char_level_decoder.generate(encoded_patches[0][-1], tokens).cpu().detach().numpy() # [128] 445 | prob = safe_normalize_probs(prob) 446 | prob = top_k_sampling(prob, top_k=top_k, return_probs=True) # [128] 447 | prob = safe_normalize_probs(prob) 448 | prob = top_p_sampling(prob, top_p=top_p, return_probs=True) # [128] 449 | prob = safe_normalize_probs(prob) 450 | token = temperature_sampling(prob, temperature=temperature) # int 451 | char = chr(token) 452 | generated_patch.append(token) 453 | 454 | if len(tokens) >= self.patch_size:# or token == self.eos_token_id: 455 | break 456 | else: 457 | tokens = torch.cat((tokens, torch.tensor([token], device=self.device)), dim=0) 458 | 459 | return generated_patch -------------------------------------------------------------------------------- /NotaGenNode.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import torch 4 | from .utils import * 5 | from .config import nota_lx, nota_small, nota_medium 6 | from transformers import GPT2Config 7 | from abctoolkit.utils import Barline_regexPattern 8 | 9 | from abctoolkit.duration import calculate_bartext_duration 10 | 11 | node_dir = os.path.dirname(os.path.abspath(__file__)) 12 | comfy_path = os.path.dirname(os.path.dirname(node_dir)) 13 | output_path = os.path.join(comfy_path, "output") 14 | 15 | nota_model_path = os.path.join(comfy_path, "models", "TTS", "NotaGen") 16 | 17 | ORIGINAL_OUTPUT_FOLDER = os.path.join(output_path, 'notagen_original') 18 | INTERLEAVED_OUTPUT_FOLDER = os.path.join(output_path, 'notagen_interleaved') 19 | 20 | os.makedirs(ORIGINAL_OUTPUT_FOLDER, exist_ok=True) 21 | os.makedirs(INTERLEAVED_OUTPUT_FOLDER, exist_ok=True) 22 | 23 | MODEL_CACHE = None 24 | PATCHILIZER = None 25 | class NotaGenRun: 26 | model_names = ["notagenx.pth", "notagen_small.pth", "notagen_medium.pth", "notagen_large.pth"] 27 | periods = ["Baroque", "Classical", "Romantic"] 28 | composers = ["Bach, Johann Sebastian", "Corelli, Arcangelo", "Handel, George Frideric", "Scarlatti, Domenico", "Vivaldi, Antonio", "Beethoven, Ludwig van", 29 | "Haydn, Joseph", "Mozart, Wolfgang Amadeus", "Paradis, Maria Theresia von", "Reichardt, Louise", "Saint-Georges, Joseph Bologne", "Schroter, Corona", 30 | "Bartok, Bela", "Berlioz, Hector", "Bizet, Georges", "Boulanger, Lili", "Boulton, Harold", "Brahms, Johannes", "Burgmuller, Friedrich", 31 | "Butterworth, George", "Chaminade, Cecile", "Chausson, Ernest", "Chopin, Frederic", "Cornelius, Peter", "Debussy, Claude", "Dvorak, Antonin", 32 | "Faisst, Clara", "Faure, Gabriel", "Franz, Robert", "Gonzaga, Chiquinha", "Grandval, Clemence de", "Grieg, Edvard", "Hensel, Fanny", 33 | "Holmes, Augusta Mary Anne", "Jaell, Marie", "Kinkel, Johanna", "Kralik, Mathilde", "Lang, Josephine", "Lehmann, Liza", "Liszt, Franz", 34 | "Mayer, Emilie", "Medtner, Nikolay", "Mendelssohn, Felix", "Munktell, Helena", "Parratt, Walter", "Prokofiev, Sergey", "Rachmaninoff, Sergei", 35 | "Ravel, Maurice", "Saint-Saens, Camille", "Satie, Erik", "Schubert, Franz", "Schumann, Clara", "Schumann, Robert", "Scriabin, Aleksandr", 36 | "Shostakovich, Dmitry", "Sibelius, Jean", "Smetana, Bedrich", "Tchaikovsky, Pyotr", "Viardot, Pauline", "Warlock, Peter", "Wolf, Hugo", "Zumsteeg, Emilie"] 37 | instrumentations = ["Chamber", "Choral", "Keyboard", "Orchestral", "Vocal-Orchestral", "Art Song"] 38 | 39 | def __init__(self): 40 | self.device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") 41 | self.node_dir = node_dir 42 | self.model_name = None 43 | 44 | @classmethod 45 | def INPUT_TYPES(s): 46 | return { 47 | "required": { 48 | "model": (s.model_names, {"default": "notagenx.pth"}), 49 | "period": (s.periods, {"default": "Romantic"}), 50 | "composer": (s.composers, {"default": "Bach, Johann Sebastian"}), 51 | "instrumentation": (s.instrumentations, {"default": "Keyboard"}), 52 | "custom_prompt": ("STRING", { 53 | "default": "Romantic | Bach, Johann Sebastian | Keyboard", 54 | "multiline": True, 55 | "tooltip": "Custom prompt must follow format: ||" 56 | }), 57 | "unload_model":("BOOLEAN", {"default": True}), 58 | "temperature": ("FLOAT", {"default": 0.8, "min": 0, "max": 5, "step": 0.1}), 59 | "top_k": ("INT", {"default": 50, "min": 0}), 60 | "top_p": ("FLOAT", {"default": 0.95, "min": 0, "max": 1, "step": 0.01}), 61 | "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), 62 | }, 63 | } 64 | 65 | RETURN_TYPES = ("AUDIO", "IMAGE", "STRING") 66 | RETURN_NAMES = ("audio", "score", "message") 67 | FUNCTION = "inference_patch" 68 | CATEGORY = "🎤MW/MW-NotaGen" 69 | 70 | def inference_patch(self, model, period, composer, instrumentation, 71 | custom_prompt, 72 | unload_model, 73 | temperature, 74 | top_k, 75 | top_p, 76 | seed): 77 | if seed != 0: 78 | torch.manual_seed(seed) 79 | torch.cuda.manual_seed(seed) 80 | 81 | if model == "notagenx.pth" or model == "notagen_large.pth": 82 | cf = nota_lx 83 | elif model == "notagen_small.pth": 84 | cf = nota_small 85 | elif model == "notagen_medium.pth": 86 | cf = nota_medium 87 | patch_size = cf["PATCH_SIZE"] 88 | patch_length = cf["PATCH_LENGTH"] 89 | char_num_layers = cf["CHAR_NUM_LAYERS"] 90 | patch_num_layers = cf["PATCH_NUM_LAYERS"] 91 | hidden_size = cf["HIDDEN_SIZE"] 92 | 93 | patch_config = GPT2Config(num_hidden_layers=patch_num_layers, 94 | max_length=patch_length, 95 | max_position_embeddings=patch_length, 96 | n_embd=hidden_size, 97 | num_attention_heads=hidden_size // 64, 98 | vocab_size=1) 99 | byte_config = GPT2Config(num_hidden_layers=char_num_layers, 100 | max_length=patch_size + 1, 101 | max_position_embeddings=patch_size + 1, 102 | hidden_size=hidden_size, 103 | num_attention_heads=hidden_size // 64, 104 | vocab_size=128) 105 | 106 | model_file_path = os.path.join(nota_model_path, model) 107 | 108 | global MODEL_CACHE, PATCHILIZER 109 | if MODEL_CACHE is None or self.model_name != model: 110 | self.model_name = model 111 | MODEL_CACHE = NotaGenLMHeadModel(encoder_config=patch_config, decoder_config=byte_config, model=model) 112 | print("Parameter Number: " + str(sum(p.numel() for p in MODEL_CACHE.parameters() if p.requires_grad))) 113 | 114 | NOTA_MODEL = torch.load(model_file_path, map_location="cpu") 115 | MODEL_CACHE.load_state_dict(NOTA_MODEL['model']) 116 | MODEL_CACHE = MODEL_CACHE.to(self.device) 117 | MODEL_CACHE.eval() 118 | 119 | if custom_prompt.strip(): 120 | period, composer, instrumentation = [i.strip() for i in custom_prompt.split('|')] 121 | 122 | prompt_lines=[ 123 | '%' + period + '\n', 124 | '%' + composer + '\n', 125 | '%' + instrumentation + '\n'] 126 | 127 | if PATCHILIZER is None or self.model_name != model: 128 | self.model_name = model 129 | PATCHILIZER = Patchilizer(model) 130 | 131 | bos_patch = [PATCHILIZER.bos_token_id] * (patch_size - 1) + [PATCHILIZER.eos_token_id] 132 | num_gen = 0 133 | unreduced_xml_path = None 134 | save_xml_original = False 135 | while num_gen <= 5: #num_samples: 136 | 137 | start_time = time.time() 138 | 139 | prompt_patches = PATCHILIZER.patchilize_metadata(prompt_lines) 140 | byte_list = list(''.join(prompt_lines)) 141 | print(''.join(byte_list), end='') 142 | 143 | prompt_patches = [[ord(c) for c in patch] + [PATCHILIZER.special_token_id] * (patch_size - len(patch)) for patch 144 | in prompt_patches] 145 | prompt_patches.insert(0, bos_patch) 146 | 147 | input_patches = torch.tensor(prompt_patches, device=self.device).reshape(1, -1) 148 | 149 | failure_flag = False 150 | end_flag = False 151 | cut_index = None 152 | 153 | tunebody_flag = False 154 | while True: 155 | predicted_patch = MODEL_CACHE.generate(input_patches.unsqueeze(0), 156 | top_k=top_k, 157 | top_p=top_p, 158 | temperature=temperature) 159 | if not tunebody_flag and PATCHILIZER.decode([predicted_patch]).startswith('[r:'): 160 | tunebody_flag = True 161 | r0_patch = torch.tensor([ord(c) for c in '[r:0/']).unsqueeze(0).to(self.device) 162 | temp_input_patches = torch.concat([input_patches, r0_patch], axis=-1) 163 | predicted_patch = MODEL_CACHE.generate(temp_input_patches.unsqueeze(0), 164 | top_k=top_k, 165 | top_p=top_p, 166 | temperature=temperature) 167 | predicted_patch = [ord(c) for c in '[r:0/'] + predicted_patch 168 | if predicted_patch[0] == PATCHILIZER.bos_token_id and predicted_patch[1] == PATCHILIZER.eos_token_id: 169 | end_flag = True 170 | break 171 | next_patch = PATCHILIZER.decode([predicted_patch]) 172 | 173 | for char in next_patch: 174 | byte_list.append(char) 175 | print(char, end='') 176 | 177 | patch_end_flag = False 178 | for j in range(len(predicted_patch)): 179 | if patch_end_flag: 180 | predicted_patch[j] = PATCHILIZER.special_token_id 181 | if predicted_patch[j] == PATCHILIZER.eos_token_id: 182 | patch_end_flag = True 183 | 184 | predicted_patch = torch.tensor([predicted_patch], device=self.device) 185 | input_patches = torch.cat([input_patches, predicted_patch], dim=1) 186 | 187 | if len(byte_list) > 102400: 188 | failure_flag = True 189 | break 190 | if time.time() - start_time > 20 * 60: 191 | failure_flag = True 192 | break 193 | 194 | if input_patches.shape[1] >= patch_length * patch_size and not end_flag: 195 | print('Stream generating...') 196 | abc_code = ''.join(byte_list) 197 | abc_lines = abc_code.split('\n') 198 | 199 | tunebody_index = None 200 | for i, line in enumerate(abc_lines): 201 | if line.startswith('[r:') or line.startswith('[V:'): 202 | tunebody_index = i 203 | break 204 | if tunebody_index is None or tunebody_index == len(abc_lines) - 1: 205 | break 206 | 207 | metadata_lines = abc_lines[:tunebody_index] 208 | tunebody_lines = abc_lines[tunebody_index:] 209 | 210 | metadata_lines = [line + '\n' for line in metadata_lines] 211 | if not abc_code.endswith('\n'): 212 | tunebody_lines = [tunebody_lines[i] + '\n' for i in range(len(tunebody_lines) - 1)] + [ 213 | tunebody_lines[-1]] 214 | else: 215 | tunebody_lines = [tunebody_lines[i] + '\n' for i in range(len(tunebody_lines))] 216 | 217 | if cut_index is None: 218 | cut_index = len(tunebody_lines) // 2 219 | 220 | abc_code_slice = ''.join(metadata_lines + tunebody_lines[-cut_index:]) 221 | input_patches = PATCHILIZER.encode_generate(abc_code_slice) 222 | 223 | input_patches = [item for sublist in input_patches for item in sublist] 224 | input_patches = torch.tensor([input_patches], device=self.device) 225 | input_patches = input_patches.reshape(1, -1) 226 | 227 | if not failure_flag: 228 | generation_time_cost = time.time() - start_time 229 | 230 | abc_text = ''.join(byte_list) 231 | prompt_text = "-".join([period, composer, instrumentation]).replace(" ", "").replace(",", "-") 232 | filename = prompt_text + "_" + time.strftime("%Y%m%d-%H%M%S") + \ 233 | "_" + str(int(generation_time_cost)) + ".abc" 234 | 235 | unreduced_output_path = os.path.join(INTERLEAVED_OUTPUT_FOLDER, filename) 236 | 237 | abc_lines = abc_text.split('\n') 238 | abc_lines = list(filter(None, abc_lines)) 239 | abc_lines = [line + '\n' for line in abc_lines] 240 | 241 | try: 242 | abc_lines = self.rest_unreduce(abc_lines) 243 | 244 | with open(unreduced_output_path, 'w') as file: 245 | file.writelines(abc_lines) 246 | print(f"Saved to {unreduced_output_path}",) 247 | unreduced_xml_path = self.convert_abc2xml(unreduced_output_path, INTERLEAVED_OUTPUT_FOLDER) 248 | if unreduced_xml_path: 249 | save_xml_original = True 250 | else: 251 | print("Conversion xml failed.") 252 | num_gen += 1 253 | save_xml_original = False 254 | 255 | except: 256 | num_gen += 1 257 | continue 258 | else: 259 | 260 | original_output_path = os.path.join(ORIGINAL_OUTPUT_FOLDER, filename) 261 | with open(original_output_path, 'w') as w: 262 | w.write(abc_text) 263 | print(f"Saved to {original_output_path}",) 264 | 265 | if save_xml_original: 266 | original_xml_path = self.convert_abc2xml(original_output_path, ORIGINAL_OUTPUT_FOLDER) 267 | if original_xml_path: 268 | print(f"Conversion to {original_xml_path}",) 269 | break 270 | else: 271 | num_gen += 1 272 | continue 273 | 274 | else: 275 | print('Generation failed.') 276 | num_gen += 1 277 | if num_gen > 5: 278 | raise Exception("All generation attempts failed after 6 tries. Try again.") 279 | 280 | if unreduced_xml_path: 281 | mp3_path = self.xml2mp3(unreduced_xml_path) 282 | png_paths = self.xml2png(unreduced_xml_path) 283 | 284 | audio = None 285 | if mp3_path and os.path.exists(mp3_path): 286 | import torchaudio 287 | waveform, sample_rate = torchaudio.load(mp3_path) 288 | audio = {"waveform": waveform.unsqueeze(0), "sample_rate": sample_rate} 289 | else: 290 | audio = self.get_empty_audio() 291 | 292 | images = [] 293 | if png_paths: 294 | from PIL import Image, ImageOps 295 | import numpy as np 296 | 297 | for image_path in png_paths: 298 | i = Image.open(image_path) 299 | 300 | image = Image.new("RGB", i.size, (255, 255, 255)) 301 | 302 | image.paste(i, mask=i.split()[3]) 303 | 304 | image = image.convert("RGB") 305 | image = np.array(image).astype(np.float32) / 255.0 306 | image = torch.from_numpy(image)[None,] 307 | images.append(image) 308 | 309 | if len(images) > 1: 310 | image1 = images[0] 311 | for image2 in images[1:]: 312 | image1 = torch.cat((image1, image2), dim=0) 313 | else: 314 | image1 = images[0] 315 | else: 316 | image1 = self.get_empty_image() 317 | 318 | if unload_model: 319 | import gc 320 | PATCHILIZER = None 321 | MODEL_CACHE = None 322 | gc.collect() 323 | torch.cuda.empty_cache() 324 | 325 | return ( 326 | audio, 327 | image1, 328 | f"Saved to {INTERLEAVED_OUTPUT_FOLDER} and {ORIGINAL_OUTPUT_FOLDER}", 329 | ) 330 | 331 | else: 332 | if unload_model: 333 | import gc 334 | PATCHILIZER = None 335 | MODEL_CACHE = None 336 | gc.collect() 337 | torch.cuda.empty_cache() 338 | 339 | print(f".abc and .xml was saved to {INTERLEAVED_OUTPUT_FOLDER} and {ORIGINAL_OUTPUT_FOLDER}") 340 | raise Exception("Conversion of .mp3 and .png failed, try again or check if MuseScore4 installation was successful.") 341 | 342 | def get_empty_audio(self): 343 | """Return empty audio""" 344 | return {"waveform": torch.zeros(1, 2, 1), "sample_rate": 44100} 345 | 346 | def get_empty_image(self): 347 | """Return empty image""" 348 | import numpy as np 349 | return torch.from_numpy(np.zeros((1, 64, 64, 3), dtype=np.float32)) 350 | 351 | def rest_unreduce(self, abc_lines): 352 | 353 | tunebody_index = None 354 | for i in range(len(abc_lines)): 355 | if '[V:' in abc_lines[i]: 356 | tunebody_index = i 357 | break 358 | 359 | metadata_lines = abc_lines[: tunebody_index] 360 | tunebody_lines = abc_lines[tunebody_index:] 361 | 362 | part_symbol_list = [] 363 | voice_group_list = [] 364 | for line in metadata_lines: 365 | if line.startswith('%%score'): 366 | for round_bracket_match in re.findall(r'\((.*?)\)', line): 367 | voice_group_list.append(round_bracket_match.split()) 368 | existed_voices = [item for sublist in voice_group_list for item in sublist] 369 | if line.startswith('V:'): 370 | symbol = line.split()[0] 371 | part_symbol_list.append(symbol) 372 | if symbol[2:] not in existed_voices: 373 | voice_group_list.append([symbol[2:]]) 374 | z_symbol_list = [] 375 | x_symbol_list = [] 376 | for voice_group in voice_group_list: 377 | z_symbol_list.append('V:' + voice_group[0]) 378 | for j in range(1, len(voice_group)): 379 | x_symbol_list.append('V:' + voice_group[j]) 380 | 381 | part_symbol_list.sort(key=lambda x: int(x[2:])) 382 | 383 | unreduced_tunebody_lines = [] 384 | 385 | for i, line in enumerate(tunebody_lines): 386 | unreduced_line = '' 387 | 388 | line = re.sub(r'^\[r:[^\]]*\]', '', line) 389 | 390 | pattern = r'\[V:(\d+)\](.*?)(?=\[V:|$)' 391 | matches = re.findall(pattern, line) 392 | 393 | line_bar_dict = {} 394 | for match in matches: 395 | key = f'V:{match[0]}' 396 | value = match[1] 397 | line_bar_dict[key] = value 398 | 399 | dur_dict = {} 400 | for symbol, bartext in line_bar_dict.items(): 401 | right_barline = ''.join(re.split(Barline_regexPattern, bartext)[-2:]) 402 | bartext = bartext[:-len(right_barline)] 403 | try: 404 | bar_dur = calculate_bartext_duration(bartext) 405 | except: 406 | bar_dur = None 407 | if bar_dur is not None: 408 | if bar_dur not in dur_dict.keys(): 409 | dur_dict[bar_dur] = 1 410 | else: 411 | dur_dict[bar_dur] += 1 412 | 413 | try: 414 | ref_dur = max(dur_dict, key=dur_dict.get) 415 | except: 416 | pass 417 | 418 | if i == 0: 419 | prefix_left_barline = line.split('[V:')[0] 420 | else: 421 | prefix_left_barline = '' 422 | 423 | for symbol in part_symbol_list: 424 | if symbol in line_bar_dict.keys(): 425 | symbol_bartext = line_bar_dict[symbol] 426 | else: 427 | if symbol in z_symbol_list: 428 | symbol_bartext = prefix_left_barline + 'z' + str(ref_dur) + right_barline 429 | elif symbol in x_symbol_list: 430 | symbol_bartext = prefix_left_barline + 'x' + str(ref_dur) + right_barline 431 | unreduced_line += '[' + symbol + ']' + symbol_bartext 432 | 433 | unreduced_tunebody_lines.append(unreduced_line + '\n') 434 | 435 | unreduced_lines = metadata_lines + unreduced_tunebody_lines 436 | 437 | return unreduced_lines 438 | 439 | def wait_for_file(self, file_path, timeout=15, check_interval=0.3): 440 | """Wait for file generation to complete""" 441 | start_time = time.time() 442 | 443 | while time.time() - start_time < timeout: 444 | if os.path.exists(file_path): 445 | 446 | if file_path.endswith('.mp3'): 447 | initial_size = os.path.getsize(file_path) 448 | time.sleep(check_interval) 449 | if os.path.getsize(file_path) == initial_size: 450 | return True 451 | else: 452 | return True 453 | time.sleep(check_interval) 454 | return False 455 | 456 | def wait_for_png_sequence(self, base_path, timeout=15, check_interval=0.3): 457 | """Wait for PNG sequence generation to complete""" 458 | import glob 459 | 460 | start_time = time.time() 461 | last_count = 0 462 | stable_count = 0 463 | 464 | while time.time() - start_time < timeout: 465 | current_files = glob.glob(f"{base_path}-*.png") 466 | current_count = len(current_files) 467 | 468 | if current_count > 0: 469 | if current_count == last_count: 470 | stable_count += 1 471 | if stable_count >= 3: 472 | return sorted(current_files) 473 | else: 474 | stable_count = 0 475 | 476 | last_count = current_count 477 | time.sleep(check_interval) 478 | 479 | return None 480 | 481 | def xml2mp3(self, xml_path): 482 | import subprocess 483 | import sys 484 | import tempfile 485 | 486 | mp3_path = xml_path.rsplit(".", 1)[0] + ".mp3" 487 | 488 | if sys.platform == "linux": 489 | try: 490 | 491 | display_number = 100 492 | os.environ["DISPLAY"] = f":{display_number}" 493 | 494 | tmp_dir = tempfile.mkdtemp() 495 | xvfb_lock_file = os.path.join(tmp_dir, f".X{display_number}-lock") 496 | if os.path.exists(xvfb_lock_file): 497 | print(f"清理旧的 Xvfb 锁文件: {xvfb_lock_file}") 498 | os.remove(xvfb_lock_file) 499 | 500 | subprocess.run(["pkill", "Xvfb"], stderr=subprocess.DEVNULL) 501 | time.sleep(1) 502 | 503 | xvfb_process = subprocess.Popen(["Xvfb", f":{display_number}", "-screen", "0", "1024x768x24"]) 504 | time.sleep(2) 505 | 506 | os.environ["QT_QPA_PLATFORM"] = "offscreen" 507 | 508 | subprocess.run( 509 | ['mscore', '-o', mp3_path, xml_path], 510 | check=True, 511 | capture_output=True, 512 | ) 513 | 514 | if self.wait_for_file(mp3_path): 515 | print(f"Conversion to {mp3_path} completed") 516 | return mp3_path 517 | else: 518 | print("MP3 conversion timeout") 519 | return None 520 | except subprocess.CalledProcessError as e: 521 | print(f"Conversion failed: {e.stderr}" if e.stderr else "Unknown error") 522 | return None 523 | finally: 524 | 525 | xvfb_process.terminate() 526 | xvfb_process.wait() 527 | else: 528 | try: 529 | import shutil 530 | musescore_executable_path = shutil.which('MuseScore4') 531 | print(musescore_executable_path) 532 | subprocess.run( 533 | [musescore_executable_path, '-o', mp3_path, xml_path], 534 | check=True, 535 | capture_output=True, 536 | ) 537 | 538 | if self.wait_for_file(mp3_path): 539 | print(f"Conversion to {mp3_path} completed") 540 | return mp3_path 541 | else: 542 | print("MP3 conversion timeout") 543 | return None 544 | except subprocess.CalledProcessError as e: 545 | print(f"Conversion failed: {e.stderr}" if e.stderr else "Unknown error") 546 | return None 547 | 548 | def xml2png(self, xml_path): 549 | import subprocess 550 | import sys 551 | import tempfile 552 | 553 | base_png_path = xml_path.rsplit(".", 1)[0] 554 | 555 | if sys.platform == "linux": 556 | try: 557 | 558 | display_number = 100 559 | os.environ["DISPLAY"] = f":{display_number}" 560 | 561 | tmp_dir = tempfile.mkdtemp() 562 | xvfb_lock_file = os.path.join(tmp_dir, f".X{display_number}-lock") 563 | if os.path.exists(xvfb_lock_file): 564 | print(f"清理旧的 Xvfb 锁文件: {xvfb_lock_file}") 565 | os.remove(xvfb_lock_file) 566 | 567 | subprocess.run(["pkill", "Xvfb"], stderr=subprocess.DEVNULL) 568 | time.sleep(1) 569 | 570 | xvfb_process = subprocess.Popen(["Xvfb", f":{display_number}", "-screen", "0", "1024x768x24"]) 571 | time.sleep(2) 572 | 573 | os.environ["QT_QPA_PLATFORM"] = "offscreen" 574 | 575 | subprocess.run( 576 | ['mscore', '-o', f"{base_png_path}.png", xml_path], 577 | check=True, 578 | capture_output=True, 579 | ) 580 | 581 | png_files = self.wait_for_png_sequence(base_png_path) 582 | if png_files: 583 | print(f"Converted to {len(png_files)} PNG files") 584 | return png_files 585 | else: 586 | print("PNG conversion timeout") 587 | return None 588 | except subprocess.CalledProcessError as e: 589 | print(f"Conversion failed: {e.stderr}" if e.stderr else "Unknown error") 590 | return None 591 | finally: 592 | 593 | xvfb_process.terminate() 594 | xvfb_process.wait() 595 | else: 596 | try: 597 | import shutil 598 | musescore_executable_path = shutil.which('MuseScore4') 599 | print(musescore_executable_path) 600 | subprocess.run( 601 | [musescore_executable_path, '-o', f"{base_png_path}.png", xml_path], 602 | check=True, 603 | capture_output=True, 604 | ) 605 | 606 | png_files = self.wait_for_png_sequence(base_png_path) 607 | if png_files: 608 | print(f"Converted to {len(png_files)} PNG files") 609 | return png_files 610 | else: 611 | print("PNG conversion timeout") 612 | return None 613 | except subprocess.CalledProcessError as e: 614 | print(f"Conversion failed: {e.stderr}" if e.stderr else "Unknown error") 615 | return None 616 | 617 | def convert_abc2xml(self, abc_path, output_dir): 618 | import sys 619 | import os 620 | sys.path.append(self.node_dir) 621 | from abc2xml import getXmlDocs, writefile, readfile, info 622 | xml_path = abc_path.rsplit(".", 1)[0] + ".xml" 623 | try: 624 | fnm, ext = os.path.splitext(abc_path) 625 | abctext = readfile(abc_path) 626 | 627 | skip, num = 0, 1 628 | show_whole_rests = False 629 | line_breaks = False 630 | force_string_fret = False 631 | 632 | xml_docs = getXmlDocs(abctext, skip, num, show_whole_rests, line_breaks, force_string_fret) 633 | 634 | for itune, xmldoc in enumerate(xml_docs): 635 | fnmNum = '%02d' % (itune + 1) if len(xml_docs) > 1 else '' 636 | writefile(output_dir, fnm, fnmNum, xmldoc, '', False) 637 | print(f"Conversion to {xml_path}",) 638 | return xml_path 639 | except Exception as e: 640 | print(f"Conversion failed: {str(e)}") 641 | return None 642 | 643 | NODE_CLASS_MAPPINGS = { 644 | "NotaGenRun": NotaGenRun, 645 | } 646 | 647 | NODE_DISPLAY_NAME_MAPPINGS = { 648 | "NotaGenRun": "NotaGen Run", 649 | } -------------------------------------------------------------------------------- /abc2xml.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=latin-1 3 | ''' 4 | Copyright (C) 2012-2018: Willem G. Vree 5 | Contributions: Nils Liberg, Nicolas Froment, Norman Schmidt, Reinier Maliepaard, Martin Tarenskeen, 6 | Paul Villiger, Alexander Scheutzow, Herbert Schneider, David Randolph, Michael Strasser 7 | 8 | This program is free software; you can redistribute it and/or modify it under the terms of the 9 | Lesser GNU General Public License as published by the Free Software Foundation; 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | See the Lesser GNU General Public License for more details. . 14 | ''' 15 | 16 | from functools import reduce 17 | from pyparsing import Word, OneOrMore, Optional, Literal, NotAny, MatchFirst 18 | from pyparsing import Group, oneOf, Suppress, ZeroOrMore, Combine, FollowedBy 19 | from pyparsing import srange, CharsNotIn, StringEnd, LineEnd, White, Regex 20 | from pyparsing import nums, alphas, alphanums, ParseException, Forward 21 | try: import xml.etree.cElementTree as E 22 | except: import xml.etree.ElementTree as E 23 | import types, sys, os, re, datetime 24 | 25 | VERSION = 245 26 | 27 | python3 = sys.version_info[0] > 2 28 | lmap = lambda f, xs: list (map (f, xs)) # eager map for python 3 29 | if python3: 30 | int_type = int 31 | list_type = list 32 | str_type = str 33 | uni_type = str 34 | stdin = sys.stdin.buffer if sys.stdin else None # read binary if stdin available! 35 | else: 36 | int_type = types.IntType 37 | list_type = types.ListType 38 | str_type = types.StringTypes 39 | uni_type = types.UnicodeType 40 | stdin = sys.stdin 41 | 42 | info_list = [] # diagnostic messages 43 | def info (s, warn=1): 44 | x = (warn and '-- ' or '') + s 45 | info_list.append (x + '\n') # collect messages 46 | if __name__ == '__main__': # only write to stdout when called as main progeam 47 | try: sys.stderr.write (x + '\n') 48 | except: sys.stderr.write (repr (x) + '\n') 49 | 50 | def getInfo (): # get string of diagnostic messages, then clear messages 51 | global info_list 52 | xs = ''.join (info_list) 53 | info_list = [] 54 | return xs 55 | 56 | def abc_grammar (): # header, voice and lyrics grammar for ABC 57 | #----------------------------------------------------------------- 58 | # expressions that catch and skip some syntax errors (see corresponding parse expressions) 59 | #----------------------------------------------------------------- 60 | b1 = Word (u"-,'<>\u2019#", exact=1) # catch misplaced chars in chords 61 | b2 = Regex ('[^H-Wh-w~=]*') # same in user defined symbol definition 62 | b3 = Regex ('[^=]*') # same, second part 63 | 64 | #----------------------------------------------------------------- 65 | # ABC header (field_str elements are matched later with reg. epr's) 66 | #----------------------------------------------------------------- 67 | 68 | number = Word (nums).setParseAction (lambda t: int (t[0])) 69 | field_str = Regex (r'[^]]*') # match anything until end of field 70 | field_str.setParseAction (lambda t: t[0].strip ()) # and strip spacing 71 | 72 | userdef_symbol = Word (srange ('[H-Wh-w~]'), exact=1) 73 | fieldId = oneOf ('K L M Q P I T C O A Z N G H R B D F S E r Y') # info fields 74 | X_field = Literal ('X') + Suppress (':') + field_str 75 | U_field = Literal ('U') + Suppress (':') + b2 + Optional (userdef_symbol, 'H') + b3 + Suppress ('=') + field_str 76 | V_field = Literal ('V') + Suppress (':') + Word (alphanums + '_') + field_str 77 | inf_fld = fieldId + Suppress (':') + field_str 78 | ifield = Suppress ('[') + (X_field | U_field | V_field | inf_fld) + Suppress (']') 79 | abc_header = OneOrMore (ifield) + StringEnd () 80 | 81 | #--------------------------------------------------------------------------------- 82 | # I:score with recursive part groups and {* grand staff marker 83 | #--------------------------------------------------------------------------------- 84 | 85 | voiceId = Suppress (Optional ('*')) + Word (alphanums + '_') 86 | voice_gr = Suppress ('(') + OneOrMore (voiceId | Suppress ('|')) + Suppress (')') 87 | simple_part = voiceId | voice_gr | Suppress ('|') 88 | grand_staff = oneOf ('{* {') + OneOrMore (simple_part) + Suppress ('}') 89 | part = Forward () 90 | part_seq = OneOrMore (part | Suppress ('|')) 91 | brace_gr = Suppress ('{') + part_seq + Suppress ('}') 92 | bracket_gr = Suppress ('[') + part_seq + Suppress (']') 93 | part <<= MatchFirst (simple_part | grand_staff | brace_gr | bracket_gr | Suppress ('|')) 94 | abc_scoredef = Suppress (oneOf ('staves score')) + OneOrMore (part) 95 | 96 | #---------------------------------------- 97 | # ABC lyric lines (white space sensitive) 98 | #---------------------------------------- 99 | 100 | skip_note = oneOf ('* -') 101 | extend_note = Literal ('_') 102 | measure_end = Literal ('|') 103 | syl_str = CharsNotIn ('*-_| \t\n\\]') 104 | syl_chars = Combine (OneOrMore (syl_str | Regex (r'\\.'))) 105 | white = Word (' \t') 106 | syllable = syl_chars + Optional ('-') 107 | lyr_elem = (syllable | skip_note | extend_note | measure_end) + Optional (white).suppress () 108 | lyr_line = Optional (white).suppress () + ZeroOrMore (lyr_elem) 109 | 110 | syllable.setParseAction (lambda t: pObj ('syl', t)) 111 | skip_note.setParseAction (lambda t: pObj ('skip', t)) 112 | extend_note.setParseAction (lambda t: pObj ('ext', t)) 113 | measure_end.setParseAction (lambda t: pObj ('sbar', t)) 114 | lyr_line_wsp = lyr_line.leaveWhitespace () # parse actions must be set before calling leaveWhitespace 115 | 116 | #--------------------------------------------------------------------------------- 117 | # ABC voice (not white space sensitive, beams detected in note/rest parse actions) 118 | #--------------------------------------------------------------------------------- 119 | 120 | inline_field = Suppress ('[') + (inf_fld | U_field | V_field) + Suppress (']') 121 | lyr_fld = Suppress ('[') + Suppress ('w') + Suppress (':') + lyr_line_wsp + Suppress (']') # lyric line 122 | lyr_blk = OneOrMore (lyr_fld) # verses 123 | fld_or_lyr = inline_field | lyr_blk # inline field or block of lyric verses 124 | 125 | note_length = Optional (number, 1) + Group (ZeroOrMore ('/')) + Optional (number, 2) 126 | octaveHigh = OneOrMore ("'").setParseAction (lambda t: len(t)) 127 | octaveLow = OneOrMore (',').setParseAction (lambda t: -len(t)) 128 | octave = octaveHigh | octaveLow 129 | 130 | basenote = oneOf ('C D E F G A B c d e f g a b y') # includes spacer for parse efficiency 131 | accidental = oneOf ('^^ __ ^ _ =') 132 | rest_sym = oneOf ('x X z Z') 133 | slur_beg = oneOf ("( (, (' .( .(, .('") + ~Word (nums) # no tuplet_start 134 | slur_ends = OneOrMore (oneOf (') .)')) 135 | 136 | long_decoration = Combine (oneOf ('! +') + CharsNotIn ('!+ \n') + oneOf ('! +')) 137 | staccato = Literal ('.') + ~Literal ('|') # avoid dotted barline 138 | pizzicato = Literal ('!+!') # special case: plus sign is old style deco marker 139 | decoration = slur_beg | staccato | userdef_symbol | long_decoration | pizzicato 140 | decorations = OneOrMore (decoration) 141 | 142 | tie = oneOf ('.- -') 143 | rest = Optional (accidental) + rest_sym + note_length 144 | pitch = Optional (accidental) + basenote + Optional (octave, 0) 145 | note = pitch + note_length + Optional (tie) + Optional (slur_ends) 146 | dec_note = Optional (decorations) + pitch + note_length + Optional (tie) + Optional (slur_ends) 147 | chord_note = dec_note | rest | b1 148 | grace_notes = Forward () 149 | chord = Suppress ('[') + OneOrMore (chord_note | grace_notes) + Suppress (']') + note_length + Optional (tie) + Optional (slur_ends) 150 | stem = note | chord | rest 151 | 152 | broken = Combine (OneOrMore ('<') | OneOrMore ('>')) 153 | 154 | tuplet_num = Suppress ('(') + number 155 | tuplet_into = Suppress (':') + Optional (number, 0) 156 | tuplet_notes = Suppress (':') + Optional (number, 0) 157 | tuplet_start = tuplet_num + Optional (tuplet_into + Optional (tuplet_notes)) 158 | 159 | acciaccatura = Literal ('/') 160 | grace_stem = Optional (decorations) + stem 161 | grace_notes <<= Group (Suppress ('{') + Optional (acciaccatura) + OneOrMore (grace_stem) + Suppress ('}')) 162 | 163 | text_expression = Optional (oneOf ('^ _ < > @'), '^') + Optional (CharsNotIn ('"'), "") 164 | chord_accidental = oneOf ('# b =') 165 | triad = oneOf ('ma Maj maj M mi min m aug dim o + -') 166 | seventh = oneOf ('7 ma7 Maj7 M7 maj7 mi7 min7 m7 dim7 o7 -7 aug7 +7 m7b5 mi7b5') 167 | sixth = oneOf ('6 ma6 M6 mi6 min6 m6') 168 | ninth = oneOf ('9 ma9 M9 maj9 Maj9 mi9 min9 m9') 169 | elevn = oneOf ('11 ma11 M11 maj11 Maj11 mi11 min11 m11') 170 | thirt = oneOf ('13 ma13 M13 maj13 Maj13 mi13 min13 m13') 171 | suspended = oneOf ('sus sus2 sus4') 172 | chord_degree = Combine (Optional (chord_accidental) + oneOf ('2 4 5 6 7 9 11 13')) 173 | chord_kind = Optional (seventh | sixth | ninth | elevn | thirt | triad) + Optional (suspended) 174 | chord_root = oneOf ('C D E F G A B') + Optional (chord_accidental) 175 | chord_bass = oneOf ('C D E F G A B') + Optional (chord_accidental) # needs a different parse action 176 | chordsym = chord_root + chord_kind + ZeroOrMore (chord_degree) + Optional (Suppress ('/') + chord_bass) 177 | chord_sym = chordsym + Optional (Literal ('(') + CharsNotIn (')') + Literal (')')).suppress () 178 | chord_or_text = Suppress ('"') + (chord_sym ^ text_expression) + Suppress ('"') 179 | 180 | volta_nums = Optional ('[').suppress () + Combine (Word (nums) + ZeroOrMore (oneOf (', -') + Word (nums))) 181 | volta_text = Literal ('[').suppress () + Regex (r'"[^"]+"') 182 | volta = volta_nums | volta_text 183 | invisible_barline = oneOf ('[|] []') 184 | dashed_barline = oneOf (': .|') 185 | double_rep = Literal (':') + FollowedBy (':') # otherwise ambiguity with dashed barline 186 | voice_overlay = Combine (OneOrMore ('&')) 187 | bare_volta = FollowedBy (Literal ('[') + Word (nums)) # no barline, but volta follows (volta is parsed in next measure) 188 | bar_left = (oneOf ('[|: |: [: :') + Optional (volta)) | Optional ('|').suppress () + volta | oneOf ('| [|') 189 | bars = ZeroOrMore (':') + ZeroOrMore ('[') + OneOrMore (oneOf ('| ]')) 190 | bar_right = invisible_barline | double_rep | Combine (bars) | dashed_barline | voice_overlay | bare_volta 191 | 192 | errors = ~bar_right + Optional (Word (' \n')) + CharsNotIn (':&|', exact=1) 193 | linebreak = Literal ('$') | ~decorations + Literal ('!') # no need for I:linebreak !!! 194 | element = fld_or_lyr | broken | decorations | stem | chord_or_text | grace_notes | tuplet_start | linebreak | errors 195 | measure = Group (ZeroOrMore (inline_field) + Optional (bar_left) + ZeroOrMore (element) + bar_right + Optional (linebreak) + Optional (lyr_blk)) 196 | noBarMeasure = Group (ZeroOrMore (inline_field) + Optional (bar_left) + OneOrMore (element) + Optional (linebreak) + Optional (lyr_blk)) 197 | abc_voice = ZeroOrMore (measure) + Optional (noBarMeasure | Group (bar_left)) + ZeroOrMore (inline_field).suppress () + StringEnd () 198 | 199 | #---------------------------------------- 200 | # I:percmap note [step] [midi] [note-head] 201 | #---------------------------------------- 202 | 203 | white2 = (white | StringEnd ()).suppress () 204 | w3 = Optional (white2) 205 | percid = Word (alphanums + '-') 206 | step = basenote + Optional (octave, 0) 207 | pitchg = Group (Optional (accidental, '') + step + FollowedBy (white2)) 208 | stepg = Group (step + FollowedBy (white2)) | Literal ('*') 209 | midi = (Literal ('*') | number | pitchg | percid) 210 | nhd = Optional (Combine (percid + Optional ('+')), '') 211 | perc_wsp = Literal ('percmap') + w3 + pitchg + w3 + Optional (stepg, '*') + w3 + Optional (midi, '*') + w3 + nhd 212 | abc_percmap = perc_wsp.leaveWhitespace () 213 | 214 | #---------------------------------------------------------------- 215 | # Parse actions to convert all relevant results into an abstract 216 | # syntax tree where all tree nodes are instances of pObj 217 | #---------------------------------------------------------------- 218 | 219 | ifield.setParseAction (lambda t: pObj ('field', t)) 220 | grand_staff.setParseAction (lambda t: pObj ('grand', t, 1)) # 1 = keep ordered list of results 221 | brace_gr.setParseAction (lambda t: pObj ('bracegr', t, 1)) 222 | bracket_gr.setParseAction (lambda t: pObj ('bracketgr', t, 1)) 223 | voice_gr.setParseAction (lambda t: pObj ('voicegr', t, 1)) 224 | voiceId.setParseAction (lambda t: pObj ('vid', t, 1)) 225 | abc_scoredef.setParseAction (lambda t: pObj ('score', t, 1)) 226 | note_length.setParseAction (lambda t: pObj ('dur', (t[0], (t[2] << len (t[1])) >> 1))) 227 | chordsym.setParseAction (lambda t: pObj ('chordsym', t)) 228 | chord_root.setParseAction (lambda t: pObj ('root', t)) 229 | chord_kind.setParseAction (lambda t: pObj ('kind', t)) 230 | chord_degree.setParseAction (lambda t: pObj ('degree', t)) 231 | chord_bass.setParseAction (lambda t: pObj ('bass', t)) 232 | text_expression.setParseAction (lambda t: pObj ('text', t)) 233 | inline_field.setParseAction (lambda t: pObj ('inline', t)) 234 | lyr_fld.setParseAction (lambda t: pObj ('lyr_fld', t, 1)) 235 | lyr_blk.setParseAction (lambda t: pObj ('lyr_blk', t, 1)) # 1 = keep ordered list of lyric lines 236 | grace_notes.setParseAction (doGrace) 237 | acciaccatura.setParseAction (lambda t: pObj ('accia', t)) 238 | note.setParseAction (noteActn) 239 | rest.setParseAction (restActn) 240 | decorations.setParseAction (lambda t: pObj ('deco', t)) 241 | pizzicato.setParseAction (lambda t: ['!plus!']) # translate !+! 242 | slur_ends.setParseAction (lambda t: pObj ('slurs', t)) 243 | chord.setParseAction (lambda t: pObj ('chord', t, 1)) 244 | dec_note.setParseAction (noteActn) 245 | tie.setParseAction (lambda t: pObj ('tie', t)) 246 | pitch.setParseAction (lambda t: pObj ('pitch', t)) 247 | bare_volta.setParseAction (lambda t: ['|']) # return barline that user forgot 248 | dashed_barline.setParseAction (lambda t: ['.|']) 249 | bar_right.setParseAction (lambda t: pObj ('rbar', t)) 250 | bar_left.setParseAction (lambda t: pObj ('lbar', t)) 251 | broken.setParseAction (lambda t: pObj ('broken', t)) 252 | tuplet_start.setParseAction (lambda t: pObj ('tup', t)) 253 | linebreak.setParseAction (lambda t: pObj ('linebrk', t)) 254 | measure.setParseAction (doMaat) 255 | noBarMeasure.setParseAction (doMaat) 256 | b1.setParseAction (errorWarn) 257 | b2.setParseAction (errorWarn) 258 | b3.setParseAction (errorWarn) 259 | errors.setParseAction (errorWarn) 260 | 261 | return abc_header, abc_voice, abc_scoredef, abc_percmap 262 | 263 | class pObj (object): # every relevant parse result is converted into a pObj 264 | def __init__ (s, name, t, seq=0): # t = list of nested parse results 265 | s.name = name # name uniqueliy identifies this pObj 266 | rest = [] # collect parse results that are not a pObj 267 | attrs = {} # new attributes 268 | for x in t: # nested pObj's become attributes of this pObj 269 | if type (x) == pObj: 270 | attrs [x.name] = attrs.get (x.name, []) + [x] 271 | else: 272 | rest.append (x) # collect non-pObj's (mostly literals) 273 | for name, xs in attrs.items (): 274 | if len (xs) == 1: xs = xs[0] # only list if more then one pObj 275 | setattr (s, name, xs) # create the new attributes 276 | s.t = rest # all nested non-pObj's (mostly literals) 277 | s.objs = seq and t or [] # for nested ordered (lyric) pObj's 278 | 279 | def __repr__ (s): # make a nice string representation of a pObj 280 | r = [] 281 | for nm in dir (s): 282 | if nm.startswith ('_'): continue # skip build in attributes 283 | elif nm == 'name': continue # redundant 284 | else: 285 | x = getattr (s, nm) 286 | if not x: continue # s.t may be empty (list of non-pObj's) 287 | if type (x) == list_type: r.extend (x) 288 | else: r.append (x) 289 | xs = [] 290 | for x in r: # recursively call __repr__ and convert all strings to latin-1 291 | if isinstance (x, str_type): xs.append (x) # string -> no recursion 292 | else: xs.append (repr (x)) # pObj -> recursive call 293 | return '(' + s.name + ' ' +','.join (xs) + ')' 294 | 295 | global prevloc # global to remember previous match position of a note/rest 296 | prevloc = 0 297 | def detectBeamBreak (line, loc, t): 298 | global prevloc # location in string 'line' of previous note match 299 | xs = line[prevloc:loc+1] # string between previous and current note match 300 | xs = xs.lstrip () # first note match starts on a space! 301 | prevloc = loc # location in string 'line' of current note match 302 | b = pObj ('bbrk', [' ' in xs]) # space somewhere between two notes -> beambreak 303 | t.insert (0, b) # insert beambreak as a nested parse result 304 | 305 | def noteActn (line, loc, t): # detect beambreak between previous and current note/rest 306 | if 'y' in t[0].t: return [] # discard spacer 307 | detectBeamBreak (line, loc, t) # adds beambreak to parse result t as side effect 308 | return pObj ('note', t) 309 | 310 | def restActn (line, loc, t): # detect beambreak between previous and current note/rest 311 | detectBeamBreak (line, loc, t) # adds beambreak to parse result t as side effect 312 | return pObj ('rest', t) 313 | 314 | def errorWarn (line, loc, t): # warning for misplaced symbols and skip them 315 | if not t[0]: return [] # only warn if catched string not empty 316 | info ('**misplaced symbol: %s' % t[0], warn=0) 317 | lineCopy = line [:] 318 | if loc > 40: 319 | lineCopy = line [loc - 40: loc + 40] 320 | loc = 40 321 | info (lineCopy.replace ('\n', ' '), warn=0) 322 | info (loc * '-' + '^', warn=0) 323 | return [] 324 | 325 | #------------------------------------------------------------- 326 | # transformations of a measure (called by parse action doMaat) 327 | #------------------------------------------------------------- 328 | 329 | def simplify (a, b): # divide a and b by their greatest common divisor 330 | x, y = a, b 331 | while b: a, b = b, a % b 332 | return x // a, y // a 333 | 334 | def doBroken (prev, brk, x): 335 | if not prev: info ('error in broken rhythm: %s' % x); return # no changes 336 | nom1, den1 = prev.dur.t # duration of first note/chord 337 | nom2, den2 = x.dur.t # duration of second note/chord 338 | if brk == '>': 339 | nom1, den1 = simplify (3 * nom1, 2 * den1) 340 | nom2, den2 = simplify (1 * nom2, 2 * den2) 341 | elif brk == '<': 342 | nom1, den1 = simplify (1 * nom1, 2 * den1) 343 | nom2, den2 = simplify (3 * nom2, 2 * den2) 344 | elif brk == '>>': 345 | nom1, den1 = simplify (7 * nom1, 4 * den1) 346 | nom2, den2 = simplify (1 * nom2, 4 * den2) 347 | elif brk == '<<': 348 | nom1, den1 = simplify (1 * nom1, 4 * den1) 349 | nom2, den2 = simplify (7 * nom2, 4 * den2) 350 | else: return # give up 351 | prev.dur.t = nom1, den1 # change duration of previous note/chord 352 | x.dur.t = nom2, den2 # and current note/chord 353 | 354 | def convertBroken (t): # convert broken rhythms to normal note durations 355 | prev = None # the last note/chord before the broken symbol 356 | brk = '' # the broken symbol 357 | remove = [] # indexes to broken symbols (to be deleted) in measure 358 | for i, x in enumerate (t): # scan all elements in measure 359 | if x.name == 'note' or x.name == 'chord' or x.name == 'rest': 360 | if brk: # a broken symbol was encountered before 361 | doBroken (prev, brk, x) # change duration previous note/chord/rest and current one 362 | brk = '' 363 | else: 364 | prev = x # remember the last note/chord/rest 365 | elif x.name == 'broken': 366 | brk = x.t[0] # remember the broken symbol (=string) 367 | remove.insert (0, i) # and its index, highest index first 368 | for i in remove: del t[i] # delete broken symbols from high to low 369 | 370 | def ptc2midi (n): # convert parsed pitch attribute to a midi number 371 | pt = getattr (n, 'pitch', '') 372 | if pt: 373 | p = pt.t 374 | if len (p) == 3: acc, step, oct = p 375 | else: acc = ''; step, oct = p 376 | nUp = step.upper () 377 | oct = (4 if nUp == step else 5) + int (oct) 378 | midi = oct * 12 + [0,2,4,5,7,9,11]['CDEFGAB'.index (nUp)] + {'^':1,'_':-1}.get (acc, 0) + 12 379 | else: midi = 130 # all non pitch objects first 380 | return midi 381 | 382 | def convertChord (t): # convert chord to sequence of notes in musicXml-style 383 | ins = [] 384 | for i, x in enumerate (t): 385 | if x.name == 'chord': 386 | if hasattr (x, 'rest') and not hasattr (x, 'note'): # chords containing only rests 387 | if type (x.rest) == list_type: x.rest = x.rest[0] # more rests == one rest 388 | ins.insert (0, (i, [x.rest])) # just output a single rest, no chord 389 | continue 390 | num1, den1 = x.dur.t # chord duration 391 | tie = getattr (x, 'tie', None) # chord tie 392 | slurs = getattr (x, 'slurs', []) # slur endings 393 | if type (x.note) != list_type: x.note = [x.note] # when chord has only one note ... 394 | elms = []; j = 0 # sort chord notes, highest first 395 | nss = sorted (x.objs, key = ptc2midi, reverse=1) if mxm.orderChords else x.objs 396 | for nt in nss: # all chord elements (note | decorations | rest | grace note) 397 | if nt.name == 'note': 398 | num2, den2 = nt.dur.t # note duration * chord duration 399 | nt.dur.t = simplify (num1 * num2, den1 * den2) 400 | if tie: nt.tie = tie # tie on all chord notes 401 | if j == 0 and slurs: nt.slurs = slurs # slur endings only on first chord note 402 | if j > 0: nt.chord = pObj ('chord', [1]) # label all but first as chord notes 403 | else: # remember all pitches of the chord in the first note 404 | pitches = [n.pitch for n in x.note] # to implement conversion of erroneous ties to slurs 405 | nt.pitches = pObj ('pitches', pitches) 406 | j += 1 407 | if nt.name not in ['dur','tie','slurs','rest']: elms.append (nt) 408 | ins.insert (0, (i, elms)) # chord position, [note|decotation|grace note] 409 | for i, notes in ins: # insert from high to low 410 | for nt in reversed (notes): 411 | t.insert (i+1, nt) # insert chord notes after chord 412 | del t[i] # remove chord itself 413 | 414 | def doMaat (t): # t is a Group() result -> the measure is in t[0] 415 | convertBroken (t[0]) # remove all broken rhythms and convert to normal durations 416 | convertChord (t[0]) # replace chords by note sequences in musicXML style 417 | 418 | def doGrace (t): # t is a Group() result -> the grace sequence is in t[0] 419 | convertChord (t[0]) # a grace sequence may have chords 420 | for nt in t[0]: # flag all notes within the grace sequence 421 | if nt.name == 'note': nt.grace = 1 # set grace attribute 422 | return t[0] # ungroup the parse result 423 | #-------------------- 424 | # musicXML generation 425 | #---------------------------------- 426 | 427 | def compChordTab (): # avoid some typing work: returns mapping constant {ABC chordsyms -> musicXML kind} 428 | maj, min, aug, dim, dom, ch7, ch6, ch9, ch11, ch13, hd = 'major minor augmented diminished dominant -seventh -sixth -ninth -11th -13th half-diminished'.split () 429 | triad = zip ('ma Maj maj M mi min m aug dim o + -'.split (), [maj, maj, maj, maj, min, min, min, aug, dim, dim, aug, min]) 430 | seventh = zip ('7 ma7 Maj7 M7 maj7 mi7 min7 m7 dim7 o7 -7 aug7 +7 m7b5 mi7b5'.split (), 431 | [dom, maj+ch7, maj+ch7, maj+ch7, maj+ch7, min+ch7, min+ch7, min+ch7, dim+ch7, dim+ch7, min+ch7, aug+ch7, aug+ch7, hd, hd]) 432 | sixth = zip ('6 ma6 M6 mi6 min6 m6'.split (), [maj+ch6, maj+ch6, maj+ch6, min+ch6, min+ch6, min+ch6]) 433 | ninth = zip ('9 ma9 M9 maj9 Maj9 mi9 min9 m9'.split (), [dom+ch9, maj+ch9, maj+ch9, maj+ch9, maj+ch9, min+ch9, min+ch9, min+ch9]) 434 | elevn = zip ('11 ma11 M11 maj11 Maj11 mi11 min11 m11'.split (), [dom+ch11, maj+ch11, maj+ch11, maj+ch11, maj+ch11, min+ch11, min+ch11, min+ch11]) 435 | thirt = zip ('13 ma13 M13 maj13 Maj13 mi13 min13 m13'.split (), [dom+ch13, maj+ch13, maj+ch13, maj+ch13, maj+ch13, min+ch13, min+ch13, min+ch13]) 436 | sus = zip ('sus sus4 sus2'.split (), ['suspended-fourth', 'suspended-fourth', 'suspended-second']) 437 | return dict (list (triad) + list (seventh) + list (sixth) + list (ninth) + list (elevn) + list (thirt) + list (sus)) 438 | 439 | def addElem (parent, child, level): 440 | indent = 2 441 | chldrn = list (parent) 442 | if chldrn: 443 | chldrn[-1].tail += indent * ' ' 444 | else: 445 | parent.text = '\n' + level * indent * ' ' 446 | parent.append (child) 447 | child.tail = '\n' + (level-1) * indent * ' ' 448 | 449 | def addElemT (parent, tag, text, level): 450 | e = E.Element (tag) 451 | e.text = text 452 | addElem (parent, e, level) 453 | return e 454 | 455 | def mkTmod (tmnum, tmden, lev): 456 | tmod = E.Element ('time-modification') 457 | addElemT (tmod, 'actual-notes', str (tmnum), lev + 1) 458 | addElemT (tmod, 'normal-notes', str (tmden), lev + 1) 459 | return tmod 460 | 461 | def addDirection (parent, elems, lev, gstaff, subelms=[], placement='below', cue_on=0): 462 | dir = E.Element ('direction', placement=placement) 463 | addElem (parent, dir, lev) 464 | if type (elems) != list_type: elems = [(elems, subelms)] # ugly hack to provide for multiple direction types 465 | for elem, subelms in elems: # add direction types 466 | typ = E.Element ('direction-type') 467 | addElem (dir, typ, lev + 1) 468 | addElem (typ, elem, lev + 2) 469 | for subel in subelms: addElem (elem, subel, lev + 3) 470 | if cue_on: addElem (dir, E.Element ('level', size='cue'), lev + 1) 471 | if gstaff: addElemT (dir, 'staff', str (gstaff), lev + 1) 472 | return dir 473 | 474 | def removeElems (root_elem, parent_str, elem_str): 475 | for p in root_elem.findall (parent_str): 476 | e = p.find (elem_str) 477 | if e != None: p.remove (e) 478 | 479 | def alignLyr (vce, lyrs): 480 | empty_el = pObj ('leeg', '*') 481 | for k, lyr in enumerate (lyrs): # lyr = one full line of lyrics 482 | i = 0 # syl counter 483 | for elem in vce: # reiterate the voice block for each lyrics line 484 | if elem.name == 'note' and not (hasattr (elem, 'chord') or hasattr (elem, 'grace')): 485 | if i >= len (lyr): lr = empty_el 486 | else: lr = lyr [i] 487 | lr.t[0] = lr.t[0].replace ('%5d',']') 488 | elem.objs.append (lr) 489 | if lr.name != 'sbar': i += 1 490 | if elem.name == 'rbar' and i < len (lyr) and lyr[i].name == 'sbar': i += 1 491 | return vce 492 | 493 | slur_move = re.compile (r'(?<][<>]?)(\)+)') # (? I: 520 | x2 = r1.sub ('', x) # remove comment 521 | while x2.endswith ('*') and not (x2.startswith ('w:') or x2.startswith ('+:') or 'percmap' in x2): 522 | x2 = x2[:-1] # remove old syntax for right adjusting 523 | if not x2: continue # empty line 524 | if x2[:2] == 'W:': 525 | field = x2 [2:].strip () 526 | ftype = mxm.metaMap.get ('W', 'W') # respect the (user defined --meta) mapping of various ABC fields to XML meta data types 527 | c = mxm.metadata.get (ftype, '') 528 | mxm.metadata [ftype] = c + '\n' + field if c else field # concatenate multiple info fields with new line as separator 529 | continue # skip W: lyrics 530 | if x2[:2] == '+:': # field continuation 531 | fln += x2[2:] 532 | continue 533 | ro = r2.match (x2) # single field on a line 534 | if ro: # field -> inline_field, escape all ']' 535 | if fcont: # old style \-info-continuation active 536 | fcont = x2 [-1] == '\\' # possible further \-info-continuation 537 | fln += re.sub (r'^.:(.*?)\\*$', r'\1', x2) # add continuation, remove .: and \ 538 | continue 539 | if fln: mln += escField (fln) 540 | if x2.startswith ('['): x2 = x2.strip ('[]') 541 | fcont = x2 [-1] == '\\' # first encounter of old style \-info-continuation 542 | fln = x2.rstrip ('\\') # remove continuation from field and inline brackets 543 | continue 544 | if nx == 1: # x2 is a new music line 545 | fcont = 0 # stop \-continuations (-> only adjacent \-info-continuations are joined) 546 | if fln: 547 | mln += escField (fln) 548 | fln = '' 549 | if mcont: 550 | mcont = x2 [-1] == '\\' 551 | mln += x2.rstrip ('\\') 552 | else: 553 | if mln: xs.append (mln); mln = '' 554 | mcont = x2 [-1] == '\\' 555 | mln = x2.rstrip ('\\') 556 | if not mcont: xs.append (mln); mln = '' 557 | if fln: mln += escField (fln) 558 | if mln: xs.append (mln) 559 | 560 | hs = re.split (r'(\[K:[^]]*\])', xs [0]) # look for end of header K: 561 | if len (hs) == 1: header = hs[0]; xs [0] = '' # no K: present 562 | else: header = hs [0] + hs [1]; xs [0] = ''.join (hs[2:]) # h[1] is the first K: 563 | abctext = '\n'.join (xs) # the rest is body text 564 | hfs, vfs = [], [] 565 | for x in header[1:-1].split (']['): 566 | if x[0] == 'V': vfs.append (x) # filter voice- and midi-definitions 567 | elif x[:6] == 'I:MIDI': vfs.append (x) # from the header to vfs 568 | elif x[:9] == 'I:percmap': vfs.append (x) # and also percmap 569 | else: hfs.append (x) # all other fields stay in header 570 | header = '[' + ']['.join (hfs) + ']' # restore the header 571 | abctext = ('[' + ']['.join (vfs) + ']' if vfs else '') + abctext # prepend voice/midi from header before abctext 572 | 573 | xs = abctext.split ('[V:') 574 | if len (xs) == 1: abctext = '[V:1]' + abctext # abc has no voice defs at all 575 | elif re.sub (r'\[[A-Z]:[^]]*\]', '', xs[0]).strip (): # remove inline fields from starting text, if any 576 | abctext = '[V:1]' + abctext # abc with voices has no V: at start 577 | 578 | r1 = re.compile (r'\[V:\s*(\S*)[ \]]') # get voice id from V: field (skip spaces betwee V: and ID) 579 | vmap = {} # {voice id -> [voice abc string]} 580 | vorder = {} # mark document order of voices 581 | xs = re.split (r'(\[V:[^]]*\])', abctext) # split on every V-field (V-fields included in split result list) 582 | if len (xs) == 1: raise ValueError ('bugs ...') 583 | else: 584 | pm = re.findall (r'\[P:.\]', xs[0]) # all P:-marks after K: but before first V: 585 | if pm: xs[2] = ''.join (pm) + xs[2] # prepend P:-marks to the text of the first voice 586 | header += re.sub (r'\[P:.\]', '', xs[0]) # clear all P:-marks from text between K: and first V: and put text in the header 587 | i = 1 588 | while i < len (xs): # xs = ['', V-field, voice abc, V-field, voice abc, ...] 589 | vce, abc = xs[i:i+2] 590 | id = r1.search (vce).group (1) # get voice ID from V-field 591 | if not id: id, vce = '1', '[V:1]' # voice def has no ID 592 | vmap[id] = vmap.get (id, []) + [vce, abc] # collect abc-text for each voice id (include V-fields) 593 | if id not in vorder: vorder [id] = i # store document order of first occurrence of voice id 594 | i += 2 595 | voices = [] 596 | ixs = sorted ([(i, id) for id, i in vorder.items ()]) # restore document order of voices 597 | for i, id in ixs: 598 | voice = ''.join (vmap [id]) # all abc of one voice 599 | voice = fixSlurs (voice) # put slurs right after the notes 600 | voices.append ((id, voice)) 601 | return header, voices 602 | 603 | def mergeMeasure (m1, m2, slur_offset, voice_offset, rOpt, is_grand=0, is_overlay=0): 604 | slurs = m2.findall ('note/notations/slur') 605 | for slr in slurs: 606 | slrnum = int (slr.get ('number')) + slur_offset 607 | slr.set ('number', str (slrnum)) # make unique slurnums in m2 608 | vs = m2.findall ('note/voice') # set all voice number elements in m2 609 | for v in vs: v.text = str (voice_offset + int (v.text)) 610 | ls = m1.findall ('note/lyric') # all lyric elements in m1 611 | lnum_max = max ([int (l.get ('number')) for l in ls] + [0]) # highest lyric number in m1 612 | ls = m2.findall ('note/lyric') # update lyric elements in m2 613 | for el in ls: 614 | n = int (el.get ('number')) 615 | el.set ('number', str (n + lnum_max)) 616 | ns = m1.findall ('note') # determine the total duration of m1, subtract all backups 617 | dur1 = sum (int (n.find ('duration').text) for n in ns 618 | if n.find ('grace') == None and n.find ('chord') == None) 619 | dur1 -= sum (int (b.text) for b in m1.findall ('backup/duration')) 620 | repbar, nns, es = 0, 0, [] # nns = number of real notes in m2 621 | for e in list (m2): # scan all elements of m2 622 | if e.tag == 'attributes': 623 | if not is_grand: continue # no attribute merging for normal voices 624 | else: nns += 1 # but we do merge (clef) attributes for a grand staff 625 | if e.tag == 'print': continue 626 | if e.tag == 'note' and (rOpt or e.find ('rest') == None): nns += 1 627 | if e.tag == 'barline' and e.find ('repeat') != None: repbar = e; 628 | es.append (e) # buffer elements to be merged 629 | if nns > 0: # only merge if m2 contains any real notes 630 | if dur1 > 0: # only insert backup if duration of m1 > 0 631 | b = E.Element ('backup') 632 | addElem (m1, b, level=3) 633 | addElemT (b, 'duration', str (dur1), level=4) 634 | for e in es: addElem (m1, e, level=3) # merge buffered elements of m2 635 | elif is_overlay and repbar: addElem (m1, repbar, level=3) # merge repeat in empty overlay 636 | 637 | def mergePartList (parts, rOpt, is_grand=0): # merge parts, make grand staff when is_grand true 638 | 639 | def delAttrs (part): # for the time being we only keep clef attributes 640 | xs = [(m, e) for m in part.findall ('measure') for e in m.findall ('attributes')] 641 | for m, e in xs: 642 | for c in list (e): 643 | if c.tag == 'clef': continue # keep clef attribute 644 | if c.tag == 'staff-details': continue # keep staff-details attribute 645 | e.remove (c) # delete all other attrinutes for higher staff numbers 646 | if len (list (e)) == 0: m.remove (e) # remove empty attributes element 647 | 648 | p1 = parts[0] 649 | for p2 in parts[1:]: 650 | if is_grand: delAttrs (p2) # delete all attributes except clef 651 | for i in range (len (p1) + 1, len (p2) + 1): # second part longer than first one 652 | maat = E.Element ('measure', number = str(i)) # append empty measures 653 | addElem (p1, maat, 2) 654 | slurs = p1.findall ('measure/note/notations/slur') # find highest slur num in first part 655 | slur_max = max ([int (slr.get ('number')) for slr in slurs] + [0]) 656 | vs = p1.findall ('measure/note/voice') # all voice number elements in first part 657 | vnum_max = max ([int (v.text) for v in vs] + [0]) # highest voice number in first part 658 | for im, m2 in enumerate (p2.findall ('measure')): # merge all measures of p2 into p1 659 | mergeMeasure (p1[im], m2, slur_max, vnum_max, rOpt, is_grand) # may change slur numbers in p1 660 | return p1 661 | 662 | def mergeParts (parts, vids, staves, rOpt, is_grand=0): 663 | if not staves: return parts, vids # no voice mapping 664 | partsnew, vidsnew = [], [] 665 | for voice_ids in staves: 666 | pixs = [] 667 | for vid in voice_ids: 668 | if vid in vids: pixs.append (vids.index (vid)) 669 | else: info ('score partname %s does not exist' % vid) 670 | if pixs: 671 | xparts = [parts[pix] for pix in pixs] 672 | if len (xparts) > 1: mergedpart = mergePartList (xparts, rOpt, is_grand) 673 | else: mergedpart = xparts [0] 674 | partsnew.append (mergedpart) 675 | vidsnew.append (vids [pixs[0]]) 676 | return partsnew, vidsnew 677 | 678 | def mergePartMeasure (part, msre, ovrlaynum, rOpt): # merge msre into last measure of part, only for overlays 679 | slur_offset = 0; # slur numbers determined by the slurstack size (as in a single voice) 680 | last_msre = list (part)[-1] # last measure in part 681 | mergeMeasure (last_msre, msre, slur_offset, ovrlaynum, rOpt, is_overlay=1) # voice offset = s.overlayVNum 682 | 683 | def pushSlur (boogStapel, stem): 684 | if stem not in boogStapel: boogStapel [stem] = [] # initialize slurstack for stem 685 | boognum = sum (map (len, boogStapel.values ())) + 1 # number of open slurs in all (overlay) voices 686 | boogStapel [stem].append (boognum) 687 | return boognum 688 | 689 | def setFristVoiceNameFromGroup (vids, vdefs): # vids = [vid], vdef = {vid -> (name, subname, voicedef)} 690 | vids = [v for v in vids if v in vdefs] # only consider defined voices 691 | if not vids: return vdefs 692 | vid0 = vids [0] # first vid of the group 693 | _, _, vdef0 = vdefs [vid0] # keep de voice definition (vdef0) when renaming vid0 694 | for vid in vids: 695 | nm, snm, vdef = vdefs [vid] 696 | if nm: # first non empty name encountered will become 697 | vdefs [vid0] = nm, snm, vdef0 # name of merged group == name of first voice in group (vid0) 698 | break 699 | return vdefs 700 | 701 | def mkGrand (p, vdefs): # transform parse subtree into list needed for s.grands 702 | xs = [] 703 | for i, x in enumerate (p.objs): # changing p.objs [i] alters the tree. changing x has no effect on the tree. 704 | if type (x) == pObj: 705 | us = mkGrand (x, vdefs) # first get transformation results of current pObj 706 | if x.name == 'grand': # x.objs contains ordered list of nested parse results within x 707 | vids = [y.objs[0] for y in x.objs[1:]] # the voice ids in the grand staff 708 | nms = [vdefs [u][0] for u in vids if u in vdefs] # the names of those voices 709 | accept = sum ([1 for nm in nms if nm]) == 1 # accept as grand staff when only one of the voices has a name 710 | if accept or us[0] == '{*': 711 | xs.append (us[1:]) # append voice ids as a list (discard first item '{' or '{*') 712 | vdefs = setFristVoiceNameFromGroup (vids, vdefs) 713 | p.objs [i] = x.objs[1] # replace voices by first one in the grand group (this modifies the parse tree) 714 | else: 715 | xs.extend (us[1:]) # extend current result with all voice ids of rejected grand staff 716 | else: xs.extend (us) # extend current result with transformed pObj 717 | else: xs.append (p.t[0]) # append the non pObj (== voice id string) 718 | return xs 719 | 720 | def mkStaves (p, vdefs): # transform parse tree into list needed for s.staves 721 | xs = [] 722 | for i, x in enumerate (p.objs): # structure and comments identical to mkGrand 723 | if type (x) == pObj: 724 | us = mkStaves (x, vdefs) 725 | if x.name == 'voicegr': 726 | xs.append (us) 727 | vids = [y.objs[0] for y in x.objs] 728 | vdefs = setFristVoiceNameFromGroup (vids, vdefs) 729 | p.objs [i] = x.objs[0] 730 | else: 731 | xs.extend (us) 732 | else: 733 | if p.t[0] not in '{*': xs.append (p.t[0]) 734 | return xs 735 | 736 | def mkGroups (p): # transform parse tree into list needed for s.groups 737 | xs = [] 738 | for x in p.objs: 739 | if type (x) == pObj: 740 | if x.name == 'vid': xs.extend (mkGroups (x)) 741 | elif x.name == 'bracketgr': xs.extend (['['] + mkGroups (x) + [']']) 742 | elif x.name == 'bracegr': xs.extend (['{'] + mkGroups (x) + ['}']) 743 | else: xs.extend (mkGroups (x) + ['}']) # x.name == 'grand' == rejected grand staff 744 | else: 745 | xs.append (p.t[0]) 746 | return xs 747 | 748 | def stepTrans (step, soct, clef): # [A-G] (1...8) 749 | if clef.startswith ('bass'): 750 | nm7 = 'C,D,E,F,G,A,B'.split (',') 751 | n = 14 + nm7.index (step) - 12 # two octaves extra to avoid negative numbers 752 | step, soct = nm7 [n % 7], soct + n // 7 - 2 # subtract two octaves again 753 | return step, soct 754 | 755 | def reduceMids (parts, vidsnew, midiInst): # remove redundant instruments from a part 756 | for pid, part in zip (vidsnew, parts): 757 | mids, repls, has_perc = {}, {}, 0 758 | for ipid, ivid, ch, prg, vol, pan in sorted (list (midiInst.values ())): 759 | if ipid != pid: continue # only instruments from part pid 760 | if ch == '10': has_perc = 1; continue # only consider non percussion instruments 761 | instId, inst = 'I%s-%s' % (ipid, ivid), (ch, prg) 762 | if inst in mids: # midi instrument already defined in this part 763 | repls [instId] = mids [inst] # remember to replace instId by inst (see below) 764 | del midiInst [instId] # instId is redundant 765 | else: mids [inst] = instId # collect unique instruments in this part 766 | if len (mids) < 2 and not has_perc: # only one instrument used -> no instrument tags needed in notes 767 | removeElems (part, 'measure/note', 'instrument') # no instrument tag needed for one- or no-instrument parts 768 | else: 769 | for e in part.findall ('measure/note/instrument'): 770 | id = e.get ('id') # replace all redundant instrument Id's 771 | if id in repls: e.set ('id', repls [id]) 772 | 773 | class stringAlloc: 774 | def __init__ (s): 775 | s.snaarVrij = [] # [[(t1, t2) ...] for each string ] 776 | s.snaarIx = [] # index in snaarVrij for each string 777 | s.curstaff = -1 # staff being allocated 778 | def beginZoek (s): # reset snaarIx at start of each voice 779 | s.snaarIx = [] 780 | for i in range (len (s.snaarVrij)): s.snaarIx.append (0) 781 | def setlines (s, stflines, stfnum): 782 | if stfnum != s.curstaff: # initialize for new staff 783 | s.curstaff = stfnum 784 | s.snaarVrij = [] 785 | for i in range (stflines): s.snaarVrij.append ([]) 786 | s.beginZoek () 787 | def isVrij (s, snaar, t1, t2): # see if string snaar is free between t1 and t2 788 | xs = s.snaarVrij [snaar] 789 | for i in range (s.snaarIx [snaar], len (xs)): 790 | tb, te = xs [i] 791 | if t1 >= te: continue # te_prev < t1 <= te 792 | if t1 >= tb: s.snaarIx [snaar] = i; return 0 # tb <= t1 < te 793 | if t2 > tb: s.snaarIx [snaar] = i; return 0 # t1 < tb < t2 794 | s.snaarIx [snaar] = i; # remember position for next call 795 | xs.insert (i, (t1,t2)) # te_prev < t1 < t2 < tb 796 | return 1 797 | xs.append ((t1,t2)) 798 | s.snaarIx [snaar] = len (xs) - 1 799 | return 1 800 | def bezet (s, snaar, t1, t2): # force allocation of note (t1,t2) on string snaar 801 | xs = s.snaarVrij [snaar] 802 | for i, (tb, te) in enumerate (xs): 803 | if t1 >= te: continue # te_prev < t1 <= te 804 | xs.insert (i, (t1, t2)) 805 | return 806 | xs.append ((t1,t2)) 807 | 808 | class MusicXml: 809 | typeMap = {1:'long', 2:'breve', 4:'whole', 8:'half', 16:'quarter', 32:'eighth', 64:'16th', 128:'32nd', 256:'64th'} 810 | dynaMap = {'p':1,'pp':1,'ppp':1,'pppp':1,'f':1,'ff':1,'fff':1,'ffff':1,'mp':1,'mf':1,'sfz':1} 811 | tempoMap = {'larghissimo':40, 'moderato':104, 'adagissimo':44, 'allegretto':112, 'lentissimo':48, 'allegro':120, 'largo':56, 812 | 'vivace':168, 'adagio':59, 'vivo':180, 'lento':62, 'presto':192, 'larghetto':66, 'allegrissimo':208, 'adagietto':76, 813 | 'vivacissimo':220, 'andante':88, 'prestissimo':240, 'andantino':96} 814 | wedgeMap = {'>(':1, '>)':1, '<(':1,'<)':1,'crescendo(':1,'crescendo)':1,'diminuendo(':1,'diminuendo)':1} 815 | artMap = {'.':'staccato','>':'accent','accent':'accent','wedge':'staccatissimo','tenuto':'tenuto', 816 | 'breath':'breath-mark','marcato':'strong-accent','^':'strong-accent','slide':'scoop'} 817 | ornMap = {'trill':'trill-mark','T':'trill-mark','turn':'turn','uppermordent':'inverted-mordent','lowermordent':'mordent', 818 | 'pralltriller':'inverted-mordent','mordent':'mordent','turn':'turn','invertedturn':'inverted-turn'} 819 | tecMap = {'upbow':'up-bow', 'downbow':'down-bow', 'plus':'stopped','open':'open-string','snap':'snap-pizzicato', 820 | 'thumb':'thumb-position'} 821 | capoMap = {'fine':('Fine','fine','yes'), 'D.S.':('D.S.','dalsegno','segno'), 'D.C.':('D.C.','dacapo','yes'),'dacapo':('D.C.','dacapo','yes'), 822 | 'dacoda':('To Coda','tocoda','coda'), 'coda':('coda','coda','coda'), 'segno':('segno','segno','segno')} 823 | sharpness = ['Fb', 'Cb','Gb','Db','Ab','Eb','Bb','F','C','G','D','A', 'E', 'B', 'F#','C#','G#','D#','A#','E#','B#'] 824 | offTab = {'maj':8, 'm':11, 'min':11, 'mix':9, 'dor':10, 'phr':12, 'lyd':7, 'loc':13} 825 | modTab = {'maj':'major', 'm':'minor', 'min':'minor', 'mix':'mixolydian', 'dor':'dorian', 'phr':'phrygian', 'lyd':'lydian', 'loc':'locrian'} 826 | clefMap = { 'alto1':('C','1'), 'alto2':('C','2'), 'alto':('C','3'), 'alto4':('C','4'), 'tenor':('C','4'), 827 | 'bass3':('F','3'), 'bass':('F','4'), 'treble':('G','2'), 'perc':('percussion',''), 'none':('',''), 'tab':('TAB','5')} 828 | clefLineMap = {'B':'treble', 'G':'alto1', 'E':'alto2', 'C':'alto', 'A':'tenor', 'F':'bass3', 'D':'bass'} 829 | alterTab = {'=':'0', '_':'-1', '__':'-2', '^':'1', '^^':'2'} 830 | accTab = {'=':'natural', '_':'flat', '__':'flat-flat', '^':'sharp', '^^':'sharp-sharp'} 831 | chordTab = compChordTab () 832 | uSyms = {'~':'roll', 'H':'fermata','L':'>','M':'lowermordent','O':'coda', 833 | 'P':'uppermordent','S':'segno','T':'trill','u':'upbow','v':'downbow'} 834 | pageFmtDef = [0.75,297,210,18,18,10,10] # the abcm2ps page formatting defaults for A4 835 | metaTab = {'O':'origin', 'A':'area', 'Z':'transcription', 'N':'notes', 'G':'group', 'H':'history', 'R':'rhythm', 836 | 'B':'book', 'D':'discography', 'F':'fileurl', 'S':'source', 'P':'partmap', 'W':'lyrics'} 837 | metaMap = {'C':'composer'} # mapping of composer is fixed 838 | metaTypes = {'composer':1,'lyricist':1,'poet':1,'arranger':1,'translator':1, 'rights':1} # valid MusicXML meta data types 839 | tuningDef = 'E2,A2,D3,G3,B3,E4'.split (',') # default string tuning (guitar) 840 | 841 | def __init__ (s): 842 | s.pageFmtCmd = [] # set by command line option -p 843 | s.reset () 844 | def reset (s, fOpt=False): 845 | s.divisions = 2520 # xml duration of 1/4 note, 2^3 * 3^2 * 5 * 7 => 5,7,9 tuplets 846 | s.ties = {} # {abc pitch tuple -> alteration} for all open ties 847 | s.slurstack = {} # stack of open slur numbers per (overlay) voice 848 | s.slurbeg = [] # type of slurs to start (when slurs are detected at element-level) 849 | s.tmnum = 0 # time modification, numerator 850 | s.tmden = 0 # time modification, denominator 851 | s.ntup = 0 # number of tuplet notes remaining 852 | s.trem = 0 # number of bars for tremolo 853 | s.intrem = 0 # mark tremolo sequence (for duration doubling) 854 | s.tupnts = [] # all tuplet modifiers with corresp. durations: [(duration, modifier), ...] 855 | s.irrtup = 0 # 1 if an irregular tuplet 856 | s.ntype = '' # the normal-type of a tuplet (== duration type of a normal tuplet note) 857 | s.unitL = (1, 8) # default unit length 858 | s.unitLcur = (1, 8) # unit length of current voice 859 | s.keyAlts = {} # alterations implied by key 860 | s.msreAlts = {} # temporarily alterations 861 | s.curVolta = '' # open volta bracket 862 | s.title = '' # title of music 863 | s.creator = {} # {creator-type -> creator string} 864 | s.metadata = {} # {metadata-type -> string} 865 | s.lyrdash = {} # {lyric number -> 1 if dash between syllables} 866 | s.usrSyms = s.uSyms # user defined symbols 867 | s.prevNote = None # xml element of previous beamed note to correct beams (start, continue) 868 | s.prevLyric = {} # xml element of previous lyric to add/correct extend type (start, continue) 869 | s.grcbbrk = False # remember any bbrk in a grace sequence 870 | s.linebrk = 0 # 1 if next measure should start with a line break 871 | s.nextdecos = [] # decorations for the next note 872 | s.prevmsre = None # the previous measure 873 | s.supports_tag = 0 # issue supports-tag in xml file when abc uses explicit linebreaks 874 | s.staveDefs = [] # collected %%staves or %%score instructions from score 875 | s.staves = [] # staves = [[voice names to be merged into one stave]] 876 | s.groups = [] # list of merged part names with interspersed {[ and }] 877 | s.grands = [] # [[vid1, vid2, ..], ...] voiceIds to be merged in a grand staff 878 | s.gStaffNums = {} # map each voice id in a grand staff to a staff number 879 | s.gNstaves = {} # map each voice id in a grand staff to total number of staves 880 | s.pageFmtAbc = [] # formatting from abc directives 881 | s.mdur = (4,4) # duration of one measure 882 | s.gtrans = 0 # octave transposition (by clef) 883 | s.midprg = ['', '', '', ''] # MIDI channel nr, program nr, volume, panning for the current part 884 | s.vid = '' # abc voice id for the current voice 885 | s.pid = '' # xml part id for the current voice 886 | s.gcue_on = 0 # insert tag in each note 887 | s.percVoice = 0 # 1 if percussion enabled 888 | s.percMap = {} # (part-id, abc_pitch, xml-octave) -> (abc staff step, midi note number, xml notehead) 889 | s.pMapFound = 0 # at least one I:percmap has been found 890 | s.vcepid = {} # voice_id -> part_id 891 | s.midiInst = {} # inst_id -> (part_id, voice_id, channel, midi_number), remember instruments used 892 | s.capo = 0 # fret position of the capodastro 893 | s.tunmid = [] # midi numbers of strings 894 | s.tunTup = [] # ordered midi numbers of strings [(midi_num, string_num), ...] (midi_num from high to low) 895 | s.fOpt = fOpt # force string/fret allocations for tab staves 896 | s.orderChords = 0 # order notes in a chord 897 | s.chordDecos = {} # decos that should be distributed to all chord notes for xml 898 | ch10 = 'acoustic-bass-drum,35;bass-drum-1,36;side-stick,37;acoustic-snare,38;hand-clap,39;electric-snare,40;low-floor-tom,41;closed-hi-hat,42;high-floor-tom,43;pedal-hi-hat,44;low-tom,45;open-hi-hat,46;low-mid-tom,47;hi-mid-tom,48;crash-cymbal-1,49;high-tom,50;ride-cymbal-1,51;chinese-cymbal,52;ride-bell,53;tambourine,54;splash-cymbal,55;cowbell,56;crash-cymbal-2,57;vibraslap,58;ride-cymbal-2,59;hi-bongo,60;low-bongo,61;mute-hi-conga,62;open-hi-conga,63;low-conga,64;high-timbale,65;low-timbale,66;high-agogo,67;low-agogo,68;cabasa,69;maracas,70;short-whistle,71;long-whistle,72;short-guiro,73;long-guiro,74;claves,75;hi-wood-block,76;low-wood-block,77;mute-cuica,78;open-cuica,79;mute-triangle,80;open-triangle,81' 899 | s.percsnd = [x.split (',') for x in ch10.split (';')] # {name -> midi number} of standard channel 10 sound names 900 | s.gTime = (0,0) # (XML begin time, XML end time) in divisions 901 | s.tabStaff = '' # == pid (part ID) for a tab staff 902 | 903 | def mkPitch (s, acc, note, oct, lev): 904 | if s.percVoice: # percussion map switched off by perc=off (see doClef) 905 | octq = int (oct) + s.gtrans # honour the octave= transposition when querying percmap 906 | tup = s.percMap.get ((s.pid, acc+note, octq), s.percMap.get (('', acc+note, octq), 0)) 907 | if tup: step, soct, midi, notehead = tup 908 | else: step, soct = note, octq 909 | octnum = (4 if step.upper() == step else 5) + int (soct) 910 | if not tup: # add percussion map for unmapped notes in this part 911 | midi = str (octnum * 12 + [0,2,4,5,7,9,11]['CDEFGAB'.index (step.upper())] + {'^':1,'_':-1}.get (acc, 0) + 12) 912 | notehead = {'^':'x', '_':'circle-x'}.get (acc, 'normal') 913 | if s.pMapFound: info ('no I:percmap for: %s%s in part %s, voice %s' % (acc+note, -oct*',' if oct<0 else oct*"'", s.pid, s.vid)) 914 | s.percMap [(s.pid, acc+note, octq)] = (note, octq, midi, notehead) 915 | else: # correct step value for clef 916 | step, octnum = stepTrans (step.upper (), octnum, s.curClef) 917 | pitch = E.Element ('unpitched') 918 | addElemT (pitch, 'display-step', step.upper (), lev + 1) 919 | addElemT (pitch, 'display-octave', str (octnum), lev + 1) 920 | return pitch, '', midi, notehead 921 | nUp = note.upper () 922 | octnum = (4 if nUp == note else 5) + int (oct) + s.gtrans 923 | pitch = E.Element ('pitch') 924 | addElemT (pitch, 'step', nUp, lev + 1) 925 | alter = '' 926 | if (note, oct) in s.ties: 927 | tied_alter, _, vnum, _ = s.ties [(note,oct)] # vnum = overlay voice number when tie started 928 | if vnum == s.overlayVnum: alter = tied_alter # tied note in the same overlay -> same alteration 929 | elif acc: 930 | s.msreAlts [(nUp, octnum)] = s.alterTab [acc] 931 | alter = s.alterTab [acc] # explicit notated alteration 932 | elif (nUp, octnum) in s.msreAlts: alter = s.msreAlts [(nUp, octnum)] # temporary alteration 933 | elif nUp in s.keyAlts: alter = s.keyAlts [nUp] # alteration implied by the key 934 | if alter: addElemT (pitch, 'alter', alter, lev + 1) 935 | addElemT (pitch, 'octave', str (octnum), lev + 1) 936 | return pitch, alter, '', '' 937 | 938 | def getNoteDecos (s, n): 939 | decos = s.nextdecos # decorations encountered so far 940 | ndeco = getattr (n, 'deco', 0) # possible decorations of notes of a chord 941 | if ndeco: # add decorations, translate used defined symbols 942 | decos += [s.usrSyms.get (d, d).strip ('!+') for d in ndeco.t] 943 | s.nextdecos = [] 944 | if s.tabStaff == s.pid and s.fOpt and n.name != 'rest': # force fret/string allocation if explicit string decoration is missing 945 | if [d for d in decos if d in '0123456789'] == []: decos.append ('0') 946 | return decos 947 | 948 | def mkNote (s, n, lev): 949 | isgrace = getattr (n, 'grace', '') 950 | ischord = getattr (n, 'chord', '') 951 | if s.ntup >= 0 and not isgrace and not ischord: 952 | s.ntup -= 1 # count tuplet notes only on non-chord, non grace notes 953 | if s.ntup == -1 and s.trem <= 0: 954 | s.intrem = 0 # tremolo pair ends at first note that is not a new tremolo pair (s.trem > 0) 955 | nnum, nden = n.dur.t # abc dutation of note 956 | if s.intrem: nnum += nnum # double duration of tremolo duplets 957 | if nden == 0: nden = 1 # occurs with illegal ABC like: "A2 1". Now interpreted as A2/1 958 | num, den = simplify (nnum * s.unitLcur[0], nden * s.unitLcur[1]) # normalised with unit length 959 | if den > 64: # limit denominator to 64 960 | num = int (round (64 * float (num) / den)) # scale note to num/64 961 | num, den = simplify (max ([num, 1]), 64) # smallest num == 1 962 | info ('duration too small: rounded to %d/%d' % (num, den)) 963 | if n.name == 'rest' and ('Z' in n.t or 'X' in n.t): 964 | num, den = s.mdur # duration of one measure 965 | noMsrRest = not (n.name == 'rest' and (num, den) == s.mdur) # not a measure rest 966 | dvs = (4 * s.divisions * num) // den # divisions is xml-duration of 1/4 967 | rdvs = dvs # real duration (will be 0 for chord/grace) 968 | num, den = simplify (num, den * 4) # scale by 1/4 for s.typeMap 969 | ndot = 0 970 | if num == 3 and noMsrRest: ndot = 1; den = den // 2 # look for dotted notes 971 | if num == 7 and noMsrRest: ndot = 2; den = den // 4 972 | nt = E.Element ('note') 973 | if isgrace: # a grace note (and possibly a chord note) 974 | grace = E.Element ('grace') 975 | if s.acciatura: grace.set ('slash', 'yes'); s.acciatura = 0 976 | addElem (nt, grace, lev + 1) 977 | dvs = rdvs = 0 # no (real) duration for a grace note 978 | if den <= 16: den = 32 # not longer than 1/8 for a grace note 979 | if s.gcue_on: # insert cue tag 980 | cue = E.Element ('cue') 981 | addElem (nt, cue, lev + 1) 982 | if ischord: # a chord note 983 | chord = E.Element ('chord') 984 | addElem (nt, chord, lev + 1) 985 | rdvs = 0 # chord notes no real duration 986 | if den not in s.typeMap: # take the nearest smaller legal duration 987 | info ('illegal duration %d/%d' % (nnum, nden)) 988 | den = min (x for x in s.typeMap.keys () if x > den) 989 | xmltype = str (s.typeMap [den]) # xml needs the note type in addition to duration 990 | acc, step, oct = '', 'C', '0' # abc-notated pitch elements (accidental, pitch step, octave) 991 | alter, midi, notehead = '', '', '' # xml alteration 992 | if n.name == 'rest': 993 | if 'x' in n.t or 'X' in n.t: nt.set ('print-object', 'no') 994 | rest = E.Element ('rest') 995 | if not noMsrRest: rest.set ('measure', 'yes') 996 | addElem (nt, rest, lev + 1) 997 | else: 998 | p = n.pitch.t # get pitch elements from parsed tokens 999 | if len (p) == 3: acc, step, oct = p 1000 | else: step, oct = p 1001 | pitch, alter, midi, notehead = s.mkPitch (acc, step, oct, lev + 1) 1002 | if midi: acc = '' # erase accidental for percussion notes 1003 | addElem (nt, pitch, lev + 1) 1004 | if s.ntup >= 0: # modify duration for tuplet notes 1005 | dvs = dvs * s.tmden // s.tmnum 1006 | if dvs: 1007 | addElemT (nt, 'duration', str (dvs), lev + 1) # skip when dvs == 0, requirement of musicXML 1008 | if not ischord: s.gTime = s.gTime [1], s.gTime [1] + dvs 1009 | ptup = (step, oct) # pitch tuple without alteration to check for ties 1010 | tstop = ptup in s.ties and s.ties[ptup][2] == s.overlayVnum # open tie on this pitch tuple in this overlay 1011 | if tstop: 1012 | tie = E.Element ('tie', type='stop') 1013 | addElem (nt, tie, lev + 1) 1014 | if getattr (n, 'tie', 0): 1015 | tie = E.Element ('tie', type='start') 1016 | addElem (nt, tie, lev + 1) 1017 | if (s.midprg != ['', '', '', ''] or midi) and n.name != 'rest': # only add when %%midi was present or percussion 1018 | instId = 'I%s-%s' % (s.pid, 'X' + midi if midi else s.vid) 1019 | chan, midi = ('10', midi) if midi else s.midprg [:2] 1020 | inst = E.Element ('instrument', id=instId) # instrument id for midi 1021 | addElem (nt, inst, lev + 1) 1022 | if instId not in s.midiInst: s.midiInst [instId] = (s.pid, s.vid, chan, midi, s.midprg [2], s.midprg [3]) # for instrument list in mkScorePart 1023 | addElemT (nt, 'voice', '1', lev + 1) # default voice, for merging later 1024 | if noMsrRest: addElemT (nt, 'type', xmltype, lev + 1) # add note type if not a measure rest 1025 | for i in range (ndot): # add dots 1026 | dot = E.Element ('dot') 1027 | addElem (nt, dot, lev + 1) 1028 | decos = s.getNoteDecos (n) # get decorations for this note 1029 | if acc and not tstop: # only add accidental if note not tied 1030 | e = E.Element ('accidental') 1031 | if 'courtesy' in decos: 1032 | e.set ('parentheses', 'yes') 1033 | decos.remove ('courtesy') 1034 | e.text = s.accTab [acc] 1035 | addElem (nt, e, lev + 1) 1036 | tupnotation = '' # start/stop notation element for tuplets 1037 | if s.ntup >= 0: # add time modification element for tuplet notes 1038 | tmod = mkTmod (s.tmnum, s.tmden, lev + 1) 1039 | addElem (nt, tmod, lev + 1) 1040 | if s.ntup > 0 and not s.tupnts: tupnotation = 'start' 1041 | s.tupnts.append ((rdvs, tmod)) # remember all tuplet modifiers with corresp. durations 1042 | if s.ntup == 0: # last tuplet note (and possible chord notes there after) 1043 | if rdvs: tupnotation = 'stop' # only insert notation in the real note (rdvs > 0) 1044 | s.cmpNormType (rdvs, lev + 1) # compute and/or add normal-type elements (-> s.ntype) 1045 | hasStem = 1 1046 | if not ischord: s.chordDecos = {} # clear on non chord note 1047 | if 'stemless' in decos or (s.nostems and n.name != 'rest') or 'stemless' in s.chordDecos: 1048 | hasStem = 0 1049 | addElemT (nt, 'stem', 'none', lev + 1) 1050 | if 'stemless' in decos: decos.remove ('stemless') # do not handle in doNotations 1051 | if hasattr (n, 'pitches'): s.chordDecos ['stemless'] = 1 # set on first chord note 1052 | if notehead: 1053 | nh = addElemT (nt, 'notehead', re.sub (r'[+-]$', '', notehead), lev + 1) 1054 | if notehead[-1] in '+-': nh.set ('filled', 'yes' if notehead[-1] == '+' else 'no') 1055 | gstaff = s.gStaffNums.get (s.vid, 0) # staff number of the current voice 1056 | if gstaff: addElemT (nt, 'staff', str (gstaff), lev + 1) 1057 | if hasStem: s.doBeams (n, nt, den, lev + 1) # no stems -> no beams in a tab staff 1058 | s.doNotations (n, decos, ptup, alter, tupnotation, tstop, nt, lev + 1) 1059 | if n.objs: s.doLyr (n, nt, lev + 1) 1060 | else: s.prevLyric = {} # clear on note without lyrics 1061 | return nt 1062 | 1063 | def cmpNormType (s, rdvs, lev): # compute the normal-type of a tuplet (only needed for Finale) 1064 | if rdvs: # the last real tuplet note (chord notes can still follow afterwards with rdvs == 0) 1065 | durs = [dur for dur, tmod in s.tupnts if dur > 0] 1066 | ndur = sum (durs) // s.tmnum # duration of the normal type 1067 | s.irrtup = any ((dur != ndur) for dur in durs) # irregular tuplet 1068 | tix = 16 * s.divisions // ndur # index in typeMap of normal-type duration 1069 | if tix in s.typeMap: 1070 | s.ntype = str (s.typeMap [tix]) # the normal-type 1071 | else: s.irrtup = 0 # give up, no normal type possible 1072 | if s.irrtup: # only add normal-type for irregular tuplets 1073 | for dur, tmod in s.tupnts: # add normal-type to all modifiers 1074 | addElemT (tmod, 'normal-type', s.ntype, lev + 1) 1075 | s.tupnts = [] # reset the tuplet buffer 1076 | 1077 | def doNotations (s, n, decos, ptup, alter, tupnotation, tstop, nt, lev): 1078 | slurs = getattr (n, 'slurs', 0) # slur ends 1079 | pts = getattr (n, 'pitches', []) # all chord notes available in the first note 1080 | ov = s.overlayVnum # current overlay voice number (0 for the main voice) 1081 | if pts: # make list of pitches in chord: [(pitch, octave), ..] 1082 | if type (pts.pitch) == pObj: pts = [pts.pitch] # chord with one note 1083 | else: pts = [tuple (p.t[-2:]) for p in pts.pitch] # normal chord 1084 | for pt, (tie_alter, nts, vnum, ntelm) in sorted (list (s.ties.items ())): # scan all open ties and delete illegal ones 1085 | if vnum != s.overlayVnum: continue # tie belongs to different overlay 1086 | if pts and pt in pts: continue # pitch tuple of tie exists in chord 1087 | if getattr (n, 'chord', 0): continue # skip chord notes 1088 | if pt == ptup: continue # skip correct single note tie 1089 | if getattr (n, 'grace', 0): continue # skip grace notes 1090 | info ('tie between different pitches: %s%s converted to slur' % pt) 1091 | del s.ties [pt] # remove the note from pending ties 1092 | e = [t for t in ntelm.findall ('tie') if t.get ('type') == 'start'][0] # get the tie start element 1093 | ntelm.remove (e) # delete start tie element 1094 | e = [t for t in nts.findall ('tied') if t.get ('type') == 'start'][0] # get the tied start element 1095 | e.tag = 'slur' # convert tie into slur 1096 | slurnum = pushSlur (s.slurstack, ov) 1097 | e.set ('number', str (slurnum)) 1098 | if slurs: slurs.t.append (')') # close slur on this note 1099 | else: slurs = pObj ('slurs', [')']) 1100 | tstart = getattr (n, 'tie', 0) # start a new tie 1101 | if not (tstop or tstart or decos or slurs or s.slurbeg or tupnotation or s.trem): return nt 1102 | nots = E.Element ('notations') # notation element needed 1103 | if s.trem: # +/- => tuple tremolo sequence / single note tremolo 1104 | if s.trem < 0: tupnotation = 'single'; s.trem = -s.trem 1105 | if not tupnotation: return # only add notation at first or last note of a tremolo sequence 1106 | orn = E.Element ('ornaments') 1107 | trm = E.Element ('tremolo', type=tupnotation) # type = start, stop or single 1108 | trm.text = str (s.trem) # the number of bars in a tremolo note 1109 | addElem (nots, orn, lev + 1) 1110 | addElem (orn, trm, lev + 2) 1111 | if tupnotation == 'stop' or tupnotation == 'single': s.trem = 0 1112 | elif tupnotation: # add tuplet type 1113 | tup = E.Element ('tuplet', type=tupnotation) 1114 | if tupnotation == 'start': tup.set ('bracket', 'yes') 1115 | addElem (nots, tup, lev + 1) 1116 | if tstop: # stop tie 1117 | del s.ties[ptup] # remove flag 1118 | tie = E.Element ('tied', type='stop') 1119 | addElem (nots, tie, lev + 1) 1120 | if tstart: # start a tie 1121 | s.ties[ptup] = (alter, nots, s.overlayVnum, nt) # remember pitch tuple to stop tie and apply same alteration 1122 | tie = E.Element ('tied', type='start') 1123 | if tstart.t[0] == '.-': tie.set ('line-type', 'dotted') 1124 | addElem (nots, tie, lev + 1) 1125 | if decos: # look for slurs and decorations 1126 | slurMap = { '(':1, '.(':1, '(,':1, "('":1, '.(,':1, ".('":1 } 1127 | arts = [] # collect articulations 1128 | for d in decos: # do all slurs and decos 1129 | if d in slurMap: s.slurbeg.append (d); continue # slurs made in while loop at the end 1130 | elif d == 'fermata' or d == 'H': 1131 | ntn = E.Element ('fermata', type='upright') 1132 | elif d == 'arpeggio': 1133 | ntn = E.Element ('arpeggiate', number='1') 1134 | elif d in ['~(', '~)']: 1135 | if d[1] == '(': tp = 'start'; s.glisnum += 1; gn = s.glisnum 1136 | else: tp = 'stop'; gn = s.glisnum; s.glisnum -= 1 1137 | if s.glisnum < 0: s.glisnum = 0; continue # stop without previous start 1138 | ntn = E.Element ('glissando', {'line-type':'wavy', 'number':'%d' % gn, 'type':tp}) 1139 | elif d in ['-(', '-)']: 1140 | if d[1] == '(': tp = 'start'; s.slidenum += 1; gn = s.slidenum 1141 | else: tp = 'stop'; gn = s.slidenum; s.slidenum -= 1 1142 | if s.slidenum < 0: s.slidenum = 0; continue # stop without previous start 1143 | ntn = E.Element ('slide', {'line-type':'solid', 'number':'%d' % gn, 'type':tp}) 1144 | else: arts.append (d); continue 1145 | addElem (nots, ntn, lev + 1) 1146 | if arts: # do only note articulations and collect staff annotations in xmldecos 1147 | rest = s.doArticulations (nt, nots, arts, lev + 1) 1148 | if rest: info ('unhandled note decorations: %s' % rest) 1149 | if slurs: # these are only slur endings 1150 | for d in slurs.t: # slurs to be closed on this note 1151 | if not s.slurstack.get (ov, 0): break # no more open old slurs for this (overlay) voice 1152 | slurnum = s.slurstack [ov].pop () 1153 | slur = E.Element ('slur', number='%d' % slurnum, type='stop') 1154 | addElem (nots, slur, lev + 1) 1155 | while s.slurbeg: # create slurs beginning on this note 1156 | stp = s.slurbeg.pop (0) 1157 | slurnum = pushSlur (s.slurstack, ov) 1158 | ntn = E.Element ('slur', number='%d' % slurnum, type='start') 1159 | if '.' in stp: ntn.set ('line-type', 'dotted') 1160 | if ',' in stp: ntn.set ('placement', 'below') 1161 | if "'" in stp: ntn.set ('placement', 'above') 1162 | addElem (nots, ntn, lev + 1) 1163 | if list (nots) != []: # only add notations if not empty 1164 | addElem (nt, nots, lev) 1165 | 1166 | def doArticulations (s, nt, nots, arts, lev): 1167 | decos = [] 1168 | for a in arts: 1169 | if a in s.artMap: 1170 | art = E.Element ('articulations') 1171 | addElem (nots, art, lev) 1172 | addElem (art, E.Element (s.artMap[a]), lev + 1) 1173 | elif a in s.ornMap: 1174 | orn = E.Element ('ornaments') 1175 | addElem (nots, orn, lev) 1176 | addElem (orn, E.Element (s.ornMap[a]), lev + 1) 1177 | elif a in ['trill(','trill)']: 1178 | orn = E.Element ('ornaments') 1179 | addElem (nots, orn, lev) 1180 | type = 'start' if a.endswith ('(') else 'stop' 1181 | if type == 'start': addElem (orn, E.Element ('trill-mark'), lev + 1) 1182 | addElem (orn, E.Element ('wavy-line', type=type), lev + 1) 1183 | elif a in s.tecMap: 1184 | tec = E.Element ('technical') 1185 | addElem (nots, tec, lev) 1186 | addElem (tec, E.Element (s.tecMap[a]), lev + 1) 1187 | elif a in '0123456': 1188 | tec = E.Element ('technical') 1189 | addElem (nots, tec, lev) 1190 | if s.tabStaff == s.pid: # current voice belongs to a tabStaff 1191 | alt = int (nt.findtext ('pitch/alter') or 0) # find midi number of current note 1192 | step = nt.findtext ('pitch/step') 1193 | oct = int (nt.findtext ('pitch/octave')) 1194 | midi = oct * 12 + [0,2,4,5,7,9,11]['CDEFGAB'.index (step)] + alt + 12 1195 | if a == '0': # no string annotation: find one 1196 | firstFit = '' 1197 | for smid, istr in s.tunTup: # midi numbers of open strings from high to low 1198 | if midi >= smid: # highest open string where this note can be played 1199 | isvrij = s.strAlloc.isVrij (istr - 1, s.gTime [0], s.gTime [1]) 1200 | a = str (istr) # string number 1201 | if not firstFit: firstFit = a 1202 | if isvrij: break 1203 | if not isvrij: # no free string, take the first fit (lowest fret) 1204 | a = firstFit 1205 | s.strAlloc.bezet (int (a) - 1, s.gTime [0], s.gTime [1]) 1206 | else: # force annotated string number 1207 | s.strAlloc.bezet (int (a) - 1, s.gTime [0], s.gTime [1]) 1208 | bmidi = s.tunmid [int (a) - 1] # midi number of allocated string (with capodastro) 1209 | fret = midi - bmidi # fret position (respecting capodastro) 1210 | if fret < 25 and fret >= 0: 1211 | addElemT (tec, 'fret', str (fret), lev + 1) 1212 | else: 1213 | altp = 'b' if alt == -1 else '#' if alt == 1 else '' 1214 | info ('fret %d out of range, note %s%d on string %s' % (fret, step+altp, oct, a)) 1215 | addElemT (tec, 'string', a, lev + 1) 1216 | else: 1217 | addElemT (tec, 'fingering', a, lev + 1) 1218 | else: decos.append (a) # return staff annotations 1219 | return decos 1220 | 1221 | def doLyr (s, n, nt, lev): 1222 | for i, lyrobj in enumerate (n.objs): 1223 | lyrel = E.Element ('lyric', number = str (i + 1)) 1224 | if lyrobj.name == 'syl': 1225 | dash = len (lyrobj.t) == 2 1226 | if dash: 1227 | if i in s.lyrdash: type = 'middle' 1228 | else: type = 'begin'; s.lyrdash [i] = 1 1229 | else: 1230 | if i in s.lyrdash: type = 'end'; del s.lyrdash [i] 1231 | else: type = 'single' 1232 | addElemT (lyrel, 'syllabic', type, lev + 1) 1233 | txt = lyrobj.t[0] # the syllabe 1234 | txt = re.sub (r'(? continue 1243 | pext.set ('type', 'continue') 1244 | ext = E.Element ('extend', type = 'stop') # always stop on current extend 1245 | addElem (lyrel, ext, lev + 1) 1246 | elif lyrobj.name == 'ext': info ('lyric extend error'); continue 1247 | else: continue # skip other lyric elements or errors 1248 | addElem (nt, lyrel, lev) 1249 | s.prevLyric [i] = lyrel # for extension (melisma) on the next note 1250 | 1251 | def doBeams (s, n, nt, den, lev): 1252 | if hasattr (n, 'chord') or hasattr (n, 'grace'): 1253 | s.grcbbrk = s.grcbbrk or n.bbrk.t[0] # remember if there was any bbrk in or before a grace sequence 1254 | return 1255 | bbrk = s.grcbbrk or n.bbrk.t[0] or den < 32 1256 | s.grcbbrk = False 1257 | if not s.prevNote: pbm = None 1258 | else: pbm = s.prevNote.find ('beam') 1259 | bm = E.Element ('beam', number='1') 1260 | bm.text = 'begin' 1261 | if pbm != None: 1262 | if bbrk: 1263 | if pbm.text == 'begin': 1264 | s.prevNote.remove (pbm) 1265 | elif pbm.text == 'continue': 1266 | pbm.text = 'end' 1267 | s.prevNote = None 1268 | else: bm.text = 'continue' 1269 | if den >= 32 and n.name != 'rest': 1270 | addElem (nt, bm, lev) 1271 | s.prevNote = nt 1272 | 1273 | def stopBeams (s): 1274 | if not s.prevNote: return 1275 | pbm = s.prevNote.find ('beam') 1276 | if pbm != None: 1277 | if pbm.text == 'begin': 1278 | s.prevNote.remove (pbm) 1279 | elif pbm.text == 'continue': 1280 | pbm.text = 'end' 1281 | s.prevNote = None 1282 | 1283 | def staffDecos (s, decos, maat, lev): 1284 | gstaff = s.gStaffNums.get (s.vid, 0) # staff number of the current voice 1285 | for d in decos: 1286 | d = s.usrSyms.get (d, d).strip ('!+') # try to replace user defined symbol 1287 | if d in s.dynaMap: 1288 | dynel = E.Element ('dynamics') 1289 | addDirection (maat, dynel, lev, gstaff, [E.Element (d)], 'below', s.gcue_on) 1290 | elif d in s.wedgeMap: # wedge 1291 | if ')' in d: type = 'stop' 1292 | else: type = 'crescendo' if '<' in d or 'crescendo' in d else 'diminuendo' 1293 | addDirection (maat, E.Element ('wedge', type=type), lev, gstaff) 1294 | elif d.startswith ('8v'): 1295 | if 'a' in d: type, plce = 'down', 'above' 1296 | else: type, plce = 'up', 'below' 1297 | if ')' in d: type = 'stop' 1298 | addDirection (maat, E.Element ('octave-shift', type=type, size='8'), lev, gstaff, placement=plce) 1299 | elif d in (['ped','ped-up']): 1300 | type = 'stop' if d.endswith ('up') else 'start' 1301 | addDirection (maat, E.Element ('pedal', type=type), lev, gstaff) 1302 | elif d in ['coda', 'segno']: 1303 | text, attr, val = s.capoMap [d] 1304 | dir = addDirection (maat, E.Element (text), lev, gstaff, placement='above') 1305 | sound = E.Element ('sound'); sound.set (attr, val) 1306 | addElem (dir, sound, lev + 1) 1307 | elif d in s.capoMap: 1308 | text, attr, val = s.capoMap [d] 1309 | words = E.Element ('words'); words.text = text 1310 | dir = addDirection (maat, words, lev, gstaff, placement='above') 1311 | sound = E.Element ('sound'); sound.set (attr, val) 1312 | addElem (dir, sound, lev + 1) 1313 | elif d == '(' or d == '.(': s.slurbeg.append (d) # start slur on next note 1314 | elif d in ['/-','//-','///-','////-']: # duplet tremolo sequence 1315 | s.tmnum, s.tmden, s.ntup, s.trem, s.intrem = 2, 1, 2, len (d) - 1, 1 1316 | elif d in ['/','//','///']: s.trem = - len (d) # single note tremolo 1317 | elif d == 'rbstop': s.rbStop = 1; # sluit een open volta aan het eind van de maat 1318 | else: s.nextdecos.append (d) # keep annotation for the next note 1319 | 1320 | def doFields (s, maat, fieldmap, lev): 1321 | def instDir (midelm, midnum, dirtxt): 1322 | instId = 'I%s-%s' % (s.pid, s.vid) 1323 | words = E.Element ('words'); words.text = dirtxt % midnum 1324 | snd = E.Element ('sound') 1325 | mi = E.Element ('midi-instrument', id=instId) 1326 | dir = addDirection (maat, words, lev, gstaff, placement='above') 1327 | addElem (dir, snd, lev + 1) 1328 | addElem (snd, mi, lev + 2) 1329 | addElemT (mi, midelm, midnum, lev + 3) 1330 | def addTrans (n): 1331 | e = E.Element ('transpose') 1332 | addElemT (e, 'chromatic', n, lev + 2) # n == signed number string given after transpose 1333 | atts.append ((9, e)) 1334 | def doClef (field): 1335 | if re.search (r'perc|map', field): # percussion clef or new style perc=on or map=perc 1336 | r = re.search (r'(perc|map)\s*=\s*(\S*)', field) 1337 | s.percVoice = 0 if r and r.group (2) not in ['on','true','perc'] else 1 1338 | field = re.sub (r'(perc|map)\s*=\s*(\S*)', '', field) # erase the perc= for proper clef matching 1339 | clef, gtrans = 0, 0 1340 | clefn = re.search (r'alto1|alto2|alto4|alto|tenor|bass3|bass|treble|perc|none|tab', field) 1341 | clefm = re.search (r"(?:^m=| m=|middle=)([A-Ga-g])([,']*)", field) 1342 | trans_oct2 = re.search (r'octave=([-+]?\d)', field) 1343 | trans = re.search (r'(?:^t=| t=|transpose=)(-?[\d]+)', field) 1344 | trans_oct = re.search (r'([+-^_])(8|15)', field) 1345 | cue_onoff = re.search (r'cue=(on|off)', field) 1346 | strings = re.search (r"strings=(\S+)", field) 1347 | stafflines = re.search (r'stafflines=\s*(\d)', field) 1348 | capo = re.search (r'capo=(\d+)', field) 1349 | if clefn: 1350 | clef = clefn.group () 1351 | if clefm: 1352 | note, octstr = clefm.groups () 1353 | nUp = note.upper () 1354 | octnum = (4 if nUp == note else 5) + (len (octstr) if "'" in octstr else -len (octstr)) 1355 | gtrans = (3 if nUp in 'AFD' else 4) - octnum 1356 | if clef not in ['perc', 'none']: clef = s.clefLineMap [nUp] 1357 | if clef: 1358 | s.gtrans = gtrans # only change global tranposition when a clef is really defined 1359 | if clef != 'none': s.curClef = clef # keep track of current abc clef (for percmap) 1360 | sign, line = s.clefMap [clef] 1361 | if not sign: return 1362 | c = E.Element ('clef') 1363 | if gstaff: c.set ('number', str (gstaff)) # only add staff number when defined 1364 | addElemT (c, 'sign', sign, lev + 2) 1365 | if line: addElemT (c, 'line', line, lev + 2) 1366 | if trans_oct: 1367 | n = trans_oct.group (1) in '-_' and -1 or 1 1368 | if trans_oct.group (2) == '15': n *= 2 # 8 => 1 octave, 15 => 2 octaves 1369 | addElemT (c, 'clef-octave-change', str (n), lev + 2) # transpose print out 1370 | if trans_oct.group (1) in '+-': s.gtrans += n # also transpose all pitches with one octave 1371 | atts.append ((7, c)) 1372 | if trans_oct2: # octave= can also be in a K: field 1373 | n = int (trans_oct2.group (1)) 1374 | s.gtrans = gtrans + n 1375 | if trans != None: # add transposition in semitones 1376 | e = E.Element ('transpose') 1377 | addElemT (e, 'chromatic', str (trans.group (1)), lev + 3) 1378 | atts.append ((9, e)) 1379 | if cue_onoff: s.gcue_on = cue_onoff.group (1) == 'on' 1380 | nlines = 0 1381 | if clef == 'tab': 1382 | s.tabStaff = s.pid 1383 | if capo: s.capo = int (capo.group (1)) 1384 | if strings: s.tuning = strings.group (1).split (',') 1385 | s.tunmid = [int (boct) * 12 + [0,2,4,5,7,9,11]['CDEFGAB'.index (bstep)] + 12 + s.capo for bstep, boct in s.tuning] 1386 | s.tunTup = sorted (zip (s.tunmid, range (len (s.tunmid), 0, -1)), reverse=1) 1387 | s.tunmid.reverse () 1388 | nlines = str (len (s.tuning)) 1389 | s.strAlloc.setlines (len (s.tuning), s.pid) 1390 | s.nostems = 'nostems' in field # tab clef without stems 1391 | s.diafret = 'diafret' in field # tab with diatonic fretting 1392 | if stafflines or nlines: 1393 | e = E.Element ('staff-details') 1394 | if gstaff: e.set ('number', str (gstaff)) # only add staff number when defined 1395 | if not nlines: nlines = stafflines.group (1) 1396 | addElemT (e, 'staff-lines', nlines, lev + 2) 1397 | if clef == 'tab': 1398 | for line, t in enumerate (s.tuning): 1399 | st = E.Element ('staff-tuning', line=str(line+1)) 1400 | addElemT (st, 'tuning-step', t[0], lev + 3) 1401 | addElemT (st, 'tuning-octave', t[1], lev + 3) 1402 | addElem (e, st, lev + 2) 1403 | if s.capo: addElemT (e, 'capo', str (s.capo), lev + 2) 1404 | atts.append ((8, e)) 1405 | s.diafret = 0 # chromatic fretting is default 1406 | atts = [] # collect xml attribute elements [(order-number, xml-element), ..] 1407 | gstaff = s.gStaffNums.get (s.vid, 0) # staff number of the current voice 1408 | for ftype, field in fieldmap.items (): 1409 | if not field: # skip empty fields 1410 | continue 1411 | if ftype == 'Div': # not an abc field, but handled as if 1412 | d = E.Element ('divisions') 1413 | d.text = field 1414 | atts.append ((1, d)) 1415 | elif ftype == 'gstaff': # make grand staff 1416 | e = E.Element ('staves') 1417 | e.text = str (field) 1418 | atts.append ((4, e)) 1419 | elif ftype == 'M': 1420 | if field == 'none': continue 1421 | if field == 'C': field = '4/4' 1422 | elif field == 'C|': field = '2/2' 1423 | t = E.Element ('time') 1424 | if '/' not in field: 1425 | info ('M:%s not recognized, 4/4 assumed' % field) 1426 | field = '4/4' 1427 | beats, btype = field.split ('/')[:2] 1428 | try: s.mdur = simplify (eval (beats), int (btype)) # measure duration for Z and X rests (eval allows M:2+3/4) 1429 | except: 1430 | info ('error in M:%s, 4/4 assumed' % field) 1431 | s.mdur = (4,4) 1432 | beats, btype = '4','4' 1433 | addElemT (t, 'beats', beats, lev + 2) 1434 | addElemT (t, 'beat-type', btype, lev + 2) 1435 | atts.append ((3, t)) 1436 | elif ftype == 'K': 1437 | accs = ['F','C','G','D','A','E','B'] # == s.sharpness [7:14] 1438 | mode = '' 1439 | key = re.match (r'\s*([A-G][#b]?)\s*([a-zA-Z]*)', field) 1440 | alts = re.search (r'\s((\s?[=^_][A-Ga-g])+)', ' ' + field) # avoid matching middle=G and m=G 1441 | if key: 1442 | key, mode = key.groups () 1443 | mode = mode.lower ()[:3] # only first three chars, no case 1444 | if mode not in s.offTab: mode = 'maj' 1445 | fifths = s.sharpness.index (key) - s.offTab [mode] 1446 | if fifths >= 0: s.keyAlts = dict (zip (accs[:fifths], fifths * ['1'])) 1447 | else: s.keyAlts = dict (zip (accs[fifths:], -fifths * ['-1'])) 1448 | elif field.startswith ('none') or field == '': # the default key 1449 | fifths = 0 1450 | mode = 'maj' 1451 | if alts: 1452 | alts = re.findall (r'[=^_][A-Ga-g]', alts.group(1)) # list of explicit alterations 1453 | alts = [(x[1], s.alterTab [x[0]]) for x in alts] # [step, alter] 1454 | for step, alter in alts: # correct permanent alterations for this key 1455 | s.keyAlts [step.upper ()] = alter 1456 | k = E.Element ('key') 1457 | koctave = [] 1458 | lowerCaseSteps = [step.upper () for step, alter in alts if step.islower ()] 1459 | for step, alter in sorted (list (s.keyAlts.items ())): 1460 | if alter == '0': # skip neutrals 1461 | del s.keyAlts [step.upper ()] # otherwise you get neutral signs on normal notes 1462 | continue 1463 | addElemT (k, 'key-step', step.upper (), lev + 2) 1464 | addElemT (k, 'key-alter', alter, lev + 2) 1465 | koctave.append ('5' if step in lowerCaseSteps else '4') 1466 | if koctave: # only key signature if not empty 1467 | for oct in koctave: 1468 | e = E.Element ('key-octave', number=oct) 1469 | addElem (k, e, lev + 2) 1470 | atts.append ((2, k)) 1471 | elif mode: 1472 | k = E.Element ('key') 1473 | addElemT (k, 'fifths', str (fifths), lev + 2) 1474 | addElemT (k, 'mode', s.modTab [mode], lev + 2) 1475 | atts.append ((2, k)) 1476 | doClef (field) 1477 | elif ftype == 'L': 1478 | try: s.unitLcur = lmap (int, field.split ('/')) 1479 | except: s.unitLcur = (1,8) 1480 | if len (s.unitLcur) == 1 or s.unitLcur[1] not in s.typeMap: 1481 | info ('L:%s is not allowed, 1/8 assumed' % field) 1482 | s.unitLcur = 1,8 1483 | elif ftype == 'V': 1484 | doClef (field) 1485 | elif ftype == 'I': 1486 | s.doField_I (ftype, field, instDir, addTrans) 1487 | elif ftype == 'Q': 1488 | s.doTempo (maat, field, lev) 1489 | elif ftype == 'P': # ad hoc translation of P: into a staff text direction 1490 | words = E.Element ('rehearsal') 1491 | words.set ('font-weight', 'bold') 1492 | words.text = field 1493 | addDirection (maat, words, lev, gstaff, placement='above') 1494 | elif ftype in 'TCOAZNGHRBDFSU': 1495 | info ('**illegal header field in body: %s, content: %s' % (ftype, field)) 1496 | else: 1497 | info ('unhandled field: %s, content: %s' % (ftype, field)) 1498 | 1499 | if atts: 1500 | att = E.Element ('attributes') # insert sub elements in the order required by musicXML 1501 | addElem (maat, att, lev) 1502 | for _, att_elem in sorted (atts, key=lambda x: x[0]): # ordering ! 1503 | addElem (att, att_elem, lev + 1) 1504 | if s.diafret: 1505 | other = E.Element ('other-direction'); other.text = 'diatonic fretting' 1506 | addDirection (maat, other, lev, 0) 1507 | 1508 | def doTempo (s, maat, field, lev): 1509 | gstaff = s.gStaffNums.get (s.vid, 0) # staff number of the current voice 1510 | t = re.search (r'(\d)/(\d\d?)\s*=\s*(\d[.\d]*)|(\d[.\d]*)', field) 1511 | rtxt = re.search (r'"([^"]*)"', field) # look for text in Q: field 1512 | if not t and not rtxt: return 1513 | elems = [] # [(element, sub-elements)] will be added as direction-types 1514 | if rtxt: 1515 | num, den, upm = 1, 4, s.tempoMap.get (rtxt.group (1).lower ().strip (), 120) 1516 | words = E.Element ('words'); words.text = rtxt.group (1) 1517 | elems.append ((words, [])) 1518 | if t: 1519 | try: 1520 | if t.group (4): num, den, upm = 1, s.unitLcur[1] , float (t.group (4)) # old syntax Q:120 1521 | else: num, den, upm = int (t.group (1)), int (t.group (2)), float (t.group (3)) 1522 | except: info ('conversion error: %s' % field); return 1523 | num, den = simplify (num, den); 1524 | dotted, den_not = (1, den // 2) if num == 3 else (0, den) 1525 | metro = E.Element ('metronome') 1526 | u = E.Element ('beat-unit'); u.text = s.typeMap.get (4 * den_not, 'quarter') 1527 | pm = E.Element ('per-minute'); pm.text = ('%.2f' % upm).rstrip ('0').rstrip ('.') 1528 | subelms = [u, E.Element ('beat-unit-dot'), pm] if dotted else [u, pm] 1529 | elems.append ((metro, subelms)) 1530 | dir = addDirection (maat, elems, lev, gstaff, [], placement='above') 1531 | if num != 1 and num != 3: info ('in Q: numerator in %d/%d not supported' % (num, den)) 1532 | qpm = 4. * num * upm / den 1533 | sound = E.Element ('sound'); sound.set ('tempo', '%.2f' % qpm) 1534 | addElem (dir, sound, lev + 1) 1535 | 1536 | def mkBarline (s, maat, loc, lev, style='', dir='', ending=''): 1537 | b = E.Element ('barline', location=loc) 1538 | if style: 1539 | addElemT (b, 'bar-style', style, lev + 1) 1540 | if s.curVolta: # first stop a current volta 1541 | end = E.Element ('ending', number=s.curVolta, type='stop') 1542 | s.curVolta = '' 1543 | if loc == 'left': # stop should always go to a right barline 1544 | bp = E.Element ('barline', location='right') 1545 | addElem (bp, end, lev + 1) 1546 | addElem (s.prevmsre, bp, lev) # prevmsre has no right barline! (ending would have stopped there) 1547 | else: 1548 | addElem (b, end, lev + 1) 1549 | if ending: 1550 | ending = ending.replace ('-',',') # MusicXML only accepts comma's 1551 | endtxt = '' 1552 | if ending.startswith ('"'): # ending is a quoted string 1553 | endtxt = ending.strip ('"') 1554 | ending = '33' # any number that is not likely to occur elsewhere 1555 | end = E.Element ('ending', number=ending, type='start') 1556 | if endtxt: end.text = endtxt # text appears in score in stead of number attribute 1557 | addElem (b, end, lev + 1) 1558 | s.curVolta = ending 1559 | if dir: 1560 | r = E.Element ('repeat', direction=dir) 1561 | addElem (b, r, lev + 1) 1562 | addElem (maat, b, lev) 1563 | 1564 | def doChordSym (s, maat, sym, lev): 1565 | alterMap = {'#':'1','=':'0','b':'-1'} 1566 | rnt = sym.root.t 1567 | chord = E.Element ('harmony') 1568 | addElem (maat, chord, lev) 1569 | root = E.Element ('root') 1570 | addElem (chord, root, lev + 1) 1571 | addElemT (root, 'root-step', rnt[0], lev + 2) 1572 | if len (rnt) == 2: addElemT (root, 'root-alter', alterMap [rnt[1]], lev + 2) 1573 | kind = s.chordTab.get (sym.kind.t[0], 'major') if sym.kind.t else 'major' 1574 | addElemT (chord, 'kind', kind, lev + 1) 1575 | if hasattr (sym, 'bass'): 1576 | bnt = sym.bass.t 1577 | bass = E.Element ('bass') 1578 | addElem (chord, bass, lev + 1) 1579 | addElemT (bass, 'bass-step', bnt[0], lev + 2) 1580 | if len (bnt) == 2: addElemT (bass, 'bass-alter', alterMap [bnt[1]], lev + 2) 1581 | degs = getattr (sym, 'degree', '') 1582 | if degs: 1583 | if type (degs) != list_type: degs = [degs] 1584 | for deg in degs: 1585 | deg = deg.t[0] 1586 | if deg[0] == '#': alter = '1'; deg = deg[1:] 1587 | elif deg[0] == 'b': alter = '-1'; deg = deg[1:] 1588 | else: alter = '0'; deg = deg 1589 | degree = E.Element ('degree') 1590 | addElem (chord, degree, lev + 1) 1591 | addElemT (degree, 'degree-value', deg, lev + 2) 1592 | addElemT (degree, 'degree-alter', alter, lev + 2) 1593 | addElemT (degree, 'degree-type', 'add', lev + 2) 1594 | 1595 | def mkMeasure (s, i, t, lev, fieldmap={}): 1596 | s.msreAlts = {} 1597 | s.ntup, s.trem, s.intrem = -1, 0, 0 1598 | s.acciatura = 0 # next grace element gets acciatura attribute 1599 | s.rbStop = 0 # sluit een open volta aan het eind van de maat 1600 | overlay = 0 1601 | maat = E.Element ('measure', number = str(i)) 1602 | if fieldmap: s.doFields (maat, fieldmap, lev + 1) 1603 | if s.linebrk: # there was a line break in the previous measure 1604 | e = E.Element ('print') 1605 | e.set ('new-system', 'yes') 1606 | addElem (maat, e, lev + 1) 1607 | s.linebrk = 0 1608 | for it, x in enumerate (t): 1609 | if x.name == 'note' or x.name == 'rest': 1610 | if x.dur.t[0] == 0: # a leading zero was used for stemmless in abcm2ps, we only support !stemless! 1611 | x.dur.t = tuple ([1, x.dur.t[1]]) 1612 | note = s.mkNote (x, lev + 1) 1613 | addElem (maat, note, lev + 1) 1614 | elif x.name == 'lbar': 1615 | bar = x.t[0] 1616 | if bar == '|' or bar == '[|': pass # skip redundant bar 1617 | elif ':' in bar: # forward repeat 1618 | volta = x.t[1] if len (x.t) == 2 else '' 1619 | s.mkBarline (maat, 'left', lev + 1, style='heavy-light', dir='forward', ending=volta) 1620 | else: # bar must be a volta number 1621 | s.mkBarline (maat, 'left', lev + 1, ending=bar) 1622 | elif x.name == 'rbar': 1623 | bar = x.t[0] 1624 | if bar == '.|': 1625 | s.mkBarline (maat, 'right', lev + 1, style='dotted') 1626 | elif ':' in bar: # backward repeat 1627 | s.mkBarline (maat, 'right', lev + 1, style='light-heavy', dir='backward') 1628 | elif bar == '||': 1629 | s.mkBarline (maat, 'right', lev + 1, style='light-light') 1630 | elif bar == '[|]' or bar == '[]': 1631 | s.mkBarline (maat, 'right', lev + 1, style='none') 1632 | elif '[' in bar or ']' in bar: 1633 | s.mkBarline (maat, 'right', lev + 1, style='light-heavy') 1634 | elif bar == '|' and s.rbStop: # normale barline hoeft niet, behalve om een volta te stoppen 1635 | s.mkBarline (maat, 'right', lev + 1, style='regular') 1636 | elif bar[0] == '&': overlay = 1 1637 | elif x.name == 'tup': 1638 | if len (x.t) == 3: n, into, nts = x.t 1639 | elif len (x.t) == 2: n, into, nts = x.t + [0] 1640 | else: n, into, nts = x.t[0], 0, 0 1641 | if into == 0: into = 3 if n in [2,4,8] else 2 1642 | if nts == 0: nts = n 1643 | s.tmnum, s.tmden, s.ntup = n, into, nts 1644 | elif x.name == 'deco': 1645 | s.staffDecos (x.t, maat, lev + 1) # output staff decos, postpone note decos to next note 1646 | elif x.name == 'text': 1647 | pos, text = x.t[:2] 1648 | place = 'above' if pos == '^' else 'below' 1649 | words = E.Element ('words') 1650 | words.text = text 1651 | gstaff = s.gStaffNums.get (s.vid, 0) # staff number of the current voice 1652 | addDirection (maat, words, lev + 1, gstaff, placement=place) 1653 | elif x.name == 'inline': 1654 | fieldtype, fieldval = x.t[0], ' '.join (x.t[1:]) 1655 | s.doFields (maat, {fieldtype:fieldval}, lev + 1) 1656 | elif x.name == 'accia': s.acciatura = 1 1657 | elif x.name == 'linebrk': 1658 | s.supports_tag = 1 1659 | if it > 0 and t[it -1].name == 'lbar': # we are at start of measure 1660 | e = E.Element ('print') # output linebreak now 1661 | e.set ('new-system', 'yes') 1662 | addElem (maat, e, lev + 1) 1663 | else: 1664 | s.linebrk = 1 # output linebreak at start of next measure 1665 | elif x.name == 'chordsym': 1666 | s.doChordSym (maat, x, lev + 1) 1667 | s.stopBeams () 1668 | s.prevmsre = maat 1669 | return maat, overlay 1670 | 1671 | def mkPart (s, maten, id, lev, attrs, nstaves, rOpt): 1672 | s.slurstack = {} 1673 | s.glisnum = 0; # xml number attribute for glissandos 1674 | s.slidenum = 0; # xml number attribute for slides 1675 | s.unitLcur = s.unitL # set the default unit length at begin of each voice 1676 | s.curVolta = '' 1677 | s.lyrdash = {} 1678 | s.linebrk = 0 1679 | s.midprg = ['', '', '', ''] # MIDI channel nr, program nr, volume, panning for the current part 1680 | s.gcue_on = 0 # reset cue note marker for each new voice 1681 | s.gtrans = 0 # reset octave transposition (by clef) 1682 | s.percVoice = 0 # 1 if percussion clef encountered 1683 | s.curClef = '' # current abc clef (for percmap) 1684 | s.nostems = 0 # for the tab clef 1685 | s.tuning = s.tuningDef # reset string tuning to default 1686 | part = E.Element ('part', id=id) 1687 | s.overlayVnum = 0 # overlay voice number to relate ties that extend from one overlayed measure to the next 1688 | gstaff = s.gStaffNums.get (s.vid, 0) # staff number of the current voice 1689 | attrs_cpy = attrs.copy () # don't change attrs itself in next line 1690 | if gstaff == 1: attrs_cpy ['gstaff'] = nstaves # make a grand staff 1691 | if 'perc' in attrs_cpy.get ('V', ''): del attrs_cpy ['K'] # remove key from percussion voice 1692 | msre, overlay = s.mkMeasure (1, maten[0], lev + 1, attrs_cpy) 1693 | addElem (part, msre, lev + 1) 1694 | for i, maat in enumerate (maten[1:]): 1695 | s.overlayVnum = s.overlayVnum + 1 if overlay else 0 1696 | msre, next_overlay = s.mkMeasure (i+2, maat, lev + 1) 1697 | if overlay: mergePartMeasure (part, msre, s.overlayVnum, rOpt) 1698 | else: addElem (part, msre, lev + 1) 1699 | overlay = next_overlay 1700 | return part 1701 | 1702 | def mkScorePart (s, id, vids_p, partAttr, lev): 1703 | def mkInst (instId, vid, midchan, midprog, midnot, vol, pan, lev): 1704 | si = E.Element ('score-instrument', id=instId) 1705 | pnm = partAttr.get (vid, [''])[0] # part name if present 1706 | addElemT (si, 'instrument-name', pnm or 'dummy', lev + 2) # MuseScore needs a name 1707 | mi = E.Element ('midi-instrument', id=instId) 1708 | if midchan: addElemT (mi, 'midi-channel', midchan, lev + 2) 1709 | if midprog: addElemT (mi, 'midi-program', str (int (midprog) + 1), lev + 2) # compatible with abc2midi 1710 | if midnot: addElemT (mi, 'midi-unpitched', str (int (midnot) + 1), lev + 2) 1711 | if vol: addElemT (mi, 'volume', '%.2f' % (int (vol) / 1.27), lev + 2) 1712 | if pan: addElemT (mi, 'pan', '%.2f' % (int (pan) / 127. * 180 - 90), lev + 2) 1713 | return (si, mi) 1714 | naam, subnm, midprg = partAttr [id] 1715 | sp = E.Element ('score-part', id='P'+id) 1716 | nm = E.Element ('part-name') 1717 | nm.text = naam 1718 | addElem (sp, nm, lev + 1) 1719 | snm = E.Element ('part-abbreviation') 1720 | snm.text = subnm 1721 | if subnm: addElem (sp, snm, lev + 1) # only add if subname was given 1722 | inst = [] 1723 | for instId, (pid, vid, chan, midprg, vol, pan) in sorted (s.midiInst.items ()): 1724 | midprg, midnot = ('0', midprg) if chan == '10' else (midprg, '') 1725 | if pid == id: inst.append (mkInst (instId, vid, chan, midprg, midnot, vol, pan, lev)) 1726 | for si, mi in inst: addElem (sp, si, lev + 1) 1727 | for si, mi in inst: addElem (sp, mi, lev + 1) 1728 | return sp 1729 | 1730 | def mkPartlist (s, vids, partAttr, lev): 1731 | def addPartGroup (sym, num): 1732 | pg = E.Element ('part-group', number=str (num), type='start') 1733 | addElem (partlist, pg, lev + 1) 1734 | addElemT (pg, 'group-symbol', sym, lev + 2) 1735 | addElemT (pg, 'group-barline', 'yes', lev + 2) 1736 | partlist = E.Element ('part-list') 1737 | g_num = 0 # xml group number 1738 | for g in (s.groups or vids): # brace/bracket or abc_voice_id 1739 | if g == '[': g_num += 1; addPartGroup ('bracket', g_num) 1740 | elif g == '{': g_num += 1; addPartGroup ('brace', g_num) 1741 | elif g in '}]': 1742 | pg = E.Element ('part-group', number=str (g_num), type='stop') 1743 | addElem (partlist, pg, lev + 1) 1744 | g_num -= 1 1745 | else: # g = abc_voice_id 1746 | if g not in vids: continue # error in %%score 1747 | sp = s.mkScorePart (g, vids, partAttr, lev + 1) 1748 | addElem (partlist, sp, lev + 1) 1749 | return partlist 1750 | 1751 | def doField_I (s, type, x, instDir, addTrans): 1752 | def instChange (midchan, midprog): # instDir -> doFields 1753 | if midchan and midchan != s.midprg [0]: instDir ('midi-channel', midchan, 'chan: %s') 1754 | if midprog and midprog != s.midprg [1]: instDir ('midi-program', str (int (midprog) + 1), 'prog: %s') 1755 | def readPfmt (x, n): # read ABC page formatting constant 1756 | if not s.pageFmtAbc: s.pageFmtAbc = s.pageFmtDef # set the default values on first change 1757 | ro = re.search (r'[^.\d]*([\d.]+)\s*(cm|in|pt)?', x) # float followed by unit 1758 | if ro: 1759 | x, unit = ro.groups () # unit == None when not present 1760 | u = {'cm':10., 'in':25.4, 'pt':25.4/72} [unit] if unit else 1. 1761 | s.pageFmtAbc [n] = float (x) * u # convert ABC values to millimeters 1762 | else: info ('error in page format: %s' % x) 1763 | def readPercMap (x): # parse I:percmap 1764 | def getMidNum (sndnm): # find midi number of GM drum sound name 1765 | pnms = sndnm.split ('-') # sound name parts (from I:percmap) 1766 | ps = s.percsnd [:] # copy of the instruments 1767 | _f = lambda ip, xs, pnm: ip < len (xs) and xs[ip].find (pnm) > -1 # part xs[ip] and pnm match 1768 | for ip, pnm in enumerate (pnms): # match all percmap sound name parts 1769 | ps = [(nm, mnum) for nm, mnum in ps if _f (ip, nm.split ('-'), pnm) ] # filter instruments 1770 | if len (ps) <= 1: break # no match or one instrument left 1771 | if len (ps) == 0: info ('drum sound: %s not found' % sndnm); return '38' 1772 | return ps [0][1] # midi number of (first) instrument found 1773 | def midiVal (acc, step, oct): # abc note -> midi note number 1774 | oct = (4 if step.upper() == step else 5) + int (oct) 1775 | return oct * 12 + [0,2,4,5,7,9,11]['CDEFGAB'.index (step.upper())] + {'^':1,'_':-1,'=':0}.get (acc, 0) + 12 1776 | p0, p1, p2, p3, p4 = abc_percmap.parseString (x).asList () # percmap, abc-note, display-step, midi, note-head 1777 | acc, astep, aoct = p1 1778 | nstep, noct = (astep, aoct) if p2 == '*' else p2 1779 | if p3 == '*': midi = str (midiVal (acc, astep, aoct)) 1780 | elif isinstance (p3, list_type): midi = str (midiVal (p3[0], p3[1], p3[2])) 1781 | elif isinstance (p3, int_type): midi = str (p3) 1782 | else: midi = getMidNum (p3.lower ()) 1783 | head = re.sub (r'(.)-([^x])', r'\1 \2', p4) # convert abc note head names to xml 1784 | s.percMap [(s.pid, acc + astep, aoct)] = (nstep, noct, midi, head) 1785 | if x.startswith ('score') or x.startswith ('staves'): 1786 | s.staveDefs += [x] # collect all voice mappings 1787 | elif x.startswith ('staffwidth'): info ('skipped I-field: %s' % x) 1788 | elif x.startswith ('staff'): # set new staff number of the current voice 1789 | r1 = re.search (r'staff *([+-]?)(\d)', x) 1790 | if r1: 1791 | sign = r1.group (1) 1792 | num = int (r1.group (2)) 1793 | gstaff = s.gStaffNums.get (s.vid, 0) # staff number of the current voice 1794 | if sign: # relative staff number 1795 | num = (sign == '-') and gstaff - num or gstaff + num 1796 | else: # absolute abc staff number 1797 | try: vabc = s.staves [num - 1][0] # vid of (first voice of) abc-staff num 1798 | except: vabc = 0; info ('abc staff %s does not exist' % num) 1799 | num = s.gStaffNumsOrg.get (vabc, 0) # xml staff number of abc-staff num 1800 | if gstaff and num > 0 and num <= s.gNstaves [s.vid]: 1801 | s.gStaffNums [s.vid] = num 1802 | else: info ('could not relocate to staff: %s' % r1.group ()) 1803 | else: info ('not a valid staff redirection: %s' % x) 1804 | elif x.startswith ('scale'): readPfmt (x, 0) 1805 | elif x.startswith ('pageheight'): readPfmt (x, 1) 1806 | elif x.startswith ('pagewidth'): readPfmt (x, 2) 1807 | elif x.startswith ('leftmargin'): readPfmt (x, 3) 1808 | elif x.startswith ('rightmargin'): readPfmt (x, 4) 1809 | elif x.startswith ('topmargin'): readPfmt (x, 5) 1810 | elif x.startswith ('botmargin'): readPfmt (x, 6) 1811 | elif x.startswith ('MIDI') or x.startswith ('midi'): 1812 | r1 = re.search (r'program *(\d*) +(\d+)', x) 1813 | r2 = re.search (r'channel *(\d+)', x) 1814 | r3 = re.search (r"drummap\s+([_=^]*)([A-Ga-g])([,']*)\s+(\d+)", x) 1815 | r4 = re.search (r'control *(\d+) +(\d+)', x) 1816 | ch_nw, prg_nw, vol_nw, pan_nw = '', '', '', '' 1817 | if r1: ch_nw, prg_nw = r1.groups () # channel nr or '', program nr 1818 | if r2: ch_nw = r2.group (1) # channel nr only 1819 | if r4: 1820 | cnum, cval = r4.groups () # controller number, controller value 1821 | if cnum == '7': vol_nw = cval 1822 | if cnum == '10': pan_nw = cval 1823 | if r1 or r2 or r4: 1824 | ch = ch_nw or s.midprg [0] 1825 | prg = prg_nw or s.midprg [1] 1826 | vol = vol_nw or s.midprg [2] 1827 | pan = pan_nw or s.midprg [3] 1828 | instId = 'I%s-%s' % (s.pid, s.vid) # only look for real instruments, no percussion 1829 | if instId in s.midiInst: instChange (ch, prg) # instChance -> doFields 1830 | s.midprg = [ch, prg, vol, pan] # mknote: new instrument -> s.midiInst 1831 | if r3: # translate drummap to percmap 1832 | acc, step, oct, midi = r3.groups () 1833 | oct = -len (oct) if ',' in x else len (oct) 1834 | notehead = 'x' if acc == '^' else 'circle-x' if acc == '_' else 'normal' 1835 | s.percMap [(s.pid, acc + step, oct)] = (step, oct, midi, notehead) 1836 | r = re.search (r'transpose[^-\d]*(-?\d+)', x) 1837 | if r: addTrans (r.group (1)) # addTrans -> doFields 1838 | elif x.startswith ('percmap'): readPercMap (x); s.pMapFound = 1 1839 | else: info ('skipped I-field: %s' % x) 1840 | 1841 | def parseStaveDef (s, vdefs): 1842 | for vid in vdefs: s.vcepid [vid] = vid # default: each voice becomes an xml part 1843 | if not s.staveDefs: return vdefs 1844 | for x in s.staveDefs [1:]: info ('%%%%%s dropped, multiple stave mappings not supported' % x) 1845 | x = s.staveDefs [0] # only the first %%score is honoured 1846 | score = abc_scoredef.parseString (x) [0] 1847 | f = lambda x: type (x) == uni_type and [x] or x 1848 | s.staves = lmap (f, mkStaves (score, vdefs)) # [[vid] for each staff] 1849 | s.grands = lmap (f, mkGrand (score, vdefs)) # [staff-id], staff-id == [vid][0] 1850 | s.groups = mkGroups (score) 1851 | vce_groups = [vids for vids in s.staves if len (vids) > 1] # all voice groups 1852 | d = {} # for each voice group: map first voice id -> all merged voice ids 1853 | for vgr in vce_groups: d [vgr[0]] = vgr 1854 | for gstaff in s.grands: # for all grand staves 1855 | if len (gstaff) == 1: continue # skip single parts 1856 | for v, stf_num in zip (gstaff, range (1, len (gstaff) + 1)): 1857 | for vx in d.get (v, [v]): # allocate staff numbers 1858 | s.gStaffNums [vx] = stf_num # to all constituant voices 1859 | s.gNstaves [vx] = len (gstaff) # also remember total number of staves 1860 | s.gStaffNumsOrg = s.gStaffNums.copy () # keep original allocation for abc -> xml staff map 1861 | for xmlpart in s.grands: 1862 | pid = xmlpart [0] # part id == first staff id == first voice id 1863 | vces = [v for stf in xmlpart for v in d.get (stf, [stf])] 1864 | for v in vces: s.vcepid [v] = pid 1865 | return vdefs 1866 | 1867 | def voiceNamesAndMaps (s, ps): # get voice names and mappings 1868 | vdefs = {} 1869 | for vid, vcedef, vce in ps: # vcedef == emtpy or first pObj == voice definition 1870 | pname, psubnm = '', '' # part name and abbreviation 1871 | if not vcedef: # simple abc without voice definitions 1872 | vdefs [vid] = pname, psubnm, '' 1873 | else: # abc with voice definitions 1874 | if vid != vcedef.t[1]: info ('voice ids unequal: %s (reg-ex) != %s (grammar)' % (vid, vcedef.t[1])) 1875 | rn = re.search (r'(?:name|nm)="([^"]*)"', vcedef.t[2]) 1876 | if rn: pname = rn.group (1) 1877 | rn = re.search (r'(?:subname|snm|sname)="([^"]*)"', vcedef.t[2]) 1878 | if rn: psubnm = rn.group (1) 1879 | vcedef.t[2] = vcedef.t[2].replace ('"%s"' % pname, '""').replace ('"%s"' % psubnm, '""') # clear voice name to avoid false clef matches later on 1880 | vdefs [vid] = pname, psubnm, vcedef.t[2] 1881 | xs = [pObj.t[1] for maat in vce for pObj in maat if pObj.name == 'inline'] # all inline statements in vce 1882 | s.staveDefs += [x.replace ('%5d',']') for x in xs if x.startswith ('score') or x.startswith ('staves')] # filter %%score and %%staves 1883 | return vdefs 1884 | 1885 | def doHeaderField (s, fld, attrmap): 1886 | type, value = fld.t[0], fld.t[1].replace ('%5d',']') # restore closing brackets (see splitHeaderVoices) 1887 | if not value: # skip empty field 1888 | return 1889 | if type == 'M': 1890 | attrmap [type] = value 1891 | elif type == 'L': 1892 | try: s.unitL = lmap (int, fld.t[1].split ('/')) 1893 | except: 1894 | info ('illegal unit length:%s, 1/8 assumed' % fld.t[1]) 1895 | s.unitL = 1,8 1896 | if len (s.unitL) == 1 or s.unitL[1] not in s.typeMap: 1897 | info ('L:%s is not allowed, 1/8 assumed' % fld.t[1]) 1898 | s.unitL = 1,8 1899 | elif type == 'K': 1900 | attrmap[type] = value 1901 | elif type == 'T': 1902 | s.title = s.title + '\n' + value if s.title else value 1903 | elif type == 'U': 1904 | sym = fld.t[2].strip ('!+') 1905 | s.usrSyms [value] = sym 1906 | elif type == 'I': 1907 | s.doField_I (type, value, lambda x,y,z:0, lambda x:0) 1908 | elif type == 'Q': 1909 | attrmap[type] = value 1910 | elif type in 'CRZNOAGHBDFSP': # part maps are treated as meta data 1911 | type = s.metaMap.get (type, type) # respect the (user defined --meta) mapping of various ABC fields to XML meta data types 1912 | c = s.metadata.get (type, '') 1913 | s.metadata [type] = c + '\n' + value if c else value # concatenate multiple info fields with new line as separator 1914 | else: 1915 | info ('skipped header: %s' % fld) 1916 | 1917 | def mkIdentification (s, score, lev): 1918 | if s.title: 1919 | xs = s.title.split ('\n') # the first T: line goes to work-title 1920 | ys = '\n'.join (xs [1:]) # place subsequent T: lines into work-number 1921 | w = E.Element ('work') 1922 | addElem (score, w, lev + 1) 1923 | if ys: addElemT (w, 'work-number', ys, lev + 2) 1924 | addElemT (w, 'work-title', xs[0], lev + 2) 1925 | ident = E.Element ('identification') 1926 | addElem (score, ident, lev + 1) 1927 | for mtype, mval in s.metadata.items (): 1928 | if mtype in s.metaTypes and mtype != 'rights': # all metaTypes are MusicXML creator types 1929 | c = E.Element ('creator', type=mtype) 1930 | c.text = mval 1931 | addElem (ident, c, lev + 2) 1932 | if 'rights' in s.metadata: 1933 | c = addElemT (ident, 'rights', s.metadata ['rights'], lev + 2) 1934 | encoding = E.Element ('encoding') 1935 | addElem (ident, encoding, lev + 2) 1936 | encoder = E.Element ('encoder') 1937 | encoder.text = 'abc2xml version %d' % VERSION 1938 | addElem (encoding, encoder, lev + 3) 1939 | if s.supports_tag: # avoids interference of auto-flowing and explicit linebreaks 1940 | suports = E.Element ('supports', attribute="new-system", element="print", type="yes", value="yes") 1941 | addElem (encoding, suports, lev + 3) 1942 | encodingDate = E.Element ('encoding-date') 1943 | encodingDate.text = str (datetime.date.today ()) 1944 | addElem (encoding, encodingDate, lev + 3) 1945 | s.addMeta (ident, lev + 2) 1946 | 1947 | def mkDefaults (s, score, lev): 1948 | if s.pageFmtCmd: s.pageFmtAbc = s.pageFmtCmd 1949 | if not s.pageFmtAbc: return # do not output the defaults if none is desired 1950 | abcScale, h, w, l, r, t, b = s.pageFmtAbc 1951 | space = abcScale * 2.117 # 2.117 = 6pt = space between staff lines for scale = 1.0 in abcm2ps 1952 | mils = 4 * space # staff height in millimeters 1953 | scale = 40. / mils # tenth's per millimeter 1954 | dflts = E.Element ('defaults') 1955 | addElem (score, dflts, lev) 1956 | scaling = E.Element ('scaling') 1957 | addElem (dflts, scaling, lev + 1) 1958 | addElemT (scaling, 'millimeters', '%g' % mils, lev + 2) 1959 | addElemT (scaling, 'tenths', '40', lev + 2) 1960 | layout = E.Element ('page-layout') 1961 | addElem (dflts, layout, lev + 1) 1962 | addElemT (layout, 'page-height', '%g' % (h * scale), lev + 2) 1963 | addElemT (layout, 'page-width', '%g' % (w * scale), lev + 2) 1964 | margins = E.Element ('page-margins', type='both') 1965 | addElem (layout, margins, lev + 2) 1966 | addElemT (margins, 'left-margin', '%g' % (l * scale), lev + 3) 1967 | addElemT (margins, 'right-margin', '%g' % (r * scale), lev + 3) 1968 | addElemT (margins, 'top-margin', '%g' % (t * scale), lev + 3) 1969 | addElemT (margins, 'bottom-margin', '%g' % (b * scale), lev + 3) 1970 | 1971 | def addMeta (s, parent, lev): 1972 | misc = E.Element ('miscellaneous') 1973 | mf = 0 1974 | for mtype, mval in sorted (s.metadata.items ()): 1975 | if mtype == 'S': 1976 | addElemT (parent, 'source', mval, lev) 1977 | elif mtype in s.metaTypes: continue # mapped meta data has already been output (in creator elements) 1978 | else: 1979 | mf = E.Element ('miscellaneous-field', name=s.metaTab [mtype]) 1980 | mf.text = mval 1981 | addElem (misc, mf, lev + 1) 1982 | if mf != 0: addElem (parent, misc, lev) 1983 | 1984 | def parse (s, abc_string, rOpt=False, bOpt=False, fOpt=False): 1985 | abctext = abc_string.replace ('[I:staff ','[I:staff') # avoid false beam breaks 1986 | s.reset (fOpt) 1987 | header, voices = splitHeaderVoices (abctext) 1988 | ps = [] 1989 | try: 1990 | lbrk_insert = 0 if re.search (r'I:linebreak\s*([!$]|none)|I:continueall\s*(1|true)', header) else bOpt 1991 | hs = abc_header.parseString (header) if header else '' 1992 | for id, voice in voices: 1993 | if lbrk_insert: # insert linebreak at EOL 1994 | r1 = re.compile (r'\[[wA-Z]:[^]]*\]') # inline field 1995 | has_abc = lambda x: r1.sub ('', x).strip () # empty if line only contains inline fields 1996 | voice = '\n'.join ([balk.rstrip ('$!') + '$' if has_abc (balk) else balk for balk in voice.splitlines ()]) 1997 | prevLeftBar = None # previous voice ended with a left-bar symbol (double repeat) 1998 | s.orderChords = s.fOpt and ('tab' in voice [:200] or [x for x in hs if x.t[0] == 'K' and 'tab' in x.t[1]]) 1999 | vce = abc_voice.parseString (voice).asList () 2000 | lyr_notes = [] # remember notes between lyric blocks 2001 | for m in vce: # all measures 2002 | for e in m: # all abc-elements 2003 | if e.name == 'lyr_blk': # -> e.objs is list of lyric lines 2004 | lyr = [line.objs for line in e.objs] # line.objs is listof syllables 2005 | alignLyr (lyr_notes, lyr) # put all syllables into corresponding notes 2006 | lyr_notes = [] 2007 | else: 2008 | lyr_notes.append (e) 2009 | if not vce: # empty voice, insert an inline field that will be rejected 2010 | vce = [[pObj ('inline', ['I', 'empty voice'])]] 2011 | if prevLeftBar: 2012 | vce[0].insert (0, prevLeftBar) # insert at begin of first measure 2013 | prevLeftBar = None 2014 | if vce[-1] and vce[-1][-1].name == 'lbar': # last measure ends with an lbar 2015 | prevLeftBar = vce[-1][-1] 2016 | if len (vce) > 1: # vce should not become empty (-> exception when taking vcelyr [0][0]) 2017 | del vce[-1] # lbar was the only element in measure vce[-1] 2018 | vcelyr = vce 2019 | elem1 = vcelyr [0][0] # the first element of the first measure 2020 | if elem1.name == 'inline'and elem1.t[0] == 'V': # is a voice definition 2021 | voicedef = elem1 2022 | del vcelyr [0][0] # do not read voicedef twice 2023 | else: 2024 | voicedef = '' 2025 | ps.append ((id, voicedef, vcelyr)) 2026 | except ParseException as err: 2027 | if err.loc > 40: # limit length of error message, compatible with markInputline 2028 | err.pstr = err.pstr [err.loc - 40: err.loc + 40] 2029 | err.loc = 40 2030 | xs = err.line[err.col-1:] 2031 | info (err.line, warn=0) 2032 | info ((err.col-1) * '-' + '^', warn=0) 2033 | if re.search (r'\[U:', xs): 2034 | info ('Error: illegal user defined symbol: %s' % xs[1:], warn=0) 2035 | elif re.search (r'\[[OAPZNGHRBDFSXTCIU]:', xs): 2036 | info ('Error: header-only field %s appears after K:' % xs[1:], warn=0) 2037 | else: 2038 | info ('Syntax error at column %d' % err.col, warn=0) 2039 | raise 2040 | 2041 | score = E.Element ('score-partwise') 2042 | attrmap = {'Div': str (s.divisions), 'K':'C treble', 'M':'4/4'} 2043 | for res in hs: 2044 | if res.name == 'field': 2045 | s.doHeaderField (res, attrmap) 2046 | else: 2047 | info ('unexpected header item: %s' % res) 2048 | 2049 | vdefs = s.voiceNamesAndMaps (ps) 2050 | vdefs = s.parseStaveDef (vdefs) 2051 | 2052 | lev = 0 2053 | vids, parts, partAttr = [], [], {} 2054 | s.strAlloc = stringAlloc () 2055 | for vid, _, vce in ps: # voice id, voice parse tree 2056 | pname, psubnm, voicedef = vdefs [vid] # part name 2057 | attrmap ['V'] = voicedef # abc text of first voice definition (after V:vid) or empty 2058 | pid = 'P%s' % vid # let part id start with an alpha 2059 | s.vid = vid # avoid parameter passing, needed in mkNote for instrument id 2060 | s.pid = s.vcepid [s.vid] # xml part-id for the current voice 2061 | s.gTime = (0, 0) # reset time 2062 | s.strAlloc.beginZoek () # reset search index 2063 | part = s.mkPart (vce, pid, lev + 1, attrmap, s.gNstaves.get (vid, 0), rOpt) 2064 | if 'Q' in attrmap: del attrmap ['Q'] # header tempo only in first part 2065 | parts.append (part) 2066 | vids.append (vid) 2067 | partAttr [vid] = (pname, psubnm, s.midprg) 2068 | if s.midprg != ['', '', '', ''] and not s.percVoice: # when a part has only rests 2069 | instId = 'I%s-%s' % (s.pid, s.vid) 2070 | if instId not in s.midiInst: s.midiInst [instId] = (s.pid, s.vid, s.midprg [0], s.midprg [1], s.midprg [2], s.midprg [3]) 2071 | parts, vidsnew = mergeParts (parts, vids, s.staves, rOpt) # merge parts into staves as indicated by %%score 2072 | parts, vidsnew = mergeParts (parts, vidsnew, s.grands, rOpt, 1) # merge grand staves 2073 | reduceMids (parts, vidsnew, s.midiInst) 2074 | 2075 | s.mkIdentification (score, lev) 2076 | s.mkDefaults (score, lev + 1) 2077 | 2078 | partlist = s.mkPartlist (vids, partAttr, lev + 1) 2079 | addElem (score, partlist, lev + 1) 2080 | for ip, part in enumerate (parts): addElem (score, part, lev + 1) 2081 | 2082 | return score 2083 | 2084 | 2085 | def decodeInput (data_string): 2086 | try: enc = 'utf-8'; unicode_string = data_string.decode (enc) 2087 | except: 2088 | try: enc = 'latin-1'; unicode_string = data_string.decode (enc) 2089 | except: raise ValueError ('data not encoded in utf-8 nor in latin-1') 2090 | info ('decoded from %s' % enc) 2091 | return unicode_string 2092 | 2093 | def ggd (a, b): # greatest common divisor 2094 | return a if b == 0 else ggd (b, a % b) 2095 | 2096 | xmlVersion = "" 2097 | def fixDoctype (elem): 2098 | if python3: xs = E.tostring (elem, encoding='unicode') # writing to file will auto-encode to utf-8 2099 | else: xs = E.tostring (elem, encoding='utf-8') # keep the string utf-8 encoded for writing to file 2100 | ys = xs.split ('\n') 2101 | ys.insert (0, xmlVersion) # crooked logic of ElementTree lib 2102 | ys.insert (1, '') 2103 | return '\n'.join (ys) 2104 | 2105 | def xml2mxl (pad, fnm, data): # write xml data to compressed .mxl file 2106 | from zipfile import ZipFile, ZIP_DEFLATED 2107 | fnmext = fnm + '.xml' # file name with extension, relative to the root within the archive 2108 | outfile = os.path.join (pad, fnm + '.mxl') 2109 | meta = '%s\n\n' % xmlVersion 2110 | meta += '\n' % fnmext 2111 | meta += '' 2112 | f = ZipFile (outfile, 'w', ZIP_DEFLATED) 2113 | f.writestr ('META-INF/container.xml', meta) 2114 | f.writestr (fnmext, data) 2115 | f.close () 2116 | info ('%s written' % outfile, warn=0) 2117 | 2118 | def convert (pad, fnm, abc_string, mxl, rOpt=False, tOpt=False, bOpt=False, fOpt=False): # not used, backwards compatibility 2119 | score = mxm.parse (abc_string, rOpt, bOpt, fOpt) 2120 | writefile (pad, fnm, '', score, mxl, tOpt) 2121 | 2122 | def writefile (pad, fnm, fnmNum, xmldoc, mxlOpt, tOpt=False): 2123 | ipad, ifnm = os.path.split (fnm) # base name of input path is 2124 | if tOpt: 2125 | x = xmldoc.findtext ('work/work-title', 'no_title') 2126 | ifnm = x.replace (',','_').replace ("'",'_').replace ('?','_') 2127 | else: 2128 | ifnm += fnmNum 2129 | xmlstr = fixDoctype (xmldoc) 2130 | if pad: 2131 | if not mxlOpt or mxlOpt in ['a', 'add']: 2132 | outfnm = os.path.join (pad, ifnm + '.xml') # joined with path from -o option 2133 | outfile = open (outfnm, 'w') 2134 | outfile.write (xmlstr) 2135 | outfile.close () 2136 | info ('%s written' % outfnm, warn=0) 2137 | if mxlOpt: xml2mxl (pad, ifnm, xmlstr) # also write a compressed version 2138 | else: 2139 | outfile = sys.stdout 2140 | outfile.write (xmlstr) 2141 | outfile.write ('\n') 2142 | 2143 | def readfile (fnmext, errmsg='read error: '): 2144 | try: 2145 | if fnmext == '-.abc': fobj = stdin # see python2/3 differences 2146 | else: fobj = open (fnmext, 'rb') 2147 | encoded_data = fobj.read () 2148 | fobj.close () 2149 | return encoded_data if type (encoded_data) == uni_type else decodeInput (encoded_data) 2150 | except Exception as e: 2151 | info (errmsg + repr (e) + ' ' + fnmext) 2152 | return None 2153 | 2154 | def expand_abc_include (abctxt): 2155 | ys = [] 2156 | for x in abctxt.splitlines (): 2157 | if x.startswith ('%%abc-include') or x.startswith ('I:abc-include'): 2158 | x = readfile (x[13:].strip (), 'include error: ') 2159 | if x != None: ys.append (x) 2160 | return '\n'.join (ys) 2161 | 2162 | abc_header, abc_voice, abc_scoredef, abc_percmap = abc_grammar () # compute grammars only once 2163 | mxm = MusicXml () # same for instance of MusicXml 2164 | 2165 | def getXmlScores (abc_string, skip=0, num=1, rOpt=False, bOpt=False, fOpt=False): # not used, backwards compatibility 2166 | return [fixDoctype (xml_doc) for xml_doc in 2167 | getXmlDocs (abc_string, skip=0, num=1, rOpt=False, bOpt=False, fOpt=False)] 2168 | 2169 | def getXmlDocs (abc_string, skip=0, num=1, rOpt=False, bOpt=False, fOpt=False): # added by David Randolph 2170 | xml_docs = [] 2171 | abctext = expand_abc_include (abc_string) 2172 | fragments = re.split (r'^\s*X:', abctext, flags=re.M) 2173 | preamble = fragments [0] # tunes can be preceeded by formatting instructions 2174 | tunes = fragments[1:] 2175 | if not tunes and preamble: tunes, preamble = ['1\n' + preamble], '' # tune without X: 2176 | for itune, tune in enumerate (tunes): 2177 | if itune < skip: continue # skip tunes, then read at most num tunes 2178 | if itune >= skip + num: break 2179 | tune = preamble + 'X:' + tune # restore preamble before each tune 2180 | try: # convert string abctext -> file pad/fnmNum.xml 2181 | score = mxm.parse (tune, rOpt, bOpt, fOpt) 2182 | ds = list (score.iter ('duration')) # need to iterate twice 2183 | ss = [int (d.text) for d in ds] 2184 | deler = reduce (ggd, ss + [21]) # greatest common divisor of all durations 2185 | for i, d in enumerate (ds): d.text = str (ss [i] // deler) 2186 | for d in score.iter ('divisions'): d.text = str (int (d.text) // deler) 2187 | xml_docs.append (score) 2188 | except ParseException: 2189 | pass # output already printed 2190 | except Exception as err: 2191 | info ('an exception occurred.\n%s' % err) 2192 | return xml_docs 2193 | 2194 | #---------------- 2195 | # Main Program 2196 | #---------------- 2197 | if __name__ == '__main__': 2198 | from optparse import OptionParser 2199 | from glob import glob 2200 | import time 2201 | 2202 | parser = OptionParser (usage='%prog [-h] [-r] [-t] [-b] [-m SKIP NUM] [-o DIR] [-p PFMT] [-z MODE] [--meta MAP] [ ...]', version='version %d' % VERSION) 2203 | parser.add_option ("-o", action="store", help="store xml files in DIR", default='', metavar='DIR') 2204 | parser.add_option ("-m", action="store", help="skip SKIP (0) tunes, then read at most NUM (1) tunes", nargs=2, type='int', default=(0,1), metavar='SKIP NUM') 2205 | parser.add_option ("-p", action="store", help="pageformat PFMT (mm) = scale (0.75), pageheight (297), pagewidth (210), leftmargin (18), rightmargin (18), topmargin (10), botmargin (10)", default='', metavar='PFMT') 2206 | parser.add_option ("-z", "--mxl", dest="mxl", help="store as compressed mxl, MODE = a(dd) or r(eplace)", default='', metavar='MODE') 2207 | parser.add_option ("-r", action="store_true", help="show whole measure rests in merged staffs", default=False) 2208 | parser.add_option ("-t", action="store_true", help="use tune title as file name", default=False) 2209 | parser.add_option ("-b", action="store_true", help="line break at EOL", default=False) 2210 | parser.add_option ("--meta", action="store", help="map infofields to XML metadata, MAP = R:poet,Z:lyricist,N:...", default='', metavar='MAP') 2211 | parser.add_option ("-f", action="store_true", help="force string/fret allocations for tab staves", default=False) 2212 | options, args = parser.parse_args () 2213 | if len (args) == 0: parser.error ('no input file given') 2214 | pad = options.o 2215 | if options.mxl and options.mxl not in ['a','add', 'r', 'replace']: 2216 | parser.error ('MODE should be a(dd) or r(eplace), not: %s' % options.mxl) 2217 | if pad: 2218 | if not os.path.exists (pad): os.mkdir (pad) 2219 | if not os.path.isdir (pad): parser.error ('%s is not a directory' % pad) 2220 | if options.p: # set page formatting values 2221 | try: # space, page-height, -width, margin-left, -right, -top, -bottom 2222 | mxm.pageFmtCmd = lmap (float, options.p.split (',')) 2223 | if len (mxm.pageFmtCmd) != 7: raise ValueError ('-p needs 7 values') 2224 | except Exception as err: parser.error (err) 2225 | for x in options.meta.split (','): 2226 | if not x: continue 2227 | try: field, tag = x.split (':') 2228 | except: parser.error ('--meta: %s cannot be split on colon' % x) 2229 | if field not in 'OAZNGHRBDFSPW': parser.error ('--meta: field %s is no valid ABC field' % field) 2230 | if tag not in mxm.metaTypes: parser.error ('--meta: tag %s is no valid XML creator type' % tag) 2231 | mxm.metaMap [field] = tag 2232 | fnmext_list = [] 2233 | for i in args: 2234 | if i == '-': fnmext_list.append ('-.abc') # represents standard input 2235 | else: fnmext_list += glob (i) 2236 | if not fnmext_list: parser.error ('none of the input files exist') 2237 | t_start = time.time () 2238 | for fnmext in fnmext_list: 2239 | fnm, ext = os.path.splitext (fnmext) 2240 | if ext.lower () not in ('.abc'): 2241 | info ('skipped input file %s, it should have extension .abc' % fnmext) 2242 | continue 2243 | if os.path.isdir (fnmext): 2244 | info ('skipped directory %s. Only files are accepted' % fnmext) 2245 | continue 2246 | abctext = readfile (fnmext) 2247 | skip, num = options.m 2248 | xml_docs = getXmlDocs (abctext, skip, num, options.r, options.b, options.f) 2249 | for itune, xmldoc in enumerate (xml_docs): 2250 | fnmNum = '%02d' % (itune + 1) if len (xml_docs) > 1 else '' 2251 | writefile (pad, fnm, fnmNum, xmldoc, options.mxl, options.t) 2252 | info ('done in %.2f secs' % (time.time () - t_start)) --------------------------------------------------------------------------------