sander-wood
commited on
Commit
•
b058428
1
Parent(s):
89f131e
Upload 8 files
Browse files- app.py +200 -0
- clamp-small-512/config.json +26 -0
- clamp-small-512/pytorch_model.bin +3 -0
- music_key_cache_512.pth +3 -0
- requirements.txt +8 -0
- utils.py +357 -0
- wikimusictext.json +0 -0
- xml2abc.py +1582 -0
app.py
ADDED
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import subprocess
|
2 |
+
import os
|
3 |
+
import gradio as gr
|
4 |
+
import json
|
5 |
+
from utils import *
|
6 |
+
from unidecode import unidecode
|
7 |
+
from transformers import AutoTokenizer
|
8 |
+
|
9 |
+
CLAMP_MODEL_NAME = 'clamp-small-512'
|
10 |
+
QUERY_MODAL = 'music'
|
11 |
+
KEY_MODAL = 'music'
|
12 |
+
TOP_N = 1
|
13 |
+
TEXT_MODEL_NAME = 'distilroberta-base'
|
14 |
+
TEXT_LENGTH = 128
|
15 |
+
device = torch.device("cpu")
|
16 |
+
|
17 |
+
# load CLaMP model
|
18 |
+
model = CLaMP.from_pretrained(CLAMP_MODEL_NAME)
|
19 |
+
music_length = model.config.max_length
|
20 |
+
model = model.to(device)
|
21 |
+
model.eval()
|
22 |
+
|
23 |
+
# initialize patchilizer, tokenizer, and softmax
|
24 |
+
patchilizer = MusicPatchilizer()
|
25 |
+
tokenizer = AutoTokenizer.from_pretrained(TEXT_MODEL_NAME)
|
26 |
+
softmax = torch.nn.Softmax(dim=1)
|
27 |
+
|
28 |
+
def compute_values(Q_e, K_e, t=1):
|
29 |
+
"""
|
30 |
+
Compute the values for the attention matrix
|
31 |
+
|
32 |
+
Args:
|
33 |
+
Q_e (torch.Tensor): Query embeddings
|
34 |
+
K_e (torch.Tensor): Key embeddings
|
35 |
+
t (float): Temperature for the softmax
|
36 |
+
|
37 |
+
Returns:
|
38 |
+
values (torch.Tensor): Values for the attention matrix
|
39 |
+
"""
|
40 |
+
# Normalize the feature representations
|
41 |
+
Q_e = torch.nn.functional.normalize(Q_e, dim=1)
|
42 |
+
K_e = torch.nn.functional.normalize(K_e, dim=1)
|
43 |
+
|
44 |
+
# Scaled pairwise cosine similarities [1, n]
|
45 |
+
logits = torch.mm(Q_e, K_e.T) * torch.exp(torch.tensor(t))
|
46 |
+
values = softmax(logits)
|
47 |
+
return values.squeeze()
|
48 |
+
|
49 |
+
|
50 |
+
def encoding_data(data, modal):
|
51 |
+
"""
|
52 |
+
Encode the data into ids
|
53 |
+
|
54 |
+
Args:
|
55 |
+
data (list): List of strings
|
56 |
+
modal (str): "music" or "text"
|
57 |
+
|
58 |
+
Returns:
|
59 |
+
ids_list (list): List of ids
|
60 |
+
"""
|
61 |
+
ids_list = []
|
62 |
+
if modal=="music":
|
63 |
+
for item in data:
|
64 |
+
patches = patchilizer.encode(item, music_length=music_length, add_eos_patch=True)
|
65 |
+
ids_list.append(torch.tensor(patches).reshape(-1))
|
66 |
+
else:
|
67 |
+
for item in data:
|
68 |
+
text_encodings = tokenizer(item,
|
69 |
+
return_tensors='pt',
|
70 |
+
truncation=True,
|
71 |
+
max_length=TEXT_LENGTH)
|
72 |
+
ids_list.append(text_encodings['input_ids'].squeeze(0))
|
73 |
+
|
74 |
+
return ids_list
|
75 |
+
|
76 |
+
|
77 |
+
def abc_filter(lines):
|
78 |
+
"""
|
79 |
+
Filter out the metadata from the abc file
|
80 |
+
|
81 |
+
Args:
|
82 |
+
lines (list): List of lines in the abc file
|
83 |
+
|
84 |
+
Returns:
|
85 |
+
music (str): Music string
|
86 |
+
"""
|
87 |
+
music = ""
|
88 |
+
for line in lines:
|
89 |
+
if line[:2] in ['A:', 'B:', 'C:', 'D:', 'F:', 'G', 'H:', 'N:', 'O:', 'R:', 'r:', 'S:', 'T:', 'W:', 'w:', 'X:', 'Z:'] \
|
90 |
+
or line=='\n' \
|
91 |
+
or (line.startswith('%') and not line.startswith('%%score')):
|
92 |
+
continue
|
93 |
+
else:
|
94 |
+
if "%" in line and not line.startswith('%%score'):
|
95 |
+
line = "%".join(line.split('%')[:-1])
|
96 |
+
music += line[:-1] + '\n'
|
97 |
+
else:
|
98 |
+
music += line + '\n'
|
99 |
+
return music
|
100 |
+
|
101 |
+
|
102 |
+
def load_music(filename):
|
103 |
+
"""
|
104 |
+
Load the music from the xml file
|
105 |
+
|
106 |
+
Args:
|
107 |
+
file (Union[str, bytes, BinaryIO, TextIO]): Input file object containing the xml file
|
108 |
+
|
109 |
+
Returns:
|
110 |
+
music (str): Music string
|
111 |
+
"""
|
112 |
+
# Get absolute path of xml2abc.py
|
113 |
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
114 |
+
xml2abc_path = os.path.join(script_dir, 'xml2abc.py')
|
115 |
+
|
116 |
+
# Use absolute path in Popen()
|
117 |
+
p = subprocess.Popen(['python', xml2abc_path, '-m', '2', '-c', '6', '-x', filename], stdout=subprocess.PIPE)
|
118 |
+
result = p.communicate()[0]
|
119 |
+
output = result.decode('utf-8').replace('\r', '')
|
120 |
+
music = unidecode(output).split('\n')
|
121 |
+
music = abc_filter(music)
|
122 |
+
|
123 |
+
return music
|
124 |
+
|
125 |
+
|
126 |
+
def get_features(ids_list, modal):
|
127 |
+
"""
|
128 |
+
Get the features from the CLaMP model
|
129 |
+
|
130 |
+
Args:
|
131 |
+
ids_list (list): List of ids
|
132 |
+
modal (str): "music" or "text"
|
133 |
+
|
134 |
+
Returns:
|
135 |
+
features_list (torch.Tensor): Tensor of features with a shape of (batch_size, hidden_size)
|
136 |
+
"""
|
137 |
+
features_list = []
|
138 |
+
print("Extracting "+modal+" features...")
|
139 |
+
with torch.no_grad():
|
140 |
+
for ids in tqdm(ids_list):
|
141 |
+
ids = ids.unsqueeze(0)
|
142 |
+
if modal=="text":
|
143 |
+
masks = torch.tensor([1]*len(ids[0])).unsqueeze(0)
|
144 |
+
features = model.text_enc(ids.to(device), attention_mask=masks.to(device))['last_hidden_state']
|
145 |
+
features = model.avg_pooling(features, masks)
|
146 |
+
features = model.text_proj(features)
|
147 |
+
else:
|
148 |
+
masks = torch.tensor([1]*(int(len(ids[0])/PATCH_LENGTH))).unsqueeze(0)
|
149 |
+
features = model.music_enc(ids, masks)['last_hidden_state']
|
150 |
+
features = model.avg_pooling(features, masks)
|
151 |
+
features = model.music_proj(features)
|
152 |
+
|
153 |
+
features_list.append(features[0])
|
154 |
+
|
155 |
+
return torch.stack(features_list).to(device)
|
156 |
+
|
157 |
+
|
158 |
+
def similar_music_recommendation(file):
|
159 |
+
"""
|
160 |
+
Recommend similar music
|
161 |
+
|
162 |
+
Args:
|
163 |
+
file (Union[str, bytes, BinaryIO, TextIO]): Input file object containing the xml file
|
164 |
+
|
165 |
+
Returns:
|
166 |
+
output (str): Output string
|
167 |
+
"""
|
168 |
+
query = load_music(file.name)
|
169 |
+
with open(KEY_MODAL+"_key_cache_"+str(music_length)+".pth", 'rb') as f:
|
170 |
+
key_cache = torch.load(f)
|
171 |
+
|
172 |
+
# encode query
|
173 |
+
query_ids = encoding_data([query], QUERY_MODAL)
|
174 |
+
query_feature = get_features(query_ids, QUERY_MODAL)
|
175 |
+
|
176 |
+
key_filenames = key_cache["filenames"]
|
177 |
+
key_features = key_cache["features"]
|
178 |
+
|
179 |
+
# compute values
|
180 |
+
values = compute_values(query_feature, key_features)
|
181 |
+
idx = torch.argsort(values)[-1]
|
182 |
+
filename = key_filenames[idx].split('/')[-1][:-4]
|
183 |
+
|
184 |
+
with open("wikimusictext.json", 'r') as f:
|
185 |
+
wikimusictext = json.load(f)
|
186 |
+
|
187 |
+
for item in wikimusictext:
|
188 |
+
if item['title']==filename:
|
189 |
+
output = "Title:\n" + item['title']+'\n\n'
|
190 |
+
output += "Artist:\n" + item['artist']+ '\n\n'
|
191 |
+
output += "Genre:\n" + item['genre']+ '\n\n'
|
192 |
+
output += "Description:\n" + item['text']+ '\n\n'
|
193 |
+
output += "ABC notation:\n" + item['music']+ '\n\n'
|
194 |
+
|
195 |
+
return output
|
196 |
+
|
197 |
+
input_file = gr.inputs.File(label="Upload MusicXML file")
|
198 |
+
output_component = gr.outputs.Textbox()
|
199 |
+
|
200 |
+
gr.Interface(similar_music_recommendation, inputs=input_file, outputs=output_component).launch()
|
clamp-small-512/config.json
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"_name_or_path": "sander-wood/clamp-small-512",
|
3 |
+
"architectures": [
|
4 |
+
"CLaMP"
|
5 |
+
],
|
6 |
+
"attention_probs_dropout_prob": 0.1,
|
7 |
+
"classifier_dropout": null,
|
8 |
+
"hidden_act": "gelu",
|
9 |
+
"hidden_dropout_prob": 0.1,
|
10 |
+
"hidden_size": 768,
|
11 |
+
"initializer_range": 0.02,
|
12 |
+
"intermediate_size": 3072,
|
13 |
+
"layer_norm_eps": 1e-12,
|
14 |
+
"max_length": 512,
|
15 |
+
"max_position_embeddings": 512,
|
16 |
+
"model_type": "bert",
|
17 |
+
"num_attention_heads": 12,
|
18 |
+
"num_hidden_layers": 6,
|
19 |
+
"pad_token_id": 0,
|
20 |
+
"position_embedding_type": "absolute",
|
21 |
+
"torch_dtype": "float32",
|
22 |
+
"transformers_version": "4.18.0",
|
23 |
+
"type_vocab_size": 2,
|
24 |
+
"use_cache": true,
|
25 |
+
"vocab_size": 1
|
26 |
+
}
|
clamp-small-512/pytorch_model.bin
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:43b1a1a32362d64096e74322b711926a03c79b31164d45100dffe640e34e2c6c
|
3 |
+
size 526605909
|
music_key_cache_512.pth
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:e72ca92708b317e877a389bb25a927ecbdb97f2d44ca50d75370b663e3133a35
|
3 |
+
size 3926507
|
requirements.txt
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
transformers==4.18.0
|
2 |
+
--find-links https://download.pytorch.org/whl/torch_stable.html
|
3 |
+
torch==1.9.1+cu111
|
4 |
+
-f https://download.pytorch.org/whl/torch_stable.html
|
5 |
+
torchvision==0.10.1+cu111
|
6 |
+
requests==2.27.1
|
7 |
+
tqdm==4.63.1
|
8 |
+
unidecode==1.3.4
|
utils.py
ADDED
@@ -0,0 +1,357 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import re
|
2 |
+
import os
|
3 |
+
import torch
|
4 |
+
import requests
|
5 |
+
from tqdm import tqdm
|
6 |
+
from unidecode import unidecode
|
7 |
+
from transformers import AutoModel, AutoConfig, BertModel, PreTrainedModel
|
8 |
+
|
9 |
+
# Constants for patch length and number of features in a patch
|
10 |
+
PATCH_LENGTH = 64
|
11 |
+
PATCH_FEATURES = 98
|
12 |
+
|
13 |
+
class MusicPatchilizer:
|
14 |
+
"""
|
15 |
+
Class for converting music data to patches and vice-versa.
|
16 |
+
|
17 |
+
Attributes:
|
18 |
+
delimiters (tuple): A tuple of strings containing the delimiters used for splitting bars.
|
19 |
+
regexPattern (str): A regular expression pattern for splitting bars.
|
20 |
+
pad_id (int): The id of the padding token.
|
21 |
+
mask_id (int): The id of the mask token.
|
22 |
+
eos_id (int): The id of the end-of-sequence token.
|
23 |
+
|
24 |
+
Methods:
|
25 |
+
split_bars(body): Splits a body of music into individual bars using the delimiters specified in `self.delimiters`.
|
26 |
+
bar2patch(bar, patch_length): Encodes a single bar as a patch of specified length.
|
27 |
+
patch2bar(patch): Converts a patch to a bar string.
|
28 |
+
encode(music, music_length, patch_length=PATCH_LENGTH, add_eos_patch=False): Encodes the input music string as a list of patches.
|
29 |
+
decode(patches): Decodes a sequence of patches into a music score.
|
30 |
+
"""
|
31 |
+
def __init__(self):
|
32 |
+
# Delimiters used for splitting bars
|
33 |
+
self.delimiters = "|:", "::", ":|", "[|", "||", "|]", "|"
|
34 |
+
# Regular expression pattern for splitting bars
|
35 |
+
self.regexPattern = '('+'|'.join(map(re.escape, self.delimiters))+')'
|
36 |
+
# Padding, mask, and end-of-sequence token ids
|
37 |
+
self.pad_id = 0
|
38 |
+
self.mask_id = 96
|
39 |
+
self.eos_id = 97
|
40 |
+
|
41 |
+
def split_bars(self, body):
|
42 |
+
"""
|
43 |
+
Splits a body of music into individual bars using the delimiters specified in `self.delimiters`.
|
44 |
+
|
45 |
+
Args:
|
46 |
+
body (str): A string containing the body of music to be split into bars.
|
47 |
+
|
48 |
+
Returns:
|
49 |
+
list: A list of strings containing the individual bars.
|
50 |
+
"""
|
51 |
+
body = "".join(body)
|
52 |
+
bars = re.split(self.regexPattern, body)
|
53 |
+
while("" in bars):
|
54 |
+
bars.remove("")
|
55 |
+
if bars[0] in self.delimiters:
|
56 |
+
bars[1] = bars[0]+bars[1]
|
57 |
+
bars = bars[1:]
|
58 |
+
bars = [bars[i*2]+bars[i*2+1] for i in range(int(len(bars)/2))]
|
59 |
+
|
60 |
+
return bars
|
61 |
+
|
62 |
+
def bar2patch(self, bar, patch_length):
|
63 |
+
"""
|
64 |
+
Encodes a single bar as a patch of specified length.
|
65 |
+
|
66 |
+
Args:
|
67 |
+
bar (str): A string containing the bar to be encoded.
|
68 |
+
patch_length (int): An integer indicating the length of the patch to be returned.
|
69 |
+
|
70 |
+
Returns:
|
71 |
+
list: A list of integer-encoded musical tokens.
|
72 |
+
"""
|
73 |
+
patch = [self.pad_id] * patch_length
|
74 |
+
|
75 |
+
for i in range(min(patch_length, len(bar))):
|
76 |
+
chr = bar[i]
|
77 |
+
idx = ord(chr)
|
78 |
+
if idx>=32 and idx<127:
|
79 |
+
patch[i] = idx-31
|
80 |
+
|
81 |
+
if i+1<patch_length:
|
82 |
+
patch[i+1] = self.eos_id
|
83 |
+
|
84 |
+
return patch
|
85 |
+
|
86 |
+
def patch2bar(self, patch):
|
87 |
+
"""
|
88 |
+
Converts a patch to a bar string.
|
89 |
+
|
90 |
+
Args:
|
91 |
+
patch (list): A list of integer-encoded musical tokens.
|
92 |
+
|
93 |
+
Returns:
|
94 |
+
str: A string containing the decoded bar.
|
95 |
+
"""
|
96 |
+
bar = ""
|
97 |
+
|
98 |
+
for idx in patch:
|
99 |
+
if idx>0 and idx<96:
|
100 |
+
bar += chr(idx+31)
|
101 |
+
else:
|
102 |
+
break
|
103 |
+
|
104 |
+
return bar
|
105 |
+
|
106 |
+
def encode(self, music, music_length, patch_length=PATCH_LENGTH, add_eos_patch=False):
|
107 |
+
"""
|
108 |
+
Encodes the input music string as a list of patches.
|
109 |
+
|
110 |
+
Args:
|
111 |
+
music (str): A string containing the music to be encoded.
|
112 |
+
music_length (int): An integer indicating the maximum number of patches to be returned.
|
113 |
+
patch_length (int): An integer indicating the length of each patch.
|
114 |
+
add_eos_patch (bool): A boolean indicating whether to add an extra patch consisting of all EOS tokens at the end of the encoded music.
|
115 |
+
|
116 |
+
Returns:
|
117 |
+
list: A list of integer-encoded patches.
|
118 |
+
"""
|
119 |
+
# Convert to ASCII and split into lines
|
120 |
+
music = unidecode(music)
|
121 |
+
lines = music.split('\n')
|
122 |
+
try:
|
123 |
+
lines.remove('')
|
124 |
+
except:
|
125 |
+
pass
|
126 |
+
|
127 |
+
body = ""
|
128 |
+
patches = []
|
129 |
+
|
130 |
+
# Iterate over lines, splitting bars and encoding each one as a patch
|
131 |
+
for line in lines:
|
132 |
+
# check if the line is a music score line or not
|
133 |
+
if len(line)>1 and ((line[0].isalpha() and line[1] == ':') or line.startswith('%%score')):
|
134 |
+
# if the current line is a music score line, encode the previous body as patches
|
135 |
+
if body!="":
|
136 |
+
bars = self.split_bars(body)
|
137 |
+
|
138 |
+
for bar in bars:
|
139 |
+
# encode each bar in the body as a patch and append to the patches list
|
140 |
+
patch = self.bar2patch(bar, patch_length)
|
141 |
+
patches.append(patch)
|
142 |
+
# reset the body variable
|
143 |
+
body = ""
|
144 |
+
# encode the current line as a patch and append to the patches list
|
145 |
+
patch = self.bar2patch(line, patch_length)
|
146 |
+
patches.append(patch)
|
147 |
+
else:
|
148 |
+
# if the line is not a music score line, append to the body variable
|
149 |
+
body += line
|
150 |
+
|
151 |
+
if body!="":
|
152 |
+
bars = self.split_bars(body)
|
153 |
+
|
154 |
+
for bar in bars:
|
155 |
+
# encode each bar in the body as a patch and append to the patches list
|
156 |
+
patch = self.bar2patch(bar, patch_length)
|
157 |
+
patches.append(patch)
|
158 |
+
|
159 |
+
# add an extra patch consisting of all EOS tokens, if required
|
160 |
+
if add_eos_patch:
|
161 |
+
eos_patch = [self.eos_id] * patch_length
|
162 |
+
patches = patches + [eos_patch]
|
163 |
+
|
164 |
+
return patches[:music_length]
|
165 |
+
|
166 |
+
def decode(self, patches):
|
167 |
+
"""
|
168 |
+
Decodes a sequence of patches into a music score.
|
169 |
+
|
170 |
+
Args:
|
171 |
+
patches (list): A list of integer-encoded patches.
|
172 |
+
|
173 |
+
Returns:
|
174 |
+
str: A string containing the decoded music score.
|
175 |
+
"""
|
176 |
+
music = ""
|
177 |
+
for patch in patches:
|
178 |
+
music += self.patch2bar(patch)+'\n'
|
179 |
+
|
180 |
+
return music
|
181 |
+
|
182 |
+
|
183 |
+
class MusicEncoder(PreTrainedModel):
|
184 |
+
"""
|
185 |
+
MusicEncoder model for encoding music patches into a sequence of hidden states.
|
186 |
+
|
187 |
+
Args:
|
188 |
+
config (:obj:`BertConfig`): Model configuration class with all the parameters of the model.
|
189 |
+
Initializing with a config file does not load the weights associated with the model, only the configuration.
|
190 |
+
Check out the :meth:`~transformers.PreTrainedModel.from_pretrained` method to load the model weights.
|
191 |
+
|
192 |
+
Attributes:
|
193 |
+
patch_embedding (:obj:`torch.nn.Linear`): A linear layer to convert the one-hot encoded patches to the hidden size of the model.
|
194 |
+
enc (:obj:`BertModel`): The BERT model used to encode the patches.
|
195 |
+
"""
|
196 |
+
def __init__(self, config):
|
197 |
+
super(MusicEncoder, self).__init__(config)
|
198 |
+
self.patch_embedding = torch.nn.Linear(PATCH_LENGTH*PATCH_FEATURES, config.hidden_size)
|
199 |
+
torch.nn.init.normal_(self.patch_embedding.weight, std=0.02)
|
200 |
+
self.enc = BertModel(config=config)
|
201 |
+
|
202 |
+
def forward(self, input_musics, music_masks):
|
203 |
+
"""
|
204 |
+
Args:
|
205 |
+
input_musics (:obj:`torch.LongTensor` of shape :obj:`(batch_size, music_length, patch_length)`):
|
206 |
+
Tensor containing the integer-encoded music patches.
|
207 |
+
music_masks (:obj:`torch.LongTensor` of shape :obj:`(batch_size, music_length)`):
|
208 |
+
Tensor containing the attention masks for the music patches.
|
209 |
+
|
210 |
+
Returns:
|
211 |
+
:obj:`tuple(torch.FloatTensor)` comprising various elements depending on the configuration (:class:`~transformers.BertConfig`) and inputs:
|
212 |
+
last_hidden_state (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, music_length, hidden_size)`):
|
213 |
+
Sequence of hidden-states at the output of the last layer of the model.
|
214 |
+
"""
|
215 |
+
# One-hot encode the input music patches
|
216 |
+
input_musics = torch.nn.functional.one_hot(input_musics, num_classes=PATCH_FEATURES)
|
217 |
+
|
218 |
+
# Reshape the input music patches to feed into the linear layer
|
219 |
+
input_musics = input_musics.reshape(len(input_musics), -1, PATCH_LENGTH*PATCH_FEATURES).type(torch.FloatTensor)
|
220 |
+
|
221 |
+
# Apply the linear layer to convert the one-hot encoded patches to hidden features
|
222 |
+
input_musics = self.patch_embedding(input_musics.to(self.device))
|
223 |
+
|
224 |
+
# Apply the BERT model to encode the music data
|
225 |
+
output = self.enc(inputs_embeds=input_musics, attention_mask=music_masks.to(self.device))
|
226 |
+
|
227 |
+
return output
|
228 |
+
|
229 |
+
|
230 |
+
class CLaMP(PreTrainedModel):
|
231 |
+
"""
|
232 |
+
CLaMP model for joint text and music encoding.
|
233 |
+
|
234 |
+
Args:
|
235 |
+
config (:obj:`BertConfig`): Model configuration class with all the parameters of the model.
|
236 |
+
Initializing with a config file does not load the weights associated with the model, only the configuration.
|
237 |
+
Check out the :meth:`~transformers.PreTrainedModel.from_pretrained` method to load the model weights.
|
238 |
+
text_model_name (:obj:`str`, `optional`, defaults to :obj:`"distilroberta-base"`):
|
239 |
+
The name of the pre-trained text model to be used for text encoding.
|
240 |
+
|
241 |
+
Attributes:
|
242 |
+
text_enc (:obj:`AutoModel`): The pre-trained text model used for text encoding.
|
243 |
+
text_proj (:obj:`torch.nn.Linear`): A linear layer to project the text encoding to the hidden size of the model.
|
244 |
+
music_enc (:obj:`MusicEncoder`): The music encoder model used for music encoding.
|
245 |
+
music_proj (:obj:`torch.nn.Linear`): A linear layer to project the music encoding to the hidden size of the model.
|
246 |
+
"""
|
247 |
+
def __init__(self, config, text_model_name="distilroberta-base"):
|
248 |
+
super(CLaMP, self).__init__(config)
|
249 |
+
self.text_enc = AutoModel.from_pretrained(text_model_name)
|
250 |
+
self.text_proj = torch.nn.Linear(config.hidden_size, config.hidden_size)
|
251 |
+
torch.nn.init.normal_(self.text_proj.weight, std=0.02)
|
252 |
+
|
253 |
+
self.music_enc = MusicEncoder(config=config)
|
254 |
+
self.music_proj = torch.nn.Linear(config.hidden_size, config.hidden_size)
|
255 |
+
torch.nn.init.normal_(self.music_proj.weight, std=0.02)
|
256 |
+
|
257 |
+
def forward(self, input_texts, text_masks, input_musics, music_masks):
|
258 |
+
"""
|
259 |
+
Args:
|
260 |
+
input_texts (:obj:`torch.LongTensor` of shape :obj:`(batch_size, text_length)`):
|
261 |
+
Tensor containing the integer-encoded text.
|
262 |
+
text_masks (:obj:`torch.LongTensor` of shape :obj:`(batch_size, text_length)`):
|
263 |
+
Tensor containing the attention masks for the text.
|
264 |
+
input_musics (:obj:`torch.LongTensor` of shape :obj:`(batch_size, music_length, patch_length)`):
|
265 |
+
Tensor containing the integer-encoded music patches.
|
266 |
+
music_masks (:obj:`torch.LongTensor` of shape :obj:`(batch_size, music_length)`):
|
267 |
+
Tensor containing the attention masks for the music patches.
|
268 |
+
|
269 |
+
Returns:
|
270 |
+
:obj:`tuple(torch.FloatTensor)` comprising various elements depending on the configuration (:class:`~transformers.BertConfig`) and inputs:
|
271 |
+
music_features (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, hidden_size)`):
|
272 |
+
The music features extracted from the music encoder.
|
273 |
+
text_features (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, hidden_size)`):
|
274 |
+
The text features extracted from the text encoder.
|
275 |
+
"""
|
276 |
+
# Encode input texts
|
277 |
+
text_features = self.text_enc(input_texts.to(self.device), attention_mask=text_masks.to(self.device))['last_hidden_state']
|
278 |
+
text_features = self.avg_pooling(text_features, text_masks)
|
279 |
+
text_features = self.text_proj(text_features)
|
280 |
+
|
281 |
+
# Encode input musics
|
282 |
+
music_features = self.music_enc(input_musics, music_masks)['last_hidden_state']
|
283 |
+
music_features = self.avg_pooling(music_features, music_masks)
|
284 |
+
music_features = self.music_proj(music_features)
|
285 |
+
|
286 |
+
return music_features, text_features
|
287 |
+
|
288 |
+
def avg_pooling(self, input_features, input_masks):
|
289 |
+
"""
|
290 |
+
Applies average pooling to the input features.
|
291 |
+
|
292 |
+
Args:
|
293 |
+
input_features (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, seq_length, hidden_size)`):
|
294 |
+
Tensor containing the input features.
|
295 |
+
input_masks (:obj:`torch.LongTensor` of shape :obj:`(batch_size, seq_length)`):
|
296 |
+
Tensor containing the attention masks for the input features.
|
297 |
+
|
298 |
+
Returns:
|
299 |
+
:obj:`torch.FloatTensor` of shape :obj:`(batch_size, hidden_size)`:
|
300 |
+
The pooled features.
|
301 |
+
"""
|
302 |
+
input_masks = input_masks.unsqueeze(-1).to(self.device)
|
303 |
+
input_features = input_features * input_masks
|
304 |
+
avg_pool = input_features.sum(dim=1) / input_masks.sum(dim=1)
|
305 |
+
|
306 |
+
return avg_pool
|
307 |
+
|
308 |
+
@classmethod
|
309 |
+
def from_pretrained(cls, pretrained_model_name_or_path, *model_args, **kwargs):
|
310 |
+
"""
|
311 |
+
Instantiate a CLaMP model from a pre-trained model configuration.
|
312 |
+
|
313 |
+
Args:
|
314 |
+
pretrained_model_name_or_path (:obj:`str`):
|
315 |
+
This can be either:
|
316 |
+
"clamp-small-512" for the small CLaMP model with 512 max sequence length.
|
317 |
+
"clamp-small-1024" for the small CLaMP model with 1024 max sequence length.
|
318 |
+
|
319 |
+
Returns:
|
320 |
+
:class:`~transformers.CLaMP`: The CLaMP model.
|
321 |
+
"""
|
322 |
+
model_dir = pretrained_model_name_or_path
|
323 |
+
|
324 |
+
# If the pre-trained model is not found locally, download it from Hugging Face
|
325 |
+
if not os.path.exists(model_dir):
|
326 |
+
# Create the model directory and download the config and pytorch model files
|
327 |
+
os.makedirs(model_dir)
|
328 |
+
config_url = f"https://huggingface.co/{pretrained_model_name_or_path}/raw/main/config.json"
|
329 |
+
model_url = f"https://huggingface.co/{pretrained_model_name_or_path}/resolve/main/pytorch_model.bin"
|
330 |
+
chunk_size = 1024 * 1024 # 1MB
|
331 |
+
|
332 |
+
# download config file
|
333 |
+
with requests.get(config_url, stream=True) as r:
|
334 |
+
r.raise_for_status()
|
335 |
+
total_size = int(r.headers.get('content-length', 0))
|
336 |
+
with open(model_dir+"/config.json", 'wb') as f:
|
337 |
+
with tqdm(total=total_size, unit='B', unit_scale=True, desc='Downloading config') as pbar:
|
338 |
+
for chunk in r.iter_content(chunk_size=chunk_size):
|
339 |
+
f.write(chunk)
|
340 |
+
pbar.update(len(chunk))
|
341 |
+
|
342 |
+
# download pytorch model file
|
343 |
+
with requests.get(model_url, stream=True) as r:
|
344 |
+
r.raise_for_status()
|
345 |
+
total_size = int(r.headers.get('content-length', 0))
|
346 |
+
with open(model_dir+"/pytorch_model.bin", 'wb') as f:
|
347 |
+
with tqdm(total=total_size, unit='B', unit_scale=True, desc='Downloading model') as pbar:
|
348 |
+
for chunk in r.iter_content(chunk_size=chunk_size):
|
349 |
+
f.write(chunk)
|
350 |
+
pbar.update(len(chunk))
|
351 |
+
|
352 |
+
# Load the model weights and configuration
|
353 |
+
config = AutoConfig.from_pretrained(pretrained_model_name_or_path, *model_args, **kwargs)
|
354 |
+
model = cls(config)
|
355 |
+
model.load_state_dict(torch.load(pretrained_model_name_or_path+str('/pytorch_model.bin')))
|
356 |
+
|
357 |
+
return model
|
wikimusictext.json
ADDED
The diff for this file is too large to render.
See raw diff
|
|
xml2abc.py
ADDED
@@ -0,0 +1,1582 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python
|
2 |
+
# coding=latin-1
|
3 |
+
'''
|
4 |
+
Copyright (C) 2012-2018: W.G. Vree
|
5 |
+
Contributions: M. Tarenskeen, N. Liberg, Paul Villiger, Janus Meuris, Larry Myerscough,
|
6 |
+
Dick Jackson, Jan Wybren de Jong, Mark Zealey.
|
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. <http://www.gnu.org/licenses/lgpl.html>.
|
14 |
+
'''
|
15 |
+
|
16 |
+
try: import xml.etree.cElementTree as E
|
17 |
+
except: import xml.etree.ElementTree as E
|
18 |
+
import os, sys, types, re, math
|
19 |
+
|
20 |
+
VERSION = 143
|
21 |
+
|
22 |
+
python3 = sys.version_info.major > 2
|
23 |
+
if python3:
|
24 |
+
tupletype = tuple
|
25 |
+
listtype = list
|
26 |
+
max_int = sys.maxsize
|
27 |
+
else:
|
28 |
+
tupletype = types.TupleType
|
29 |
+
listtype = types.ListType
|
30 |
+
max_int = sys.maxint
|
31 |
+
|
32 |
+
note_ornamentation_map = { # for notations/, modified from EasyABC
|
33 |
+
'ornaments/trill-mark': 'T',
|
34 |
+
'ornaments/mordent': 'M',
|
35 |
+
'ornaments/inverted-mordent': 'P',
|
36 |
+
'ornaments/turn': '!turn!',
|
37 |
+
'ornaments/inverted-turn': '!invertedturn!',
|
38 |
+
'technical/up-bow': 'u',
|
39 |
+
'technical/down-bow': 'v',
|
40 |
+
'technical/harmonic': '!open!',
|
41 |
+
'technical/open-string': '!open!',
|
42 |
+
'technical/stopped': '!plus!',
|
43 |
+
'technical/snap-pizzicato': '!snap!',
|
44 |
+
'technical/thumb-position': '!thumb!',
|
45 |
+
'articulations/accent': '!>!',
|
46 |
+
'articulations/strong-accent':'!^!',
|
47 |
+
'articulations/staccato': '.',
|
48 |
+
'articulations/staccatissimo':'!wedge!',
|
49 |
+
'articulations/scoop': '!slide!',
|
50 |
+
'fermata': '!fermata!',
|
51 |
+
'arpeggiate': '!arpeggio!',
|
52 |
+
'articulations/tenuto': '!tenuto!',
|
53 |
+
'articulations/staccatissimo':'!wedge!', # not sure whether this is the right translation
|
54 |
+
'articulations/spiccato': '!wedge!', # not sure whether this is the right translation
|
55 |
+
'articulations/breath-mark': '!breath!', # this may need to be tested to make sure it appears on the right side of the note
|
56 |
+
'articulations/detached-legato': '!tenuto!.',
|
57 |
+
}
|
58 |
+
|
59 |
+
dynamics_map = { # for direction/direction-type/dynamics/
|
60 |
+
'p': '!p!',
|
61 |
+
'pp': '!pp!',
|
62 |
+
'ppp': '!ppp!',
|
63 |
+
'pppp': '!pppp!',
|
64 |
+
'f': '!f!',
|
65 |
+
'ff': '!ff!',
|
66 |
+
'fff': '!fff!',
|
67 |
+
'ffff': '!ffff!',
|
68 |
+
'mp': '!mp!',
|
69 |
+
'mf': '!mf!',
|
70 |
+
'sfz': '!sfz!',
|
71 |
+
}
|
72 |
+
|
73 |
+
percSvg = '''%%beginsvg
|
74 |
+
<defs>
|
75 |
+
<text id="x" x="-3" y="0"></text>
|
76 |
+
<text id="x-" x="-3" y="0"></text>
|
77 |
+
<text id="x+" x="-3" y="0"></text>
|
78 |
+
<text id="normal" x="-3.7" y="0"></text>
|
79 |
+
<text id="normal-" x="-3.7" y="0"></text>
|
80 |
+
<text id="normal+" x="-3.7" y="0"></text>
|
81 |
+
<g id="circle-x"><text x="-3" y="0"></text><circle r="4" class="stroke"></circle></g>
|
82 |
+
<g id="circle-x-"><text x="-3" y="0"></text><circle r="4" class="stroke"></circle></g>
|
83 |
+
<path id="triangle" d="m-4 -3.2l4 6.4 4 -6.4z" class="stroke" style="stroke-width:1.4"></path>
|
84 |
+
<path id="triangle-" d="m-4 -3.2l4 6.4 4 -6.4z" class="stroke" style="stroke-width:1.4"></path>
|
85 |
+
<path id="triangle+" d="m-4 -3.2l4 6.4 4 -6.4z" class="stroke" style="fill:#000"></path>
|
86 |
+
<path id="square" d="m-3.5 3l0 -6.2 7.2 0 0 6.2z" class="stroke" style="stroke-width:1.4"></path>
|
87 |
+
<path id="square-" d="m-3.5 3l0 -6.2 7.2 0 0 6.2z" class="stroke" style="stroke-width:1.4"></path>
|
88 |
+
<path id="square+" d="m-3.5 3l0 -6.2 7.2 0 0 6.2z" class="stroke" style="fill:#000"></path>
|
89 |
+
<path id="diamond" d="m0 -3l4.2 3.2 -4.2 3.2 -4.2 -3.2z" class="stroke" style="stroke-width:1.4"></path>
|
90 |
+
<path id="diamond-" d="m0 -3l4.2 3.2 -4.2 3.2 -4.2 -3.2z" class="stroke" style="stroke-width:1.4"></path>
|
91 |
+
<path id="diamond+" d="m0 -3l4.2 3.2 -4.2 3.2 -4.2 -3.2z" class="stroke" style="fill:#000"></path>
|
92 |
+
</defs>
|
93 |
+
%%endsvg'''
|
94 |
+
|
95 |
+
tabSvg = '''%%beginsvg
|
96 |
+
<style type="text/css">
|
97 |
+
.bf {font-family:sans-serif; font-size:7px}
|
98 |
+
</style>
|
99 |
+
<defs>
|
100 |
+
<rect id="clr" x="-3" y="-1" width="6" height="5" fill="white"></rect>
|
101 |
+
<rect id="clr2" x="-3" y="-1" width="11" height="5" fill="white"></rect>'''
|
102 |
+
|
103 |
+
kopSvg = '<g id="kop%s" class="bf"><use xlink:href="#clr"></use><text x="-2" y="3">%s</text></g>\n'
|
104 |
+
kopSvg2 = '<g id="kop%s" class="bf"><use xlink:href="#clr2"></use><text x="-2" y="3">%s</text></g>\n'
|
105 |
+
|
106 |
+
def info (s, warn=1): sys.stderr.write ((warn and '-- ' or '') + s + '\n')
|
107 |
+
|
108 |
+
#-------------------
|
109 |
+
# data abstractions
|
110 |
+
#-------------------
|
111 |
+
class Measure:
|
112 |
+
def __init__ (s, p):
|
113 |
+
s.reset ()
|
114 |
+
s.ixp = p # part number
|
115 |
+
s.ixm = 0 # measure number
|
116 |
+
s.mdur = 0 # measure duration (nominal metre value in divisions)
|
117 |
+
s.divs = 0 # number of divisions per 1/4
|
118 |
+
s.mtr = 4,4 # meter
|
119 |
+
|
120 |
+
def reset (s): # reset each measure
|
121 |
+
s.attr = '' # measure signatures, tempo
|
122 |
+
s.lline = '' # left barline, but only holds ':' at start of repeat, otherwise empty
|
123 |
+
s.rline = '|' # right barline
|
124 |
+
s.lnum = '' # (left) volta number
|
125 |
+
|
126 |
+
class Note:
|
127 |
+
def __init__ (s, dur=0, n=None):
|
128 |
+
s.tijd = 0 # the time in XML division units
|
129 |
+
s.dur = dur # duration of a note in XML divisions
|
130 |
+
s.fact = None # time modification for tuplet notes (num, div)
|
131 |
+
s.tup = [''] # start(s) and/or stop(s) of tuplet
|
132 |
+
s.tupabc = '' # abc tuplet string to issue before note
|
133 |
+
s.beam = 0 # 1 = beamed
|
134 |
+
s.grace = 0 # 1 = grace note
|
135 |
+
s.before = [] # abc string that goes before the note/chord
|
136 |
+
s.after = '' # the same after the note/chord
|
137 |
+
s.ns = n and [n] or [] # notes in the chord
|
138 |
+
s.lyrs = {} # {number -> syllabe}
|
139 |
+
s.tab = None # (string number, fret number)
|
140 |
+
s.ntdec = '' # !string!, !courtesy!
|
141 |
+
|
142 |
+
class Elem:
|
143 |
+
def __init__ (s, string):
|
144 |
+
s.tijd = 0 # the time in XML division units
|
145 |
+
s.str = string # any abc string that is not a note
|
146 |
+
|
147 |
+
class Counter:
|
148 |
+
def inc (s, key, voice): s.counters [key][voice] = s.counters [key].get (voice, 0) + 1
|
149 |
+
def clear (s, vnums): # reset all counters
|
150 |
+
tups = list( zip (vnums.keys (), len (vnums) * [0]))
|
151 |
+
s.counters = {'note': dict (tups), 'nopr': dict (tups), 'nopt': dict (tups)}
|
152 |
+
def getv (s, key, voice): return s.counters[key][voice]
|
153 |
+
def prcnt (s, ip): # print summary of all non zero counters
|
154 |
+
for iv in s.counters ['note']:
|
155 |
+
if s.getv ('nopr', iv) != 0:
|
156 |
+
info ( 'part %d, voice %d has %d skipped non printable notes' % (ip, iv, s.getv ('nopr', iv)))
|
157 |
+
if s.getv ('nopt', iv) != 0:
|
158 |
+
info ( 'part %d, voice %d has %d notes without pitch' % (ip, iv, s.getv ('nopt', iv)))
|
159 |
+
if s.getv ('note', iv) == 0: # no real notes counted in this voice
|
160 |
+
info ( 'part %d, skipped empty voice %d' % (ip, iv))
|
161 |
+
|
162 |
+
class Music:
|
163 |
+
def __init__(s, options):
|
164 |
+
s.tijd = 0 # the current time
|
165 |
+
s.maxtime = 0 # maximum time in a measure
|
166 |
+
s.gMaten = [] # [voices,.. for all measures in a part]
|
167 |
+
s.gLyrics = [] # [{num: (abc_lyric_string, melis)},.. for all measures in a part]
|
168 |
+
s.vnums = {} # all used voice id's in a part (xml voice id's == numbers)
|
169 |
+
s.cnt = Counter () # global counter object
|
170 |
+
s.vceCnt = 1 # the global voice count over all parts
|
171 |
+
s.lastnote = None # the last real note record inserted in s.voices
|
172 |
+
s.bpl = options.b # the max number of bars per line when writing abc
|
173 |
+
s.cpl = options.n # the number of chars per line when writing abc
|
174 |
+
s.repbra = 0 # true if volta is used somewhere
|
175 |
+
s.nvlt = options.v # no volta on higher voice numbers
|
176 |
+
s.jscript = options.j # compatibility with javascript version
|
177 |
+
|
178 |
+
def initVoices (s, newPart=0):
|
179 |
+
s.vtimes, s.voices, s.lyrics = {}, {}, {}
|
180 |
+
for v in s.vnums:
|
181 |
+
s.vtimes [v] = 0 # {voice: the end time of the last item in each voice}
|
182 |
+
s.voices [v] = [] # {voice: [Note|Elem, ..]}
|
183 |
+
s.lyrics [v] = [] # {voice: [{num: syl}, ..]}
|
184 |
+
if newPart: s.cnt.clear (s.vnums) # clear counters once per part
|
185 |
+
|
186 |
+
def incTime (s, dt):
|
187 |
+
s.tijd += dt
|
188 |
+
if s.tijd < 0: s.tijd = 0 # erroneous <backup> element
|
189 |
+
if s.tijd > s.maxtime: s.maxtime = s.tijd
|
190 |
+
|
191 |
+
def appendElemCv (s, voices, elem):
|
192 |
+
for v in voices:
|
193 |
+
s.appendElem (v, elem) # insert element in all voices
|
194 |
+
|
195 |
+
def insertElem (s, v, elem): # insert at the start of voice v in the current measure
|
196 |
+
obj = Elem (elem)
|
197 |
+
obj.tijd = 0 # because voice is sorted later
|
198 |
+
s.voices [v].insert (0, obj)
|
199 |
+
|
200 |
+
def appendObj (s, v, obj, dur):
|
201 |
+
obj.tijd = s.tijd
|
202 |
+
s.voices [v].append (obj)
|
203 |
+
s.incTime (dur)
|
204 |
+
if s.tijd > s.vtimes[v]: s.vtimes[v] = s.tijd # don't update for inserted earlier items
|
205 |
+
|
206 |
+
def appendElem (s, v, elem, tel=0):
|
207 |
+
s.appendObj (v, Elem (elem), 0)
|
208 |
+
if tel: s.cnt.inc ('note', v) # count number of certain elements in each voice (in addition to notes)
|
209 |
+
|
210 |
+
def appendElemT (s, v, elem, tijd): # insert element at specified time
|
211 |
+
obj = Elem (elem)
|
212 |
+
obj.tijd = tijd
|
213 |
+
s.voices [v].append (obj)
|
214 |
+
|
215 |
+
def appendNote (s, v, note, noot):
|
216 |
+
note.ns.append (note.ntdec + noot)
|
217 |
+
s.appendObj (v, note, int (note.dur))
|
218 |
+
s.lastnote = note # remember last note/rest for later modifications (chord, grace)
|
219 |
+
if noot != 'z' and noot != 'x': # real notes and grace notes
|
220 |
+
s.cnt.inc ('note', v) # count number of real notes in each voice
|
221 |
+
if not note.grace: # for every real note
|
222 |
+
s.lyrics[v].append (note.lyrs) # even when it has no lyrics
|
223 |
+
|
224 |
+
def getLastRec (s, voice):
|
225 |
+
if s.gMaten: return s.gMaten[-1][voice][-1] # the last record in the last measure
|
226 |
+
return None # no previous records in the first measure
|
227 |
+
|
228 |
+
def getLastMelis (s, voice, num): # get melisma of last measure
|
229 |
+
if s.gLyrics:
|
230 |
+
lyrdict = s.gLyrics[-1][voice] # the previous lyrics dict in this voice
|
231 |
+
if num in lyrdict: return lyrdict[num][1] # lyrdict = num -> (lyric string, melisma)
|
232 |
+
return 0 # no previous lyrics in voice or line number
|
233 |
+
|
234 |
+
def addChord (s, note, noot): # careful: we assume that chord notes follow immediately
|
235 |
+
for d in note.before: # put all decorations before chord
|
236 |
+
if d not in s.lastnote.before:
|
237 |
+
s.lastnote.before += [d]
|
238 |
+
s.lastnote.ns.append (note.ntdec + noot)
|
239 |
+
|
240 |
+
def addBar (s, lbrk, m): # linebreak, measure data
|
241 |
+
if m.mdur and s.maxtime > m.mdur: info ('measure %d in part %d longer than metre' % (m.ixm+1, m.ixp+1))
|
242 |
+
s.tijd = s.maxtime # the time of the bar lines inserted here
|
243 |
+
for v in s.vnums:
|
244 |
+
if m.lline or m.lnum: # if left barline or left volta number
|
245 |
+
p = s.getLastRec (v) # get the previous barline record
|
246 |
+
if p: # in measure 1 no previous measure is available
|
247 |
+
x = p.str # p.str is the ABC barline string
|
248 |
+
if m.lline: # append begin of repeat, m.lline == ':'
|
249 |
+
x = (x + m.lline).replace (':|:','::').replace ('||','|')
|
250 |
+
if s.nvlt == 3: # add volta number only to lowest voice in part 0
|
251 |
+
if m.ixp + v == min (s.vnums): x += m.lnum
|
252 |
+
elif m.lnum: # new behaviour with I:repbra 0
|
253 |
+
x += m.lnum # add volta number(s) or text to all voices
|
254 |
+
s.repbra = 1 # signal occurrence of a volta
|
255 |
+
p.str = x # modify previous right barline
|
256 |
+
elif m.lline: # begin of new part and left repeat bar is required
|
257 |
+
s.insertElem (v, '|:')
|
258 |
+
if lbrk:
|
259 |
+
p = s.getLastRec (v) # get the previous barline record
|
260 |
+
if p: p.str += lbrk # insert linebreak char after the barlines+volta
|
261 |
+
if m.attr: # insert signatures at front of buffer
|
262 |
+
s.insertElem (v, '%s' % m.attr)
|
263 |
+
s.appendElem (v, ' %s' % m.rline) # insert current barline record at time maxtime
|
264 |
+
s.voices[v] = sortMeasure (s.voices[v], m) # make all times consistent
|
265 |
+
lyrs = s.lyrics[v] # [{number: sylabe}, .. for all notes]
|
266 |
+
lyrdict = {} # {number: (abc_lyric_string, melis)} for this voice
|
267 |
+
nums = [num for d in lyrs for num in d.keys ()] # the lyrics numbers in this measure
|
268 |
+
maxNums = max (nums + [0]) # the highest lyrics number in this measure
|
269 |
+
for i in range (maxNums, 0, -1):
|
270 |
+
xs = [syldict.get (i, '') for syldict in lyrs] # collect the syllabi with number i
|
271 |
+
melis = s.getLastMelis (v, i) # get melisma from last measure
|
272 |
+
lyrdict [i] = abcLyr (xs, melis)
|
273 |
+
s.lyrics[v] = lyrdict # {number: (abc_lyric_string, melis)} for this measure
|
274 |
+
mkBroken (s.voices[v])
|
275 |
+
s.gMaten.append (s.voices)
|
276 |
+
s.gLyrics.append (s.lyrics)
|
277 |
+
s.tijd = s.maxtime = 0
|
278 |
+
s.initVoices ()
|
279 |
+
|
280 |
+
def outVoices (s, divs, ip, isSib): # output all voices of part ip
|
281 |
+
vvmap = {} # xml voice number -> abc voice number (one part)
|
282 |
+
vnum_keys = list (s.vnums.keys ())
|
283 |
+
if s.jscript or isSib: vnum_keys.sort ()
|
284 |
+
lvc = min (vnum_keys or [1]) # lowest xml voice number of this part
|
285 |
+
for iv in vnum_keys:
|
286 |
+
if s.cnt.getv ('note', iv) == 0: # no real notes counted in this voice
|
287 |
+
continue # skip empty voices
|
288 |
+
if abcOut.denL: unitL = abcOut.denL # take the unit length from the -d option
|
289 |
+
else: unitL = compUnitLength (iv, s.gMaten, divs) # compute the best unit length for this voice
|
290 |
+
abcOut.cmpL.append (unitL) # remember for header output
|
291 |
+
vn, vl = [], {} # for voice iv: collect all notes to vn and all lyric lines to vl
|
292 |
+
for im in range (len (s.gMaten)):
|
293 |
+
measure = s.gMaten [im][iv]
|
294 |
+
vn.append (outVoice (measure, divs [im], im, ip, unitL))
|
295 |
+
checkMelismas (s.gLyrics, s.gMaten, im, iv)
|
296 |
+
for n, (lyrstr, melis) in s.gLyrics [im][iv].items ():
|
297 |
+
if n in vl:
|
298 |
+
while len (vl[n]) < im: vl[n].append ('') # fill in skipped measures
|
299 |
+
vl[n].append (lyrstr)
|
300 |
+
else:
|
301 |
+
vl[n] = im * [''] + [lyrstr] # must skip im measures
|
302 |
+
for n, lyrs in vl.items (): # fill up possibly empty lyric measures at the end
|
303 |
+
mis = len (vn) - len (lyrs)
|
304 |
+
lyrs += mis * ['']
|
305 |
+
abcOut.add ('V:%d' % s.vceCnt)
|
306 |
+
if s.repbra:
|
307 |
+
if s.nvlt == 1 and s.vceCnt > 1: abcOut.add ('I:repbra 0') # only volta on first voice
|
308 |
+
if s.nvlt == 2 and iv > lvc: abcOut.add ('I:repbra 0') # only volta on first voice of each part
|
309 |
+
if s.cpl > 0: s.bpl = 0 # option -n (max chars per line) overrules -b (max bars per line)
|
310 |
+
elif s.bpl == 0: s.cpl = 100 # the default: 100 chars per line
|
311 |
+
bn = 0 # count bars
|
312 |
+
while vn: # while still measures available
|
313 |
+
ib = 1
|
314 |
+
chunk = vn [0]
|
315 |
+
while ib < len (vn):
|
316 |
+
if s.cpl > 0 and len (chunk) + len (vn [ib]) >= s.cpl: break # line full (number of chars)
|
317 |
+
if s.bpl > 0 and ib >= s.bpl: break # line full (number of bars)
|
318 |
+
chunk += vn [ib]
|
319 |
+
ib += 1
|
320 |
+
bn += ib
|
321 |
+
abcOut.add (chunk + ' %%%d' % bn) # line with barnumer
|
322 |
+
del vn[:ib] # chop ib bars
|
323 |
+
lyrlines = sorted (vl.items ()) # order the numbered lyric lines for output
|
324 |
+
for n, lyrs in lyrlines:
|
325 |
+
abcOut.add ('w: ' + '|'.join (lyrs[:ib]) + '|')
|
326 |
+
del lyrs[:ib]
|
327 |
+
vvmap [iv] = s.vceCnt # xml voice number -> abc voice number
|
328 |
+
s.vceCnt += 1 # count voices over all parts
|
329 |
+
s.gMaten = [] # reset the follwing instance vars for each part
|
330 |
+
s.gLyrics = []
|
331 |
+
s.cnt.prcnt (ip+1) # print summary of skipped items in this part
|
332 |
+
return vvmap
|
333 |
+
|
334 |
+
class ABCoutput:
|
335 |
+
pagekeys = 'scale,pageheight,pagewidth,leftmargin,rightmargin,topmargin,botmargin'.split (',')
|
336 |
+
def __init__ (s, fnmext, pad, X, options):
|
337 |
+
s.fnmext = fnmext
|
338 |
+
s.outlist = [] # list of ABC strings
|
339 |
+
s.title = 'T:Title'
|
340 |
+
s.key = 'none'
|
341 |
+
s.clefs = {} # clefs for all abc-voices
|
342 |
+
s.mtr = 'none'
|
343 |
+
s.tempo = 0 # 0 -> no tempo field
|
344 |
+
s.tempo_units = (1,4) # note type of tempo direction
|
345 |
+
s.pad = pad # the output path or none
|
346 |
+
s.X = X + 1 # the abc tune number
|
347 |
+
s.denL = options.d # denominator of the unit length (L:) from -d option
|
348 |
+
s.volpan = int (options.m) # 0 -> no %%MIDI, 1 -> only program, 2 -> all %%MIDI
|
349 |
+
s.cmpL = [] # computed optimal unit length for all voices
|
350 |
+
s.jscript = options.j # compatibility with javascript version
|
351 |
+
s.tstep = options.t # translate percmap to voicemap
|
352 |
+
s.stemless = 0 # use U:s=!stemless!
|
353 |
+
s.shiftStem = options.s # shift note heads 3 units left
|
354 |
+
if pad:
|
355 |
+
_, base_name = os.path.split (fnmext)
|
356 |
+
s.outfile = open (os.path.join (pad, base_name), 'w')
|
357 |
+
else: s.outfile = sys.stdout
|
358 |
+
if s.jscript: s.X = 1 # always X:1 in javascript version
|
359 |
+
s.pageFmt = {}
|
360 |
+
for k in s.pagekeys: s.pageFmt [k] = None
|
361 |
+
if len (options.p) == 7:
|
362 |
+
for k, v in zip (s.pagekeys, options.p):
|
363 |
+
try: s.pageFmt [k] = float (v)
|
364 |
+
except: info ('illegal float %s for %s', (k, v)); continue
|
365 |
+
|
366 |
+
def add (s, str):
|
367 |
+
s.outlist.append (str + '\n') # collect all ABC output
|
368 |
+
|
369 |
+
def mkHeader (s, stfmap, partlist, midimap, vmpdct, koppen): # stfmap = [parts], part = [staves], stave = [voices]
|
370 |
+
accVce, accStf, staffs = [], [], stfmap[:] # staffs is consumed
|
371 |
+
for x in partlist: # collect partnames into accVce and staff groups into accStf
|
372 |
+
try: prgroupelem (x, ('', ''), '', stfmap, accVce, accStf)
|
373 |
+
except: info ('lousy musicxml: error in part-list')
|
374 |
+
staves = ' '.join (accStf)
|
375 |
+
clfnms = {}
|
376 |
+
for part, (partname, partabbrv) in zip (staffs, accVce):
|
377 |
+
if not part: continue # skip empty part
|
378 |
+
firstVoice = part[0][0] # the first voice number in this part
|
379 |
+
nm = partname.replace ('\n','\\n').replace ('.:','.').strip (':')
|
380 |
+
snm = partabbrv.replace ('\n','\\n').replace ('.:','.').strip (':')
|
381 |
+
clfnms [firstVoice] = (nm and 'nm="%s"' % nm or '') + (snm and ' snm="%s"' % snm or '')
|
382 |
+
hd = ['X:%d\n%s\n' % (s.X, s.title)]
|
383 |
+
for i, k in enumerate (s.pagekeys):
|
384 |
+
if s.jscript and k in ['pageheight','topmargin', 'botmargin']: continue
|
385 |
+
if s.pageFmt [k] != None: hd.append ('%%%%%s %.2f%s\n' % (k, s.pageFmt [k], i > 0 and 'cm' or ''))
|
386 |
+
if staves and len (accStf) > 1: hd.append ('%%score ' + staves + '\n')
|
387 |
+
tempo = s.tempo and 'Q:%d/%d=%s\n' % (s.tempo_units [0], s.tempo_units [1], s.tempo) or '' # default no tempo field
|
388 |
+
d = {} # determine the most frequently occurring unit length over all voices
|
389 |
+
for x in s.cmpL: d[x] = d.get (x, 0) + 1
|
390 |
+
if s.jscript: defLs = sorted (d.items (), key=lambda x: (-x[1], x[0])) # when tie (1) sort on key (0)
|
391 |
+
else: defLs = sorted (d.items (), key=lambda x: -x[1])
|
392 |
+
defL = s.denL and s.denL or defLs [0][0] # override default unit length with -d option
|
393 |
+
hd.append ('L:1/%d\n%sM:%s\n' % (defL, tempo, s.mtr))
|
394 |
+
hd.append ('K:%s\n' % s.key)
|
395 |
+
if s.stemless: hd.append ('U:s=!stemless!\n')
|
396 |
+
vxs = sorted (vmpdct.keys ())
|
397 |
+
for vx in vxs: hd.extend (vmpdct [vx])
|
398 |
+
s.dojef = 0 # translate percmap to voicemap
|
399 |
+
for vnum, clef in s.clefs.items ():
|
400 |
+
ch, prg, vol, pan = midimap [vnum-1][:4]
|
401 |
+
dmap = midimap [vnum - 1][4:] # map of abc percussion notes to midi notes
|
402 |
+
if dmap and 'perc' not in clef: clef = (clef + ' map=perc').strip ();
|
403 |
+
hd.append ('V:%d %s %s\n' % (vnum, clef, clfnms.get (vnum, '')))
|
404 |
+
if vnum in vmpdct:
|
405 |
+
hd.append ('%%%%voicemap tab%d\n' % vnum)
|
406 |
+
hd.append ('K:none\nM:none\n%%clef none\n%%staffscale 1.6\n%%flatbeams true\n%%stemdir down\n')
|
407 |
+
if 'perc' in clef: hd.append ('K:none\n'); # no key for a perc voice
|
408 |
+
if s.volpan > 1: # option -m 2 -> output all recognized midi commands when needed and present in xml
|
409 |
+
if ch > 0 and ch != vnum: hd.append ('%%%%MIDI channel %d\n' % ch)
|
410 |
+
if prg > 0: hd.append ('%%%%MIDI program %d\n' % (prg - 1))
|
411 |
+
if vol >= 0: hd.append ('%%%%MIDI control 7 %.0f\n' % vol) # volume == 0 is possible ...
|
412 |
+
if pan >= 0: hd.append ('%%%%MIDI control 10 %.0f\n' % pan)
|
413 |
+
elif s.volpan > 0: # default -> only output midi program command when present in xml
|
414 |
+
if dmap and ch > 0: hd.append ('%%%%MIDI channel %d\n' % ch) # also channel if percussion part
|
415 |
+
if prg > 0: hd.append ('%%%%MIDI program %d\n' % (prg - 1))
|
416 |
+
for abcNote, step, midiNote, notehead in dmap:
|
417 |
+
if not notehead: notehead = 'normal'
|
418 |
+
if abcMid (abcNote) != midiNote or abcNote != step:
|
419 |
+
if s.volpan > 0: hd.append ('%%%%MIDI drummap %s %s\n' % (abcNote, midiNote))
|
420 |
+
hd.append ('I:percmap %s %s %s %s\n' % (abcNote, step, midiNote, notehead))
|
421 |
+
s.dojef = s.tstep
|
422 |
+
if defL != s.cmpL [vnum-1]: # only if computed unit length different from header
|
423 |
+
hd.append ('L:1/%d\n' % s.cmpL [vnum-1])
|
424 |
+
s.outlist = hd + s.outlist
|
425 |
+
if koppen: # output SVG stuff needed for tablature
|
426 |
+
k1 = kopSvg.replace ('-2','-5') if s.shiftStem else kopSvg # shift note heads 3 units left
|
427 |
+
k2 = kopSvg2.replace ('-2','-5') if s.shiftStem else kopSvg2
|
428 |
+
tb = tabSvg.replace ('-3','-6') if s.shiftStem else tabSvg
|
429 |
+
ks = sorted (koppen.keys ()) # javascript compatibility
|
430 |
+
ks = [k2 % (k, k) if len (k) == 2 else k1 % (k, k) for k in ks]
|
431 |
+
tbs = map (lambda x: x.strip () + '\n', tb.splitlines ()) # javascript compatibility
|
432 |
+
s.outlist = tbs + ks + ['</defs>\n%%endsvg\n'] + s.outlist
|
433 |
+
|
434 |
+
def writeall (s): # determine the required encoding of the entire ABC output
|
435 |
+
str = ''.join (s.outlist)
|
436 |
+
if s.dojef: str = perc2map (str)
|
437 |
+
if python3: s.outfile.write (str)
|
438 |
+
else: s.outfile.write (str.encode ('utf-8'))
|
439 |
+
if s.pad: s.outfile.close () # close each file with -o option
|
440 |
+
else: s.outfile.write ('\n') # add empty line between tunes on stdout
|
441 |
+
info ('%s written with %d voices' % (s.fnmext, len (s.clefs)), warn=0)
|
442 |
+
|
443 |
+
#----------------
|
444 |
+
# functions
|
445 |
+
#----------------
|
446 |
+
def abcLyr (xs, melis): # Convert list xs to abc lyrics.
|
447 |
+
if not ''.join (xs): return '', 0 # there is no lyrics in this measure
|
448 |
+
res = []
|
449 |
+
for x in xs: # xs has for every note a lyrics syllabe or an empty string
|
450 |
+
if x == '': # note without lyrics
|
451 |
+
if melis: x = '_' # set melisma
|
452 |
+
else: x = '*' # skip note
|
453 |
+
elif x.endswith ('_') and not x.endswith ('\_'): # start of new melisma
|
454 |
+
x = x.replace ('_', '') # remove and set melis boolean
|
455 |
+
melis = 1 # so next skips will become melisma
|
456 |
+
else: melis = 0 # melisma stops on first syllable
|
457 |
+
res.append (x)
|
458 |
+
return (' '.join (res), melis)
|
459 |
+
|
460 |
+
def simplify (a, b): # divide a and b by their greatest common divisor
|
461 |
+
x, y = a, b
|
462 |
+
while b: a, b = b, a % b
|
463 |
+
return x // a, y // a
|
464 |
+
|
465 |
+
def abcdur (nx, divs, uL): # convert an musicXML duration d to abc units with L:1/uL
|
466 |
+
if nx.dur == 0: return '' # when called for elements without duration
|
467 |
+
num, den = simplify (uL * nx.dur, divs * 4) # L=1/8 -> uL = 8 units
|
468 |
+
if nx.fact: # apply tuplet time modification
|
469 |
+
numfac, denfac = nx.fact
|
470 |
+
num, den = simplify (num * numfac, den * denfac)
|
471 |
+
if den > 64: # limit the denominator to a maximum of 64
|
472 |
+
x = float (num) / den; n = math.floor (x); # when just above an integer n
|
473 |
+
if x - n < 0.1 * x: num, den = n, 1; # round to n
|
474 |
+
num64 = 64. * num / den + 1.0e-15 # to get Python2 behaviour of round
|
475 |
+
num, den = simplify (int (round (num64)), 64)
|
476 |
+
if num == 1:
|
477 |
+
if den == 1: dabc = ''
|
478 |
+
elif den == 2: dabc = '/'
|
479 |
+
else: dabc = '/%d' % den
|
480 |
+
elif den == 1: dabc = '%d' % num
|
481 |
+
else: dabc = '%d/%d' % (num, den)
|
482 |
+
return dabc
|
483 |
+
|
484 |
+
def abcMid (note): # abc note -> midi pitch
|
485 |
+
r = re.search (r"([_^]*)([A-Ga-g])([',]*)", note)
|
486 |
+
if not r: return -1
|
487 |
+
acc, n, oct = r.groups ()
|
488 |
+
nUp = n.upper ()
|
489 |
+
p = 60 + [0,2,4,5,7,9,11]['CDEFGAB'.index (nUp)] + (12 if nUp != n else 0);
|
490 |
+
if acc: p += (1 if acc[0] == '^' else -1) * len (acc)
|
491 |
+
if oct: p += (12 if oct[0] == "'" else -12) * len (oct)
|
492 |
+
return p
|
493 |
+
|
494 |
+
def staffStep (ptc, o, clef, tstep):
|
495 |
+
ndif = 0
|
496 |
+
if 'stafflines=1' in clef: ndif += 4 # meaning of one line: E (xml) -> B (abc)
|
497 |
+
if not tstep and clef.startswith ('bass'): ndif += 12 # transpose bass -> treble (C3 -> A4)
|
498 |
+
if ndif: # diatonic transposition == addition modulo 7
|
499 |
+
nm7 = 'C,D,E,F,G,A,B'.split (',')
|
500 |
+
n = nm7.index (ptc) + ndif
|
501 |
+
ptc, o = nm7 [n % 7], o + n // 7
|
502 |
+
if o > 4: ptc = ptc.lower ()
|
503 |
+
if o > 5: ptc = ptc + (o-5) * "'"
|
504 |
+
if o < 4: ptc = ptc + (4-o) * ","
|
505 |
+
return ptc
|
506 |
+
|
507 |
+
def setKey (fifths, mode):
|
508 |
+
sharpness = ['Fb', 'Cb','Gb','Db','Ab','Eb','Bb','F','C','G','D','A', 'E', 'B', 'F#','C#','G#','D#','A#','E#','B#']
|
509 |
+
offTab = {'maj':8, 'ion':8, 'm':11, 'min':11, 'aeo':11, 'mix':9, 'dor':10, 'phr':12, 'lyd':7, 'loc':13, 'non':8}
|
510 |
+
mode = mode.lower ()[:3] # only first three chars, no case
|
511 |
+
key = sharpness [offTab [mode] + fifths] + (mode if offTab [mode] != 8 else '')
|
512 |
+
accs = ['F','C','G','D','A','E','B']
|
513 |
+
if fifths >= 0: msralts = dict (zip (accs[:fifths], fifths * [1]))
|
514 |
+
else: msralts = dict (zip (accs[fifths:], -fifths * [-1]))
|
515 |
+
return key, msralts
|
516 |
+
|
517 |
+
def insTup (ix, notes, fact): # read one nested tuplet
|
518 |
+
tupcnt = 0
|
519 |
+
nx = notes [ix]
|
520 |
+
if 'start' in nx.tup:
|
521 |
+
nx.tup.remove ('start') # do recursive calls when starts remain
|
522 |
+
tix = ix # index of first tuplet note
|
523 |
+
fn, fd = fact # xml time-mod of the higher level
|
524 |
+
fnum, fden = nx.fact # xml time-mod of the current level
|
525 |
+
tupfact = fnum//fn, fden//fd # abc time mod of this level
|
526 |
+
while ix < len (notes):
|
527 |
+
nx = notes [ix]
|
528 |
+
if isinstance (nx, Elem) or nx.grace:
|
529 |
+
ix += 1 # skip all non tuplet elements
|
530 |
+
continue
|
531 |
+
if 'start' in nx.tup: # more nested tuplets to start
|
532 |
+
ix, tupcntR = insTup (ix, notes, tupfact) # ix is on the stop note!
|
533 |
+
tupcnt += tupcntR
|
534 |
+
elif nx.fact:
|
535 |
+
tupcnt += 1 # count tuplet elements
|
536 |
+
if 'stop' in nx.tup:
|
537 |
+
nx.tup.remove ('stop')
|
538 |
+
break
|
539 |
+
if not nx.fact: # stop on first non tuplet note
|
540 |
+
ix = lastix # back to last tuplet note
|
541 |
+
break
|
542 |
+
lastix = ix
|
543 |
+
ix += 1
|
544 |
+
# put abc tuplet notation before the recursive ones
|
545 |
+
tup = (tupfact[0], tupfact[1], tupcnt)
|
546 |
+
if tup == (3, 2, 3): tupPrefix = '(3'
|
547 |
+
else: tupPrefix = '(%d:%d:%d' % tup
|
548 |
+
notes [tix].tupabc = tupPrefix + notes [tix].tupabc
|
549 |
+
return ix, tupcnt # ix is on the last tuplet note
|
550 |
+
|
551 |
+
def mkBroken (vs): # introduce broken rhythms (vs: one voice, one measure)
|
552 |
+
vs = [n for n in vs if isinstance (n, Note)]
|
553 |
+
i = 0
|
554 |
+
while i < len (vs) - 1:
|
555 |
+
n1, n2 = vs[i], vs[i+1] # scan all adjacent pairs
|
556 |
+
# skip if note in tuplet or has no duration or outside beam
|
557 |
+
if not n1.fact and not n2.fact and n1.dur > 0 and n2.beam:
|
558 |
+
if n1.dur * 3 == n2.dur:
|
559 |
+
n2.dur = (2 * n2.dur) // 3
|
560 |
+
n1.dur = n1.dur * 2
|
561 |
+
n1.after = '<' + n1.after
|
562 |
+
i += 1 # do not chain broken rhythms
|
563 |
+
elif n2.dur * 3 == n1.dur:
|
564 |
+
n1.dur = (2 * n1.dur) // 3
|
565 |
+
n2.dur = n2.dur * 2
|
566 |
+
n1.after = '>' + n1.after
|
567 |
+
i += 1 # do not chain broken rhythms
|
568 |
+
i += 1
|
569 |
+
|
570 |
+
def outVoice (measure, divs, im, ip, unitL): # note/elem objects of one measure in one voice
|
571 |
+
ix = 0
|
572 |
+
while ix < len (measure): # set all (nested) tuplet annotations
|
573 |
+
nx = measure [ix]
|
574 |
+
if isinstance (nx, Note) and nx.fact and not nx.grace:
|
575 |
+
ix, tupcnt = insTup (ix, measure, (1, 1)) # read one tuplet, insert annotation(s)
|
576 |
+
ix += 1
|
577 |
+
vs = []
|
578 |
+
for nx in measure:
|
579 |
+
if isinstance (nx, Note):
|
580 |
+
durstr = abcdur (nx, divs, unitL) # xml -> abc duration string
|
581 |
+
chord = len (nx.ns) > 1
|
582 |
+
cns = [nt[:-1] for nt in nx.ns if nt.endswith ('-')]
|
583 |
+
tie = ''
|
584 |
+
if chord and len (cns) == len (nx.ns): # all chord notes tied
|
585 |
+
nx.ns = cns # chord notes without tie
|
586 |
+
tie = '-' # one tie for whole chord
|
587 |
+
s = nx.tupabc + ''.join (nx.before)
|
588 |
+
if chord: s += '['
|
589 |
+
for nt in nx.ns: s += nt
|
590 |
+
if chord: s += ']' + tie
|
591 |
+
if s.endswith ('-'): s, tie = s[:-1], '-' # split off tie
|
592 |
+
s += durstr + tie # and put it back again
|
593 |
+
s += nx.after
|
594 |
+
nospace = nx.beam
|
595 |
+
else:
|
596 |
+
if isinstance (nx.str, listtype): nx.str = nx.str [0]
|
597 |
+
s = nx.str
|
598 |
+
nospace = 1
|
599 |
+
if nospace: vs.append (s)
|
600 |
+
else: vs.append (' ' + s)
|
601 |
+
vs = ''.join (vs) # ad hoc: remove multiple pedal directions
|
602 |
+
while vs.find ('!ped!!ped!') >= 0: vs = vs.replace ('!ped!!ped!','!ped!')
|
603 |
+
while vs.find ('!ped-up!!ped-up!') >= 0: vs = vs.replace ('!ped-up!!ped-up!','!ped-up!')
|
604 |
+
while vs.find ('!8va(!!8va)!') >= 0: vs = vs.replace ('!8va(!!8va)!','') # remove empty ottava's
|
605 |
+
return vs
|
606 |
+
|
607 |
+
def sortMeasure (voice, m):
|
608 |
+
voice.sort (key=lambda o: o.tijd) # sort on time
|
609 |
+
time = 0
|
610 |
+
v = []
|
611 |
+
rs = [] # holds rests in between notes
|
612 |
+
for i, nx in enumerate (voice): # establish sequentiality
|
613 |
+
if nx.tijd > time and chkbug (nx.tijd - time, m):
|
614 |
+
v.append (Note (nx.tijd - time, 'x')) # fill hole with invisble rest
|
615 |
+
rs.append (len (v) - 1)
|
616 |
+
if isinstance (nx, Elem):
|
617 |
+
if nx.tijd < time: nx.tijd = time # shift elems without duration to where they fit
|
618 |
+
v.append (nx)
|
619 |
+
time = nx.tijd
|
620 |
+
continue
|
621 |
+
if nx.tijd < time: # overlapping element
|
622 |
+
if nx.ns[0] == 'z': continue # discard overlapping rest
|
623 |
+
if v[-1].tijd <= nx.tijd: # we can do something
|
624 |
+
if v[-1].ns[0] == 'z': # shorten rest
|
625 |
+
v[-1].dur = nx.tijd - v[-1].tijd
|
626 |
+
if v[-1].dur == 0: del v[-1] # nothing left
|
627 |
+
info ('overlap in part %d, measure %d: rest shortened' % (m.ixp+1, m.ixm+1))
|
628 |
+
else: # make a chord of overlap
|
629 |
+
v[-1].ns += nx.ns
|
630 |
+
info ('overlap in part %d, measure %d: added chord' % (m.ixp+1, m.ixm+1))
|
631 |
+
nx.dur = (nx.tijd + nx.dur) - time # the remains
|
632 |
+
if nx.dur <= 0: continue # nothing left
|
633 |
+
nx.tijd = time # append remains
|
634 |
+
else: # give up
|
635 |
+
info ('overlapping notes in one voice! part %d, measure %d, note %s discarded' % (m.ixp+1, m.ixm+1, isinstance (nx, Note) and nx.ns or nx.str))
|
636 |
+
continue
|
637 |
+
v.append (nx)
|
638 |
+
if isinstance (nx, Note):
|
639 |
+
if nx.ns [0] in 'zx':
|
640 |
+
rs.append (len (v) - 1) # remember rests between notes
|
641 |
+
elif len (rs):
|
642 |
+
if nx.beam and not nx.grace: # copy beam into rests
|
643 |
+
for j in rs: v[j].beam = nx.beam
|
644 |
+
rs = [] # clear rests on each note
|
645 |
+
time = nx.tijd + nx.dur
|
646 |
+
# when a measure contains no elements and no forwards -> no incTime -> s.maxtime = 0 -> right barline
|
647 |
+
# is inserted at time == 0 (in addbar) and is only element in the voice when sortMeasure is called
|
648 |
+
if time == 0: info ('empty measure in part %d, measure %d, it should contain at least a rest to advance the time!' % (m.ixp+1, m.ixm+1))
|
649 |
+
return v
|
650 |
+
|
651 |
+
def getPartlist (ps): # correct part-list (from buggy xml-software)
|
652 |
+
xs = [] # the corrected part-list
|
653 |
+
e = [] # stack of opened part-groups
|
654 |
+
for x in list (ps): # insert missing stops, delete double starts
|
655 |
+
if x.tag == 'part-group':
|
656 |
+
num, type = x.get ('number'), x.get ('type')
|
657 |
+
if type == 'start':
|
658 |
+
if num in e: # missing stop: insert one
|
659 |
+
xs.append (E.Element ('part-group', number = num, type = 'stop'))
|
660 |
+
xs.append (x)
|
661 |
+
else: # normal start
|
662 |
+
xs.append (x)
|
663 |
+
e.append (num)
|
664 |
+
else:
|
665 |
+
if num in e: # normal stop
|
666 |
+
e.remove (num)
|
667 |
+
xs.append (x)
|
668 |
+
else: pass # double stop: skip it
|
669 |
+
else: xs.append (x)
|
670 |
+
for num in reversed (e): # fill missing stops at the end
|
671 |
+
xs.append (E.Element ('part-group', number = num, type = 'stop'))
|
672 |
+
return xs
|
673 |
+
|
674 |
+
def parseParts (xs, d, e): # -> [elems on current level], rest of xs
|
675 |
+
if not xs: return [],[]
|
676 |
+
x = xs.pop (0)
|
677 |
+
if x.tag == 'part-group':
|
678 |
+
num, type = x.get ('number'), x.get ('type')
|
679 |
+
if type == 'start': # go one level deeper
|
680 |
+
s = [x.findtext (n, '') for n in ['group-symbol','group-barline','group-name','group-abbreviation']]
|
681 |
+
d [num] = s # remember groupdata by group number
|
682 |
+
e.append (num) # make stack of open group numbers
|
683 |
+
elemsnext, rest1 = parseParts (xs, d, e) # parse one level deeper to next stop
|
684 |
+
elems, rest2 = parseParts (rest1, d, e) # parse the rest on this level
|
685 |
+
return [elemsnext] + elems, rest2
|
686 |
+
else: # stop: close level and return group-data
|
687 |
+
nums = e.pop () # last open group number in stack order
|
688 |
+
if xs and xs[0].get ('type') == 'stop': # two consequetive stops
|
689 |
+
if num != nums: # in the wrong order (tempory solution)
|
690 |
+
d[nums], d[num] = d[num], d[nums] # exchange values (only works for two stops!!!)
|
691 |
+
sym = d[num] # retrieve an return groupdata as last element of the group
|
692 |
+
return [sym], xs
|
693 |
+
else:
|
694 |
+
elems, rest = parseParts (xs, d, e) # parse remaining elements on current level
|
695 |
+
name = x.findtext ('part-name',''), x.findtext ('part-abbreviation','')
|
696 |
+
return [name] + elems, rest
|
697 |
+
|
698 |
+
def bracePart (part): # put a brace on multistaff part and group voices
|
699 |
+
if not part: return [] # empty part in the score
|
700 |
+
brace = []
|
701 |
+
for ivs in part:
|
702 |
+
if len (ivs) == 1: # stave with one voice
|
703 |
+
brace.append ('%s' % ivs[0])
|
704 |
+
else: # stave with multiple voices
|
705 |
+
brace += ['('] + ['%s' % iv for iv in ivs] + [')']
|
706 |
+
brace.append ('|')
|
707 |
+
del brace[-1] # no barline at the end
|
708 |
+
if len (part) > 1:
|
709 |
+
brace = ['{'] + brace + ['}']
|
710 |
+
return brace
|
711 |
+
|
712 |
+
def prgroupelem (x, gnm, bar, pmap, accVce, accStf): # collect partnames (accVce) and %%score map (accStf)
|
713 |
+
if type (x) == tupletype: # partname-tuple = (part-name, part-abbrev)
|
714 |
+
y = pmap.pop (0)
|
715 |
+
if gnm[0]: x = [n1 + ':' + n2 for n1, n2 in zip (gnm, x)] # put group-name before part-name
|
716 |
+
accVce.append (x)
|
717 |
+
accStf.extend (bracePart (y))
|
718 |
+
elif len (x) == 2 and type (x[0]) == tupletype: # misuse of group just to add extra name to stave
|
719 |
+
y = pmap.pop (0)
|
720 |
+
nms = [n1 + ':' + n2 for n1, n2 in zip (x[0], x[1][2:])] # x[0] = partname-tuple, x[1][2:] = groupname-tuple
|
721 |
+
accVce.append (nms)
|
722 |
+
accStf.extend (bracePart (y))
|
723 |
+
else:
|
724 |
+
prgrouplist (x, bar, pmap, accVce, accStf)
|
725 |
+
|
726 |
+
def prgrouplist (x, pbar, pmap, accVce, accStf): # collect partnames, scoremap for a part-group
|
727 |
+
sym, bar, gnm, gabbr = x[-1] # bracket symbol, continue barline, group-name-tuple
|
728 |
+
bar = bar == 'yes' or pbar # pbar -> the parent has bar
|
729 |
+
accStf.append (sym == 'brace' and '{' or '[')
|
730 |
+
for z in x[:-1]:
|
731 |
+
prgroupelem (z, (gnm, gabbr), bar, pmap, accVce, accStf)
|
732 |
+
if bar: accStf.append ('|')
|
733 |
+
if bar: del accStf [-1] # remove last one before close
|
734 |
+
accStf.append (sym == 'brace' and '}' or ']')
|
735 |
+
|
736 |
+
def compUnitLength (iv, maten, divs): # compute optimal unit length
|
737 |
+
uLmin, minLen = 0, max_int
|
738 |
+
for uL in [4,8,16]: # try 1/4, 1/8 and 1/16
|
739 |
+
vLen = 0 # total length of abc duration strings in this voice
|
740 |
+
for im, m in enumerate (maten): # all measures
|
741 |
+
for e in m[iv]: # all notes in voice iv
|
742 |
+
if isinstance (e, Elem) or e.dur == 0: continue # no real durations
|
743 |
+
vLen += len (abcdur (e, divs [im], uL)) # add len of duration string
|
744 |
+
if vLen < minLen: uLmin, minLen = uL, vLen # remember the smallest
|
745 |
+
return uLmin
|
746 |
+
|
747 |
+
def doSyllable (syl):
|
748 |
+
txt = ''
|
749 |
+
for e in syl:
|
750 |
+
if e.tag == 'elision': txt += '~'
|
751 |
+
elif e.tag == 'text': # escape - and space characters
|
752 |
+
txt += (e.text or '').replace ('_','\_').replace('-', r'\-').replace(' ', '~')
|
753 |
+
if not txt: return txt
|
754 |
+
if syl.findtext('syllabic') in ['begin', 'middle']: txt += '-'
|
755 |
+
if syl.find('extend') is not None: txt += '_'
|
756 |
+
return txt
|
757 |
+
|
758 |
+
def checkMelismas (lyrics, maten, im, iv):
|
759 |
+
if im == 0: return
|
760 |
+
maat = maten [im][iv] # notes of the current measure
|
761 |
+
curlyr = lyrics [im][iv] # lyrics dict of current measure
|
762 |
+
prvlyr = lyrics [im-1][iv] # lyrics dict of previous measure
|
763 |
+
for n, (lyrstr, melis) in prvlyr.items (): # all lyric numbers in the previous measure
|
764 |
+
if n not in curlyr and melis: # melisma required, but no lyrics present -> make one!
|
765 |
+
ms = getMelisma (maat) # get a melisma for the current measure
|
766 |
+
if ms: curlyr [n] = (ms, 0) # set melisma as the n-th lyrics of the current measure
|
767 |
+
|
768 |
+
def getMelisma (maat): # get melisma from notes in maat
|
769 |
+
ms = []
|
770 |
+
for note in maat: # every note should get an underscore
|
771 |
+
if not isinstance (note, Note): continue # skip Elem's
|
772 |
+
if note.grace: continue # skip grace notes
|
773 |
+
if note.ns [0] in 'zx': break # stop on first rest
|
774 |
+
ms.append ('_')
|
775 |
+
return ' '.join (ms)
|
776 |
+
|
777 |
+
def perc2map (abcIn):
|
778 |
+
fillmap = {'diamond':1, 'triangle':1, 'square':1, 'normal':1};
|
779 |
+
abc = map (lambda x: x.strip (), percSvg.splitlines ())
|
780 |
+
id='default'
|
781 |
+
maps = {'default': []};
|
782 |
+
dmaps = {'default': []}
|
783 |
+
r1 = re.compile (r'V:\s*(\S+)')
|
784 |
+
ls = abcIn.splitlines ()
|
785 |
+
for x in ls:
|
786 |
+
if 'I:percmap' in x:
|
787 |
+
noot, step, midi, kop = map (lambda x: x.strip (), x.split ()[1:])
|
788 |
+
if kop in fillmap: kop = kop + '+' + ',' + kop
|
789 |
+
x = '%%%%map perc%s %s print=%s midi=%s heads=%s' % (id, noot, step, midi, kop)
|
790 |
+
maps [id].append (x)
|
791 |
+
if '%%MIDI' in x: dmaps [id].append (x)
|
792 |
+
if 'V:' in x:
|
793 |
+
r = r1.match (x)
|
794 |
+
if r:
|
795 |
+
id = r.group (1);
|
796 |
+
if id not in maps: maps [id] = []; dmaps [id] = []
|
797 |
+
ids = sorted (maps.keys ())
|
798 |
+
for id in ids: abc += maps [id]
|
799 |
+
id='default'
|
800 |
+
for x in ls:
|
801 |
+
if 'I:percmap' in x: continue
|
802 |
+
if '%%MIDI' in x: continue
|
803 |
+
if 'V:' in x or 'K:' in x:
|
804 |
+
r = r1.match (x)
|
805 |
+
if r: id = r.group (1)
|
806 |
+
abc.append (x)
|
807 |
+
if id in dmaps and len (dmaps [id]) > 0: abc.extend (dmaps [id]); del dmaps [id]
|
808 |
+
if 'perc' in x and 'map=' not in x: x += ' map=perc';
|
809 |
+
if 'map=perc' in x and len (maps [id]) > 0: abc.append ('%%voicemap perc' + id);
|
810 |
+
if 'map=off' in x: abc.append ('%%voicemap');
|
811 |
+
else:
|
812 |
+
abc.append (x)
|
813 |
+
return '\n'.join (abc) + '\n'
|
814 |
+
|
815 |
+
def addoct (ptc, o): # xml staff step, xml octave number
|
816 |
+
p = ptc
|
817 |
+
if o > 4: p = ptc.lower ()
|
818 |
+
if o > 5: p = p + (o-5) * "'"
|
819 |
+
if o < 4: p = p + (4-o) * ","
|
820 |
+
return p # abc pitch == abc note without accidental
|
821 |
+
|
822 |
+
def chkbug (dt, m):
|
823 |
+
if dt > m.divs / 16: return 1 # duration should be > 1/64 note
|
824 |
+
info ('MuseScore bug: incorrect duration, smaller then 1/64! in measure %d, part %d' % (m.ixm, m.ixp))
|
825 |
+
return 0
|
826 |
+
|
827 |
+
#----------------
|
828 |
+
# parser
|
829 |
+
#----------------
|
830 |
+
class Parser:
|
831 |
+
note_alts = [ # 3 alternative notations of the same note for tablature mapping
|
832 |
+
[x.strip () for x in '=C, ^C, =D, ^D, =E, =F, ^F, =G, ^G, =A, ^A, =B'.split (',')],
|
833 |
+
[x.strip () for x in '^B, _D,^^C, _E, _F, ^E, _G,^^F, _A,^^G, _B, _C'.split (',')],
|
834 |
+
[x.strip () for x in '__D,^^B,__E,__F,^^D,__G,^^E,__A,_/A,__B,__C,^^A'.split (',')] ]
|
835 |
+
step_map = {'C':0,'D':2,'E':4,'F':5,'G':7,'A':9,'B':11}
|
836 |
+
def __init__ (s, options):
|
837 |
+
# unfold repeats, number of chars per line, credit filter level, volta option
|
838 |
+
s.slurBuf = {} # dict of open slurs keyed by slur number
|
839 |
+
s.dirStk = {} # {direction-type + number -> (type, voice | time)} dict for proper closing
|
840 |
+
s.ingrace = 0 # marks a sequence of grace notes
|
841 |
+
s.msc = Music (options) # global music data abstraction
|
842 |
+
s.unfold = options.u # turn unfolding repeats on
|
843 |
+
s.ctf = options.c # credit text filter level
|
844 |
+
s.gStfMap = [] # [[abc voice numbers] for all parts]
|
845 |
+
s.midiMap = [] # midi-settings for each abc voice, in order
|
846 |
+
s.drumInst = {} # inst_id -> midi pitch for channel 10 notes
|
847 |
+
s.drumNotes = {} # (xml voice, abc note) -> (midi note, note head)
|
848 |
+
s.instMid = [] # [{inst id -> midi-settings} for all parts]
|
849 |
+
s.midDflt = [-1,-1,-1,-91] # default midi settings for channel, program, volume, panning
|
850 |
+
s.msralts = {} # xml-notenames (without octave) with accidentals from the key
|
851 |
+
s.curalts = {} # abc-notenames (with voice number) with passing accidentals
|
852 |
+
s.stfMap = {} # xml staff number -> [xml voice number]
|
853 |
+
s.vce2stf = {} # xml voice number -> allocated staff number
|
854 |
+
s.clefMap = {} # xml staff number -> abc clef (for header only)
|
855 |
+
s.curClef = {} # xml staff number -> current abc clef
|
856 |
+
s.stemDir = {} # xml voice number -> current stem direction
|
857 |
+
s.clefOct = {} # xml staff number -> current clef-octave-change
|
858 |
+
s.curStf = {} # xml voice number -> current xml staff number
|
859 |
+
s.nolbrk = options.x; # generate no linebreaks ($)
|
860 |
+
s.jscript = options.j # compatibility with javascript version
|
861 |
+
s.ornaments = sorted (note_ornamentation_map.items ())
|
862 |
+
s.doPageFmt = len (options.p) == 1 # translate xml page format
|
863 |
+
s.tstep = options.t # clef determines step on staff (percussion)
|
864 |
+
s.dirtov1 = options.v1 # all directions to first voice of staff
|
865 |
+
s.ped = options.ped # render pedal directions
|
866 |
+
s.wstems = options.stm # translate stem elements
|
867 |
+
s.pedVce = None # voice for pedal directions
|
868 |
+
s.repeat_str = {} # staff number -> [measure number, repeat-text]
|
869 |
+
s.tabVceMap = {} # abc voice num -> [%%map ...] for tab voices
|
870 |
+
s.koppen = {} # noteheads needed for %%map
|
871 |
+
|
872 |
+
def matchSlur (s, type2, n, v2, note2, grace, stopgrace): # match slur number n in voice v2, add abc code to before/after
|
873 |
+
if type2 not in ['start', 'stop']: return # slur type continue has no abc equivalent
|
874 |
+
if n == None: n = '1'
|
875 |
+
if n in s.slurBuf:
|
876 |
+
type1, v1, note1, grace1 = s.slurBuf [n]
|
877 |
+
if type2 != type1: # slur complete, now check the voice
|
878 |
+
if v2 == v1: # begins and ends in the same voice: keep it
|
879 |
+
if type1 == 'start' and (not grace1 or not stopgrace): # normal slur: start before stop and no grace slur
|
880 |
+
note1.before = ['('] + note1.before # keep left-right order!
|
881 |
+
note2.after += ')'
|
882 |
+
# no else: don't bother with reversed stave spanning slurs
|
883 |
+
del s.slurBuf [n] # slur finished, remove from stack
|
884 |
+
else: # double definition, keep the last
|
885 |
+
info ('double slur numbers %s-%s in part %d, measure %d, voice %d note %s, first discarded' % (type2, n, s.msr.ixp+1, s.msr.ixm+1, v2, note2.ns))
|
886 |
+
s.slurBuf [n] = (type2, v2, note2, grace)
|
887 |
+
else: # unmatched slur, put in dict
|
888 |
+
s.slurBuf [n] = (type2, v2, note2, grace)
|
889 |
+
|
890 |
+
def doNotations (s, note, nttn, isTab):
|
891 |
+
for key, val in s.ornaments:
|
892 |
+
if nttn.find (key) != None: note.before += [val] # just concat all ornaments
|
893 |
+
trem = nttn.find ('ornaments/tremolo')
|
894 |
+
if trem != None:
|
895 |
+
type = trem.get ('type')
|
896 |
+
if type == 'single':
|
897 |
+
note.before.insert (0, '!%s!' % (int (trem.text) * '/'))
|
898 |
+
else:
|
899 |
+
note.fact = None # no time modification in ABC
|
900 |
+
if s.tstep: # abc2svg version
|
901 |
+
if type == 'stop': note.before.insert (0, '!trem%s!' % trem.text);
|
902 |
+
else: # abc2xml version
|
903 |
+
if type == 'start': note.before.insert (0, '!%s-!' % (int (trem.text) * '/'));
|
904 |
+
fingering = nttn.findall ('technical/fingering')
|
905 |
+
for finger in fingering: # handle multiple finger annotations
|
906 |
+
if not isTab: note.before += ['!%s!' % finger.text] # fingering goes before chord (addChord)
|
907 |
+
snaar = nttn.find ('technical/string')
|
908 |
+
if snaar != None and isTab:
|
909 |
+
if s.tstep:
|
910 |
+
fret = nttn.find ('technical/fret')
|
911 |
+
if fret != None: note.tab = (snaar.text, fret.text)
|
912 |
+
else:
|
913 |
+
deco = '!%s!' % snaar.text # no double string decos (bug in musescore)
|
914 |
+
if deco not in note.ntdec: note.ntdec += deco
|
915 |
+
wvln = nttn.find ('ornaments/wavy-line')
|
916 |
+
if wvln != None:
|
917 |
+
if wvln.get ('type') == 'start': note.before = ['!trill(!'] + note.before # keep left-right order!
|
918 |
+
elif wvln.get ('type') == 'stop': note.before = ['!trill)!'] + note.before
|
919 |
+
glis = nttn.find ('glissando')
|
920 |
+
if glis == None: glis = nttn.find ('slide') # treat slide as glissando
|
921 |
+
if glis != None:
|
922 |
+
lt = '~' if glis.get ('line-type') =='wavy' else '-'
|
923 |
+
if glis.get ('type') == 'start': note.before = ['!%s(!' % lt] + note.before # keep left-right order!
|
924 |
+
elif glis.get ('type') == 'stop': note.before = ['!%s)!' % lt] + note.before
|
925 |
+
|
926 |
+
def tabnote (s, alt, ptc, oct, v, ntrec):
|
927 |
+
p = s.step_map [ptc] + int (alt or '0') # p in -2 .. 13
|
928 |
+
if p > 11: oct += 1 # octave correction
|
929 |
+
if p < 0: oct -= 1
|
930 |
+
p = p % 12 # remap p into 0..11
|
931 |
+
snaar_nw, fret_nw = ntrec.tab # the computed/annotated allocation of nt
|
932 |
+
for i in range (4): # support same note on 4 strings
|
933 |
+
na = s.note_alts [i % 3] [p] # get alternative representation of same note
|
934 |
+
o = oct
|
935 |
+
if na in ['^B', '^^B']: o -= 1 # because in adjacent octave
|
936 |
+
if na in ['_C', '__C']: o += 1
|
937 |
+
if '/' in na or i == 3: o = 9 # emergency notation for 4th string case
|
938 |
+
nt = addoct (na, o)
|
939 |
+
snaar, fret = s.tabmap.get ((v, nt), ('', '')) # the current allocation of nt
|
940 |
+
if not snaar: break # note not yet allocated
|
941 |
+
if snaar_nw == snaar: return nt # use present allocation
|
942 |
+
if i == 3: # new allocaion needed but none is free
|
943 |
+
fmt = 'rejected: voice %d note %3s string %s fret %2s remains: string %s fret %s'
|
944 |
+
info (fmt % (v, nt, snaar_nw, fret_nw, snaar, fret), 1)
|
945 |
+
ntrec.tab = (snaar, fret)
|
946 |
+
s.tabmap [v, nt] = ntrec.tab # for tablature map (voice, note) -> (string, fret)
|
947 |
+
return nt # ABC code always in key C (with midi pitch alterations)
|
948 |
+
|
949 |
+
def ntAbc (s, ptc, oct, note, v, ntrec, isTab): # pitch, octave -> abc notation
|
950 |
+
acc2alt = {'double-flat':-2,'flat-flat':-2,'flat':-1,'natural':0,'sharp':1,'sharp-sharp':2,'double-sharp':2}
|
951 |
+
oct += s.clefOct.get (s.curStf [v], 0) # minus clef-octave-change value
|
952 |
+
acc = note.findtext ('accidental') # should be the notated accidental
|
953 |
+
alt = note.findtext ('pitch/alter') # pitch alteration (midi)
|
954 |
+
if ntrec.tab: return s.tabnote (alt, ptc, oct, v, ntrec) # implies s.tstep is true (options.t was given)
|
955 |
+
elif isTab and s.tstep:
|
956 |
+
nt = ['__','_','','^','^^'][int (alt or '0') + 2] + addoct (ptc, oct)
|
957 |
+
info ('no string notation found for note %s in voice %d' % (nt, v), 1)
|
958 |
+
p = addoct (ptc, oct)
|
959 |
+
if alt == None and s.msralts.get (ptc, 0): alt = 0 # no alt but key implies alt -> natural!!
|
960 |
+
if alt == None and (p, v) in s.curalts: alt = 0 # no alt but previous note had one -> natural!!
|
961 |
+
if acc == None and alt == None: return p # no acc, no alt
|
962 |
+
elif acc != None:
|
963 |
+
alt = acc2alt [acc] # acc takes precedence over the pitch here!
|
964 |
+
else: # now see if we really must add an accidental
|
965 |
+
alt = int (float (alt))
|
966 |
+
if (p, v) in s.curalts: # the note in this voice has been altered before
|
967 |
+
if alt == s.curalts [(p, v)]: return p # alteration still the same
|
968 |
+
elif alt == s.msralts.get (ptc, 0): return p # alteration implied by the key
|
969 |
+
tieElms = note.findall ('tie') + note.findall ('notations/tied') # in xml we have separate notated ties and playback ties
|
970 |
+
if 'stop' in [e.get ('type') for e in tieElms]: return p # don't alter tied notes
|
971 |
+
info ('accidental %d added in part %d, measure %d, voice %d note %s' % (alt, s.msr.ixp+1, s.msr.ixm+1, v+1, p))
|
972 |
+
s.curalts [(p, v)] = alt
|
973 |
+
p = ['__','_','=','^','^^'][alt+2] + p # and finally ... prepend the accidental
|
974 |
+
return p
|
975 |
+
|
976 |
+
def doNote (s, n): # parse a musicXML note tag
|
977 |
+
note = Note ()
|
978 |
+
v = int (n.findtext ('voice', '1'))
|
979 |
+
if s.isSib: v += 100 * int (n.findtext ('staff', '1')) # repair bug in Sibelius
|
980 |
+
chord = n.find ('chord') != None
|
981 |
+
p = n.findtext ('pitch/step') or n.findtext ('unpitched/display-step')
|
982 |
+
o = n.findtext ('pitch/octave') or n.findtext ('unpitched/display-octave')
|
983 |
+
r = n.find ('rest')
|
984 |
+
numer = n.findtext ('time-modification/actual-notes')
|
985 |
+
if numer:
|
986 |
+
denom = n.findtext ('time-modification/normal-notes')
|
987 |
+
note.fact = (int (numer), int (denom))
|
988 |
+
note.tup = [x.get ('type') for x in n.findall ('notations/tuplet')]
|
989 |
+
dur = n.findtext ('duration')
|
990 |
+
grc = n.find ('grace')
|
991 |
+
note.grace = grc != None
|
992 |
+
note.before, note.after = [], '' # strings with ABC stuff that goes before or after a note/chord
|
993 |
+
if note.grace and not s.ingrace: # open a grace sequence
|
994 |
+
s.ingrace = 1
|
995 |
+
note.before = ['{']
|
996 |
+
if grc.get ('slash') == 'yes': note.before += ['/'] # acciaccatura
|
997 |
+
stopgrace = not note.grace and s.ingrace
|
998 |
+
if stopgrace: # close the grace sequence
|
999 |
+
s.ingrace = 0
|
1000 |
+
s.msc.lastnote.after += '}' # close grace on lastenote.after
|
1001 |
+
if dur == None or note.grace: dur = 0
|
1002 |
+
if r == None and n.get ('print-object') == 'no':
|
1003 |
+
if chord: return
|
1004 |
+
r = 1 # turn invisible notes (that advance the time) into invisible rests
|
1005 |
+
note.dur = int (dur)
|
1006 |
+
if r == None and (not p or not o): # not a rest and no pitch
|
1007 |
+
s.msc.cnt.inc ('nopt', v) # count unpitched notes
|
1008 |
+
o, p = 5,'E' # make it an E5 ??
|
1009 |
+
isTab = s.curClef and s.curClef.get (s.curStf [v], '').startswith ('tab')
|
1010 |
+
nttn = n.find ('notations') # add ornaments
|
1011 |
+
if nttn != None: s.doNotations (note, nttn, isTab)
|
1012 |
+
e = n.find ('stem') if r == None else None # no !stemless! before rest
|
1013 |
+
if e != None and e.text == 'none' and (not isTab or v in s.hasStems or s.tstep):
|
1014 |
+
note.before += ['s']; abcOut.stemless = 1;
|
1015 |
+
e = n.find ('accidental')
|
1016 |
+
if e != None and e.get ('parentheses') == 'yes': note.ntdec += '!courtesy!'
|
1017 |
+
if r != None: noot = 'x' if n.get ('print-object') == 'no' or isTab else 'z'
|
1018 |
+
else: noot = s.ntAbc (p, int (o), n, v, note, isTab)
|
1019 |
+
if n.find ('unpitched') != None:
|
1020 |
+
clef = s.curClef [s.curStf [v]] # the current clef for this voice
|
1021 |
+
step = staffStep (p, int (o), clef, s.tstep) # (clef independent) step value of note on the staff
|
1022 |
+
instr = n.find ('instrument')
|
1023 |
+
instId = instr.get ('id') if instr != None else 'dummyId'
|
1024 |
+
midi = s.drumInst.get (instId, abcMid (noot))
|
1025 |
+
nh = n.findtext ('notehead', '').replace (' ','-') # replace spaces in xml notehead names for percmap
|
1026 |
+
if nh == 'x': noot = '^' + noot.replace ('^','').replace ('_','')
|
1027 |
+
if nh in ['circle-x','diamond','triangle']: noot = '_' + noot.replace ('^','').replace ('_','')
|
1028 |
+
if nh and n.find ('notehead').get ('filled','') == 'yes': nh += '+'
|
1029 |
+
if nh and n.find ('notehead').get ('filled','') == 'no': nh += '-'
|
1030 |
+
s.drumNotes [(v, noot)] = (step, midi, nh) # keep data for percussion map
|
1031 |
+
tieElms = n.findall ('tie') + n.findall ('notations/tied') # in xml we have separate notated ties and playback ties
|
1032 |
+
if 'start' in [e.get ('type') for e in tieElms]: # n can have stop and start tie
|
1033 |
+
noot = noot + '-'
|
1034 |
+
note.beam = sum ([1 for b in n.findall('beam') if b.text in ['continue', 'end']]) + int (note.grace)
|
1035 |
+
lyrlast = 0; rsib = re.compile (r'^.*verse')
|
1036 |
+
for e in n.findall ('lyric'):
|
1037 |
+
lyrnum = int (rsib.sub ('', e.get ('number', '1'))) # also do Sibelius numbers
|
1038 |
+
if lyrnum == 0: lyrnum = lyrlast + 1 # and correct Sibelius bugs
|
1039 |
+
else: lyrlast = lyrnum
|
1040 |
+
note.lyrs [lyrnum] = doSyllable (e)
|
1041 |
+
stemdir = n.findtext ('stem')
|
1042 |
+
if s.wstems and (stemdir == 'up' or stemdir == 'down'):
|
1043 |
+
if stemdir != s.stemDir.get (v, ''):
|
1044 |
+
s.stemDir [v] = stemdir
|
1045 |
+
s.msc.appendElem (v, '[I:stemdir %s]' % stemdir)
|
1046 |
+
if chord: s.msc.addChord (note, noot)
|
1047 |
+
else:
|
1048 |
+
xmlstaff = int (n.findtext ('staff', '1'))
|
1049 |
+
if s.curStf [v] != xmlstaff: # the note should go to another staff
|
1050 |
+
dstaff = xmlstaff - s.curStf [v] # relative new staff number
|
1051 |
+
s.curStf [v] = xmlstaff # remember the new staff for this voice
|
1052 |
+
s.msc.appendElem (v, '[I:staff %+d]' % dstaff) # insert a move before the note
|
1053 |
+
s.msc.appendNote (v, note, noot)
|
1054 |
+
for slur in n.findall ('notations/slur'): # s.msc.lastnote points to the last real note/chord inserted above
|
1055 |
+
s.matchSlur (slur.get ('type'), slur.get ('number'), v, s.msc.lastnote, note.grace, stopgrace) # match slur definitions
|
1056 |
+
|
1057 |
+
def doAttr (s, e): # parse a musicXML attribute tag
|
1058 |
+
teken = {'C1':'alto1','C2':'alto2','C3':'alto','C4':'tenor','F4':'bass','F3':'bass3','G2':'treble','TAB':'tab','percussion':'perc'}
|
1059 |
+
dvstxt = e.findtext ('divisions')
|
1060 |
+
if dvstxt: s.msr.divs = int (dvstxt)
|
1061 |
+
steps = int (e.findtext ('transpose/chromatic', '0')) # for transposing instrument
|
1062 |
+
fifths = e.findtext ('key/fifths')
|
1063 |
+
first = s.msc.tijd == 0 and s.msr.ixm == 0 # first attributes in first measure
|
1064 |
+
if fifths:
|
1065 |
+
key, s.msralts = setKey (int (fifths), e.findtext ('key/mode','major'))
|
1066 |
+
if first and not steps and abcOut.key == 'none':
|
1067 |
+
abcOut.key = key # first measure -> header, if not transposing instrument or percussion part!
|
1068 |
+
elif key != abcOut.key or not first:
|
1069 |
+
s.msr.attr += '[K:%s]' % key # otherwise -> voice
|
1070 |
+
beats = e.findtext ('time/beats')
|
1071 |
+
if beats:
|
1072 |
+
unit = e.findtext ('time/beat-type')
|
1073 |
+
mtr = beats + '/' + unit
|
1074 |
+
if first: abcOut.mtr = mtr # first measure -> header
|
1075 |
+
else: s.msr.attr += '[M:%s]' % mtr # otherwise -> voice
|
1076 |
+
s.msr.mtr = int (beats), int (unit)
|
1077 |
+
s.msr.mdur = (s.msr.divs * s.msr.mtr[0] * 4) // s.msr.mtr[1] # duration of measure in xml-divisions
|
1078 |
+
for ms in e.findall('measure-style'):
|
1079 |
+
n = int (ms.get ('number', '1')) # staff number
|
1080 |
+
voices = s.stfMap [n] # all voices of staff n
|
1081 |
+
for mr in ms.findall('measure-repeat'):
|
1082 |
+
ty = mr.get('type')
|
1083 |
+
if ty == 'start': # remember start measure number and text voor each staff
|
1084 |
+
s.repeat_str [n] = [s.msr.ixm, mr.text]
|
1085 |
+
for v in voices: # insert repeat into all voices, value will be overwritten at stop
|
1086 |
+
s.msc.insertElem (v, s.repeat_str [n])
|
1087 |
+
elif ty == 'stop': # calculate repeat measure count for this staff n
|
1088 |
+
start_ix, text_ = s.repeat_str [n]
|
1089 |
+
repeat_count = s.msr.ixm - start_ix
|
1090 |
+
if text_:
|
1091 |
+
mid_str = "%s " % text_
|
1092 |
+
repeat_count /= int (text_)
|
1093 |
+
else:
|
1094 |
+
mid_str = "" # overwrite repeat with final string
|
1095 |
+
s.repeat_str [n][0] = '[I:repeat %s%d]' % (mid_str, repeat_count)
|
1096 |
+
del s.repeat_str [n] # remove closed repeats
|
1097 |
+
toct = e.findtext ('transpose/octave-change', '')
|
1098 |
+
if toct: steps += 12 * int (toct) # extra transposition of toct octaves
|
1099 |
+
for clef in e.findall ('clef'): # a part can have multiple staves
|
1100 |
+
n = int (clef.get ('number', '1')) # local staff number for this clef
|
1101 |
+
sgn = clef.findtext ('sign')
|
1102 |
+
line = clef.findtext ('line', '') if sgn not in ['percussion','TAB'] else ''
|
1103 |
+
cs = teken.get (sgn + line, '')
|
1104 |
+
oct = clef.findtext ('clef-octave-change', '') or '0'
|
1105 |
+
if oct: cs += {-2:'-15', -1:'-8', 1:'+8', 2:'+15'}.get (int (oct), '')
|
1106 |
+
s.clefOct [n] = -int (oct); # xml playback pitch -> abc notation pitch
|
1107 |
+
if steps: cs += ' transpose=' + str (steps)
|
1108 |
+
stfdtl = e.find ('staff-details')
|
1109 |
+
if stfdtl and int (stfdtl.get ('number', '1')) == n:
|
1110 |
+
lines = stfdtl.findtext ('staff-lines')
|
1111 |
+
if lines:
|
1112 |
+
lns= '|||' if lines == '3' and sgn == 'TAB' else lines
|
1113 |
+
cs += ' stafflines=%s' % lns
|
1114 |
+
s.stafflines = int (lines) # remember for tab staves
|
1115 |
+
strings = stfdtl.findall ('staff-tuning')
|
1116 |
+
if strings:
|
1117 |
+
tuning = [st.findtext ('tuning-step') + st.findtext ('tuning-octave') for st in strings]
|
1118 |
+
cs += ' strings=%s' % ','.join (tuning)
|
1119 |
+
capo = stfdtl.findtext ('capo')
|
1120 |
+
if capo: cs += ' capo=%s' % capo
|
1121 |
+
s.curClef [n] = cs # keep track of current clef (for percmap)
|
1122 |
+
if first: s.clefMap [n] = cs # clef goes to header (where it is mapped to voices)
|
1123 |
+
else:
|
1124 |
+
voices = s.stfMap[n] # clef change to all voices of staff n
|
1125 |
+
for v in voices:
|
1126 |
+
if n != s.curStf [v]: # voice is not at its home staff n
|
1127 |
+
dstaff = n - s.curStf [v]
|
1128 |
+
s.curStf [v] = n # reset current staff at start of measure to home position
|
1129 |
+
s.msc.appendElem (v, '[I:staff %+d]' % dstaff)
|
1130 |
+
s.msc.appendElem (v, '[K:%s]' % cs)
|
1131 |
+
|
1132 |
+
def findVoice (s, i, es):
|
1133 |
+
stfnum = int (es[i].findtext ('staff',1)) # directions belong to a staff
|
1134 |
+
vs = s.stfMap [stfnum] # voices in this staff
|
1135 |
+
v1 = vs [0] if vs else 1 # directions to first voice of staff
|
1136 |
+
if s.dirtov1: return stfnum, v1, v1 # option --v1
|
1137 |
+
for e in es [i+1:]: # or to the voice of the next note
|
1138 |
+
if e.tag == 'note':
|
1139 |
+
v = int (e.findtext ('voice', '1'))
|
1140 |
+
if s.isSib: v += 100 * int (e.findtext ('staff', '1')) # repair bug in Sibelius
|
1141 |
+
stf = s.vce2stf [v] # use our own staff allocation
|
1142 |
+
return stf, v, v1 # voice of next note, first voice of staff
|
1143 |
+
if e.tag == 'backup': break
|
1144 |
+
return stfnum, v1, v1 # no note found, fall back to v1
|
1145 |
+
|
1146 |
+
def doDirection (s, e, i, es): # parse a musicXML direction tag
|
1147 |
+
def addDirection (x, vs, tijd, stfnum):
|
1148 |
+
if not x: return
|
1149 |
+
vs = s.stfMap [stfnum] if '!8v' in x else [vs] # ottava's go to all voices of staff
|
1150 |
+
for v in vs:
|
1151 |
+
if tijd != None: # insert at time of encounter
|
1152 |
+
s.msc.appendElemT (v, x.replace ('(',')').replace ('ped','ped-up'), tijd)
|
1153 |
+
else:
|
1154 |
+
s.msc.appendElem (v, x)
|
1155 |
+
def startStop (dtype, vs, stfnum=1):
|
1156 |
+
typmap = {'down':'!8va(!', 'up':'!8vb(!', 'crescendo':'!<(!', 'diminuendo':'!>(!', 'start':'!ped!'}
|
1157 |
+
type = t.get ('type', '')
|
1158 |
+
k = dtype + t.get ('number', '1') # key to match the closing direction
|
1159 |
+
if type in typmap: # opening the direction
|
1160 |
+
x = typmap [type]
|
1161 |
+
if k in s.dirStk: # closing direction already encountered
|
1162 |
+
stype, tijd = s.dirStk [k]; del s.dirStk [k]
|
1163 |
+
if stype == 'stop':
|
1164 |
+
addDirection (x, vs, tijd, stfnum)
|
1165 |
+
else:
|
1166 |
+
info ('%s direction %s has no stop in part %d, measure %d, voice %d' % (dtype, stype, s.msr.ixp+1, s.msr.ixm+1, vs+1))
|
1167 |
+
s.dirStk [k] = ((type , vs)) # remember voice and type for closing
|
1168 |
+
else:
|
1169 |
+
s.dirStk [k] = ((type , vs)) # remember voice and type for closing
|
1170 |
+
elif type == 'stop':
|
1171 |
+
if k in s.dirStk: # matching open direction found
|
1172 |
+
type, vs = s.dirStk [k]; del s.dirStk [k] # into the same voice
|
1173 |
+
if type == 'stop':
|
1174 |
+
info ('%s direction %s has double stop in part %d, measure %d, voice %d' % (dtype, type, s.msr.ixp+1, s.msr.ixm+1, vs+1))
|
1175 |
+
x = ''
|
1176 |
+
else:
|
1177 |
+
x = typmap [type].replace ('(',')').replace ('ped','ped-up')
|
1178 |
+
else: # closing direction found before opening
|
1179 |
+
s.dirStk [k] = ('stop', s.msc.tijd)
|
1180 |
+
x = '' # delay code generation until opening found
|
1181 |
+
else: raise ValueError ('wrong direction type')
|
1182 |
+
addDirection (x, vs, None, stfnum)
|
1183 |
+
tempo, wrdstxt = None, ''
|
1184 |
+
plcmnt = e.get ('placement')
|
1185 |
+
stf, vs, v1 = s.findVoice (i, es)
|
1186 |
+
jmp = '' # for jump sound elements: dacapo, dalsegno and family
|
1187 |
+
jmps = [('dacapo','D.C.'),('dalsegno','D.S.'),('tocoda','dacoda'),('fine','fine'),('coda','O'),('segno','S')]
|
1188 |
+
t = e.find ('sound') # there are many possible attributes for sound
|
1189 |
+
if t != None:
|
1190 |
+
minst = t.find ('midi-instrument')
|
1191 |
+
if minst:
|
1192 |
+
prg = t.findtext ('midi-instrument/midi-program')
|
1193 |
+
chn = t.findtext ('midi-instrument/midi-channel')
|
1194 |
+
vids = [v for v, id in s.vceInst.items () if id == minst.get ('id')]
|
1195 |
+
if vids: vs = vids [0] # direction for the indentified voice, not the staff
|
1196 |
+
parm, inst = ('program', str (int (prg) - 1)) if prg else ('channel', chn)
|
1197 |
+
if inst and abcOut.volpan > 0: s.msc.appendElem (vs, '[I:MIDI= %s %s]' % (parm, inst))
|
1198 |
+
tempo = t.get ('tempo') # look for tempo attribute
|
1199 |
+
if tempo:
|
1200 |
+
tempo = '%.0f' % float (tempo) # hope it is a number and insert in voice 1
|
1201 |
+
tempo_units = (1,4) # always 1/4 for sound elements!
|
1202 |
+
for r, v in jmps:
|
1203 |
+
if t.get (r, ''): jmp = v; break
|
1204 |
+
dirtypes = e.findall ('direction-type')
|
1205 |
+
for dirtyp in dirtypes:
|
1206 |
+
units = { 'whole': (1,1), 'half': (1,2), 'quarter': (1,4), 'eighth': (1,8) }
|
1207 |
+
metr = dirtyp.find ('metronome')
|
1208 |
+
if metr != None:
|
1209 |
+
t = metr.findtext ('beat-unit', '')
|
1210 |
+
if t in units: tempo_units = units [t]
|
1211 |
+
else: tempo_units = units ['quarter']
|
1212 |
+
if metr.find ('beat-unit-dot') != None:
|
1213 |
+
tempo_units = simplify (tempo_units [0] * 3, tempo_units [1] * 2)
|
1214 |
+
tmpro = re.search ('[.\d]+', metr.findtext ('per-minute')) # look for a number
|
1215 |
+
if tmpro: tempo = tmpro.group () # overwrites the value set by the sound element of this direction
|
1216 |
+
t = dirtyp.find ('wedge')
|
1217 |
+
if t != None: startStop ('wedge', vs)
|
1218 |
+
allwrds = dirtyp.findall ('words') # insert text annotations
|
1219 |
+
if not allwrds: allwrds = dirtyp.findall ('rehearsal') # treat rehearsal mark as text annotation
|
1220 |
+
for wrds in allwrds:
|
1221 |
+
if jmp: # ignore the words when a jump sound element is present in this direction
|
1222 |
+
s.msc.appendElem (vs, '!%s!' % jmp , 1) # to voice
|
1223 |
+
break
|
1224 |
+
plc = plcmnt == 'below' and '_' or '^'
|
1225 |
+
if float (wrds.get ('default-y', '0')) < 0: plc = '_'
|
1226 |
+
wrdstxt += (wrds.text or '').replace ('"','\\"').replace ('\n', '\\n')
|
1227 |
+
wrdstxt = wrdstxt.strip ()
|
1228 |
+
for key, val in dynamics_map.items ():
|
1229 |
+
if dirtyp.find ('dynamics/' + key) != None:
|
1230 |
+
s.msc.appendElem (vs, val, 1) # to voice
|
1231 |
+
if dirtyp.find ('coda') != None: s.msc.appendElem (vs, 'O', 1)
|
1232 |
+
if dirtyp.find ('segno') != None: s.msc.appendElem (vs, 'S', 1)
|
1233 |
+
t = dirtyp.find ('octave-shift')
|
1234 |
+
if t != None: startStop ('octave-shift', vs, stf) # assume size == 8 for the time being
|
1235 |
+
t = dirtyp.find ('pedal')
|
1236 |
+
if t != None and s.ped:
|
1237 |
+
if not s.pedVce: s.pedVce = vs
|
1238 |
+
startStop ('pedal', s.pedVce)
|
1239 |
+
if dirtyp.findtext ('other-direction') == 'diatonic fretting': s.diafret = 1;
|
1240 |
+
if tempo:
|
1241 |
+
tempo = '%.0f' % float (tempo) # hope it is a number and insert in voice 1
|
1242 |
+
if s.msc.tijd == 0 and s.msr.ixm == 0: # first measure -> header
|
1243 |
+
abcOut.tempo = tempo
|
1244 |
+
abcOut.tempo_units = tempo_units
|
1245 |
+
else:
|
1246 |
+
s.msc.appendElem (v1, '[Q:%d/%d=%s]' % (tempo_units [0], tempo_units [1], tempo)) # otherwise -> 1st voice
|
1247 |
+
if wrdstxt: s.msc.appendElem (vs, '"%s%s"' % (plc, wrdstxt), 1) # to voice, but after tempo
|
1248 |
+
|
1249 |
+
def doHarmony (s, e, i, es): # parse a musicXMl harmony tag
|
1250 |
+
_, vt, _ = s.findVoice (i, es)
|
1251 |
+
short = {'major':'', 'minor':'m', 'augmented':'+', 'diminished':'dim', 'dominant':'7', 'half-diminished':'m7b5'}
|
1252 |
+
accmap = {'major':'maj', 'dominant':'', 'minor':'m', 'diminished':'dim', 'augmented':'+', 'suspended':'sus'}
|
1253 |
+
modmap = {'second':'2', 'fourth':'4', 'seventh':'7', 'sixth':'6', 'ninth':'9', '11th':'11', '13th':'13'}
|
1254 |
+
altmap = {'1':'#', '0':'', '-1':'b'}
|
1255 |
+
root = e.findtext ('root/root-step','')
|
1256 |
+
alt = altmap.get (e.findtext ('root/root-alter'), '')
|
1257 |
+
sus = ''
|
1258 |
+
kind = e.findtext ('kind', '')
|
1259 |
+
if kind in short: kind = short [kind]
|
1260 |
+
elif '-' in kind: # xml chord names: <triad name>-<modification>
|
1261 |
+
triad, mod = kind.split ('-')
|
1262 |
+
kind = accmap.get (triad, '') + modmap.get (mod, '')
|
1263 |
+
if kind.startswith ('sus'): kind, sus = '', kind # sus-suffix goes to the end
|
1264 |
+
elif kind == 'none': kind = e.find ('kind').get ('text','')
|
1265 |
+
degrees = e.findall ('degree')
|
1266 |
+
for d in degrees: # chord alterations
|
1267 |
+
kind += altmap.get (d.findtext ('degree-alter'),'') + d.findtext ('degree-value','')
|
1268 |
+
kind = kind.replace ('79','9').replace ('713','13').replace ('maj6','6')
|
1269 |
+
bass = e.findtext ('bass/bass-step','') + altmap.get (e.findtext ('bass/bass-alter'),'')
|
1270 |
+
s.msc.appendElem (vt, '"%s%s%s%s%s"' % (root, alt, kind, sus, bass and '/' + bass), 1)
|
1271 |
+
|
1272 |
+
def doBarline (s, e): # 0 = no repeat, 1 = begin repeat, 2 = end repeat
|
1273 |
+
rep = e.find ('repeat')
|
1274 |
+
if rep != None: rep = rep.get ('direction')
|
1275 |
+
if s.unfold: # unfold repeat, don't translate barlines
|
1276 |
+
return rep and (rep == 'forward' and 1 or 2) or 0
|
1277 |
+
loc = e.get ('location', 'right') # right is the default
|
1278 |
+
if loc == 'right': # only change style for the right side
|
1279 |
+
style = e.findtext ('bar-style')
|
1280 |
+
if style == 'light-light': s.msr.rline = '||'
|
1281 |
+
elif style == 'light-heavy': s.msr.rline = '|]'
|
1282 |
+
if rep != None: # repeat found
|
1283 |
+
if rep == 'forward': s.msr.lline = ':'
|
1284 |
+
else: s.msr.rline = ':|' # override barline style
|
1285 |
+
end = e.find ('ending')
|
1286 |
+
if end != None:
|
1287 |
+
if end.get ('type') == 'start':
|
1288 |
+
n = end.get ('number', '1').replace ('.','').replace (' ','')
|
1289 |
+
try: list (map (int, n.split (','))) # should be a list of integers
|
1290 |
+
except: n = '"%s"' % n.strip () # illegal musicXML
|
1291 |
+
s.msr.lnum = n # assume a start is always at the beginning of a measure
|
1292 |
+
elif s.msr.rline == '|': # stop and discontinue the same in ABC ?
|
1293 |
+
s.msr.rline = '||' # to stop on a normal barline use || in ABC ?
|
1294 |
+
return 0
|
1295 |
+
|
1296 |
+
def doPrint (s, e): # print element, measure number -> insert a line break
|
1297 |
+
if e.get ('new-system') == 'yes' or e.get ('new-page') == 'yes':
|
1298 |
+
if not s.nolbrk: return '$' # a line break
|
1299 |
+
|
1300 |
+
def doPartList (s, e): # translate the start/stop-event-based xml-partlist into proper tree
|
1301 |
+
for sp in e.findall ('part-list/score-part'):
|
1302 |
+
midi = {}
|
1303 |
+
for m in sp.findall ('midi-instrument'):
|
1304 |
+
x = [m.findtext (p, s.midDflt [i]) for i,p in enumerate (['midi-channel','midi-program','volume','pan'])]
|
1305 |
+
pan = float (x[3])
|
1306 |
+
if pan >= -90 and pan <= 90: # would be better to map behind-pannings
|
1307 |
+
pan = (float (x[3]) + 90) / 180 * 127 # xml between -90 and +90
|
1308 |
+
midi [m.get ('id')] = [int (x[0]), int (x[1]), float (x[2]) * 1.27, pan] # volume 100 -> midi 127
|
1309 |
+
up = m.findtext ('midi-unpitched')
|
1310 |
+
if up: s.drumInst [m.get ('id')] = int (up) - 1 # store midi-pitch for channel 10 notes
|
1311 |
+
s.instMid.append (midi)
|
1312 |
+
ps = e.find ('part-list') # partlist = [groupelem]
|
1313 |
+
xs = getPartlist (ps) # groupelem = partname | grouplist
|
1314 |
+
partlist, _ = parseParts (xs, {}, []) # grouplist = [groupelem, ..., groupdata]
|
1315 |
+
return partlist # groupdata = [group-symbol, group-barline, group-name, group-abbrev]
|
1316 |
+
|
1317 |
+
def mkTitle (s, e):
|
1318 |
+
def filterCredits (y): # y == filter level, higher filters less
|
1319 |
+
cs = []
|
1320 |
+
for x in credits: # skip redundant credit lines
|
1321 |
+
if y < 6 and (x in title or x in mvttl): continue # sure skip
|
1322 |
+
if y < 5 and (x in composer or x in lyricist): continue # almost sure skip
|
1323 |
+
if y < 4 and ((title and title in x) or (mvttl and mvttl in x)): continue # may skip too much
|
1324 |
+
if y < 3 and ([1 for c in composer if c in x] or [1 for c in lyricist if c in x]): continue # skips too much
|
1325 |
+
if y < 2 and re.match (r'^[\d\W]*$', x): continue # line only contains numbers and punctuation
|
1326 |
+
cs.append (x)
|
1327 |
+
if y == 0 and (title + mvttl): cs = '' # default: only credit when no title set
|
1328 |
+
return cs
|
1329 |
+
title = e.findtext ('work/work-title', '').strip ()
|
1330 |
+
mvttl = e.findtext ('movement-title', '').strip ()
|
1331 |
+
composer, lyricist, credits = [], [], []
|
1332 |
+
for creator in e.findall ('identification/creator'):
|
1333 |
+
if creator.text:
|
1334 |
+
if creator.get ('type') == 'composer':
|
1335 |
+
composer += [line.strip () for line in creator.text.split ('\n')]
|
1336 |
+
elif creator.get ('type') in ('lyricist', 'transcriber'):
|
1337 |
+
lyricist += [line.strip () for line in creator.text.split ('\n')]
|
1338 |
+
for rights in e.findall ('identification/rights'):
|
1339 |
+
if rights.text:
|
1340 |
+
lyricist += [line.strip () for line in rights.text.split ('\n')]
|
1341 |
+
for credit in e.findall('credit'):
|
1342 |
+
cs = ''.join (e.text or '' for e in credit.findall('credit-words'))
|
1343 |
+
credits += [re.sub (r'\s*[\r\n]\s*', ' ', cs)]
|
1344 |
+
credits = filterCredits (s.ctf)
|
1345 |
+
if title: title = 'T:%s\n' % title.replace ('\n', '\nT:')
|
1346 |
+
if mvttl: title += 'T:%s\n' % mvttl.replace ('\n', '\nT:')
|
1347 |
+
if credits: title += '\n'.join (['T:%s' % c for c in credits]) + '\n'
|
1348 |
+
if composer: title += '\n'.join (['C:%s' % c for c in composer]) + '\n'
|
1349 |
+
if lyricist: title += '\n'.join (['Z:%s' % c for c in lyricist]) + '\n'
|
1350 |
+
if title: abcOut.title = title[:-1]
|
1351 |
+
s.isSib = 'Sibelius' in (e.findtext ('identification/encoding/software') or '')
|
1352 |
+
if s.isSib: info ('Sibelius MusicXMl is unreliable')
|
1353 |
+
|
1354 |
+
def doDefaults (s, e):
|
1355 |
+
if not s.doPageFmt: return # return if -pf option absent
|
1356 |
+
d = e.find ('defaults');
|
1357 |
+
if d == None: return;
|
1358 |
+
mils = d.findtext ('scaling/millimeters') # mills == staff height (mm)
|
1359 |
+
tenths = d.findtext ('scaling/tenths') # staff height in tenths
|
1360 |
+
if not mils or not tenths: return
|
1361 |
+
xmlScale = float (mils) / float (tenths) / 10 # tenths -> mm
|
1362 |
+
space = 10 * xmlScale # space between staff lines == 10 tenths
|
1363 |
+
abcScale = space / 0.2117 # 0.2117 cm = 6pt = space between staff lines for scale = 1.0 in abcm2ps
|
1364 |
+
abcOut.pageFmt ['scale'] = abcScale
|
1365 |
+
eks = 2 * ['page-layout/'] + 4 * ['page-layout/page-margins/']
|
1366 |
+
eks = [a+b for a,b in zip (eks, 'page-height,page-width,left-margin,right-margin,top-margin,bottom-margin'.split (','))]
|
1367 |
+
for i in range (6):
|
1368 |
+
v = d.findtext (eks [i])
|
1369 |
+
k = abcOut.pagekeys [i+1] # pagekeys [0] == scale already done, skip it
|
1370 |
+
if not abcOut.pageFmt [k] and v:
|
1371 |
+
try: abcOut.pageFmt [k] = float (v) * xmlScale # -> cm
|
1372 |
+
except: info ('illegal value %s for xml element %s', (v, eks [i])); continue # just skip illegal values
|
1373 |
+
|
1374 |
+
def locStaffMap (s, part, maten): # map voice to staff with majority voting
|
1375 |
+
vmap = {} # {voice -> {staff -> n}} count occurrences of voice in staff
|
1376 |
+
s.vceInst = {} # {voice -> instrument id} for this part
|
1377 |
+
s.msc.vnums = {} # voice id's
|
1378 |
+
s.hasStems = {} # XML voice nums with at least one note with a stem (for tab key)
|
1379 |
+
s.stfMap, s.clefMap = {}, {} # staff -> [voices], staff -> clef
|
1380 |
+
ns = part.findall ('measure/note')
|
1381 |
+
for n in ns: # count staff allocations for all notes
|
1382 |
+
v = int (n.findtext ('voice', '1'))
|
1383 |
+
if s.isSib: v += 100 * int (n.findtext ('staff', '1')) # repair bug in Sibelius
|
1384 |
+
s.msc.vnums [v] = 1 # collect all used voice id's in this part
|
1385 |
+
sn = int (n.findtext ('staff', '1'))
|
1386 |
+
s.stfMap [sn] = []
|
1387 |
+
if v not in vmap:
|
1388 |
+
vmap [v] = {sn:1}
|
1389 |
+
else:
|
1390 |
+
d = vmap[v] # counter for voice v
|
1391 |
+
d[sn] = d.get (sn, 0) + 1 # ++ number of allocations for staff sn
|
1392 |
+
x = n.find ('instrument')
|
1393 |
+
if x != None: s.vceInst [v] = x.get ('id')
|
1394 |
+
x, noRest = n.findtext ('stem'), n.find ('rest') == None
|
1395 |
+
if noRest and (not x or x != 'none'): s.hasStems [v] = 1 # XML voice v has at least one stem
|
1396 |
+
vks = list (vmap.keys ())
|
1397 |
+
if s.jscript or s.isSib: vks.sort ()
|
1398 |
+
for v in vks: # choose staff with most allocations for each voice
|
1399 |
+
xs = [(n, sn) for sn, n in vmap[v].items ()]
|
1400 |
+
xs.sort ()
|
1401 |
+
stf = xs[-1][1] # the winner: staff with most notes of voice v
|
1402 |
+
s.stfMap [stf].append (v)
|
1403 |
+
s.vce2stf [v] = stf # reverse map
|
1404 |
+
s.curStf [v] = stf # current staff of XML voice v
|
1405 |
+
|
1406 |
+
def addStaffMap (s, vvmap): # vvmap: xml voice number -> global abc voice number
|
1407 |
+
part = [] # default: brace on staffs of one part
|
1408 |
+
for stf, voices in sorted (s.stfMap.items ()): # s.stfMap has xml staff and voice numbers
|
1409 |
+
locmap = [vvmap [iv] for iv in voices if iv in vvmap]
|
1410 |
+
nostem = [(iv not in s.hasStems) for iv in voices if iv in vvmap] # same order as locmap
|
1411 |
+
if locmap: # abc voice number of staff stf
|
1412 |
+
part.append (locmap)
|
1413 |
+
clef = s.clefMap.get (stf, 'treble') # {xml staff number -> clef}
|
1414 |
+
for i, iv in enumerate (locmap):
|
1415 |
+
clef_attr = ''
|
1416 |
+
if clef.startswith ('tab'):
|
1417 |
+
if nostem [i] and 'nostems' not in clef: clef_attr = ' nostems'
|
1418 |
+
if s.diafret and 'diafret' not in clef: clef_attr += ' diafret' # for all voices in the part
|
1419 |
+
abcOut.clefs [iv] = clef + clef_attr # add nostems when all notes of voice had no stem
|
1420 |
+
s.gStfMap.append (part)
|
1421 |
+
|
1422 |
+
def addMidiMap (s, ip, vvmap): # map abc voices to midi settings
|
1423 |
+
instr = s.instMid [ip] # get the midi settings for this part
|
1424 |
+
if instr.values (): defInstr = list(instr.values ())[0] # default settings = first instrument
|
1425 |
+
else: defInstr = s.midDflt # no instruments defined
|
1426 |
+
xs = []
|
1427 |
+
for v, vabc in vvmap.items (): # xml voice num, abc voice num
|
1428 |
+
ks = sorted (s.drumNotes.items ())
|
1429 |
+
ds = [(nt, step, midi, head) for (vd, nt), (step, midi, head) in ks if v == vd] # map perc notes
|
1430 |
+
id = s.vceInst.get (v, '') # get the instrument-id for part with multiple instruments
|
1431 |
+
if id in instr: # id is defined as midi-instrument in part-list
|
1432 |
+
xs.append ((vabc, instr [id] + ds)) # get midi settings for id
|
1433 |
+
else: xs.append ((vabc, defInstr + ds)) # only one instrument for this part
|
1434 |
+
xs.sort () # put abc voices in order
|
1435 |
+
s.midiMap.extend ([midi for v, midi in xs])
|
1436 |
+
snaarmap = ['E','G','B','d', 'f', 'a', "c'", "e'"]
|
1437 |
+
diamap = '0,1-,1,1+,2,3,3,4,4,5,6,6+,7,8-,8,8+,9,10,10,11,11,12,13,13+,14'.split (',')
|
1438 |
+
for k in sorted (s.tabmap.keys ()): # add %%map's for all tab voices
|
1439 |
+
v, noot = k;
|
1440 |
+
snaar, fret = s.tabmap [k];
|
1441 |
+
if s.diafret: fret = diamap [int (fret)]
|
1442 |
+
vabc = vvmap [v]
|
1443 |
+
snaar = s.stafflines - int (snaar)
|
1444 |
+
xs = s.tabVceMap.get (vabc, [])
|
1445 |
+
xs.append ('%%%%map tab%d %s print=%s heads=kop%s\n' % (vabc, noot, snaarmap [snaar], fret))
|
1446 |
+
s.tabVceMap [vabc] = xs
|
1447 |
+
s.koppen [fret] = 1 # collect noteheads for SVG defs
|
1448 |
+
|
1449 |
+
def parse (s, fobj):
|
1450 |
+
vvmapAll = {} # collect xml->abc voice maps (vvmap) of all parts
|
1451 |
+
e = E.parse (fobj)
|
1452 |
+
s.mkTitle (e)
|
1453 |
+
s.doDefaults (e)
|
1454 |
+
partlist = s.doPartList (e)
|
1455 |
+
parts = e.findall ('part')
|
1456 |
+
for ip, p in enumerate (parts):
|
1457 |
+
maten = p.findall ('measure')
|
1458 |
+
s.locStaffMap (p, maten) # {voice -> staff} for this part
|
1459 |
+
s.drumNotes = {} # (xml voice, abc note) -> (midi note, note head)
|
1460 |
+
s.clefOct = {} # xml staff number -> current clef-octave-change
|
1461 |
+
s.curClef = {} # xml staff number -> current abc clef
|
1462 |
+
s.stemDir = {} # xml voice number -> current stem direction
|
1463 |
+
s.tabmap = {} # (xml voice, abc note) -> (string, fret)
|
1464 |
+
s.diafret = 0 # use diatonic fretting
|
1465 |
+
s.stafflines = 5
|
1466 |
+
s.msc.initVoices (newPart = 1) # create all voices
|
1467 |
+
aantalHerhaald = 0 # keep track of number of repititions
|
1468 |
+
herhaalMaat = 0 # target measure of the repitition
|
1469 |
+
divisions = [] # current value of <divisions> for each measure
|
1470 |
+
s.msr = Measure (ip) # various measure data
|
1471 |
+
while s.msr.ixm < len (maten):
|
1472 |
+
maat = maten [s.msr.ixm]
|
1473 |
+
herhaal, lbrk = 0, ''
|
1474 |
+
s.msr.reset ()
|
1475 |
+
s.curalts = {} # passing accidentals are reset each measure
|
1476 |
+
es = list (maat)
|
1477 |
+
for i, e in enumerate (es):
|
1478 |
+
if e.tag == 'note': s.doNote (e)
|
1479 |
+
elif e.tag == 'attributes': s.doAttr (e)
|
1480 |
+
elif e.tag == 'direction': s.doDirection (e, i, es)
|
1481 |
+
elif e.tag == 'sound': s.doDirection (maat, i, es) # sound element directly in measure!
|
1482 |
+
elif e.tag == 'harmony': s.doHarmony (e, i, es)
|
1483 |
+
elif e.tag == 'barline': herhaal = s.doBarline (e)
|
1484 |
+
elif e.tag == 'backup':
|
1485 |
+
dt = int (e.findtext ('duration'))
|
1486 |
+
if chkbug (dt, s.msr): s.msc.incTime (-dt)
|
1487 |
+
elif e.tag == 'forward':
|
1488 |
+
dt = int (e.findtext ('duration'))
|
1489 |
+
if chkbug (dt, s.msr): s.msc.incTime (dt)
|
1490 |
+
elif e.tag == 'print': lbrk = s.doPrint (e)
|
1491 |
+
s.msc.addBar (lbrk, s.msr)
|
1492 |
+
divisions.append (s.msr.divs)
|
1493 |
+
if herhaal == 1:
|
1494 |
+
herhaalMaat = s.msr.ixm
|
1495 |
+
s.msr.ixm += 1
|
1496 |
+
elif herhaal == 2:
|
1497 |
+
if aantalHerhaald < 1: # jump
|
1498 |
+
s.msr.ixm = herhaalMaat
|
1499 |
+
aantalHerhaald += 1
|
1500 |
+
else:
|
1501 |
+
aantalHerhaald = 0 # reset
|
1502 |
+
s.msr.ixm += 1 # just continue
|
1503 |
+
else: s.msr.ixm += 1 # on to the next measure
|
1504 |
+
for rv in s.repeat_str.values (): # close hanging measure-repeats without stop
|
1505 |
+
rv [0] = '[I:repeat %s %d]' % (rv [1], 1)
|
1506 |
+
vvmap = s.msc.outVoices (divisions, ip, s.isSib)
|
1507 |
+
s.addStaffMap (vvmap) # update global staff map
|
1508 |
+
s.addMidiMap (ip, vvmap)
|
1509 |
+
vvmapAll.update (vvmap)
|
1510 |
+
if vvmapAll: # skip output if no part has any notes
|
1511 |
+
abcOut.mkHeader (s.gStfMap, partlist, s.midiMap, s.tabVceMap, s.koppen)
|
1512 |
+
abcOut.writeall ()
|
1513 |
+
else: info ('nothing written, %s has no notes ...' % abcOut.fnmext)
|
1514 |
+
|
1515 |
+
#----------------
|
1516 |
+
# Main Program
|
1517 |
+
#----------------
|
1518 |
+
if __name__ == '__main__':
|
1519 |
+
from optparse import OptionParser
|
1520 |
+
from glob import glob
|
1521 |
+
from zipfile import ZipFile
|
1522 |
+
ustr = '%prog [-h] [-u] [-m] [-c C] [-d D] [-n CPL] [-b BPL] [-o DIR] [-v V]\n'
|
1523 |
+
ustr += '[-x] [-p PFMT] [-t] [-s] [-i] [--v1] [--noped] [--stems] <file1> [<file2> ...]'
|
1524 |
+
parser = OptionParser (usage=ustr, version=str(VERSION))
|
1525 |
+
parser.add_option ("-u", action="store_true", help="unfold simple repeats")
|
1526 |
+
parser.add_option ("-m", action="store", help="0 -> no %%MIDI, 1 -> minimal %%MIDI, 2-> all %%MIDI", default=0)
|
1527 |
+
parser.add_option ("-c", action="store", type="int", help="set credit text filter to C", default=0, metavar='C')
|
1528 |
+
parser.add_option ("-d", action="store", type="int", help="set L:1/D", default=0, metavar='D')
|
1529 |
+
parser.add_option ("-n", action="store", type="int", help="CPL: max number of characters per line (default 100)", default=0, metavar='CPL')
|
1530 |
+
parser.add_option ("-b", action="store", type="int", help="BPL: max number of bars per line", default=0, metavar='BPL')
|
1531 |
+
parser.add_option ("-o", action="store", help="store abc files in DIR", default='', metavar='DIR')
|
1532 |
+
parser.add_option ("-v", action="store", type="int", help="set volta typesetting behaviour to V", default=0, metavar='V')
|
1533 |
+
parser.add_option ("-x", action="store_true", help="output no line breaks")
|
1534 |
+
parser.add_option ("-p", action="store", help="pageformat PFMT (cm) = scale, pageheight, pagewidth, leftmargin, rightmargin, topmargin, botmargin", default='', metavar='PFMT')
|
1535 |
+
parser.add_option ("-j", action="store_true", help="switch for compatibility with javascript version")
|
1536 |
+
parser.add_option ("-t", action="store_true", help="translate perc- and tab-staff to ABC code with %%map, %%voicemap")
|
1537 |
+
parser.add_option ("-s", action="store_true", help="shift node heads 3 units left in a tab staff")
|
1538 |
+
parser.add_option ("--v1", action="store_true", help="start-stop directions allways to first voice of staff")
|
1539 |
+
parser.add_option ("--noped", action="store_false", help="skip all pedal directions", dest='ped', default=True)
|
1540 |
+
parser.add_option ("--stems", action="store_true", help="translate stem directions", dest='stm', default=False)
|
1541 |
+
parser.add_option ("-i", action="store_true", help="read xml file from standard input")
|
1542 |
+
options, args = parser.parse_args ()
|
1543 |
+
if options.n < 0: parser.error ('only values >= 0')
|
1544 |
+
if options.b < 0: parser.error ('only values >= 0')
|
1545 |
+
if options.d and options.d not in [2**n for n in range (10)]:
|
1546 |
+
parser.error ('D should be on of %s' % ','.join ([str(2**n) for n in range (10)]))
|
1547 |
+
options.p = options.p and options.p.split (',') or [] # ==> [] | [string]
|
1548 |
+
if len (args) == 0 and not options.i: parser.error ('no input file given')
|
1549 |
+
pad = options.o
|
1550 |
+
if pad:
|
1551 |
+
if not os.path.exists (pad): os.mkdir (pad)
|
1552 |
+
if not os.path.isdir (pad): parser.error ('%s is not a directory' % pad)
|
1553 |
+
fnmext_list = []
|
1554 |
+
for i in args: fnmext_list += glob (i)
|
1555 |
+
if options.i: fnmext_list = ['stdin.xml']
|
1556 |
+
if not fnmext_list: parser.error ('none of the input files exist')
|
1557 |
+
for X, fnmext in enumerate (fnmext_list):
|
1558 |
+
fnm, ext = os.path.splitext (fnmext)
|
1559 |
+
if ext.lower () not in ('.xml','.mxl','.musicxml'):
|
1560 |
+
info ('skipped input file %s, it should have extension .xml or .mxl' % fnmext)
|
1561 |
+
continue
|
1562 |
+
if os.path.isdir (fnmext):
|
1563 |
+
info ('skipped directory %s. Only files are accepted' % fnmext)
|
1564 |
+
continue
|
1565 |
+
if fnmext == 'stdin.xml':
|
1566 |
+
fobj = sys.stdin
|
1567 |
+
elif ext.lower () == '.mxl': # extract .xml file from .mxl file
|
1568 |
+
z = ZipFile(fnmext)
|
1569 |
+
for n in z.namelist(): # assume there is always an xml file in a mxl archive !!
|
1570 |
+
if (n[:4] != 'META') and (n[-4:].lower() == '.xml'):
|
1571 |
+
fobj = z.open (n)
|
1572 |
+
break # assume only one MusicXML file per archive
|
1573 |
+
else:
|
1574 |
+
fobj = open (fnmext, 'rb') # open regular xml file
|
1575 |
+
|
1576 |
+
abcOut = ABCoutput (fnm + '.abc', pad, X, options) # create global ABC output object
|
1577 |
+
psr = Parser (options) # xml parser
|
1578 |
+
try:
|
1579 |
+
psr.parse (fobj) # parse file fobj and write abc to <fnm>.abc
|
1580 |
+
except:
|
1581 |
+
etype, value, traceback = sys.exc_info () # works in python 2 & 3
|
1582 |
+
info ('** %s occurred: %s in %s' % (etype, value, fnmext), 0)
|