Spaces:
Running
Running
MuGeminorum
commited on
Commit
•
0487c7d
1
Parent(s):
92976f7
upl base codes
Browse files- .gitattributes +11 -11
- .gitignore +3 -0
- README.md +4 -4
- app.py +163 -0
- convert.py +132 -0
- requirements.txt +5 -0
- xml2abc.py +2156 -0
.gitattributes
CHANGED
@@ -1,35 +1,35 @@
|
|
1 |
*.7z filter=lfs diff=lfs merge=lfs -text
|
2 |
*.arrow filter=lfs diff=lfs merge=lfs -text
|
3 |
*.bin filter=lfs diff=lfs merge=lfs -text
|
|
|
4 |
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
6 |
*.ftz filter=lfs diff=lfs merge=lfs -text
|
7 |
*.gz filter=lfs diff=lfs merge=lfs -text
|
8 |
*.h5 filter=lfs diff=lfs merge=lfs -text
|
9 |
*.joblib filter=lfs diff=lfs merge=lfs -text
|
10 |
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
12 |
*.model filter=lfs diff=lfs merge=lfs -text
|
13 |
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
16 |
*.onnx filter=lfs diff=lfs merge=lfs -text
|
17 |
*.ot filter=lfs diff=lfs merge=lfs -text
|
18 |
*.parquet filter=lfs diff=lfs merge=lfs -text
|
19 |
*.pb filter=lfs diff=lfs merge=lfs -text
|
20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
22 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
23 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
24 |
*.rar filter=lfs diff=lfs merge=lfs -text
|
25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
26 |
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
27 |
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
29 |
*.tflite filter=lfs diff=lfs merge=lfs -text
|
30 |
*.tgz filter=lfs diff=lfs merge=lfs -text
|
31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
32 |
*.xz filter=lfs diff=lfs merge=lfs -text
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
-
*.
|
35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
*.7z filter=lfs diff=lfs merge=lfs -text
|
2 |
*.arrow filter=lfs diff=lfs merge=lfs -text
|
3 |
*.bin filter=lfs diff=lfs merge=lfs -text
|
4 |
+
*.bin.* filter=lfs diff=lfs merge=lfs -text
|
5 |
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
|
|
6 |
*.ftz filter=lfs diff=lfs merge=lfs -text
|
7 |
*.gz filter=lfs diff=lfs merge=lfs -text
|
8 |
*.h5 filter=lfs diff=lfs merge=lfs -text
|
9 |
*.joblib filter=lfs diff=lfs merge=lfs -text
|
10 |
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
|
|
11 |
*.model filter=lfs diff=lfs merge=lfs -text
|
12 |
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
13 |
*.onnx filter=lfs diff=lfs merge=lfs -text
|
14 |
*.ot filter=lfs diff=lfs merge=lfs -text
|
15 |
*.parquet filter=lfs diff=lfs merge=lfs -text
|
16 |
*.pb filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
17 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
18 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
19 |
*.rar filter=lfs diff=lfs merge=lfs -text
|
|
|
20 |
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
21 |
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
|
|
22 |
*.tflite filter=lfs diff=lfs merge=lfs -text
|
23 |
*.tgz filter=lfs diff=lfs merge=lfs -text
|
|
|
24 |
*.xz filter=lfs diff=lfs merge=lfs -text
|
25 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
26 |
+
*.zstandard filter=lfs diff=lfs merge=lfs -text
|
27 |
+
*.tfevents* filter=lfs diff=lfs merge=lfs -text
|
28 |
+
*.db* filter=lfs diff=lfs merge=lfs -text
|
29 |
+
*.ark* filter=lfs diff=lfs merge=lfs -text
|
30 |
+
**/*ckpt*data* filter=lfs diff=lfs merge=lfs -text
|
31 |
+
**/*ckpt*.meta filter=lfs diff=lfs merge=lfs -text
|
32 |
+
**/*ckpt*.index filter=lfs diff=lfs merge=lfs -text
|
33 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
34 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
35 |
+
*.AppImage filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
example/*
|
2 |
+
*__pycache__*
|
3 |
+
test.py
|
README.md
CHANGED
@@ -1,10 +1,10 @@
|
|
1 |
---
|
2 |
title: Piano Trans
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: gradio
|
7 |
-
sdk_version: 4.
|
8 |
app_file: app.py
|
9 |
pinned: false
|
10 |
license: mit
|
|
|
1 |
---
|
2 |
title: Piano Trans
|
3 |
+
emoji: 🎹🎵
|
4 |
+
colorFrom: purple
|
5 |
+
colorTo: gray
|
6 |
sdk: gradio
|
7 |
+
sdk_version: 4.24.0
|
8 |
app_file: app.py
|
9 |
pinned: false
|
10 |
license: mit
|
app.py
ADDED
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import re
|
3 |
+
import torch
|
4 |
+
import shutil
|
5 |
+
import requests
|
6 |
+
import gradio as gr
|
7 |
+
from piano_transcription_inference import PianoTranscription, load_audio, sample_rate
|
8 |
+
from modelscope import snapshot_download
|
9 |
+
from tempfile import NamedTemporaryFile
|
10 |
+
from pydub.utils import mediainfo
|
11 |
+
from urllib.parse import urlparse
|
12 |
+
from convert import *
|
13 |
+
|
14 |
+
CACHE_DIR = "./__pycache__"
|
15 |
+
WEIGHTS_PATH = (
|
16 |
+
snapshot_download("monetjoe/CRNN_note_F1_0.9677_pedal_F1_0.9186")
|
17 |
+
+ "/CRNN_note_F1=0.9677_pedal_F1=0.9186.pth"
|
18 |
+
)
|
19 |
+
|
20 |
+
|
21 |
+
def clean_cache(cache_dir=CACHE_DIR):
|
22 |
+
if os.path.exists(cache_dir):
|
23 |
+
shutil.rmtree(cache_dir)
|
24 |
+
|
25 |
+
os.mkdir(cache_dir)
|
26 |
+
|
27 |
+
|
28 |
+
def get_audio_file_type(file_path):
|
29 |
+
try:
|
30 |
+
# 获取媒体信息
|
31 |
+
info = mediainfo(file_path)
|
32 |
+
# 返回文件格式
|
33 |
+
return "." + info["format_name"]
|
34 |
+
except Exception as e:
|
35 |
+
print(f"Error occurred: {e}")
|
36 |
+
return None
|
37 |
+
|
38 |
+
|
39 |
+
def download_audio(url, save_path):
|
40 |
+
with NamedTemporaryFile(delete=False, suffix="_temp") as tmp_file:
|
41 |
+
temp_file_path = tmp_file.name
|
42 |
+
|
43 |
+
# 发送HTTP GET请求并下载内容
|
44 |
+
response = requests.get(url, stream=True)
|
45 |
+
|
46 |
+
# 检查请求是否成功
|
47 |
+
if response.status_code == 200:
|
48 |
+
# 将音频内容写入临时文件
|
49 |
+
for chunk in response.iter_content(chunk_size=8192):
|
50 |
+
tmp_file.write(chunk)
|
51 |
+
else:
|
52 |
+
print(f"Failed to download file: HTTP {response.status_code}")
|
53 |
+
return ""
|
54 |
+
|
55 |
+
ext = get_audio_file_type(temp_file_path)
|
56 |
+
full_path = f"{save_path}{ext}"
|
57 |
+
|
58 |
+
# 重命名临时文件以包含正确的扩展名
|
59 |
+
shutil.move(temp_file_path, full_path)
|
60 |
+
|
61 |
+
return full_path
|
62 |
+
|
63 |
+
|
64 |
+
def is_url(s: str):
|
65 |
+
try:
|
66 |
+
# 解析字符串
|
67 |
+
result = urlparse(s)
|
68 |
+
# 检查scheme(如http, https)和netloc(域名)
|
69 |
+
return all([result.scheme, result.netloc])
|
70 |
+
except:
|
71 |
+
# 如果解析过程中发生异常,则返回False
|
72 |
+
return False
|
73 |
+
|
74 |
+
|
75 |
+
def audio2midi(audio_path: str):
|
76 |
+
# Load audio
|
77 |
+
audio, _ = load_audio(audio_path, sr=sample_rate, mono=True)
|
78 |
+
# Transcriptor
|
79 |
+
transcriptor = PianoTranscription(
|
80 |
+
device="cuda" if torch.cuda.is_available() else "cpu",
|
81 |
+
checkpoint_path=WEIGHTS_PATH,
|
82 |
+
)
|
83 |
+
# device: 'cuda' | 'cpu' Transcribe and write out to MIDI file
|
84 |
+
midi_path = f"{CACHE_DIR}/output.mid"
|
85 |
+
# midi_path = audio_path.replace(audio_path.split(".")[-1], "mid")
|
86 |
+
transcriptor.transcribe(audio, midi_path)
|
87 |
+
return midi_path, os.path.basename(audio_path).split(".")[-2].capitalize()
|
88 |
+
|
89 |
+
|
90 |
+
def inference(audio_path: str):
|
91 |
+
clean_cache()
|
92 |
+
midi, title = audio2midi(audio_path)
|
93 |
+
xml = midi2xml(midi, title)
|
94 |
+
abc = xml2abc(xml)
|
95 |
+
mxl = xml2mxl(xml)
|
96 |
+
pdf, jpg = xml2jpg(xml)
|
97 |
+
|
98 |
+
return midi, pdf, xml, mxl, abc, jpg
|
99 |
+
|
100 |
+
|
101 |
+
def get_first_integer(input_string):
|
102 |
+
match = re.search(r"\d+", input_string)
|
103 |
+
if match:
|
104 |
+
return str(int(match.group()))
|
105 |
+
else:
|
106 |
+
return ""
|
107 |
+
|
108 |
+
|
109 |
+
def infer(audio_url: str):
|
110 |
+
clean_cache()
|
111 |
+
download_path = f"{CACHE_DIR}/output"
|
112 |
+
if is_url(audio_url):
|
113 |
+
if "163" in audio_url and not audio_url.endswith(".mp3"):
|
114 |
+
song_id = get_first_integer(audio_url.split("?id=")[1])
|
115 |
+
audio_url = f"https://music.163.com/song/media/outer/url?id={song_id}.mp3"
|
116 |
+
print(audio_url)
|
117 |
+
|
118 |
+
download_path = download_audio(audio_url, download_path)
|
119 |
+
|
120 |
+
midi, title = audio2midi(download_path)
|
121 |
+
xml = midi2xml(midi, title)
|
122 |
+
abc = xml2abc(xml)
|
123 |
+
mxl = xml2mxl(xml)
|
124 |
+
pdf, jpg = xml2jpg(xml)
|
125 |
+
|
126 |
+
return download_path, midi, pdf, xml, mxl, abc, jpg
|
127 |
+
|
128 |
+
|
129 |
+
with gr.Blocks() as iface:
|
130 |
+
with gr.Tab("Upload mode"):
|
131 |
+
gr.Interface(
|
132 |
+
fn=inference,
|
133 |
+
inputs=gr.Audio(label="Please upload 100% of the audio before clicking submit", type="filepath"),
|
134 |
+
outputs=[
|
135 |
+
gr.components.File(label="Download MIDI"),
|
136 |
+
gr.components.File(label="Download PDF score"),
|
137 |
+
gr.components.File(label="Download MusicXML"),
|
138 |
+
gr.components.File(label="Download MXL"),
|
139 |
+
gr.Textbox(label="abc notation", show_copy_button=True),
|
140 |
+
gr.Image(label="Staff", type="filepath"),
|
141 |
+
],
|
142 |
+
allow_flagging="never",
|
143 |
+
)
|
144 |
+
|
145 |
+
with gr.Tab("Direct link mode"):
|
146 |
+
gr.Interface(
|
147 |
+
fn=infer,
|
148 |
+
inputs=gr.Textbox(
|
149 |
+
label="Input audio direct link URL (Netease Cloud Music can directly input non-VIP song page link automatically resolved)"
|
150 |
+
),
|
151 |
+
outputs=[
|
152 |
+
gr.Audio(label="Download audio", type="filepath"),
|
153 |
+
gr.components.File(label="Download MIDI"),
|
154 |
+
gr.components.File(label="Download PDF score"),
|
155 |
+
gr.components.File(label="Download MusicXML"),
|
156 |
+
gr.components.File(label="Download MXL"),
|
157 |
+
gr.Textbox(label="abc notation", show_copy_button=True),
|
158 |
+
gr.Image(label="Staff", type="filepath"),
|
159 |
+
],
|
160 |
+
allow_flagging="never",
|
161 |
+
)
|
162 |
+
|
163 |
+
iface.launch()
|
convert.py
ADDED
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import sys
|
3 |
+
import fitz
|
4 |
+
import requests
|
5 |
+
import subprocess
|
6 |
+
from PIL import Image
|
7 |
+
from music21 import converter
|
8 |
+
|
9 |
+
|
10 |
+
def download(url: str, directory: str, filename: str):
|
11 |
+
if directory != "" and not os.path.exists(directory):
|
12 |
+
os.makedirs(directory)
|
13 |
+
# Create the full path for the file to be saved
|
14 |
+
file_path = os.path.join(directory, filename)
|
15 |
+
# Send a GET request to the URL
|
16 |
+
response = requests.get(url, stream=True)
|
17 |
+
# Check if the request was successful
|
18 |
+
if response.status_code == 200:
|
19 |
+
# Open the file in write-binary mode
|
20 |
+
with open(file_path, "wb") as file:
|
21 |
+
# Write the contents of the response to the file
|
22 |
+
for chunk in response.iter_content(chunk_size=1024):
|
23 |
+
if chunk: # Filter out keep-alive new chunks
|
24 |
+
file.write(chunk)
|
25 |
+
|
26 |
+
print(f"The file has been downloaded and saved to {file_path}")
|
27 |
+
|
28 |
+
else:
|
29 |
+
print(f"Failed to download the file. Status code: {response.status_code}")
|
30 |
+
|
31 |
+
return os.path.join(directory, filename)
|
32 |
+
|
33 |
+
|
34 |
+
if sys.platform.startswith("linux"):
|
35 |
+
apkname = "MuseScore.AppImage"
|
36 |
+
extra_dir = "squashfs-root"
|
37 |
+
if not os.path.exists(apkname):
|
38 |
+
download(
|
39 |
+
url="https://master.dl.sourceforge.net/project/musescore.mirror/v4.2.0/MuseScore-4.2.0.233521125-x86_64.AppImage?viasf=1",
|
40 |
+
directory="",
|
41 |
+
filename=apkname,
|
42 |
+
)
|
43 |
+
|
44 |
+
if not os.path.exists(extra_dir):
|
45 |
+
subprocess.run(["chmod", "+x", f"./{apkname}"])
|
46 |
+
subprocess.run([f"./{apkname}", "--appimage-extract"])
|
47 |
+
|
48 |
+
MSCORE = f"./{extra_dir}/AppRun"
|
49 |
+
os.environ["QT_QPA_PLATFORM"] = "offscreen"
|
50 |
+
|
51 |
+
else:
|
52 |
+
MSCORE = "D:/Program Files/MuseScore 3/bin/MuseScore3.exe"
|
53 |
+
|
54 |
+
|
55 |
+
def add_title_to_xml(xml_path: str, title: str):
|
56 |
+
midi_data = converter.parse(xml_path)
|
57 |
+
# 将标题添加到 MIDI 文件中
|
58 |
+
midi_data.metadata.movementName = title
|
59 |
+
midi_data.metadata.composer = "Transcripted by AI"
|
60 |
+
# 保存修改后的 MIDI 文件
|
61 |
+
midi_data.write("musicxml", fp=xml_path)
|
62 |
+
|
63 |
+
|
64 |
+
def xml2abc(xml_path: str):
|
65 |
+
result = subprocess.run(
|
66 |
+
f"python xml2abc.py {xml_path}", stdout=subprocess.PIPE, text=True
|
67 |
+
)
|
68 |
+
if result.returncode == 0:
|
69 |
+
return result.stdout
|
70 |
+
|
71 |
+
return ""
|
72 |
+
|
73 |
+
|
74 |
+
def xml2mxl(xml_path: str):
|
75 |
+
mxl_file = xml_path.replace(".musicxml", ".mxl")
|
76 |
+
command = [MSCORE, "-o", mxl_file, xml_path]
|
77 |
+
result = subprocess.run(command)
|
78 |
+
print(result)
|
79 |
+
return mxl_file
|
80 |
+
|
81 |
+
|
82 |
+
def midi2xml(mid_file: str, title: str):
|
83 |
+
xml_file = mid_file.replace(".mid", ".musicxml")
|
84 |
+
command = [MSCORE, "-o", xml_file, mid_file]
|
85 |
+
result = subprocess.run(command)
|
86 |
+
add_title_to_xml(xml_file, title)
|
87 |
+
print(result)
|
88 |
+
return xml_file
|
89 |
+
|
90 |
+
|
91 |
+
def xml2midi(xml_file: str):
|
92 |
+
midi_file = xml_file.replace(".musicxml", ".mid")
|
93 |
+
command = [MSCORE, "-o", midi_file, xml_file]
|
94 |
+
result = subprocess.run(command)
|
95 |
+
print(result)
|
96 |
+
return midi_file
|
97 |
+
|
98 |
+
|
99 |
+
def pdf2img(pdf_path: str):
|
100 |
+
output_path = pdf_path.replace(".pdf", ".jpg")
|
101 |
+
doc = fitz.open(pdf_path)
|
102 |
+
# 创建一个图像列表
|
103 |
+
images = []
|
104 |
+
for page_number in range(doc.page_count):
|
105 |
+
page = doc[page_number]
|
106 |
+
# 将页面渲染为图像
|
107 |
+
image = page.get_pixmap()
|
108 |
+
# 将图像添加到列表
|
109 |
+
images.append(
|
110 |
+
Image.frombytes("RGB", [image.width, image.height], image.samples)
|
111 |
+
)
|
112 |
+
# 竖向合并图像
|
113 |
+
merged_image = Image.new(
|
114 |
+
"RGB", (images[0].width, sum(image.height for image in images))
|
115 |
+
)
|
116 |
+
y_offset = 0
|
117 |
+
for image in images:
|
118 |
+
merged_image.paste(image, (0, y_offset))
|
119 |
+
y_offset += image.height
|
120 |
+
# 保存合并后的图像为JPG
|
121 |
+
merged_image.save(output_path, "JPEG")
|
122 |
+
# 关闭PDF文档
|
123 |
+
doc.close()
|
124 |
+
return output_path
|
125 |
+
|
126 |
+
|
127 |
+
def xml2jpg(xml_file: str):
|
128 |
+
pdf_score = xml_file.replace(".musicxml", ".pdf")
|
129 |
+
command = [MSCORE, "-o", pdf_score, xml_file]
|
130 |
+
result = subprocess.run(command)
|
131 |
+
print(result)
|
132 |
+
return pdf_score, pdf2img(pdf_score)
|
requirements.txt
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
librosa==0.9.2
|
2 |
+
piano_transcription_inference
|
3 |
+
pymupdf
|
4 |
+
music21
|
5 |
+
modelscope
|
xml2abc.py
ADDED
@@ -0,0 +1,2156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
17 |
+
import xml.etree.cElementTree as E
|
18 |
+
except:
|
19 |
+
import xml.etree.ElementTree as E
|
20 |
+
import os
|
21 |
+
import sys
|
22 |
+
import types
|
23 |
+
import re
|
24 |
+
import math
|
25 |
+
|
26 |
+
VERSION = 145
|
27 |
+
|
28 |
+
python3 = sys.version_info.major > 2
|
29 |
+
if python3:
|
30 |
+
tupletype = tuple
|
31 |
+
listtype = list
|
32 |
+
max_int = sys.maxsize
|
33 |
+
else:
|
34 |
+
tupletype = types.TupleType
|
35 |
+
listtype = types.ListType
|
36 |
+
max_int = sys.maxint
|
37 |
+
|
38 |
+
note_ornamentation_map = { # for notations/, modified from EasyABC
|
39 |
+
'ornaments/trill-mark': 'T',
|
40 |
+
'ornaments/mordent': 'M',
|
41 |
+
'ornaments/inverted-mordent': 'P',
|
42 |
+
'ornaments/turn': '!turn!',
|
43 |
+
'ornaments/inverted-turn': '!invertedturn!',
|
44 |
+
'technical/up-bow': 'u',
|
45 |
+
'technical/down-bow': 'v',
|
46 |
+
'technical/harmonic': '!open!',
|
47 |
+
'technical/open-string': '!open!',
|
48 |
+
'technical/stopped': '!plus!',
|
49 |
+
'technical/snap-pizzicato': '!snap!',
|
50 |
+
'technical/thumb-position': '!thumb!',
|
51 |
+
'articulations/accent': '!>!',
|
52 |
+
'articulations/strong-accent': '!^!',
|
53 |
+
'articulations/staccato': '.',
|
54 |
+
'articulations/staccatissimo': '!wedge!',
|
55 |
+
'articulations/scoop': '!slide!',
|
56 |
+
'fermata': '!fermata!',
|
57 |
+
'arpeggiate': '!arpeggio!',
|
58 |
+
'articulations/tenuto': '!tenuto!',
|
59 |
+
# not sure whether this is the right translation
|
60 |
+
'articulations/staccatissimo': '!wedge!',
|
61 |
+
# not sure whether this is the right translation
|
62 |
+
'articulations/spiccato': '!wedge!',
|
63 |
+
# this may need to be tested to make sure it appears on the right side of the note
|
64 |
+
'articulations/breath-mark': '!breath!',
|
65 |
+
'articulations/detached-legato': '!tenuto!.',
|
66 |
+
}
|
67 |
+
|
68 |
+
dynamics_map = { # for direction/direction-type/dynamics/
|
69 |
+
'p': '!p!',
|
70 |
+
'pp': '!pp!',
|
71 |
+
'ppp': '!ppp!',
|
72 |
+
'pppp': '!pppp!',
|
73 |
+
'f': '!f!',
|
74 |
+
'ff': '!ff!',
|
75 |
+
'fff': '!fff!',
|
76 |
+
'ffff': '!ffff!',
|
77 |
+
'mp': '!mp!',
|
78 |
+
'mf': '!mf!',
|
79 |
+
'sfz': '!sfz!',
|
80 |
+
}
|
81 |
+
|
82 |
+
percSvg = '''%%beginsvg
|
83 |
+
<defs>
|
84 |
+
<text id="x" x="-3" y="0"></text>
|
85 |
+
<text id="x-" x="-3" y="0"></text>
|
86 |
+
<text id="x+" x="-3" y="0"></text>
|
87 |
+
<text id="normal" x="-3.7" y="0"></text>
|
88 |
+
<text id="normal-" x="-3.7" y="0"></text>
|
89 |
+
<text id="normal+" x="-3.7" y="0"></text>
|
90 |
+
<g id="circle-x"><text x="-3" y="0"></text><circle r="4" class="stroke"></circle></g>
|
91 |
+
<g id="circle-x-"><text x="-3" y="0"></text><circle r="4" class="stroke"></circle></g>
|
92 |
+
<path id="triangle" d="m-4 -3.2l4 6.4 4 -6.4z" class="stroke" style="stroke-width:1.4"></path>
|
93 |
+
<path id="triangle-" d="m-4 -3.2l4 6.4 4 -6.4z" class="stroke" style="stroke-width:1.4"></path>
|
94 |
+
<path id="triangle+" d="m-4 -3.2l4 6.4 4 -6.4z" class="stroke" style="fill:#000"></path>
|
95 |
+
<path id="square" d="m-3.5 3l0 -6.2 7.2 0 0 6.2z" class="stroke" style="stroke-width:1.4"></path>
|
96 |
+
<path id="square-" d="m-3.5 3l0 -6.2 7.2 0 0 6.2z" class="stroke" style="stroke-width:1.4"></path>
|
97 |
+
<path id="square+" d="m-3.5 3l0 -6.2 7.2 0 0 6.2z" class="stroke" style="fill:#000"></path>
|
98 |
+
<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>
|
99 |
+
<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>
|
100 |
+
<path id="diamond+" d="m0 -3l4.2 3.2 -4.2 3.2 -4.2 -3.2z" class="stroke" style="fill:#000"></path>
|
101 |
+
</defs>
|
102 |
+
%%endsvg'''
|
103 |
+
|
104 |
+
tabSvg = '''%%beginsvg
|
105 |
+
<style type="text/css">
|
106 |
+
.bf {font-family:sans-serif; font-size:7px}
|
107 |
+
</style>
|
108 |
+
<defs>
|
109 |
+
<rect id="clr" x="-3" y="-1" width="6" height="5" fill="white"></rect>
|
110 |
+
<rect id="clr2" x="-3" y="-1" width="11" height="5" fill="white"></rect>'''
|
111 |
+
|
112 |
+
kopSvg = '<g id="kop%s" class="bf"><use xlink:href="#clr"></use><text x="-2" y="3">%s</text></g>\n'
|
113 |
+
kopSvg2 = '<g id="kop%s" class="bf"><use xlink:href="#clr2"></use><text x="-2" y="3">%s</text></g>\n'
|
114 |
+
|
115 |
+
info_list = [] # diagnostic messages
|
116 |
+
|
117 |
+
|
118 |
+
def info(s, warn=1):
|
119 |
+
x = ('-- ' if warn else '') + s + '\n'
|
120 |
+
info_list.append(x)
|
121 |
+
if __name__ == '__main__': # only when run from the command line
|
122 |
+
sys.stderr.write(x)
|
123 |
+
|
124 |
+
# -------------------
|
125 |
+
# data abstractions
|
126 |
+
# -------------------
|
127 |
+
|
128 |
+
|
129 |
+
class Measure:
|
130 |
+
def __init__(s, p):
|
131 |
+
s.reset()
|
132 |
+
s.ixp = p # part number
|
133 |
+
s.ixm = 0 # measure number
|
134 |
+
s.mdur = 0 # measure duration (nominal metre value in divisions)
|
135 |
+
s.divs = 0 # number of divisions per 1/4
|
136 |
+
s.mtr = 4, 4 # meter
|
137 |
+
|
138 |
+
def reset(s): # reset each measure
|
139 |
+
s.attr = '' # measure signatures, tempo
|
140 |
+
s.lline = '' # left barline, but only holds ':' at start of repeat, otherwise empty
|
141 |
+
s.rline = '|' # right barline
|
142 |
+
s.lnum = '' # (left) volta number
|
143 |
+
|
144 |
+
|
145 |
+
class Note:
|
146 |
+
def __init__(s, dur=0, n=None):
|
147 |
+
s.tijd = 0 # the time in XML division units
|
148 |
+
s.dur = dur # duration of a note in XML divisions
|
149 |
+
s.fact = None # time modification for tuplet notes (num, div)
|
150 |
+
s.tup = [''] # start(s) and/or stop(s) of tuplet
|
151 |
+
s.tupabc = '' # abc tuplet string to issue before note
|
152 |
+
s.beam = 0 # 1 = beamed
|
153 |
+
s.grace = 0 # 1 = grace note
|
154 |
+
s.before = [] # abc string that goes before the note/chord
|
155 |
+
s.after = '' # the same after the note/chord
|
156 |
+
s.ns = n and [n] or [] # notes in the chord
|
157 |
+
s.lyrs = {} # {number -> syllabe}
|
158 |
+
s.tab = None # (string number, fret number)
|
159 |
+
s.ntdec = '' # !string!, !courtesy!
|
160 |
+
|
161 |
+
|
162 |
+
class Elem:
|
163 |
+
def __init__(s, string):
|
164 |
+
s.tijd = 0 # the time in XML division units
|
165 |
+
s.str = string # any abc string that is not a note
|
166 |
+
|
167 |
+
|
168 |
+
class Counter:
|
169 |
+
def inc(s, key, voice): s.counters[key][voice] = s.counters[key].get(
|
170 |
+
voice, 0) + 1
|
171 |
+
|
172 |
+
def clear(s, vnums): # reset all counters
|
173 |
+
tups = list(zip(vnums.keys(), len(vnums) * [0]))
|
174 |
+
s.counters = {'note': dict(tups), 'nopr': dict(
|
175 |
+
tups), 'nopt': dict(tups)}
|
176 |
+
|
177 |
+
def getv(s, key, voice): return s.counters[key][voice]
|
178 |
+
|
179 |
+
def prcnt(s, ip): # print summary of all non zero counters
|
180 |
+
for iv in s.counters['note']:
|
181 |
+
if s.getv('nopr', iv) != 0:
|
182 |
+
info('part %d, voice %d has %d skipped non printable notes' %
|
183 |
+
(ip, iv, s.getv('nopr', iv)))
|
184 |
+
if s.getv('nopt', iv) != 0:
|
185 |
+
info('part %d, voice %d has %d notes without pitch' %
|
186 |
+
(ip, iv, s.getv('nopt', iv)))
|
187 |
+
if s.getv('note', iv) == 0: # no real notes counted in this voice
|
188 |
+
info('part %d, skipped empty voice %d' % (ip, iv))
|
189 |
+
|
190 |
+
|
191 |
+
class Music:
|
192 |
+
def __init__(s, options):
|
193 |
+
s.tijd = 0 # the current time
|
194 |
+
s.maxtime = 0 # maximum time in a measure
|
195 |
+
s.gMaten = [] # [voices,.. for all measures in a part]
|
196 |
+
# [{num: (abc_lyric_string, melis)},.. for all measures in a part]
|
197 |
+
s.gLyrics = []
|
198 |
+
# all used voice id's in a part (xml voice id's == numbers)
|
199 |
+
s.vnums = {}
|
200 |
+
s.cnt = Counter() # global counter object
|
201 |
+
s.vceCnt = 1 # the global voice count over all parts
|
202 |
+
s.lastnote = None # the last real note record inserted in s.voices
|
203 |
+
s.bpl = options.b # the max number of bars per line when writing abc
|
204 |
+
s.cpl = options.n # the number of chars per line when writing abc
|
205 |
+
s.repbra = 0 # true if volta is used somewhere
|
206 |
+
s.nvlt = options.v # no volta on higher voice numbers
|
207 |
+
s.jscript = options.j # compatibility with javascript version
|
208 |
+
|
209 |
+
def initVoices(s, newPart=0):
|
210 |
+
s.vtimes, s.voices, s.lyrics = {}, {}, {}
|
211 |
+
for v in s.vnums:
|
212 |
+
# {voice: the end time of the last item in each voice}
|
213 |
+
s.vtimes[v] = 0
|
214 |
+
s.voices[v] = [] # {voice: [Note|Elem, ..]}
|
215 |
+
s.lyrics[v] = [] # {voice: [{num: syl}, ..]}
|
216 |
+
if newPart:
|
217 |
+
s.cnt.clear(s.vnums) # clear counters once per part
|
218 |
+
|
219 |
+
def incTime(s, dt):
|
220 |
+
s.tijd += dt
|
221 |
+
if s.tijd < 0:
|
222 |
+
s.tijd = 0 # erroneous <backup> element
|
223 |
+
if s.tijd > s.maxtime:
|
224 |
+
s.maxtime = s.tijd
|
225 |
+
|
226 |
+
def appendElemCv(s, voices, elem):
|
227 |
+
for v in voices:
|
228 |
+
s.appendElem(v, elem) # insert element in all voices
|
229 |
+
|
230 |
+
def insertElem(s, v, elem): # insert at the start of voice v in the current measure
|
231 |
+
obj = Elem(elem)
|
232 |
+
obj.tijd = 0 # because voice is sorted later
|
233 |
+
s.voices[v].insert(0, obj)
|
234 |
+
|
235 |
+
def appendObj(s, v, obj, dur):
|
236 |
+
obj.tijd = s.tijd
|
237 |
+
s.voices[v].append(obj)
|
238 |
+
s.incTime(dur)
|
239 |
+
if s.tijd > s.vtimes[v]:
|
240 |
+
s.vtimes[v] = s.tijd # don't update for inserted earlier items
|
241 |
+
|
242 |
+
def appendElem(s, v, elem, tel=0):
|
243 |
+
s.appendObj(v, Elem(elem), 0)
|
244 |
+
if tel:
|
245 |
+
# count number of certain elements in each voice (in addition to notes)
|
246 |
+
s.cnt.inc('note', v)
|
247 |
+
|
248 |
+
def appendElemT(s, v, elem, tijd): # insert element at specified time
|
249 |
+
obj = Elem(elem)
|
250 |
+
obj.tijd = tijd
|
251 |
+
s.voices[v].append(obj)
|
252 |
+
|
253 |
+
def appendNote(s, v, note, noot):
|
254 |
+
note.ns.append(note.ntdec + noot)
|
255 |
+
s.appendObj(v, note, int(note.dur))
|
256 |
+
# remember last note/rest for later modifications (chord, grace)
|
257 |
+
s.lastnote = note
|
258 |
+
if noot != 'z' and noot != 'x': # real notes and grace notes
|
259 |
+
s.cnt.inc('note', v) # count number of real notes in each voice
|
260 |
+
if not note.grace: # for every real note
|
261 |
+
s.lyrics[v].append(note.lyrs) # even when it has no lyrics
|
262 |
+
|
263 |
+
def getLastRec(s, voice):
|
264 |
+
if s.gMaten:
|
265 |
+
# the last record in the last measure
|
266 |
+
return s.gMaten[-1][voice][-1]
|
267 |
+
return None # no previous records in the first measure
|
268 |
+
|
269 |
+
def getLastMelis(s, voice, num): # get melisma of last measure
|
270 |
+
if s.gLyrics:
|
271 |
+
# the previous lyrics dict in this voice
|
272 |
+
lyrdict = s.gLyrics[-1][voice]
|
273 |
+
if num in lyrdict:
|
274 |
+
# lyrdict = num -> (lyric string, melisma)
|
275 |
+
return lyrdict[num][1]
|
276 |
+
return 0 # no previous lyrics in voice or line number
|
277 |
+
|
278 |
+
def addChord(s, note, noot): # careful: we assume that chord notes follow immediately
|
279 |
+
for d in note.before: # put all decorations before chord
|
280 |
+
if d not in s.lastnote.before:
|
281 |
+
s.lastnote.before += [d]
|
282 |
+
s.lastnote.ns.append(note.ntdec + noot)
|
283 |
+
|
284 |
+
def addBar(s, lbrk, m): # linebreak, measure data
|
285 |
+
if m.mdur and s.maxtime > m.mdur:
|
286 |
+
info('measure %d in part %d longer than metre' % (m.ixm+1, m.ixp+1))
|
287 |
+
s.tijd = s.maxtime # the time of the bar lines inserted here
|
288 |
+
for v in s.vnums:
|
289 |
+
if m.lline or m.lnum: # if left barline or left volta number
|
290 |
+
p = s.getLastRec(v) # get the previous barline record
|
291 |
+
if p: # in measure 1 no previous measure is available
|
292 |
+
x = p.str # p.str is the ABC barline string
|
293 |
+
if m.lline: # append begin of repeat, m.lline == ':'
|
294 |
+
x = (x + m.lline).replace(':|:',
|
295 |
+
'::').replace('||', '|')
|
296 |
+
if s.nvlt == 3: # add volta number only to lowest voice in part 0
|
297 |
+
if m.ixp + v == min(s.vnums):
|
298 |
+
x += m.lnum
|
299 |
+
elif m.lnum: # new behaviour with I:repbra 0
|
300 |
+
# add volta number(s) or text to all voices
|
301 |
+
x += m.lnum
|
302 |
+
s.repbra = 1 # signal occurrence of a volta
|
303 |
+
p.str = x # modify previous right barline
|
304 |
+
elif m.lline: # begin of new part and left repeat bar is required
|
305 |
+
s.insertElem(v, '|:')
|
306 |
+
if lbrk:
|
307 |
+
p = s.getLastRec(v) # get the previous barline record
|
308 |
+
if p:
|
309 |
+
p.str += lbrk # insert linebreak char after the barlines+volta
|
310 |
+
if m.attr: # insert signatures at front of buffer
|
311 |
+
s.insertElem(v, '%s' % m.attr)
|
312 |
+
# insert current barline record at time maxtime
|
313 |
+
s.appendElem(v, ' %s' % m.rline)
|
314 |
+
# make all times consistent
|
315 |
+
s.voices[v] = sortMeasure(s.voices[v], m)
|
316 |
+
lyrs = s.lyrics[v] # [{number: sylabe}, .. for all notes]
|
317 |
+
# {number: (abc_lyric_string, melis)} for this voice
|
318 |
+
lyrdict = {}
|
319 |
+
# the lyrics numbers in this measure
|
320 |
+
nums = [num for d in lyrs for num in d.keys()]
|
321 |
+
# the highest lyrics number in this measure
|
322 |
+
maxNums = max(nums + [0])
|
323 |
+
for i in range(maxNums, 0, -1):
|
324 |
+
# collect the syllabi with number i
|
325 |
+
xs = [syldict.get(i, '') for syldict in lyrs]
|
326 |
+
melis = s.getLastMelis(v, i) # get melisma from last measure
|
327 |
+
lyrdict[i] = abcLyr(xs, melis)
|
328 |
+
# {number: (abc_lyric_string, melis)} for this measure
|
329 |
+
s.lyrics[v] = lyrdict
|
330 |
+
mkBroken(s.voices[v])
|
331 |
+
s.gMaten.append(s.voices)
|
332 |
+
s.gLyrics.append(s.lyrics)
|
333 |
+
s.tijd = s.maxtime = 0
|
334 |
+
s.initVoices()
|
335 |
+
|
336 |
+
def outVoices(s, divs, ip, isSib): # output all voices of part ip
|
337 |
+
# xml voice number -> abc voice number (one part)
|
338 |
+
vvmap = {}
|
339 |
+
vnum_keys = list(s.vnums.keys())
|
340 |
+
if s.jscript or isSib:
|
341 |
+
vnum_keys.sort()
|
342 |
+
lvc = min(vnum_keys or [1]) # lowest xml voice number of this part
|
343 |
+
for iv in vnum_keys:
|
344 |
+
if s.cnt.getv('note', iv) == 0: # no real notes counted in this voice
|
345 |
+
continue # skip empty voices
|
346 |
+
if abcOut.denL:
|
347 |
+
unitL = abcOut.denL # take the unit length from the -d option
|
348 |
+
else:
|
349 |
+
# compute the best unit length for this voice
|
350 |
+
unitL = compUnitLength(iv, s.gMaten, divs)
|
351 |
+
abcOut.cmpL.append(unitL) # remember for header output
|
352 |
+
vn, vl = [], {} # for voice iv: collect all notes to vn and all lyric lines to vl
|
353 |
+
for im in range(len(s.gMaten)):
|
354 |
+
measure = s.gMaten[im][iv]
|
355 |
+
vn.append(outVoice(measure, divs[im], im, ip, unitL))
|
356 |
+
checkMelismas(s.gLyrics, s.gMaten, im, iv)
|
357 |
+
for n, (lyrstr, melis) in s.gLyrics[im][iv].items():
|
358 |
+
if n in vl:
|
359 |
+
while len(vl[n]) < im:
|
360 |
+
vl[n].append('') # fill in skipped measures
|
361 |
+
vl[n].append(lyrstr)
|
362 |
+
else:
|
363 |
+
vl[n] = im * [''] + [lyrstr] # must skip im measures
|
364 |
+
for n, lyrs in vl.items(): # fill up possibly empty lyric measures at the end
|
365 |
+
mis = len(vn) - len(lyrs)
|
366 |
+
lyrs += mis * ['']
|
367 |
+
abcOut.add('V:%d' % s.vceCnt)
|
368 |
+
if s.repbra:
|
369 |
+
if s.nvlt == 1 and s.vceCnt > 1:
|
370 |
+
abcOut.add('I:repbra 0') # only volta on first voice
|
371 |
+
if s.nvlt == 2 and iv > lvc:
|
372 |
+
# only volta on first voice of each part
|
373 |
+
abcOut.add('I:repbra 0')
|
374 |
+
if s.cpl > 0:
|
375 |
+
# option -n (max chars per line) overrules -b (max bars per line)
|
376 |
+
s.bpl = 0
|
377 |
+
elif s.bpl == 0:
|
378 |
+
s.cpl = 100 # the default: 100 chars per line
|
379 |
+
bn = 0 # count bars
|
380 |
+
while vn: # while still measures available
|
381 |
+
ib = 1
|
382 |
+
chunk = vn[0]
|
383 |
+
while ib < len(vn):
|
384 |
+
if s.cpl > 0 and len(chunk) + len(vn[ib]) >= s.cpl:
|
385 |
+
break # line full (number of chars)
|
386 |
+
if s.bpl > 0 and ib >= s.bpl:
|
387 |
+
# line full (number of bars)
|
388 |
+
break
|
389 |
+
chunk += vn[ib]
|
390 |
+
ib += 1
|
391 |
+
bn += ib
|
392 |
+
abcOut.add(chunk + ' %%%d' % bn) # line with barnumer
|
393 |
+
del vn[:ib] # chop ib bars
|
394 |
+
# order the numbered lyric lines for output
|
395 |
+
lyrlines = sorted(vl.items())
|
396 |
+
for n, lyrs in lyrlines:
|
397 |
+
abcOut.add('w: ' + '|'.join(lyrs[:ib]) + '|')
|
398 |
+
del lyrs[:ib]
|
399 |
+
vvmap[iv] = s.vceCnt # xml voice number -> abc voice number
|
400 |
+
s.vceCnt += 1 # count voices over all parts
|
401 |
+
s.gMaten = [] # reset the follwing instance vars for each part
|
402 |
+
s.gLyrics = []
|
403 |
+
# print summary of skipped items in this part
|
404 |
+
s.cnt.prcnt(ip+1)
|
405 |
+
return vvmap
|
406 |
+
|
407 |
+
|
408 |
+
class ABCoutput:
|
409 |
+
pagekeys = 'scale,pageheight,pagewidth,leftmargin,rightmargin,topmargin,botmargin'.split(
|
410 |
+
',')
|
411 |
+
|
412 |
+
def __init__(s, fnmext, pad, X, options):
|
413 |
+
s.fnmext = fnmext
|
414 |
+
s.outlist = [] # list of ABC strings
|
415 |
+
s.title = 'T:Title'
|
416 |
+
s.key = 'none'
|
417 |
+
s.clefs = {} # clefs for all abc-voices
|
418 |
+
s.mtr = 'none'
|
419 |
+
s.tempo = 0 # 0 -> no tempo field
|
420 |
+
s.tempo_units = (1, 4) # note type of tempo direction
|
421 |
+
s.pad = pad # the output path or none
|
422 |
+
s.X = X + 1 # the abc tune number
|
423 |
+
# denominator of the unit length (L:) from -d option
|
424 |
+
s.denL = options.d
|
425 |
+
# 0 -> no %%MIDI, 1 -> only program, 2 -> all %%MIDI
|
426 |
+
s.volpan = int(options.m)
|
427 |
+
s.cmpL = [] # computed optimal unit length for all voices
|
428 |
+
s.jscript = options.j # compatibility with javascript version
|
429 |
+
s.tstep = options.t # translate percmap to voicemap
|
430 |
+
s.stemless = 0 # use U:s=!stemless!
|
431 |
+
s.shiftStem = options.s # shift note heads 3 units left
|
432 |
+
s.dojef = 0 # => s.tstep in mkHeader
|
433 |
+
if pad:
|
434 |
+
_, base_name = os.path.split(fnmext)
|
435 |
+
s.outfile = open(os.path.join(pad, base_name), 'w')
|
436 |
+
else:
|
437 |
+
s.outfile = sys.stdout
|
438 |
+
if s.jscript:
|
439 |
+
s.X = 1 # always X:1 in javascript version
|
440 |
+
s.pageFmt = {}
|
441 |
+
for k in s.pagekeys:
|
442 |
+
s.pageFmt[k] = None
|
443 |
+
if len(options.p) == 7:
|
444 |
+
for k, v in zip(s.pagekeys, options.p):
|
445 |
+
try:
|
446 |
+
s.pageFmt[k] = float(v)
|
447 |
+
except:
|
448 |
+
info('illegal float %s for %s', (k, v))
|
449 |
+
continue
|
450 |
+
|
451 |
+
def add(s, str):
|
452 |
+
s.outlist.append(str + '\n') # collect all ABC output
|
453 |
+
|
454 |
+
# stfmap = [parts], part = [staves], stave = [voices]
|
455 |
+
def mkHeader(s, stfmap, partlist, midimap, vmpdct, koppen):
|
456 |
+
accVce, accStf, staffs = [], [], stfmap[:] # staffs is consumed
|
457 |
+
for x in partlist: # collect partnames into accVce and staff groups into accStf
|
458 |
+
try:
|
459 |
+
prgroupelem(x, ('', ''), '', stfmap, accVce, accStf)
|
460 |
+
except:
|
461 |
+
info('lousy musicxml: error in part-list')
|
462 |
+
staves = ' '.join(accStf)
|
463 |
+
clfnms = {}
|
464 |
+
for part, (partname, partabbrv) in zip(staffs, accVce):
|
465 |
+
if not part:
|
466 |
+
continue # skip empty part
|
467 |
+
firstVoice = part[0][0] # the first voice number in this part
|
468 |
+
nm = partname.replace('\n', '\\n').replace('.:', '.').strip(':')
|
469 |
+
snm = partabbrv.replace('\n', '\\n').replace('.:', '.').strip(':')
|
470 |
+
clfnms[firstVoice] = (nm and 'nm="%s"' %
|
471 |
+
nm or '') + (snm and ' snm="%s"' % snm or '')
|
472 |
+
hd = ['X:%d\n%s\n' % (s.X, s.title)]
|
473 |
+
for i, k in enumerate(s.pagekeys):
|
474 |
+
if s.jscript and k in ['pageheight', 'topmargin', 'botmargin']:
|
475 |
+
continue
|
476 |
+
if s.pageFmt[k] != None:
|
477 |
+
hd.append('%%%%%s %.2f%s\n' %
|
478 |
+
(k, s.pageFmt[k], i > 0 and 'cm' or ''))
|
479 |
+
if staves and len(accStf) > 1:
|
480 |
+
hd.append('%%score ' + staves + '\n')
|
481 |
+
# default no tempo field
|
482 |
+
tempo = s.tempo and 'Q:%d/%d=%s\n' % (
|
483 |
+
s.tempo_units[0], s.tempo_units[1], s.tempo) or ''
|
484 |
+
d = {} # determine the most frequently occurring unit length over all voices
|
485 |
+
for x in s.cmpL:
|
486 |
+
d[x] = d.get(x, 0) + 1
|
487 |
+
if s.jscript:
|
488 |
+
# when tie (1) sort on key (0)
|
489 |
+
defLs = sorted(d.items(), key=lambda x: (-x[1], x[0]))
|
490 |
+
else:
|
491 |
+
defLs = sorted(d.items(), key=lambda x: -x[1])
|
492 |
+
# override default unit length with -d option
|
493 |
+
defL = s.denL and s.denL or defLs[0][0]
|
494 |
+
hd.append('L:1/%d\n%sM:%s\n' % (defL, tempo, s.mtr))
|
495 |
+
hd.append('I:linebreak $\nK:%s\n' % s.key)
|
496 |
+
if s.stemless:
|
497 |
+
hd.append('U:s=!stemless!\n')
|
498 |
+
vxs = sorted(vmpdct.keys())
|
499 |
+
for vx in vxs:
|
500 |
+
hd.extend(vmpdct[vx])
|
501 |
+
s.dojef = 0 # translate percmap to voicemap
|
502 |
+
for vnum, clef in s.clefs.items():
|
503 |
+
ch, prg, vol, pan = midimap[vnum-1][:4]
|
504 |
+
# map of abc percussion notes to midi notes
|
505 |
+
dmap = midimap[vnum - 1][4:]
|
506 |
+
if dmap and 'perc' not in clef:
|
507 |
+
clef = (clef + ' map=perc').strip()
|
508 |
+
hd.append('V:%d %s %s\n' % (vnum, clef, clfnms.get(vnum, '')))
|
509 |
+
if vnum in vmpdct:
|
510 |
+
hd.append('%%%%voicemap tab%d\n' % vnum)
|
511 |
+
hd.append(
|
512 |
+
'K:none\nM:none\n%%clef none\n%%staffscale 1.6\n%%flatbeams true\n%%stemdir down\n')
|
513 |
+
if 'perc' in clef:
|
514 |
+
hd.append('K:none\n') # no key for a perc voice
|
515 |
+
if s.volpan > 1: # option -m 2 -> output all recognized midi commands when needed and present in xml
|
516 |
+
if ch > 0 and ch != vnum:
|
517 |
+
hd.append('%%%%MIDI channel %d\n' % ch)
|
518 |
+
if prg > 0:
|
519 |
+
hd.append('%%%%MIDI program %d\n' % (prg - 1))
|
520 |
+
if vol >= 0:
|
521 |
+
# volume == 0 is possible ...
|
522 |
+
hd.append('%%%%MIDI control 7 %.0f\n' % vol)
|
523 |
+
if pan >= 0:
|
524 |
+
hd.append('%%%%MIDI control 10 %.0f\n' % pan)
|
525 |
+
elif s.volpan > 0: # default -> only output midi program command when present in xml
|
526 |
+
if dmap and ch > 0:
|
527 |
+
# also channel if percussion part
|
528 |
+
hd.append('%%%%MIDI channel %d\n' % ch)
|
529 |
+
if prg > 0:
|
530 |
+
hd.append('%%%%MIDI program %d\n' % (prg - 1))
|
531 |
+
for abcNote, step, midiNote, notehead in dmap:
|
532 |
+
if not notehead:
|
533 |
+
notehead = 'normal'
|
534 |
+
if abcMid(abcNote) != midiNote or abcNote != step:
|
535 |
+
if s.volpan > 0:
|
536 |
+
hd.append('%%%%MIDI drummap %s %s\n' %
|
537 |
+
(abcNote, midiNote))
|
538 |
+
hd.append('I:percmap %s %s %s %s\n' %
|
539 |
+
(abcNote, step, midiNote, notehead))
|
540 |
+
s.dojef = s.tstep # == options.t
|
541 |
+
if defL != s.cmpL[vnum-1]: # only if computed unit length different from header
|
542 |
+
hd.append('L:1/%d\n' % s.cmpL[vnum-1])
|
543 |
+
s.outlist = hd + s.outlist
|
544 |
+
if koppen: # output SVG stuff needed for tablature
|
545 |
+
# shift note heads 3 units left
|
546 |
+
k1 = kopSvg.replace('-2', '-5') if s.shiftStem else kopSvg
|
547 |
+
k2 = kopSvg2.replace('-2', '-5') if s.shiftStem else kopSvg2
|
548 |
+
tb = tabSvg.replace('-3', '-6') if s.shiftStem else tabSvg
|
549 |
+
# javascript compatibility
|
550 |
+
ks = sorted(koppen.keys())
|
551 |
+
ks = [k2 % (k, k) if len(k) == 2 else k1 % (k, k) for k in ks]
|
552 |
+
# javascript compatibility
|
553 |
+
tbs = map(lambda x: x.strip() + '\n', tb.splitlines())
|
554 |
+
s.outlist = tbs + ks + ['</defs>\n%%endsvg\n'] + s.outlist
|
555 |
+
|
556 |
+
def getABC(s):
|
557 |
+
str = ''.join(s.outlist)
|
558 |
+
if s.dojef:
|
559 |
+
str = perc2map(str)
|
560 |
+
return str
|
561 |
+
|
562 |
+
def writeall(s): # determine the required encoding of the entire ABC output
|
563 |
+
str = s.getABC()
|
564 |
+
if python3:
|
565 |
+
s.outfile.write(str)
|
566 |
+
else:
|
567 |
+
s.outfile.write(str.encode('utf-8'))
|
568 |
+
if s.pad:
|
569 |
+
s.outfile.close() # close each file with -o option
|
570 |
+
else:
|
571 |
+
# add empty line between tunes on stdout
|
572 |
+
s.outfile.write('\n')
|
573 |
+
# info('%s written with %d voices' % (s.fnmext, len(s.clefs)), warn=0)
|
574 |
+
|
575 |
+
# ----------------
|
576 |
+
# functions
|
577 |
+
# ----------------
|
578 |
+
|
579 |
+
|
580 |
+
def abcLyr(xs, melis): # Convert list xs to abc lyrics.
|
581 |
+
if not ''.join(xs):
|
582 |
+
return '', 0 # there is no lyrics in this measure
|
583 |
+
res = []
|
584 |
+
for x in xs: # xs has for every note a lyrics syllabe or an empty string
|
585 |
+
if x == '': # note without lyrics
|
586 |
+
if melis:
|
587 |
+
x = '_' # set melisma
|
588 |
+
else:
|
589 |
+
x = '*' # skip note
|
590 |
+
elif x.endswith('_') and not x.endswith('\_'): # start of new melisma
|
591 |
+
x = x.replace('_', '') # remove and set melis boolean
|
592 |
+
melis = 1 # so next skips will become melisma
|
593 |
+
else:
|
594 |
+
melis = 0 # melisma stops on first syllable
|
595 |
+
res.append(x)
|
596 |
+
return (' '.join(res), melis)
|
597 |
+
|
598 |
+
|
599 |
+
def simplify(a, b): # divide a and b by their greatest common divisor
|
600 |
+
x, y = a, b
|
601 |
+
while b:
|
602 |
+
a, b = b, a % b
|
603 |
+
return x // a, y // a
|
604 |
+
|
605 |
+
|
606 |
+
def abcdur(nx, divs, uL): # convert an musicXML duration d to abc units with L:1/uL
|
607 |
+
if nx.dur == 0:
|
608 |
+
return '' # when called for elements without duration
|
609 |
+
num, den = simplify(uL * nx.dur, divs * 4) # L=1/8 -> uL = 8 units
|
610 |
+
if nx.fact: # apply tuplet time modification
|
611 |
+
numfac, denfac = nx.fact
|
612 |
+
num, den = simplify(num * numfac, den * denfac)
|
613 |
+
if den > 64: # limit the denominator to a maximum of 64
|
614 |
+
x = float(num) / den
|
615 |
+
n = math.floor(x) # when just above an integer n
|
616 |
+
if x - n < 0.1 * x:
|
617 |
+
num, den = n, 1 # round to n
|
618 |
+
num64 = 64. * num / den + 1.0e-15 # to get Python2 behaviour of round
|
619 |
+
num, den = simplify(int(round(num64)), 64)
|
620 |
+
if num == 1:
|
621 |
+
if den == 1:
|
622 |
+
dabc = ''
|
623 |
+
elif den == 2:
|
624 |
+
dabc = '/'
|
625 |
+
else:
|
626 |
+
dabc = '/%d' % den
|
627 |
+
elif den == 1:
|
628 |
+
dabc = '%d' % num
|
629 |
+
else:
|
630 |
+
dabc = '%d/%d' % (num, den)
|
631 |
+
return dabc
|
632 |
+
|
633 |
+
|
634 |
+
def abcMid(note): # abc note -> midi pitch
|
635 |
+
r = re.search(r"([_^]*)([A-Ga-g])([',]*)", note)
|
636 |
+
if not r:
|
637 |
+
return -1
|
638 |
+
acc, n, oct = r.groups()
|
639 |
+
nUp = n.upper()
|
640 |
+
p = 60 + [0, 2, 4, 5, 7, 9,
|
641 |
+
11]['CDEFGAB'.index(nUp)] + (12 if nUp != n else 0)
|
642 |
+
if acc:
|
643 |
+
p += (1 if acc[0] == '^' else -1) * len(acc)
|
644 |
+
if oct:
|
645 |
+
p += (12 if oct[0] == "'" else -12) * len(oct)
|
646 |
+
return p
|
647 |
+
|
648 |
+
|
649 |
+
def staffStep(ptc, o, clef, tstep):
|
650 |
+
ndif = 0
|
651 |
+
if 'stafflines=1' in clef:
|
652 |
+
ndif += 4 # meaning of one line: E (xml) -> B (abc)
|
653 |
+
if not tstep and clef.startswith('bass'):
|
654 |
+
ndif += 12 # transpose bass -> treble (C3 -> A4)
|
655 |
+
if ndif: # diatonic transposition == addition modulo 7
|
656 |
+
nm7 = 'C,D,E,F,G,A,B'.split(',')
|
657 |
+
n = nm7.index(ptc) + ndif
|
658 |
+
ptc, o = nm7[n % 7], o + n // 7
|
659 |
+
if o > 4:
|
660 |
+
ptc = ptc.lower()
|
661 |
+
if o > 5:
|
662 |
+
ptc = ptc + (o-5) * "'"
|
663 |
+
if o < 4:
|
664 |
+
ptc = ptc + (4-o) * ","
|
665 |
+
return ptc
|
666 |
+
|
667 |
+
|
668 |
+
def setKey(fifths, mode):
|
669 |
+
sharpness = ['Fb', 'Cb', 'Gb', 'Db', 'Ab', 'Eb', 'Bb', 'F', 'C',
|
670 |
+
'G', 'D', 'A', 'E', 'B', 'F#', 'C#', 'G#', 'D#', 'A#', 'E#', 'B#']
|
671 |
+
offTab = {'maj': 8, 'ion': 8, 'm': 11, 'min': 11, 'aeo': 11,
|
672 |
+
'mix': 9, 'dor': 10, 'phr': 12, 'lyd': 7, 'loc': 13, 'non': 8}
|
673 |
+
mode = mode.lower()[:3] # only first three chars, no case
|
674 |
+
key = sharpness[offTab[mode] + fifths] + \
|
675 |
+
(mode if offTab[mode] != 8 else '')
|
676 |
+
accs = ['F', 'C', 'G', 'D', 'A', 'E', 'B']
|
677 |
+
if fifths >= 0:
|
678 |
+
msralts = dict(zip(accs[:fifths], fifths * [1]))
|
679 |
+
else:
|
680 |
+
msralts = dict(zip(accs[fifths:], -fifths * [-1]))
|
681 |
+
return key, msralts
|
682 |
+
|
683 |
+
|
684 |
+
def insTup(ix, notes, fact): # read one nested tuplet
|
685 |
+
tupcnt = 0
|
686 |
+
nx = notes[ix]
|
687 |
+
if 'start' in nx.tup:
|
688 |
+
nx.tup.remove('start') # do recursive calls when starts remain
|
689 |
+
tix = ix # index of first tuplet note
|
690 |
+
fn, fd = fact # xml time-mod of the higher level
|
691 |
+
fnum, fden = nx.fact # xml time-mod of the current level
|
692 |
+
tupfact = fnum//fn, fden//fd # abc time mod of this level
|
693 |
+
while ix < len(notes):
|
694 |
+
lastix = ix - 1
|
695 |
+
nx = notes[ix]
|
696 |
+
if isinstance(nx, Elem) or nx.grace:
|
697 |
+
ix += 1 # skip all non tuplet elements
|
698 |
+
continue
|
699 |
+
if 'start' in nx.tup: # more nested tuplets to start
|
700 |
+
# ix is on the stop note!
|
701 |
+
ix, tupcntR = insTup(ix, notes, tupfact)
|
702 |
+
tupcnt += tupcntR
|
703 |
+
elif nx.fact:
|
704 |
+
tupcnt += 1 # count tuplet elements
|
705 |
+
if 'stop' in nx.tup:
|
706 |
+
nx.tup.remove('stop')
|
707 |
+
break
|
708 |
+
if not nx.fact: # stop on first non tuplet note
|
709 |
+
ix = lastix # back to last tuplet note
|
710 |
+
break
|
711 |
+
ix += 1
|
712 |
+
# put abc tuplet notation before the recursive ones
|
713 |
+
tup = (tupfact[0], tupfact[1], tupcnt)
|
714 |
+
if tup == (3, 2, 3):
|
715 |
+
tupPrefix = '(3'
|
716 |
+
else:
|
717 |
+
tupPrefix = '(%d:%d:%d' % tup
|
718 |
+
notes[tix].tupabc = tupPrefix + notes[tix].tupabc
|
719 |
+
return ix, tupcnt # ix is on the last tuplet note
|
720 |
+
|
721 |
+
|
722 |
+
def mkBroken(vs): # introduce broken rhythms (vs: one voice, one measure)
|
723 |
+
vs = [n for n in vs if isinstance(n, Note)]
|
724 |
+
i = 0
|
725 |
+
while i < len(vs) - 1:
|
726 |
+
n1, n2 = vs[i], vs[i+1] # scan all adjacent pairs
|
727 |
+
# skip if note in tuplet or has no duration or outside beam
|
728 |
+
if not n1.fact and not n2.fact and n1.dur > 0 and n2.beam:
|
729 |
+
if n1.dur * 3 == n2.dur:
|
730 |
+
n2.dur = (2 * n2.dur) // 3
|
731 |
+
n1.dur = n1.dur * 2
|
732 |
+
n1.after = '<' + n1.after
|
733 |
+
i += 1 # do not chain broken rhythms
|
734 |
+
elif n2.dur * 3 == n1.dur:
|
735 |
+
n1.dur = (2 * n1.dur) // 3
|
736 |
+
n2.dur = n2.dur * 2
|
737 |
+
n1.after = '>' + n1.after
|
738 |
+
i += 1 # do not chain broken rhythms
|
739 |
+
i += 1
|
740 |
+
|
741 |
+
|
742 |
+
def outVoice(measure, divs, im, ip, unitL): # note/elem objects of one measure in one voice
|
743 |
+
ix = 0
|
744 |
+
while ix < len(measure): # set all (nested) tuplet annotations
|
745 |
+
nx = measure[ix]
|
746 |
+
if isinstance(nx, Note) and nx.fact and not nx.grace:
|
747 |
+
# read one tuplet, insert annotation(s)
|
748 |
+
ix, tupcnt = insTup(ix, measure, (1, 1))
|
749 |
+
ix += 1
|
750 |
+
vs = []
|
751 |
+
for nx in measure:
|
752 |
+
if isinstance(nx, Note):
|
753 |
+
# xml -> abc duration string
|
754 |
+
durstr = abcdur(nx, divs, unitL)
|
755 |
+
chord = len(nx.ns) > 1
|
756 |
+
cns = [nt[:-1] for nt in nx.ns if nt.endswith('-')]
|
757 |
+
tie = ''
|
758 |
+
if chord and len(cns) == len(nx.ns): # all chord notes tied
|
759 |
+
nx.ns = cns # chord notes without tie
|
760 |
+
tie = '-' # one tie for whole chord
|
761 |
+
s = nx.tupabc + ''.join(nx.before)
|
762 |
+
if chord:
|
763 |
+
s += '['
|
764 |
+
for nt in nx.ns:
|
765 |
+
s += nt
|
766 |
+
if chord:
|
767 |
+
s += ']' + tie
|
768 |
+
if s.endswith('-'):
|
769 |
+
s, tie = s[:-1], '-' # split off tie
|
770 |
+
s += durstr + tie # and put it back again
|
771 |
+
s += nx.after
|
772 |
+
nospace = nx.beam
|
773 |
+
else:
|
774 |
+
if isinstance(nx.str, listtype):
|
775 |
+
nx.str = nx.str[0]
|
776 |
+
s = nx.str
|
777 |
+
nospace = 1
|
778 |
+
if nospace:
|
779 |
+
vs.append(s)
|
780 |
+
else:
|
781 |
+
vs.append(' ' + s)
|
782 |
+
vs = ''.join(vs) # ad hoc: remove multiple pedal directions
|
783 |
+
while vs.find('!ped!!ped!') >= 0:
|
784 |
+
vs = vs.replace('!ped!!ped!', '!ped!')
|
785 |
+
while vs.find('!ped-up!!ped-up!') >= 0:
|
786 |
+
vs = vs.replace('!ped-up!!ped-up!', '!ped-up!')
|
787 |
+
while vs.find('!8va(!!8va)!') >= 0:
|
788 |
+
vs = vs.replace('!8va(!!8va)!', '') # remove empty ottava's
|
789 |
+
return vs
|
790 |
+
|
791 |
+
|
792 |
+
def sortMeasure(voice, m):
|
793 |
+
voice.sort(key=lambda o: o.tijd) # sort on time
|
794 |
+
time = 0
|
795 |
+
v = []
|
796 |
+
rs = [] # holds rests in between notes
|
797 |
+
for i, nx in enumerate(voice): # establish sequentiality
|
798 |
+
if nx.tijd > time and chkbug(nx.tijd - time, m):
|
799 |
+
# fill hole with invisble rest
|
800 |
+
v.append(Note(nx.tijd - time, 'x'))
|
801 |
+
rs.append(len(v) - 1)
|
802 |
+
if isinstance(nx, Elem):
|
803 |
+
if nx.tijd < time:
|
804 |
+
nx.tijd = time # shift elems without duration to where they fit
|
805 |
+
v.append(nx)
|
806 |
+
time = nx.tijd
|
807 |
+
continue
|
808 |
+
if nx.tijd < time: # overlapping element
|
809 |
+
if nx.ns[0] == 'z':
|
810 |
+
continue # discard overlapping rest
|
811 |
+
if v[-1].tijd <= nx.tijd: # we can do something
|
812 |
+
if v[-1].ns[0] == 'z': # shorten rest
|
813 |
+
v[-1].dur = nx.tijd - v[-1].tijd
|
814 |
+
if v[-1].dur == 0:
|
815 |
+
del v[-1] # nothing left
|
816 |
+
info('overlap in part %d, measure %d: rest shortened' %
|
817 |
+
(m.ixp+1, m.ixm+1))
|
818 |
+
else: # make a chord of overlap
|
819 |
+
v[-1].ns += nx.ns
|
820 |
+
info('overlap in part %d, measure %d: added chord' %
|
821 |
+
(m.ixp+1, m.ixm+1))
|
822 |
+
nx.dur = (nx.tijd + nx.dur) - time # the remains
|
823 |
+
if nx.dur <= 0:
|
824 |
+
continue # nothing left
|
825 |
+
nx.tijd = time # append remains
|
826 |
+
else: # give up
|
827 |
+
info('overlapping notes in one voice! part %d, measure %d, note %s discarded' % (
|
828 |
+
m.ixp+1, m.ixm+1, isinstance(nx, Note) and nx.ns or nx.str))
|
829 |
+
continue
|
830 |
+
v.append(nx)
|
831 |
+
if isinstance(nx, Note):
|
832 |
+
if nx.ns[0] in 'zx':
|
833 |
+
rs.append(len(v) - 1) # remember rests between notes
|
834 |
+
elif len(rs):
|
835 |
+
if nx.beam and not nx.grace: # copy beam into rests
|
836 |
+
for j in rs:
|
837 |
+
v[j].beam = nx.beam
|
838 |
+
rs = [] # clear rests on each note
|
839 |
+
time = nx.tijd + nx.dur
|
840 |
+
# when a measure contains no elements and no forwards -> no incTime -> s.maxtime = 0 -> right barline
|
841 |
+
# is inserted at time == 0 (in addbar) and is only element in the voice when sortMeasure is called
|
842 |
+
if time == 0:
|
843 |
+
info('empty measure in part %d, measure %d, it should contain at least a rest to advance the time!' % (
|
844 |
+
m.ixp+1, m.ixm+1))
|
845 |
+
return v
|
846 |
+
|
847 |
+
|
848 |
+
def getPartlist(ps): # correct part-list (from buggy xml-software)
|
849 |
+
xs = [] # the corrected part-list
|
850 |
+
e = [] # stack of opened part-groups
|
851 |
+
for x in list(ps): # insert missing stops, delete double starts
|
852 |
+
if x.tag == 'part-group':
|
853 |
+
num, type = x.get('number'), x.get('type')
|
854 |
+
if type == 'start':
|
855 |
+
if num in e: # missing stop: insert one
|
856 |
+
xs.append(E.Element('part-group', number=num, type='stop'))
|
857 |
+
xs.append(x)
|
858 |
+
else: # normal start
|
859 |
+
xs.append(x)
|
860 |
+
e.append(num)
|
861 |
+
else:
|
862 |
+
if num in e: # normal stop
|
863 |
+
e.remove(num)
|
864 |
+
xs.append(x)
|
865 |
+
else:
|
866 |
+
pass # double stop: skip it
|
867 |
+
else:
|
868 |
+
xs.append(x)
|
869 |
+
for num in reversed(e): # fill missing stops at the end
|
870 |
+
xs.append(E.Element('part-group', number=num, type='stop'))
|
871 |
+
return xs
|
872 |
+
|
873 |
+
|
874 |
+
def parseParts(xs, d, e): # -> [elems on current level], rest of xs
|
875 |
+
if not xs:
|
876 |
+
return [], []
|
877 |
+
x = xs.pop(0)
|
878 |
+
if x.tag == 'part-group':
|
879 |
+
num, type = x.get('number'), x.get('type')
|
880 |
+
if type == 'start': # go one level deeper
|
881 |
+
s = [x.findtext(n, '') for n in ['group-symbol',
|
882 |
+
'group-barline', 'group-name', 'group-abbreviation']]
|
883 |
+
d[num] = s # remember groupdata by group number
|
884 |
+
e.append(num) # make stack of open group numbers
|
885 |
+
# parse one level deeper to next stop
|
886 |
+
elemsnext, rest1 = parseParts(xs, d, e)
|
887 |
+
# parse the rest on this level
|
888 |
+
elems, rest2 = parseParts(rest1, d, e)
|
889 |
+
return [elemsnext] + elems, rest2
|
890 |
+
else: # stop: close level and return group-data
|
891 |
+
nums = e.pop() # last open group number in stack order
|
892 |
+
if xs and xs[0].get('type') == 'stop': # two consequetive stops
|
893 |
+
# in the wrong order (tempory solution)
|
894 |
+
if num != nums:
|
895 |
+
# exchange values (only works for two stops!!!)
|
896 |
+
d[nums], d[num] = d[num], d[nums]
|
897 |
+
# retrieve an return groupdata as last element of the group
|
898 |
+
sym = d[num]
|
899 |
+
return [sym], xs
|
900 |
+
else:
|
901 |
+
# parse remaining elements on current level
|
902 |
+
elems, rest = parseParts(xs, d, e)
|
903 |
+
name = x.findtext('part-name', ''), x.findtext('part-abbreviation', '')
|
904 |
+
return [name] + elems, rest
|
905 |
+
|
906 |
+
|
907 |
+
def bracePart(part): # put a brace on multistaff part and group voices
|
908 |
+
if not part:
|
909 |
+
return [] # empty part in the score
|
910 |
+
brace = []
|
911 |
+
for ivs in part:
|
912 |
+
if len(ivs) == 1: # stave with one voice
|
913 |
+
brace.append('%s' % ivs[0])
|
914 |
+
else: # stave with multiple voices
|
915 |
+
brace += ['('] + ['%s' % iv for iv in ivs] + [')']
|
916 |
+
brace.append('|')
|
917 |
+
del brace[-1] # no barline at the end
|
918 |
+
if len(part) > 1:
|
919 |
+
brace = ['{'] + brace + ['}']
|
920 |
+
return brace
|
921 |
+
|
922 |
+
|
923 |
+
# collect partnames (accVce) and %%score map (accStf)
|
924 |
+
def prgroupelem(x, gnm, bar, pmap, accVce, accStf):
|
925 |
+
if type(x) == tupletype: # partname-tuple = (part-name, part-abbrev)
|
926 |
+
y = pmap.pop(0)
|
927 |
+
if gnm[0]:
|
928 |
+
# put group-name before part-name
|
929 |
+
x = [n1 + ':' + n2 for n1, n2 in zip(gnm, x)]
|
930 |
+
accVce.append(x)
|
931 |
+
accStf.extend(bracePart(y))
|
932 |
+
# misuse of group just to add extra name to stave
|
933 |
+
elif len(x) == 2 and type(x[0]) == tupletype:
|
934 |
+
y = pmap.pop(0)
|
935 |
+
# x[0] = partname-tuple, x[1][2:] = groupname-tuple
|
936 |
+
nms = [n1 + ':' + n2 for n1, n2 in zip(x[0], x[1][2:])]
|
937 |
+
accVce.append(nms)
|
938 |
+
accStf.extend(bracePart(y))
|
939 |
+
else:
|
940 |
+
prgrouplist(x, bar, pmap, accVce, accStf)
|
941 |
+
|
942 |
+
|
943 |
+
# collect partnames, scoremap for a part-group
|
944 |
+
def prgrouplist(x, pbar, pmap, accVce, accStf):
|
945 |
+
# bracket symbol, continue barline, group-name-tuple
|
946 |
+
sym, bar, gnm, gabbr = x[-1]
|
947 |
+
bar = bar == 'yes' or pbar # pbar -> the parent has bar
|
948 |
+
accStf.append(sym == 'brace' and '{' or '[')
|
949 |
+
for z in x[:-1]:
|
950 |
+
prgroupelem(z, (gnm, gabbr), bar, pmap, accVce, accStf)
|
951 |
+
if bar:
|
952 |
+
accStf.append('|')
|
953 |
+
if bar:
|
954 |
+
del accStf[-1] # remove last one before close
|
955 |
+
accStf.append(sym == 'brace' and '}' or ']')
|
956 |
+
|
957 |
+
|
958 |
+
def compUnitLength(iv, maten, divs): # compute optimal unit length
|
959 |
+
uLmin, minLen = 0, max_int
|
960 |
+
for uL in [4, 8, 16]: # try 1/4, 1/8 and 1/16
|
961 |
+
vLen = 0 # total length of abc duration strings in this voice
|
962 |
+
for im, m in enumerate(maten): # all measures
|
963 |
+
for e in m[iv]: # all notes in voice iv
|
964 |
+
if isinstance(e, Elem) or e.dur == 0:
|
965 |
+
continue # no real durations
|
966 |
+
# add len of duration string
|
967 |
+
vLen += len(abcdur(e, divs[im], uL))
|
968 |
+
if vLen < minLen:
|
969 |
+
uLmin, minLen = uL, vLen # remember the smallest
|
970 |
+
return uLmin
|
971 |
+
|
972 |
+
|
973 |
+
def doSyllable(syl):
|
974 |
+
txt = ''
|
975 |
+
for e in syl:
|
976 |
+
if e.tag == 'elision':
|
977 |
+
txt += '~'
|
978 |
+
elif e.tag == 'text': # escape - and space characters
|
979 |
+
txt += (e.text or '').replace('_',
|
980 |
+
'\_').replace('-', r'\-').replace(' ', '~')
|
981 |
+
if not txt:
|
982 |
+
return txt
|
983 |
+
if syl.findtext('syllabic') in ['begin', 'middle']:
|
984 |
+
txt += '-'
|
985 |
+
if syl.find('extend') is not None:
|
986 |
+
txt += '_'
|
987 |
+
return txt
|
988 |
+
|
989 |
+
|
990 |
+
def checkMelismas(lyrics, maten, im, iv):
|
991 |
+
if im == 0:
|
992 |
+
return
|
993 |
+
maat = maten[im][iv] # notes of the current measure
|
994 |
+
curlyr = lyrics[im][iv] # lyrics dict of current measure
|
995 |
+
prvlyr = lyrics[im-1][iv] # lyrics dict of previous measure
|
996 |
+
for n, (lyrstr, melis) in prvlyr.items(): # all lyric numbers in the previous measure
|
997 |
+
if n not in curlyr and melis: # melisma required, but no lyrics present -> make one!
|
998 |
+
ms = getMelisma(maat) # get a melisma for the current measure
|
999 |
+
if ms:
|
1000 |
+
# set melisma as the n-th lyrics of the current measure
|
1001 |
+
curlyr[n] = (ms, 0)
|
1002 |
+
|
1003 |
+
|
1004 |
+
def getMelisma(maat): # get melisma from notes in maat
|
1005 |
+
ms = []
|
1006 |
+
for note in maat: # every note should get an underscore
|
1007 |
+
if not isinstance(note, Note):
|
1008 |
+
continue # skip Elem's
|
1009 |
+
if note.grace:
|
1010 |
+
continue # skip grace notes
|
1011 |
+
if note.ns[0] in 'zx':
|
1012 |
+
break # stop on first rest
|
1013 |
+
ms.append('_')
|
1014 |
+
return ' '.join(ms)
|
1015 |
+
|
1016 |
+
|
1017 |
+
def perc2map(abcIn):
|
1018 |
+
fillmap = {'diamond': 1, 'triangle': 1, 'square': 1, 'normal': 1}
|
1019 |
+
abc = map(lambda x: x.strip(), percSvg.splitlines())
|
1020 |
+
id = 'default'
|
1021 |
+
maps = {'default': []}
|
1022 |
+
dmaps = {'default': []}
|
1023 |
+
r1 = re.compile(r'V:\s*(\S+)')
|
1024 |
+
ls = abcIn.splitlines()
|
1025 |
+
for x in ls:
|
1026 |
+
if 'I:percmap' in x:
|
1027 |
+
noot, step, midi, kop = map(lambda x: x.strip(), x.split()[1:])
|
1028 |
+
if kop in fillmap:
|
1029 |
+
kop = kop + '+' + ',' + kop
|
1030 |
+
x = '%%%%map perc%s %s print=%s midi=%s heads=%s' % (
|
1031 |
+
id, noot, step, midi, kop)
|
1032 |
+
maps[id].append(x)
|
1033 |
+
if '%%MIDI' in x:
|
1034 |
+
dmaps[id].append(x)
|
1035 |
+
if 'V:' in x:
|
1036 |
+
r = r1.match(x)
|
1037 |
+
if r:
|
1038 |
+
id = r.group(1)
|
1039 |
+
if id not in maps:
|
1040 |
+
maps[id] = []
|
1041 |
+
dmaps[id] = []
|
1042 |
+
ids = sorted(maps.keys())
|
1043 |
+
for id in ids:
|
1044 |
+
abc += maps[id]
|
1045 |
+
id = 'default'
|
1046 |
+
for x in ls:
|
1047 |
+
if 'I:percmap' in x:
|
1048 |
+
continue
|
1049 |
+
if '%%MIDI' in x:
|
1050 |
+
continue
|
1051 |
+
if 'V:' in x or 'K:' in x:
|
1052 |
+
r = r1.match(x)
|
1053 |
+
if r:
|
1054 |
+
id = r.group(1)
|
1055 |
+
abc.append(x)
|
1056 |
+
if id in dmaps and len(dmaps[id]) > 0:
|
1057 |
+
abc.extend(dmaps[id])
|
1058 |
+
del dmaps[id]
|
1059 |
+
if 'perc' in x and 'map=' not in x:
|
1060 |
+
x += ' map=perc'
|
1061 |
+
if 'map=perc' in x and len(maps[id]) > 0:
|
1062 |
+
abc.append('%%voicemap perc' + id)
|
1063 |
+
if 'map=off' in x:
|
1064 |
+
abc.append('%%voicemap')
|
1065 |
+
else:
|
1066 |
+
abc.append(x)
|
1067 |
+
return '\n'.join(abc) + '\n'
|
1068 |
+
|
1069 |
+
|
1070 |
+
def addoct(ptc, o): # xml staff step, xml octave number
|
1071 |
+
p = ptc
|
1072 |
+
if o > 4:
|
1073 |
+
p = ptc.lower()
|
1074 |
+
if o > 5:
|
1075 |
+
p = p + (o-5) * "'"
|
1076 |
+
if o < 4:
|
1077 |
+
p = p + (4-o) * ","
|
1078 |
+
return p # abc pitch == abc note without accidental
|
1079 |
+
|
1080 |
+
|
1081 |
+
def chkbug(dt, m):
|
1082 |
+
if dt > m.divs / 16:
|
1083 |
+
return 1 # duration should be > 1/64 note
|
1084 |
+
info('MuseScore bug: incorrect duration, smaller then 1/64! in measure %d, part %d' % (m.ixm, m.ixp))
|
1085 |
+
return 0
|
1086 |
+
|
1087 |
+
# ----------------
|
1088 |
+
# parser
|
1089 |
+
# ----------------
|
1090 |
+
|
1091 |
+
|
1092 |
+
class Parser:
|
1093 |
+
note_alts = [ # 3 alternative notations of the same note for tablature mapping
|
1094 |
+
[x.strip()
|
1095 |
+
for x in '=C, ^C, =D, ^D, =E, =F, ^F, =G, ^G, =A, ^A, =B'.split(',')],
|
1096 |
+
[x.strip()
|
1097 |
+
for x in '^B, _D,^^C, _E, _F, ^E, _G,^^F, _A,^^G, _B, _C'.split(',')],
|
1098 |
+
[x.strip() for x in '__D,^^B,__E,__F,^^D,__G,^^E,__A,_/A,__B,__C,^^A'.split(',')]]
|
1099 |
+
step_map = {'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11}
|
1100 |
+
|
1101 |
+
def __init__(s, options):
|
1102 |
+
# unfold repeats, number of chars per line, credit filter level, volta option
|
1103 |
+
s.slurBuf = {} # dict of open slurs keyed by slur number
|
1104 |
+
# {direction-type + number -> (type, voice | time)} dict for proper closing
|
1105 |
+
s.dirStk = {}
|
1106 |
+
s.ingrace = 0 # marks a sequence of grace notes
|
1107 |
+
s.msc = Music(options) # global music data abstraction
|
1108 |
+
s.unfold = options.u # turn unfolding repeats on
|
1109 |
+
s.ctf = options.c # credit text filter level
|
1110 |
+
s.gStfMap = [] # [[abc voice numbers] for all parts]
|
1111 |
+
s.midiMap = [] # midi-settings for each abc voice, in order
|
1112 |
+
s.drumInst = {} # inst_id -> midi pitch for channel 10 notes
|
1113 |
+
s.drumNotes = {} # (xml voice, abc note) -> (midi note, note head)
|
1114 |
+
s.instMid = [] # [{inst id -> midi-settings} for all parts]
|
1115 |
+
# default midi settings for channel, program, volume, panning
|
1116 |
+
s.midDflt = [-1, -1, -1, -91]
|
1117 |
+
# xml-notenames (without octave) with accidentals from the key
|
1118 |
+
s.msralts = {}
|
1119 |
+
# abc-notenames (with voice number) with passing accidentals
|
1120 |
+
s.curalts = {}
|
1121 |
+
s.stfMap = {} # xml staff number -> [xml voice number]
|
1122 |
+
s.vce2stf = {} # xml voice number -> allocated staff number
|
1123 |
+
s.clefMap = {} # xml staff number -> abc clef (for header only)
|
1124 |
+
s.curClef = {} # xml staff number -> current abc clef
|
1125 |
+
s.stemDir = {} # xml voice number -> current stem direction
|
1126 |
+
s.clefOct = {} # xml staff number -> current clef-octave-change
|
1127 |
+
s.curStf = {} # xml voice number -> current xml staff number
|
1128 |
+
s.nolbrk = options.x # generate no linebreaks ($)
|
1129 |
+
s.jscript = options.j # compatibility with javascript version
|
1130 |
+
s.ornaments = sorted(note_ornamentation_map.items())
|
1131 |
+
s.doPageFmt = len(options.p) == 1 # translate xml page format
|
1132 |
+
s.tstep = options.t # clef determines step on staff (percussion)
|
1133 |
+
s.dirtov1 = options.v1 # all directions to first voice of staff
|
1134 |
+
s.ped = options.ped # render pedal directions
|
1135 |
+
s.wstems = options.stm # translate stem elements
|
1136 |
+
s.pedVce = None # voice for pedal directions
|
1137 |
+
s.repeat_str = {} # staff number -> [measure number, repeat-text]
|
1138 |
+
s.tabVceMap = {} # abc voice num -> [%%map ...] for tab voices
|
1139 |
+
s.koppen = {} # noteheads needed for %%map
|
1140 |
+
|
1141 |
+
# match slur number n in voice v2, add abc code to before/after
|
1142 |
+
def matchSlur(s, type2, n, v2, note2, grace, stopgrace):
|
1143 |
+
if type2 not in ['start', 'stop']:
|
1144 |
+
return # slur type continue has no abc equivalent
|
1145 |
+
if n == None:
|
1146 |
+
n = '1'
|
1147 |
+
if n in s.slurBuf:
|
1148 |
+
type1, v1, note1, grace1 = s.slurBuf[n]
|
1149 |
+
if type2 != type1: # slur complete, now check the voice
|
1150 |
+
if v2 == v1: # begins and ends in the same voice: keep it
|
1151 |
+
# normal slur: start before stop and no grace slur
|
1152 |
+
if type1 == 'start' and (not grace1 or not stopgrace):
|
1153 |
+
# keep left-right order!
|
1154 |
+
note1.before = ['('] + note1.before
|
1155 |
+
note2.after += ')'
|
1156 |
+
# no else: don't bother with reversed stave spanning slurs
|
1157 |
+
del s.slurBuf[n] # slur finished, remove from stack
|
1158 |
+
else: # double definition, keep the last
|
1159 |
+
info('double slur numbers %s-%s in part %d, measure %d, voice %d note %s, first discarded' %
|
1160 |
+
(type2, n, s.msr.ixp+1, s.msr.ixm+1, v2, note2.ns))
|
1161 |
+
s.slurBuf[n] = (type2, v2, note2, grace)
|
1162 |
+
else: # unmatched slur, put in dict
|
1163 |
+
s.slurBuf[n] = (type2, v2, note2, grace)
|
1164 |
+
|
1165 |
+
def doNotations(s, note, nttn, isTab):
|
1166 |
+
for key, val in s.ornaments:
|
1167 |
+
if nttn.find(key) != None:
|
1168 |
+
note.before += [val] # just concat all ornaments
|
1169 |
+
trem = nttn.find('ornaments/tremolo')
|
1170 |
+
if trem != None:
|
1171 |
+
type = trem.get('type')
|
1172 |
+
if type == 'single':
|
1173 |
+
note.before.insert(0, '!%s!' % (int(trem.text) * '/'))
|
1174 |
+
else:
|
1175 |
+
note.fact = None # no time modification in ABC
|
1176 |
+
if s.tstep: # abc2svg version
|
1177 |
+
if type == 'stop':
|
1178 |
+
note.before.insert(0, '!trem%s!' % trem.text)
|
1179 |
+
else: # abc2xml version
|
1180 |
+
if type == 'start':
|
1181 |
+
note.before.insert(0, '!%s-!' % (int(trem.text) * '/'))
|
1182 |
+
fingering = nttn.findall('technical/fingering')
|
1183 |
+
for finger in fingering: # handle multiple finger annotations
|
1184 |
+
if not isTab:
|
1185 |
+
# fingering goes before chord (addChord)
|
1186 |
+
note.before += ['!%s!' % finger.text]
|
1187 |
+
snaar = nttn.find('technical/string')
|
1188 |
+
if snaar != None and isTab:
|
1189 |
+
if s.tstep:
|
1190 |
+
fret = nttn.find('technical/fret')
|
1191 |
+
if fret != None:
|
1192 |
+
note.tab = (snaar.text, fret.text)
|
1193 |
+
else:
|
1194 |
+
# no double string decos (bug in musescore)
|
1195 |
+
deco = '!%s!' % snaar.text
|
1196 |
+
if deco not in note.ntdec:
|
1197 |
+
note.ntdec += deco
|
1198 |
+
wvlns = nttn.findall('ornaments/wavy-line')
|
1199 |
+
for wvln in wvlns:
|
1200 |
+
if wvln.get('type') == 'start':
|
1201 |
+
# keep left-right order!
|
1202 |
+
note.before = ['!trill(!'] + note.before
|
1203 |
+
elif wvln.get('type') == 'stop':
|
1204 |
+
note.after += '!trill)!'
|
1205 |
+
glis = nttn.find('glissando')
|
1206 |
+
if glis == None:
|
1207 |
+
glis = nttn.find('slide') # treat slide as glissando
|
1208 |
+
if glis != None:
|
1209 |
+
lt = '~' if glis.get('line-type') == 'wavy' else '-'
|
1210 |
+
if glis.get('type') == 'start':
|
1211 |
+
# keep left-right order!
|
1212 |
+
note.before = ['!%s(!' % lt] + note.before
|
1213 |
+
elif glis.get('type') == 'stop':
|
1214 |
+
note.before = ['!%s)!' % lt] + note.before
|
1215 |
+
|
1216 |
+
def tabnote(s, alt, ptc, oct, v, ntrec):
|
1217 |
+
p = s.step_map[ptc] + int(alt or '0') # p in -2 .. 13
|
1218 |
+
if p > 11:
|
1219 |
+
oct += 1 # octave correction
|
1220 |
+
if p < 0:
|
1221 |
+
oct -= 1
|
1222 |
+
p = p % 12 # remap p into 0..11
|
1223 |
+
snaar_nw, fret_nw = ntrec.tab # the computed/annotated allocation of nt
|
1224 |
+
for i in range(4): # support same note on 4 strings
|
1225 |
+
# get alternative representation of same note
|
1226 |
+
na = s.note_alts[i % 3][p]
|
1227 |
+
o = oct
|
1228 |
+
if na in ['^B', '^^B']:
|
1229 |
+
o -= 1 # because in adjacent octave
|
1230 |
+
if na in ['_C', '__C']:
|
1231 |
+
o += 1
|
1232 |
+
if '/' in na or i == 3:
|
1233 |
+
o = 9 # emergency notation for 4th string case
|
1234 |
+
nt = addoct(na, o)
|
1235 |
+
# the current allocation of nt
|
1236 |
+
snaar, fret = s.tabmap.get((v, nt), ('', ''))
|
1237 |
+
if not snaar:
|
1238 |
+
break # note not yet allocated
|
1239 |
+
if snaar_nw == snaar:
|
1240 |
+
return nt # use present allocation
|
1241 |
+
if i == 3: # new allocaion needed but none is free
|
1242 |
+
fmt = 'rejected: voice %d note %3s string %s fret %2s remains: string %s fret %s'
|
1243 |
+
info(fmt % (v, nt, snaar_nw, fret_nw, snaar, fret), 1)
|
1244 |
+
ntrec.tab = (snaar, fret)
|
1245 |
+
# for tablature map (voice, note) -> (string, fret)
|
1246 |
+
s.tabmap[v, nt] = ntrec.tab
|
1247 |
+
# ABC code always in key C (with midi pitch alterations)
|
1248 |
+
return nt
|
1249 |
+
|
1250 |
+
def ntAbc(s, ptc, oct, note, v, ntrec, isTab): # pitch, octave -> abc notation
|
1251 |
+
acc2alt = {'double-flat': -2, 'flat-flat': -2, 'flat': -1,
|
1252 |
+
'natural': 0, 'sharp': 1, 'sharp-sharp': 2, 'double-sharp': 2}
|
1253 |
+
oct += s.clefOct.get(s.curStf[v], 0) # minus clef-octave-change value
|
1254 |
+
acc = note.findtext('accidental') # should be the notated accidental
|
1255 |
+
alt = note.findtext('pitch/alter') # pitch alteration (midi)
|
1256 |
+
if ntrec.tab:
|
1257 |
+
# implies s.tstep is true (options.t was given)
|
1258 |
+
return s.tabnote(alt, ptc, oct, v, ntrec)
|
1259 |
+
elif isTab and s.tstep:
|
1260 |
+
nt = ['__', '_', '', '^', '^^'][int(
|
1261 |
+
alt or '0') + 2] + addoct(ptc, oct)
|
1262 |
+
info('no string notation found for note %s in voice %d' % (nt, v), 1)
|
1263 |
+
p = addoct(ptc, oct)
|
1264 |
+
if alt == None and s.msralts.get(ptc, 0):
|
1265 |
+
alt = 0 # no alt but key implies alt -> natural!!
|
1266 |
+
if alt == None and (p, v) in s.curalts:
|
1267 |
+
alt = 0 # no alt but previous note had one -> natural!!
|
1268 |
+
if acc == None and alt == None:
|
1269 |
+
return p # no acc, no alt
|
1270 |
+
elif acc != None:
|
1271 |
+
alt = acc2alt[acc] # acc takes precedence over the pitch here!
|
1272 |
+
else: # now see if we really must add an accidental
|
1273 |
+
alt = int(float(alt))
|
1274 |
+
if (p, v) in s.curalts: # the note in this voice has been altered before
|
1275 |
+
if alt == s.curalts[(p, v)]:
|
1276 |
+
return p # alteration still the same
|
1277 |
+
elif alt == s.msralts.get(ptc, 0):
|
1278 |
+
return p # alteration implied by the key
|
1279 |
+
# in xml we have separate notated ties and playback ties
|
1280 |
+
tieElms = note.findall('tie') + note.findall('notations/tied')
|
1281 |
+
if 'stop' in [e.get('type') for e in tieElms]:
|
1282 |
+
return p # don't alter tied notes
|
1283 |
+
info('accidental %d added in part %d, measure %d, voice %d note %s' % (
|
1284 |
+
alt, s.msr.ixp+1, s.msr.ixm+1, v+1, p))
|
1285 |
+
s.curalts[(p, v)] = alt
|
1286 |
+
# and finally ... prepend the accidental
|
1287 |
+
p = ['__', '_', '=', '^', '^^'][alt+2] + p
|
1288 |
+
return p
|
1289 |
+
|
1290 |
+
def doNote(s, n): # parse a musicXML note tag
|
1291 |
+
note = Note()
|
1292 |
+
v = int(n.findtext('voice', '1'))
|
1293 |
+
if s.isSib:
|
1294 |
+
v += 100 * int(n.findtext('staff', '1')) # repair bug in Sibelius
|
1295 |
+
chord = n.find('chord') != None
|
1296 |
+
p = n.findtext('pitch/step') or n.findtext('unpitched/display-step')
|
1297 |
+
o = n.findtext(
|
1298 |
+
'pitch/octave') or n.findtext('unpitched/display-octave')
|
1299 |
+
r = n.find('rest')
|
1300 |
+
numer = n.findtext('time-modification/actual-notes')
|
1301 |
+
if numer:
|
1302 |
+
denom = n.findtext('time-modification/normal-notes')
|
1303 |
+
note.fact = (int(numer), int(denom))
|
1304 |
+
note.tup = [x.get('type') for x in n.findall('notations/tuplet')]
|
1305 |
+
dur = n.findtext('duration')
|
1306 |
+
grc = n.find('grace')
|
1307 |
+
note.grace = grc != None
|
1308 |
+
# strings with ABC stuff that goes before or after a note/chord
|
1309 |
+
note.before, note.after = [], ''
|
1310 |
+
if note.grace and not s.ingrace: # open a grace sequence
|
1311 |
+
s.ingrace = 1
|
1312 |
+
note.before = ['{']
|
1313 |
+
if grc.get('slash') == 'yes':
|
1314 |
+
note.before += ['/'] # acciaccatura
|
1315 |
+
stopgrace = not note.grace and s.ingrace
|
1316 |
+
if stopgrace: # close the grace sequence
|
1317 |
+
s.ingrace = 0
|
1318 |
+
s.msc.lastnote.after += '}' # close grace on lastenote.after
|
1319 |
+
if dur == None or note.grace:
|
1320 |
+
dur = 0
|
1321 |
+
if r == None and n.get('print-object') == 'no':
|
1322 |
+
if chord:
|
1323 |
+
return
|
1324 |
+
# turn invisible notes (that advance the time) into invisible rests
|
1325 |
+
r = 1
|
1326 |
+
note.dur = int(dur)
|
1327 |
+
if r == None and (not p or not o): # not a rest and no pitch
|
1328 |
+
s.msc.cnt.inc('nopt', v) # count unpitched notes
|
1329 |
+
o, p = 5, 'E' # make it an E5 ??
|
1330 |
+
isTab = s.curClef and s.curClef.get(s.curStf[v], '').startswith('tab')
|
1331 |
+
nttn = n.find('notations') # add ornaments
|
1332 |
+
if nttn != None:
|
1333 |
+
s.doNotations(note, nttn, isTab)
|
1334 |
+
e = n.find('stem') if r == None else None # no !stemless! before rest
|
1335 |
+
if e != None and e.text == 'none' and (not isTab or v in s.hasStems or s.tstep):
|
1336 |
+
note.before += ['s']
|
1337 |
+
abcOut.stemless = 1
|
1338 |
+
e = n.find('accidental')
|
1339 |
+
if e != None and e.get('parentheses') == 'yes':
|
1340 |
+
note.ntdec += '!courtesy!'
|
1341 |
+
if r != None:
|
1342 |
+
noot = 'x' if n.get('print-object') == 'no' or isTab else 'z'
|
1343 |
+
else:
|
1344 |
+
noot = s.ntAbc(p, int(o), n, v, note, isTab)
|
1345 |
+
if n.find('unpitched') != None:
|
1346 |
+
clef = s.curClef[s.curStf[v]] # the current clef for this voice
|
1347 |
+
# (clef independent) step value of note on the staff
|
1348 |
+
step = staffStep(p, int(o), clef, s.tstep)
|
1349 |
+
instr = n.find('instrument')
|
1350 |
+
instId = instr.get('id') if instr != None else 'dummyId'
|
1351 |
+
midi = s.drumInst.get(instId, abcMid(noot))
|
1352 |
+
# replace spaces in xml notehead names for percmap
|
1353 |
+
nh = n.findtext('notehead', '').replace(' ', '-')
|
1354 |
+
if nh == 'x':
|
1355 |
+
noot = '^' + noot.replace('^', '').replace('_', '')
|
1356 |
+
if nh in ['circle-x', 'diamond', 'triangle']:
|
1357 |
+
noot = '_' + noot.replace('^', '').replace('_', '')
|
1358 |
+
if nh and n.find('notehead').get('filled', '') == 'yes':
|
1359 |
+
nh += '+'
|
1360 |
+
if nh and n.find('notehead').get('filled', '') == 'no':
|
1361 |
+
nh += '-'
|
1362 |
+
# keep data for percussion map
|
1363 |
+
s.drumNotes[(v, noot)] = (step, midi, nh)
|
1364 |
+
# in xml we have separate notated ties and playback ties
|
1365 |
+
tieElms = n.findall('tie') + n.findall('notations/tied')
|
1366 |
+
# n can have stop and start tie
|
1367 |
+
if 'start' in [e.get('type') for e in tieElms]:
|
1368 |
+
noot = noot + '-'
|
1369 |
+
note.beam = sum([1 for b in n.findall('beam') if b.text in [
|
1370 |
+
'continue', 'end']]) + int(note.grace)
|
1371 |
+
lyrlast = 0
|
1372 |
+
rsib = re.compile(r'^.*verse')
|
1373 |
+
for e in n.findall('lyric'):
|
1374 |
+
# also do Sibelius numbers
|
1375 |
+
lyrnum = int(rsib.sub('', e.get('number', '1')))
|
1376 |
+
if lyrnum == 0:
|
1377 |
+
lyrnum = lyrlast + 1 # and correct Sibelius bugs
|
1378 |
+
else:
|
1379 |
+
lyrlast = lyrnum
|
1380 |
+
note.lyrs[lyrnum] = doSyllable(e)
|
1381 |
+
stemdir = n.findtext('stem')
|
1382 |
+
if s.wstems and (stemdir == 'up' or stemdir == 'down'):
|
1383 |
+
if stemdir != s.stemDir.get(v, ''):
|
1384 |
+
s.stemDir[v] = stemdir
|
1385 |
+
s.msc.appendElem(v, '[I:stemdir %s]' % stemdir)
|
1386 |
+
if chord:
|
1387 |
+
s.msc.addChord(note, noot)
|
1388 |
+
else:
|
1389 |
+
xmlstaff = int(n.findtext('staff', '1'))
|
1390 |
+
if s.curStf[v] != xmlstaff: # the note should go to another staff
|
1391 |
+
dstaff = xmlstaff - s.curStf[v] # relative new staff number
|
1392 |
+
# remember the new staff for this voice
|
1393 |
+
s.curStf[v] = xmlstaff
|
1394 |
+
# insert a move before the note
|
1395 |
+
s.msc.appendElem(v, '[I:staff %+d]' % dstaff)
|
1396 |
+
s.msc.appendNote(v, note, noot)
|
1397 |
+
# s.msc.lastnote points to the last real note/chord inserted above
|
1398 |
+
for slur in n.findall('notations/slur'):
|
1399 |
+
s.matchSlur(slur.get('type'), slur.get(
|
1400 |
+
'number'), v, s.msc.lastnote, note.grace, stopgrace) # match slur definitions
|
1401 |
+
|
1402 |
+
def doAttr(s, e): # parse a musicXML attribute tag
|
1403 |
+
teken = {'C1': 'alto1', 'C2': 'alto2', 'C3': 'alto', 'C4': 'tenor', 'F4': 'bass',
|
1404 |
+
'F3': 'bass3', 'G2': 'treble', 'TAB': 'tab', 'percussion': 'perc'}
|
1405 |
+
dvstxt = e.findtext('divisions')
|
1406 |
+
if dvstxt:
|
1407 |
+
s.msr.divs = int(dvstxt)
|
1408 |
+
# for transposing instrument
|
1409 |
+
steps = int(e.findtext('transpose/chromatic', '0'))
|
1410 |
+
fifths = e.findtext('key/fifths')
|
1411 |
+
first = s.msc.tijd == 0 and s.msr.ixm == 0 # first attributes in first measure
|
1412 |
+
if fifths:
|
1413 |
+
key, s.msralts = setKey(
|
1414 |
+
int(fifths), e.findtext('key/mode', 'major'))
|
1415 |
+
if first and not steps and abcOut.key == 'none':
|
1416 |
+
# first measure -> header, if not transposing instrument or percussion part!
|
1417 |
+
abcOut.key = key
|
1418 |
+
elif key != abcOut.key or not first:
|
1419 |
+
s.msr.attr += '[K:%s]' % key # otherwise -> voice
|
1420 |
+
beats = e.findtext('time/beats')
|
1421 |
+
if beats:
|
1422 |
+
unit = e.findtext('time/beat-type')
|
1423 |
+
mtr = beats + '/' + unit
|
1424 |
+
if first:
|
1425 |
+
abcOut.mtr = mtr # first measure -> header
|
1426 |
+
else:
|
1427 |
+
s.msr.attr += '[M:%s]' % mtr # otherwise -> voice
|
1428 |
+
s.msr.mtr = int(beats), int(unit)
|
1429 |
+
# duration of measure in xml-divisions
|
1430 |
+
s.msr.mdur = (s.msr.divs * s.msr.mtr[0] * 4) // s.msr.mtr[1]
|
1431 |
+
for ms in e.findall('measure-style'):
|
1432 |
+
n = int(ms.get('number', '1')) # staff number
|
1433 |
+
voices = s.stfMap[n] # all voices of staff n
|
1434 |
+
for mr in ms.findall('measure-repeat'):
|
1435 |
+
ty = mr.get('type')
|
1436 |
+
if ty == 'start': # remember start measure number and text voor each staff
|
1437 |
+
s.repeat_str[n] = [s.msr.ixm, mr.text]
|
1438 |
+
for v in voices: # insert repeat into all voices, value will be overwritten at stop
|
1439 |
+
s.msc.insertElem(v, s.repeat_str[n])
|
1440 |
+
elif ty == 'stop': # calculate repeat measure count for this staff n
|
1441 |
+
start_ix, text_ = s.repeat_str[n]
|
1442 |
+
repeat_count = s.msr.ixm - start_ix
|
1443 |
+
if text_:
|
1444 |
+
mid_str = "%s " % text_
|
1445 |
+
repeat_count /= int(text_)
|
1446 |
+
else:
|
1447 |
+
mid_str = "" # overwrite repeat with final string
|
1448 |
+
s.repeat_str[n][0] = '[I:repeat %s%d]' % (
|
1449 |
+
mid_str, repeat_count)
|
1450 |
+
del s.repeat_str[n] # remove closed repeats
|
1451 |
+
toct = e.findtext('transpose/octave-change', '')
|
1452 |
+
if toct:
|
1453 |
+
steps += 12 * int(toct) # extra transposition of toct octaves
|
1454 |
+
for clef in e.findall('clef'): # a part can have multiple staves
|
1455 |
+
# local staff number for this clef
|
1456 |
+
n = int(clef.get('number', '1'))
|
1457 |
+
sgn = clef.findtext('sign')
|
1458 |
+
line = clef.findtext('line', '') if sgn not in [
|
1459 |
+
'percussion', 'TAB'] else ''
|
1460 |
+
cs = teken.get(sgn + line, '')
|
1461 |
+
oct = clef.findtext('clef-octave-change', '') or '0'
|
1462 |
+
if oct:
|
1463 |
+
cs += {-2: '-15', -1: '-8', 1: '+8',
|
1464 |
+
2: '+15'}.get(int(oct), '')
|
1465 |
+
# xml playback pitch -> abc notation pitch
|
1466 |
+
s.clefOct[n] = -int(oct)
|
1467 |
+
if steps:
|
1468 |
+
cs += ' transpose=' + str(steps)
|
1469 |
+
stfdtl = e.find('staff-details')
|
1470 |
+
if stfdtl and int(stfdtl.get('number', '1')) == n:
|
1471 |
+
lines = stfdtl.findtext('staff-lines')
|
1472 |
+
if lines:
|
1473 |
+
lns = '|||' if lines == '3' and sgn == 'TAB' else lines
|
1474 |
+
cs += ' stafflines=%s' % lns
|
1475 |
+
s.stafflines = int(lines) # remember for tab staves
|
1476 |
+
strings = stfdtl.findall('staff-tuning')
|
1477 |
+
if strings:
|
1478 |
+
tuning = [st.findtext(
|
1479 |
+
'tuning-step') + st.findtext('tuning-octave') for st in strings]
|
1480 |
+
cs += ' strings=%s' % ','.join(tuning)
|
1481 |
+
capo = stfdtl.findtext('capo')
|
1482 |
+
if capo:
|
1483 |
+
cs += ' capo=%s' % capo
|
1484 |
+
# keep track of current clef (for percmap)
|
1485 |
+
s.curClef[n] = cs
|
1486 |
+
if first:
|
1487 |
+
# clef goes to header (where it is mapped to voices)
|
1488 |
+
s.clefMap[n] = cs
|
1489 |
+
else:
|
1490 |
+
# clef change to all voices of staff n
|
1491 |
+
voices = s.stfMap[n]
|
1492 |
+
for v in voices:
|
1493 |
+
if n != s.curStf[v]: # voice is not at its home staff n
|
1494 |
+
dstaff = n - s.curStf[v]
|
1495 |
+
# reset current staff at start of measure to home position
|
1496 |
+
s.curStf[v] = n
|
1497 |
+
s.msc.appendElem(v, '[I:staff %+d]' % dstaff)
|
1498 |
+
s.msc.appendElem(v, '[K:%s]' % cs)
|
1499 |
+
|
1500 |
+
def findVoice(s, i, es):
|
1501 |
+
# directions belong to a staff
|
1502 |
+
stfnum = int(es[i].findtext('staff', 1))
|
1503 |
+
vs = s.stfMap[stfnum] # voices in this staff
|
1504 |
+
# directions to first voice of staff
|
1505 |
+
v1 = vs[0] if vs else 1
|
1506 |
+
if s.dirtov1:
|
1507 |
+
return stfnum, v1, v1 # option --v1
|
1508 |
+
for e in es[i+1:]: # or to the voice of the next note
|
1509 |
+
if e.tag == 'note':
|
1510 |
+
v = int(e.findtext('voice', '1'))
|
1511 |
+
if s.isSib:
|
1512 |
+
# repair bug in Sibelius
|
1513 |
+
v += 100 * int(e.findtext('staff', '1'))
|
1514 |
+
# use our own staff allocation
|
1515 |
+
stf = s.vce2stf[v]
|
1516 |
+
return stf, v, v1 # voice of next note, first voice of staff
|
1517 |
+
if e.tag == 'backup':
|
1518 |
+
break
|
1519 |
+
return stfnum, v1, v1 # no note found, fall back to v1
|
1520 |
+
|
1521 |
+
def doDirection(s, e, i, es): # parse a musicXML direction tag
|
1522 |
+
def addDirection(x, vs, tijd, stfnum):
|
1523 |
+
if not x:
|
1524 |
+
return
|
1525 |
+
vs = s.stfMap[stfnum] if '!8v' in x else [
|
1526 |
+
vs] # ottava's go to all voices of staff
|
1527 |
+
for v in vs:
|
1528 |
+
if tijd != None: # insert at time of encounter
|
1529 |
+
s.msc.appendElemT(v, x.replace(
|
1530 |
+
'(', ')').replace('ped', 'ped-up'), tijd)
|
1531 |
+
else:
|
1532 |
+
s.msc.appendElem(v, x)
|
1533 |
+
|
1534 |
+
def startStop(dtype, vs, stfnum=1):
|
1535 |
+
typmap = {'down': '!8va(!', 'up': '!8vb(!', 'crescendo': '!<(!',
|
1536 |
+
'diminuendo': '!>(!', 'start': '!ped!'}
|
1537 |
+
type = t.get('type', '')
|
1538 |
+
# key to match the closing direction
|
1539 |
+
k = dtype + t.get('number', '1')
|
1540 |
+
if type in typmap: # opening the direction
|
1541 |
+
x = typmap[type]
|
1542 |
+
if k in s.dirStk: # closing direction already encountered
|
1543 |
+
stype, tijd = s.dirStk[k]
|
1544 |
+
del s.dirStk[k]
|
1545 |
+
if stype == 'stop':
|
1546 |
+
addDirection(x, vs, tijd, stfnum)
|
1547 |
+
else:
|
1548 |
+
info('%s direction %s has no stop in part %d, measure %d, voice %d' % (
|
1549 |
+
dtype, stype, s.msr.ixp+1, s.msr.ixm+1, vs+1))
|
1550 |
+
# remember voice and type for closing
|
1551 |
+
s.dirStk[k] = ((type, vs))
|
1552 |
+
else:
|
1553 |
+
# remember voice and type for closing
|
1554 |
+
s.dirStk[k] = ((type, vs))
|
1555 |
+
elif type == 'stop':
|
1556 |
+
if k in s.dirStk: # matching open direction found
|
1557 |
+
type, vs = s.dirStk[k]
|
1558 |
+
del s.dirStk[k] # into the same voice
|
1559 |
+
if type == 'stop':
|
1560 |
+
info('%s direction %s has double stop in part %d, measure %d, voice %d' % (
|
1561 |
+
dtype, type, s.msr.ixp+1, s.msr.ixm+1, vs+1))
|
1562 |
+
x = ''
|
1563 |
+
else:
|
1564 |
+
x = typmap[type].replace(
|
1565 |
+
'(', ')').replace('ped', 'ped-up')
|
1566 |
+
else: # closing direction found before opening
|
1567 |
+
s.dirStk[k] = ('stop', s.msc.tijd)
|
1568 |
+
x = '' # delay code generation until opening found
|
1569 |
+
else:
|
1570 |
+
raise ValueError('wrong direction type')
|
1571 |
+
addDirection(x, vs, None, stfnum)
|
1572 |
+
tempo, wrdstxt = None, ''
|
1573 |
+
plcmnt = e.get('placement')
|
1574 |
+
stf, vs, v1 = s.findVoice(i, es)
|
1575 |
+
jmp = '' # for jump sound elements: dacapo, dalsegno and family
|
1576 |
+
jmps = [('dacapo', 'D.C.'), ('dalsegno', 'D.S.'), ('tocoda',
|
1577 |
+
'dacoda'), ('fine', 'fine'), ('coda', 'O'), ('segno', 'S')]
|
1578 |
+
# there are many possible attributes for sound
|
1579 |
+
t = e.find('sound')
|
1580 |
+
if t != None:
|
1581 |
+
minst = t.find('midi-instrument')
|
1582 |
+
if minst:
|
1583 |
+
prg = t.findtext('midi-instrument/midi-program')
|
1584 |
+
chn = t.findtext('midi-instrument/midi-channel')
|
1585 |
+
vids = [v for v, id in s.vceInst.items() if id ==
|
1586 |
+
minst.get('id')]
|
1587 |
+
if vids:
|
1588 |
+
# direction for the indentified voice, not the staff
|
1589 |
+
vs = vids[0]
|
1590 |
+
parm, inst = ('program', str(int(prg) - 1)
|
1591 |
+
) if prg else ('channel', chn)
|
1592 |
+
if inst and abcOut.volpan > 0:
|
1593 |
+
s.msc.appendElem(vs, '[I:MIDI= %s %s]' % (parm, inst))
|
1594 |
+
tempo = t.get('tempo') # look for tempo attribute
|
1595 |
+
if tempo:
|
1596 |
+
# hope it is a number and insert in voice 1
|
1597 |
+
tempo = '%.0f' % float(tempo)
|
1598 |
+
tempo_units = (1, 4) # always 1/4 for sound elements!
|
1599 |
+
for r, v in jmps:
|
1600 |
+
if t.get(r, ''):
|
1601 |
+
jmp = v
|
1602 |
+
break
|
1603 |
+
dirtypes = e.findall('direction-type')
|
1604 |
+
for dirtyp in dirtypes:
|
1605 |
+
units = {'whole': (1, 1), 'half': (
|
1606 |
+
1, 2), 'quarter': (1, 4), 'eighth': (1, 8)}
|
1607 |
+
metr = dirtyp.find('metronome')
|
1608 |
+
if metr != None:
|
1609 |
+
t = metr.findtext('beat-unit', '')
|
1610 |
+
if t in units:
|
1611 |
+
tempo_units = units[t]
|
1612 |
+
else:
|
1613 |
+
tempo_units = units['quarter']
|
1614 |
+
if metr.find('beat-unit-dot') != None:
|
1615 |
+
tempo_units = simplify(
|
1616 |
+
tempo_units[0] * 3, tempo_units[1] * 2)
|
1617 |
+
# look for a number
|
1618 |
+
tmpro = re.search('[.\d]+', metr.findtext('per-minute'))
|
1619 |
+
if tmpro:
|
1620 |
+
tempo = tmpro.group() # overwrites the value set by the sound element of this direction
|
1621 |
+
t = dirtyp.find('wedge')
|
1622 |
+
if t != None:
|
1623 |
+
startStop('wedge', vs)
|
1624 |
+
allwrds = dirtyp.findall('words') # insert text annotations
|
1625 |
+
if not allwrds:
|
1626 |
+
# treat rehearsal mark as text annotation
|
1627 |
+
allwrds = dirtyp.findall('rehearsal')
|
1628 |
+
for wrds in allwrds:
|
1629 |
+
if jmp: # ignore the words when a jump sound element is present in this direction
|
1630 |
+
s.msc.appendElem(vs, '!%s!' % jmp, 1) # to voice
|
1631 |
+
break
|
1632 |
+
plc = plcmnt == 'below' and '_' or '^'
|
1633 |
+
if float(wrds.get('default-y', '0')) < 0:
|
1634 |
+
plc = '_'
|
1635 |
+
wrdstxt += (wrds.text or '').replace('"',
|
1636 |
+
'\\"').replace('\n', '\\n')
|
1637 |
+
wrdstxt = wrdstxt.strip()
|
1638 |
+
for key, val in dynamics_map.items():
|
1639 |
+
if dirtyp.find('dynamics/' + key) != None:
|
1640 |
+
s.msc.appendElem(vs, val, 1) # to voice
|
1641 |
+
if dirtyp.find('coda') != None:
|
1642 |
+
s.msc.appendElem(vs, 'O', 1)
|
1643 |
+
if dirtyp.find('segno') != None:
|
1644 |
+
s.msc.appendElem(vs, 'S', 1)
|
1645 |
+
t = dirtyp.find('octave-shift')
|
1646 |
+
if t != None:
|
1647 |
+
# assume size == 8 for the time being
|
1648 |
+
startStop('octave-shift', vs, stf)
|
1649 |
+
t = dirtyp.find('pedal')
|
1650 |
+
if t != None and s.ped:
|
1651 |
+
if not s.pedVce:
|
1652 |
+
s.pedVce = vs
|
1653 |
+
startStop('pedal', s.pedVce)
|
1654 |
+
if dirtyp.findtext('other-direction') == 'diatonic fretting':
|
1655 |
+
s.diafret = 1
|
1656 |
+
if tempo:
|
1657 |
+
# hope it is a number and insert in voice 1
|
1658 |
+
tempo = '%.0f' % float(tempo)
|
1659 |
+
if s.msc.tijd == 0 and s.msr.ixm == 0: # first measure -> header
|
1660 |
+
abcOut.tempo = tempo
|
1661 |
+
abcOut.tempo_units = tempo_units
|
1662 |
+
else:
|
1663 |
+
# otherwise -> 1st voice
|
1664 |
+
s.msc.appendElem(v1, '[Q:%d/%d=%s]' %
|
1665 |
+
(tempo_units[0], tempo_units[1], tempo))
|
1666 |
+
if wrdstxt:
|
1667 |
+
s.msc.appendElem(vs, '"%s%s"' % (plc, wrdstxt),
|
1668 |
+
1) # to voice, but after tempo
|
1669 |
+
|
1670 |
+
def doHarmony(s, e, i, es): # parse a musicXMl harmony tag
|
1671 |
+
_, vt, _ = s.findVoice(i, es)
|
1672 |
+
short = {'major': '', 'minor': 'm', 'augmented': '+',
|
1673 |
+
'diminished': 'dim', 'dominant': '7', 'half-diminished': 'm7b5'}
|
1674 |
+
accmap = {'major': 'maj', 'dominant': '', 'minor': 'm',
|
1675 |
+
'diminished': 'dim', 'augmented': '+', 'suspended': 'sus'}
|
1676 |
+
modmap = {'second': '2', 'fourth': '4', 'seventh': '7',
|
1677 |
+
'sixth': '6', 'ninth': '9', '11th': '11', '13th': '13'}
|
1678 |
+
altmap = {'1': '#', '0': '', '-1': 'b'}
|
1679 |
+
root = e.findtext('root/root-step', '')
|
1680 |
+
alt = altmap.get(e.findtext('root/root-alter'), '')
|
1681 |
+
sus = ''
|
1682 |
+
kind = e.findtext('kind', '')
|
1683 |
+
if kind in short:
|
1684 |
+
kind = short[kind]
|
1685 |
+
elif '-' in kind: # xml chord names: <triad name>-<modification>
|
1686 |
+
triad, mod = kind.split('-')
|
1687 |
+
kind = accmap.get(triad, '') + modmap.get(mod, '')
|
1688 |
+
if kind.startswith('sus'):
|
1689 |
+
kind, sus = '', kind # sus-suffix goes to the end
|
1690 |
+
elif kind == 'none':
|
1691 |
+
kind = e.find('kind').get('text', '')
|
1692 |
+
degrees = e.findall('degree')
|
1693 |
+
for d in degrees: # chord alterations
|
1694 |
+
kind += altmap.get(d.findtext('degree-alter'),
|
1695 |
+
'') + d.findtext('degree-value', '')
|
1696 |
+
kind = kind.replace('79', '9').replace(
|
1697 |
+
'713', '13').replace('maj6', '6')
|
1698 |
+
bass = e.findtext('bass/bass-step', '') + \
|
1699 |
+
altmap.get(e.findtext('bass/bass-alter'), '')
|
1700 |
+
s.msc.appendElem(vt, '"%s%s%s%s%s"' %
|
1701 |
+
(root, alt, kind, sus, bass and '/' + bass), 1)
|
1702 |
+
|
1703 |
+
def doBarline(s, e): # 0 = no repeat, 1 = begin repeat, 2 = end repeat
|
1704 |
+
rep = e.find('repeat')
|
1705 |
+
if rep != None:
|
1706 |
+
rep = rep.get('direction')
|
1707 |
+
if s.unfold: # unfold repeat, don't translate barlines
|
1708 |
+
return rep and (rep == 'forward' and 1 or 2) or 0
|
1709 |
+
loc = e.get('location', 'right') # right is the default
|
1710 |
+
if loc == 'right': # only change style for the right side
|
1711 |
+
style = e.findtext('bar-style')
|
1712 |
+
if style == 'light-light':
|
1713 |
+
s.msr.rline = '||'
|
1714 |
+
elif style == 'light-heavy':
|
1715 |
+
s.msr.rline = '|]'
|
1716 |
+
if rep != None: # repeat found
|
1717 |
+
if rep == 'forward':
|
1718 |
+
s.msr.lline = ':'
|
1719 |
+
else:
|
1720 |
+
s.msr.rline = ':|' # override barline style
|
1721 |
+
end = e.find('ending')
|
1722 |
+
if end != None:
|
1723 |
+
if end.get('type') == 'start':
|
1724 |
+
n = end.get('number', '1').replace('.', '').replace(' ', '')
|
1725 |
+
try:
|
1726 |
+
# should be a list of integers
|
1727 |
+
list(map(int, n.split(',')))
|
1728 |
+
except:
|
1729 |
+
n = '"%s"' % n.strip() # illegal musicXML
|
1730 |
+
s.msr.lnum = n # assume a start is always at the beginning of a measure
|
1731 |
+
elif s.msr.rline == '|': # stop and discontinue the same in ABC ?
|
1732 |
+
s.msr.rline = '||' # to stop on a normal barline use || in ABC ?
|
1733 |
+
return 0
|
1734 |
+
|
1735 |
+
def doPrint(s, e): # print element, measure number -> insert a line break
|
1736 |
+
if e.get('new-system') == 'yes' or e.get('new-page') == 'yes':
|
1737 |
+
if not s.nolbrk:
|
1738 |
+
return '$' # a line break
|
1739 |
+
|
1740 |
+
def doPartList(s, e): # translate the start/stop-event-based xml-partlist into proper tree
|
1741 |
+
for sp in e.findall('part-list/score-part'):
|
1742 |
+
midi = {}
|
1743 |
+
for m in sp.findall('midi-instrument'):
|
1744 |
+
x = [m.findtext(p, s.midDflt[i]) for i, p in enumerate(
|
1745 |
+
['midi-channel', 'midi-program', 'volume', 'pan'])]
|
1746 |
+
pan = float(x[3])
|
1747 |
+
if pan >= -90 and pan <= 90: # would be better to map behind-pannings
|
1748 |
+
pan = (float(x[3]) + 90) / 180 * \
|
1749 |
+
127 # xml between -90 and +90
|
1750 |
+
midi[m.get('id')] = [int(x[0]), int(x[1]), float(
|
1751 |
+
x[2]) * 1.27, pan] # volume 100 -> midi 127
|
1752 |
+
up = m.findtext('midi-unpitched')
|
1753 |
+
if up:
|
1754 |
+
# store midi-pitch for channel 10 notes
|
1755 |
+
s.drumInst[m.get('id')] = int(up) - 1
|
1756 |
+
s.instMid.append(midi)
|
1757 |
+
ps = e.find('part-list') # partlist = [groupelem]
|
1758 |
+
# groupelem = partname | grouplist
|
1759 |
+
xs = getPartlist(ps)
|
1760 |
+
# grouplist = [groupelem, ..., groupdata]
|
1761 |
+
partlist, _ = parseParts(xs, {}, [])
|
1762 |
+
# groupdata = [group-symbol, group-barline, group-name, group-abbrev]
|
1763 |
+
return partlist
|
1764 |
+
|
1765 |
+
def mkTitle(s, e):
|
1766 |
+
def filterCredits(y): # y == filter level, higher filters less
|
1767 |
+
cs = []
|
1768 |
+
for x in credits: # skip redundant credit lines
|
1769 |
+
if y < 6 and (x in title or x in mvttl):
|
1770 |
+
continue # sure skip
|
1771 |
+
if y < 5 and (x in composer or x in lyricist):
|
1772 |
+
continue # almost sure skip
|
1773 |
+
if y < 4 and ((title and title in x) or (mvttl and mvttl in x)):
|
1774 |
+
continue # may skip too much
|
1775 |
+
if y < 3 and ([1 for c in composer if c in x] or [1 for c in lyricist if c in x]):
|
1776 |
+
continue # skips too much
|
1777 |
+
if y < 2 and re.match(r'^[\d\W]*$', x):
|
1778 |
+
continue # line only contains numbers and punctuation
|
1779 |
+
cs.append(x)
|
1780 |
+
if y == 0 and (title + mvttl):
|
1781 |
+
cs = '' # default: only credit when no title set
|
1782 |
+
return cs
|
1783 |
+
title = e.findtext('work/work-title', '').strip()
|
1784 |
+
mvttl = e.findtext('movement-title', '').strip()
|
1785 |
+
composer, lyricist, credits = [], [], []
|
1786 |
+
for creator in e.findall('identification/creator'):
|
1787 |
+
if creator.text:
|
1788 |
+
if creator.get('type') == 'composer':
|
1789 |
+
composer += [line.strip()
|
1790 |
+
for line in creator.text.split('\n')]
|
1791 |
+
elif creator.get('type') in ('lyricist', 'transcriber'):
|
1792 |
+
lyricist += [line.strip()
|
1793 |
+
for line in creator.text.split('\n')]
|
1794 |
+
for rights in e.findall('identification/rights'):
|
1795 |
+
if rights.text:
|
1796 |
+
lyricist += [line.strip() for line in rights.text.split('\n')]
|
1797 |
+
for credit in e.findall('credit'):
|
1798 |
+
cs = ''.join(e.text or '' for e in credit.findall('credit-words'))
|
1799 |
+
credits += [re.sub(r'\s*[\r\n]\s*', ' ', cs)]
|
1800 |
+
credits = filterCredits(s.ctf)
|
1801 |
+
if title:
|
1802 |
+
title = 'T:%s\n' % title.replace('\n', '\nT:')
|
1803 |
+
if mvttl:
|
1804 |
+
title += 'T:%s\n' % mvttl.replace('\n', '\nT:')
|
1805 |
+
if credits:
|
1806 |
+
title += '\n'.join(['T:%s' % c for c in credits]) + '\n'
|
1807 |
+
if composer:
|
1808 |
+
title += '\n'.join(['C:%s' % c for c in composer]) + '\n'
|
1809 |
+
if lyricist:
|
1810 |
+
title += '\n'.join(['Z:%s' % c for c in lyricist]) + '\n'
|
1811 |
+
if title:
|
1812 |
+
abcOut.title = title[:-1]
|
1813 |
+
s.isSib = 'Sibelius' in (e.findtext(
|
1814 |
+
'identification/encoding/software') or '')
|
1815 |
+
if s.isSib:
|
1816 |
+
info('Sibelius MusicXMl is unreliable')
|
1817 |
+
|
1818 |
+
def doDefaults(s, e):
|
1819 |
+
if not s.doPageFmt:
|
1820 |
+
return # return if -pf option absent
|
1821 |
+
d = e.find('defaults')
|
1822 |
+
if d == None:
|
1823 |
+
return
|
1824 |
+
mils = d.findtext('scaling/millimeters') # mills == staff height (mm)
|
1825 |
+
tenths = d.findtext('scaling/tenths') # staff height in tenths
|
1826 |
+
if not mils or not tenths:
|
1827 |
+
return
|
1828 |
+
xmlScale = float(mils) / float(tenths) / 10 # tenths -> mm
|
1829 |
+
space = 10 * xmlScale # space between staff lines == 10 tenths
|
1830 |
+
# 0.2117 cm = 6pt = space between staff lines for scale = 1.0 in abcm2ps
|
1831 |
+
abcScale = space / 0.2117
|
1832 |
+
abcOut.pageFmt['scale'] = abcScale
|
1833 |
+
eks = 2 * ['page-layout/'] + 4 * ['page-layout/page-margins/']
|
1834 |
+
eks = [
|
1835 |
+
a+b for a, b in zip(eks, 'page-height,page-width,left-margin,right-margin,top-margin,bottom-margin'.split(','))]
|
1836 |
+
for i in range(6):
|
1837 |
+
v = d.findtext(eks[i])
|
1838 |
+
# pagekeys [0] == scale already done, skip it
|
1839 |
+
k = abcOut.pagekeys[i+1]
|
1840 |
+
if not abcOut.pageFmt[k] and v:
|
1841 |
+
try:
|
1842 |
+
abcOut.pageFmt[k] = float(v) * xmlScale # -> cm
|
1843 |
+
except:
|
1844 |
+
info('illegal value %s for xml element %s', (v, eks[i]))
|
1845 |
+
continue # just skip illegal values
|
1846 |
+
|
1847 |
+
def locStaffMap(s, part, maten): # map voice to staff with majority voting
|
1848 |
+
vmap = {} # {voice -> {staff -> n}} count occurrences of voice in staff
|
1849 |
+
s.vceInst = {} # {voice -> instrument id} for this part
|
1850 |
+
s.msc.vnums = {} # voice id's
|
1851 |
+
# XML voice nums with at least one note with a stem (for tab key)
|
1852 |
+
s.hasStems = {}
|
1853 |
+
s.stfMap, s.clefMap = {}, {} # staff -> [voices], staff -> clef
|
1854 |
+
ns = part.findall('measure/note')
|
1855 |
+
for n in ns: # count staff allocations for all notes
|
1856 |
+
v = int(n.findtext('voice', '1'))
|
1857 |
+
if s.isSib:
|
1858 |
+
# repair bug in Sibelius
|
1859 |
+
v += 100 * int(n.findtext('staff', '1'))
|
1860 |
+
s.msc.vnums[v] = 1 # collect all used voice id's in this part
|
1861 |
+
sn = int(n.findtext('staff', '1'))
|
1862 |
+
s.stfMap[sn] = []
|
1863 |
+
if v not in vmap:
|
1864 |
+
vmap[v] = {sn: 1}
|
1865 |
+
else:
|
1866 |
+
d = vmap[v] # counter for voice v
|
1867 |
+
# ++ number of allocations for staff sn
|
1868 |
+
d[sn] = d.get(sn, 0) + 1
|
1869 |
+
x = n.find('instrument')
|
1870 |
+
if x != None:
|
1871 |
+
s.vceInst[v] = x.get('id')
|
1872 |
+
x, noRest = n.findtext('stem'), n.find('rest') == None
|
1873 |
+
if noRest and (not x or x != 'none'):
|
1874 |
+
s.hasStems[v] = 1 # XML voice v has at least one stem
|
1875 |
+
vks = list(vmap.keys())
|
1876 |
+
if s.jscript or s.isSib:
|
1877 |
+
vks.sort()
|
1878 |
+
for v in vks: # choose staff with most allocations for each voice
|
1879 |
+
xs = [(n, sn) for sn, n in vmap[v].items()]
|
1880 |
+
xs.sort()
|
1881 |
+
stf = xs[-1][1] # the winner: staff with most notes of voice v
|
1882 |
+
s.stfMap[stf].append(v)
|
1883 |
+
s.vce2stf[v] = stf # reverse map
|
1884 |
+
s.curStf[v] = stf # current staff of XML voice v
|
1885 |
+
|
1886 |
+
def addStaffMap(s, vvmap): # vvmap: xml voice number -> global abc voice number
|
1887 |
+
part = [] # default: brace on staffs of one part
|
1888 |
+
# s.stfMap has xml staff and voice numbers
|
1889 |
+
for stf, voices in sorted(s.stfMap.items()):
|
1890 |
+
locmap = [vvmap[iv] for iv in voices if iv in vvmap]
|
1891 |
+
nostem = [(iv not in s.hasStems)
|
1892 |
+
for iv in voices if iv in vvmap] # same order as locmap
|
1893 |
+
if locmap: # abc voice number of staff stf
|
1894 |
+
part.append(locmap)
|
1895 |
+
# {xml staff number -> clef}
|
1896 |
+
clef = s.clefMap.get(stf, 'treble')
|
1897 |
+
for i, iv in enumerate(locmap):
|
1898 |
+
clef_attr = ''
|
1899 |
+
if clef.startswith('tab'):
|
1900 |
+
if nostem[i] and 'nostems' not in clef:
|
1901 |
+
clef_attr = ' nostems'
|
1902 |
+
if s.diafret and 'diafret' not in clef:
|
1903 |
+
clef_attr += ' diafret' # for all voices in the part
|
1904 |
+
# add nostems when all notes of voice had no stem
|
1905 |
+
abcOut.clefs[iv] = clef + clef_attr
|
1906 |
+
s.gStfMap.append(part)
|
1907 |
+
|
1908 |
+
def addMidiMap(s, ip, vvmap): # map abc voices to midi settings
|
1909 |
+
instr = s.instMid[ip] # get the midi settings for this part
|
1910 |
+
if instr.values():
|
1911 |
+
# default settings = first instrument
|
1912 |
+
defInstr = list(instr.values())[0]
|
1913 |
+
else:
|
1914 |
+
defInstr = s.midDflt # no instruments defined
|
1915 |
+
xs = []
|
1916 |
+
for v, vabc in vvmap.items(): # xml voice num, abc voice num
|
1917 |
+
ks = sorted(s.drumNotes.items())
|
1918 |
+
ds = [(nt, step, midi, head) for (vd, nt),
|
1919 |
+
(step, midi, head) in ks if v == vd] # map perc notes
|
1920 |
+
# get the instrument-id for part with multiple instruments
|
1921 |
+
id = s.vceInst.get(v, '')
|
1922 |
+
if id in instr: # id is defined as midi-instrument in part-list
|
1923 |
+
xs.append((vabc, instr[id] + ds)) # get midi settings for id
|
1924 |
+
else:
|
1925 |
+
# only one instrument for this part
|
1926 |
+
xs.append((vabc, defInstr + ds))
|
1927 |
+
xs.sort() # put abc voices in order
|
1928 |
+
s.midiMap.extend([midi for v, midi in xs])
|
1929 |
+
snaarmap = ['E', 'G', 'B', 'd', 'f', 'a', "c'", "e'"]
|
1930 |
+
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(
|
1931 |
+
',')
|
1932 |
+
for k in sorted(s.tabmap.keys()): # add %%map's for all tab voices
|
1933 |
+
v, noot = k
|
1934 |
+
snaar, fret = s.tabmap[k]
|
1935 |
+
if s.diafret:
|
1936 |
+
fret = diamap[int(fret)]
|
1937 |
+
vabc = vvmap[v]
|
1938 |
+
snaar = s.stafflines - int(snaar)
|
1939 |
+
xs = s.tabVceMap.get(vabc, [])
|
1940 |
+
xs.append('%%%%map tab%d %s print=%s heads=kop%s\n' %
|
1941 |
+
(vabc, noot, snaarmap[snaar], fret))
|
1942 |
+
s.tabVceMap[vabc] = xs
|
1943 |
+
s.koppen[fret] = 1 # collect noteheads for SVG defs
|
1944 |
+
|
1945 |
+
def parse(s, xmltxt):
|
1946 |
+
vvmapAll = {} # collect xml->abc voice maps (vvmap) of all parts
|
1947 |
+
e = E.fromstring(xmltxt)
|
1948 |
+
s.mkTitle(e)
|
1949 |
+
s.doDefaults(e)
|
1950 |
+
partlist = s.doPartList(e)
|
1951 |
+
parts = e.findall('part')
|
1952 |
+
for ip, p in enumerate(parts):
|
1953 |
+
maten = p.findall('measure')
|
1954 |
+
s.locStaffMap(p, maten) # {voice -> staff} for this part
|
1955 |
+
# (xml voice, abc note) -> (midi note, note head)
|
1956 |
+
s.drumNotes = {}
|
1957 |
+
s.clefOct = {} # xml staff number -> current clef-octave-change
|
1958 |
+
s.curClef = {} # xml staff number -> current abc clef
|
1959 |
+
s.stemDir = {} # xml voice number -> current stem direction
|
1960 |
+
s.tabmap = {} # (xml voice, abc note) -> (string, fret)
|
1961 |
+
s.diafret = 0 # use diatonic fretting
|
1962 |
+
s.stafflines = 5
|
1963 |
+
s.msc.initVoices(newPart=1) # create all voices
|
1964 |
+
aantalHerhaald = 0 # keep track of number of repititions
|
1965 |
+
herhaalMaat = 0 # target measure of the repitition
|
1966 |
+
divisions = [] # current value of <divisions> for each measure
|
1967 |
+
s.msr = Measure(ip) # various measure data
|
1968 |
+
while s.msr.ixm < len(maten):
|
1969 |
+
maat = maten[s.msr.ixm]
|
1970 |
+
herhaal, lbrk = 0, ''
|
1971 |
+
s.msr.reset()
|
1972 |
+
s.curalts = {} # passing accidentals are reset each measure
|
1973 |
+
es = list(maat)
|
1974 |
+
for i, e in enumerate(es):
|
1975 |
+
if e.tag == 'note':
|
1976 |
+
s.doNote(e)
|
1977 |
+
elif e.tag == 'attributes':
|
1978 |
+
s.doAttr(e)
|
1979 |
+
elif e.tag == 'direction':
|
1980 |
+
s.doDirection(e, i, es)
|
1981 |
+
elif e.tag == 'sound':
|
1982 |
+
# sound element directly in measure!
|
1983 |
+
s.doDirection(maat, i, es)
|
1984 |
+
elif e.tag == 'harmony':
|
1985 |
+
s.doHarmony(e, i, es)
|
1986 |
+
elif e.tag == 'barline':
|
1987 |
+
herhaal = s.doBarline(e)
|
1988 |
+
elif e.tag == 'backup':
|
1989 |
+
dt = int(e.findtext('duration'))
|
1990 |
+
if chkbug(dt, s.msr):
|
1991 |
+
s.msc.incTime(-dt)
|
1992 |
+
elif e.tag == 'forward':
|
1993 |
+
dt = int(e.findtext('duration'))
|
1994 |
+
if chkbug(dt, s.msr):
|
1995 |
+
s.msc.incTime(dt)
|
1996 |
+
elif e.tag == 'print':
|
1997 |
+
lbrk = s.doPrint(e)
|
1998 |
+
s.msc.addBar(lbrk, s.msr)
|
1999 |
+
divisions.append(s.msr.divs)
|
2000 |
+
if herhaal == 1:
|
2001 |
+
herhaalMaat = s.msr.ixm
|
2002 |
+
s.msr.ixm += 1
|
2003 |
+
elif herhaal == 2:
|
2004 |
+
if aantalHerhaald < 1: # jump
|
2005 |
+
s.msr.ixm = herhaalMaat
|
2006 |
+
aantalHerhaald += 1
|
2007 |
+
else:
|
2008 |
+
aantalHerhaald = 0 # reset
|
2009 |
+
s.msr.ixm += 1 # just continue
|
2010 |
+
else:
|
2011 |
+
s.msr.ixm += 1 # on to the next measure
|
2012 |
+
for rv in s.repeat_str.values(): # close hanging measure-repeats without stop
|
2013 |
+
rv[0] = '[I:repeat %s %d]' % (rv[1], 1)
|
2014 |
+
vvmap = s.msc.outVoices(divisions, ip, s.isSib)
|
2015 |
+
s.addStaffMap(vvmap) # update global staff map
|
2016 |
+
s.addMidiMap(ip, vvmap)
|
2017 |
+
vvmapAll.update(vvmap)
|
2018 |
+
if vvmapAll: # skip output if no part has any notes
|
2019 |
+
abcOut.mkHeader(s.gStfMap, partlist, s.midiMap,
|
2020 |
+
s.tabVceMap, s.koppen)
|
2021 |
+
else:
|
2022 |
+
info('nothing written, %s has no notes ...' % abcOut.fnmext)
|
2023 |
+
|
2024 |
+
|
2025 |
+
def vertaal(xmltxt, **options_parm):
|
2026 |
+
class options: # the default option values
|
2027 |
+
u = 0
|
2028 |
+
b = 0
|
2029 |
+
n = 0
|
2030 |
+
c = 0
|
2031 |
+
v = 0
|
2032 |
+
d = 0
|
2033 |
+
m = 0
|
2034 |
+
x = 0
|
2035 |
+
t = 0
|
2036 |
+
stm = 0
|
2037 |
+
mnum = -1
|
2038 |
+
p = 'f'
|
2039 |
+
s = 0
|
2040 |
+
j = 0
|
2041 |
+
v1 = 0
|
2042 |
+
ped = 0
|
2043 |
+
global abcOut, info_list
|
2044 |
+
info_list = []
|
2045 |
+
str = ''
|
2046 |
+
for opt in options_parm: # assign the given options
|
2047 |
+
setattr(options, opt, options_parm[opt])
|
2048 |
+
options.p = options.p.split(',') if options.p else [] # [] | [string]
|
2049 |
+
abcOut = ABCoutput('', '', 0, options)
|
2050 |
+
psr = Parser(options)
|
2051 |
+
try:
|
2052 |
+
psr.parse(xmltxt) # parse xmltxt
|
2053 |
+
str = abcOut.getABC() # ABC output
|
2054 |
+
info('%s written with %d voices' %
|
2055 |
+
(abcOut.fnmext, len(abcOut.clefs)), warn=0)
|
2056 |
+
except:
|
2057 |
+
etype, value, traceback = sys.exc_info() # works in python 2 & 3
|
2058 |
+
info('** %s occurred: %s' % (etype, value), 0)
|
2059 |
+
return (str, ''.join(info_list)) # and the diagnostic messages
|
2060 |
+
|
2061 |
+
|
2062 |
+
# ----------------
|
2063 |
+
# Main Program
|
2064 |
+
# ----------------
|
2065 |
+
if __name__ == '__main__':
|
2066 |
+
from optparse import OptionParser
|
2067 |
+
from glob import glob
|
2068 |
+
from zipfile import ZipFile
|
2069 |
+
ustr = '%prog [-h] [-u] [-m] [-c C] [-d D] [-n CPL] [-b BPL] [-o DIR] [-v V]\n'
|
2070 |
+
ustr += '[-x] [-p PFMT] [-t] [-s] [-i] [--v1] [--noped] [--stems] <file1> [<file2> ...]'
|
2071 |
+
parser = OptionParser(usage=ustr, version=str(VERSION))
|
2072 |
+
parser.add_option("-u", action="store_true", help="unfold simple repeats")
|
2073 |
+
parser.add_option("-m", action="store",
|
2074 |
+
help="0 -> no %%MIDI, 1 -> minimal %%MIDI, 2-> all %%MIDI", default=0)
|
2075 |
+
parser.add_option("-c", action="store", type="int",
|
2076 |
+
help="set credit text filter to C", default=0, metavar='C')
|
2077 |
+
parser.add_option("-d", action="store", type="int",
|
2078 |
+
help="set L:1/D", default=0, metavar='D')
|
2079 |
+
parser.add_option("-n", action="store", type="int",
|
2080 |
+
help="CPL: max number of characters per line (default 100)", default=0, metavar='CPL')
|
2081 |
+
parser.add_option("-b", action="store", type="int",
|
2082 |
+
help="BPL: max number of bars per line", default=0, metavar='BPL')
|
2083 |
+
parser.add_option("-o", action="store",
|
2084 |
+
help="store abc files in DIR", default='', metavar='DIR')
|
2085 |
+
parser.add_option("-v", action="store", type="int",
|
2086 |
+
help="set volta typesetting behaviour to V", default=0, metavar='V')
|
2087 |
+
parser.add_option("-x", action="store_true", help="output no line breaks")
|
2088 |
+
parser.add_option("-p", action="store",
|
2089 |
+
help="pageformat PFMT (cm) = scale, pageheight, pagewidth, leftmargin, rightmargin, topmargin, botmargin", default='', metavar='PFMT')
|
2090 |
+
parser.add_option("-j", action="store_true",
|
2091 |
+
help="switch for compatibility with javascript version")
|
2092 |
+
parser.add_option("-t", action="store_true",
|
2093 |
+
help="translate perc- and tab-staff to ABC code with %%map, %%voicemap")
|
2094 |
+
parser.add_option("-s", action="store_true",
|
2095 |
+
help="shift node heads 3 units left in a tab staff")
|
2096 |
+
parser.add_option("--v1", action="store_true",
|
2097 |
+
help="start-stop directions allways to first voice of staff")
|
2098 |
+
parser.add_option("--noped", action="store_false",
|
2099 |
+
help="skip all pedal directions", dest='ped', default=True)
|
2100 |
+
parser.add_option("--stems", action="store_true",
|
2101 |
+
help="translate stem directions", dest='stm', default=False)
|
2102 |
+
parser.add_option("-i", action="store_true",
|
2103 |
+
help="read xml file from standard input")
|
2104 |
+
options, args = parser.parse_args()
|
2105 |
+
if options.n < 0:
|
2106 |
+
parser.error('only values >= 0')
|
2107 |
+
if options.b < 0:
|
2108 |
+
parser.error('only values >= 0')
|
2109 |
+
if options.d and options.d not in [2**n for n in range(10)]:
|
2110 |
+
parser.error('D should be on of %s' %
|
2111 |
+
','.join([str(2**n) for n in range(10)]))
|
2112 |
+
options.p = options.p and options.p.split(',') or [] # ==> [] | [string]
|
2113 |
+
if len(args) == 0 and not options.i:
|
2114 |
+
parser.error('no input file given')
|
2115 |
+
pad = options.o
|
2116 |
+
if pad:
|
2117 |
+
if not os.path.exists(pad):
|
2118 |
+
os.mkdir(pad)
|
2119 |
+
if not os.path.isdir(pad):
|
2120 |
+
parser.error('%s is not a directory' % pad)
|
2121 |
+
fnmext_list = []
|
2122 |
+
for i in args:
|
2123 |
+
fnmext_list += glob(i)
|
2124 |
+
if options.i:
|
2125 |
+
fnmext_list = ['stdin.xml']
|
2126 |
+
if not fnmext_list:
|
2127 |
+
parser.error('none of the input files exist')
|
2128 |
+
for X, fnmext in enumerate(fnmext_list):
|
2129 |
+
fnm, ext = os.path.splitext(fnmext)
|
2130 |
+
if ext.lower() not in ('.xml', '.mxl', '.musicxml'):
|
2131 |
+
info('skipped input file %s, it should have extension .xml or .mxl' % fnmext)
|
2132 |
+
continue
|
2133 |
+
if os.path.isdir(fnmext):
|
2134 |
+
info('skipped directory %s. Only files are accepted' % fnmext)
|
2135 |
+
continue
|
2136 |
+
if fnmext == 'stdin.xml':
|
2137 |
+
fobj = sys.stdin
|
2138 |
+
elif ext.lower() == '.mxl': # extract .xml file from .mxl file
|
2139 |
+
z = ZipFile(fnmext)
|
2140 |
+
for n in z.namelist(): # assume there is always an xml file in a mxl archive !!
|
2141 |
+
if (n[:4] != 'META') and (n[-4:].lower() == '.xml'):
|
2142 |
+
fobj = z.open(n)
|
2143 |
+
break # assume only one MusicXML file per archive
|
2144 |
+
else:
|
2145 |
+
fobj = open(fnmext, 'rb') # open regular xml file
|
2146 |
+
|
2147 |
+
# create global ABC output object
|
2148 |
+
abcOut = ABCoutput(fnm + '.abc', pad, X, options)
|
2149 |
+
psr = Parser(options) # xml parser
|
2150 |
+
try:
|
2151 |
+
# parse file fobj and write abc to <fnm>.abc
|
2152 |
+
psr.parse(fobj.read())
|
2153 |
+
abcOut.writeall()
|
2154 |
+
except:
|
2155 |
+
etype, value, traceback = sys.exc_info() # works in python 2 & 3
|
2156 |
+
info('** %s occurred: %s in %s' % (etype, value, fnmext), 0)
|