Delete process_data
Browse files- process_data/README.md +0 -307
- process_data/batch_abc2xml.py +0 -58
- process_data/batch_interleaved_abc.py +0 -117
- process_data/batch_midi2mtf.py +0 -80
- process_data/batch_mtf2midi.py +0 -97
- process_data/batch_xml2abc.py +0 -57
- process_data/gpt4_summarize.py +0 -250
- process_data/utils/abc2xml.py +0 -0
- process_data/utils/pyparsing.py +0 -0
- process_data/utils/xml2abc.py +0 -1582
process_data/README.md
DELETED
@@ -1,307 +0,0 @@
|
|
1 |
-
# Data Processing Database
|
2 |
-
|
3 |
-
## Overview
|
4 |
-
This codebase contains scripts and utilities for converting between various musical data formats, including ABC notation, MusicXML, MIDI, and MTF (MIDI Text Format). Additionally, it includes a script for summarizing music metadata, which is represented in JSON format containing textual information, using the OpenAI GPT-4 API. The GPT-4 model processes this metadata to generate concise summaries in multiple languages to boost multilingual MIR. These tools are designed to facilitate the transformation and manipulation of musical files, as well as to provide concise multilingual summaries of music metadata for use with CLaMP 2.
|
5 |
-
|
6 |
-
|
7 |
-
## About ABC notation
|
8 |
-
### Standard ABC Notation
|
9 |
-
ABC notation (sheet music), a text-based sheet music representation like stave notation, is theory-oriented and ideal for presenting complex musical concepts to musicians for study and analysis. Standard ABC notation encodes each voice separately, which often results in corresponding bars being spaced far apart. This separation makes it difficult for models to accurately understand the interactions between voices in sheet music that are meant to align musically.
|
10 |
-
|
11 |
-
Example Standard ABC notation representation:
|
12 |
-
```
|
13 |
-
%%score { 1 | 2 }
|
14 |
-
L:1/8
|
15 |
-
Q:1/4=120
|
16 |
-
M:3/4
|
17 |
-
K:G
|
18 |
-
V:1 treble nm="Piano" snm="Pno."
|
19 |
-
V:2 bass
|
20 |
-
V:1
|
21 |
-
!mf!"^Allegro" d2 (GA Bc | d2) .G2 .G2 |]
|
22 |
-
V:2
|
23 |
-
[G,B,D]4 A,2 | B,6 |]
|
24 |
-
```
|
25 |
-
|
26 |
-
### Interleaved ABC Notation
|
27 |
-
In contrast, interleaved ABC notation effectively aligns multi-track music by integrating multiple voices of the same bar into a single line, ensuring that all parts remain synchronized. This format combines voices in-line and tags each bar with its corresponding voice (e.g., `[V:1]` for treble and `[V:2]` for bass). By directly aligning related bars, interleaved ABC notation enhances the model’s understanding of how different voices interact within the same bar.
|
28 |
-
|
29 |
-
Below is the same data optimized with M3 encoding, where each bar or header corresponds to a patch:
|
30 |
-
```
|
31 |
-
%%score { 1 | 2 }
|
32 |
-
L:1/8
|
33 |
-
Q:1/4=120
|
34 |
-
M:3/4
|
35 |
-
K:G
|
36 |
-
V:1 treble nm="Piano" snm="Pno."
|
37 |
-
V:2 bass
|
38 |
-
[V:1]!mf!"^Allegro" d2 (GA Bc|[V:2][G,B,D]4 A,2|
|
39 |
-
[V:1]d2) .G2 .G2|][V:2]B,6|]
|
40 |
-
```
|
41 |
-
|
42 |
-
## About MTF
|
43 |
-
### Raw MIDI Messages
|
44 |
-
MIDI (performance data) precisely encodes performance information related to timing and dynamics, thus suitable for music production and live performance. Raw MIDI messages contain essential musical instructions and metadata, extracted directly from a MIDI file. These include events like note on/off, tempo changes, key signatures, and control changes, which define how the music is performed. The [mido library](https://mido.readthedocs.io/) allows for reading these messages in their native format, as seen below. Each message can include multiple parameters, making the output comprehensive but sometimes redundant.
|
45 |
-
|
46 |
-
```
|
47 |
-
MetaMessage ('time_signature', numerator=3, denominator=4, clocks_per_click=24, notated_32nd_notes_per_beat=8, time=0)
|
48 |
-
MetaMessage('key_signature', key='G', time=0)
|
49 |
-
MetaMessage('set_tempo', tempo=500000, time=0)
|
50 |
-
control_change channel=0 control=121 value=0 time=0
|
51 |
-
program_change channel=0 program=0 time=0
|
52 |
-
control_change channel=0 control=7 value=100 time=0
|
53 |
-
control_change channel=0 control=10 value=64 time=0
|
54 |
-
control_change channel=0 control=91 value=0 time=0
|
55 |
-
control_change channel=0 control=93 value=0 time=0
|
56 |
-
MetaMessage('midi_port', port=0, time=0)
|
57 |
-
note_on channel=0 note=74 velocity=80 time=0
|
58 |
-
MetaMessage('key_signature', key='G', time=0)
|
59 |
-
MetaMessage('midi_port', port=0, time=0)
|
60 |
-
note_on channel=0 note=55 velocity=80 time=0
|
61 |
-
note_on channel=0 note=59 velocity=80 time=0
|
62 |
-
note_on channel=0 note=62 velocity=80 time=0
|
63 |
-
note_on channel=0 note=74 velocity=0 time=455
|
64 |
-
note_on channel=0 note=67 velocity=80 time=25
|
65 |
-
note_on channel=0 note=67 velocity=0 time=239
|
66 |
-
note_on channel=0 note=69 velocity=80 time=1
|
67 |
-
note_on channel=0 note=55 velocity=0 time=191
|
68 |
-
note_on channel=0 note=59 velocity=0 time=0
|
69 |
-
note_on channel=0 note=62 velocity=0 time=0
|
70 |
-
note_on channel=0 note=69 velocity=0 time=48
|
71 |
-
note_on channel=0 note=71 velocity=80 time=1
|
72 |
-
note_on channel=0 note=57 velocity=80 time=0
|
73 |
-
note_on channel=0 note=71 velocity=0 time=239
|
74 |
-
note_on channel=0 note=72 velocity=80 time=1
|
75 |
-
note_on channel=0 note=57 velocity=0 time=215
|
76 |
-
note_on channel=0 note=72 velocity=0 time=24
|
77 |
-
note_on channel=0 note=74 velocity=80 time=1
|
78 |
-
note_on channel=0 note=59 velocity=80 time=0
|
79 |
-
note_on channel=0 note=74 velocity=0 time=455
|
80 |
-
note_on channel=0 note=67 velocity=80 time=25
|
81 |
-
note_on channel=0 note=67 velocity=0 time=239
|
82 |
-
note_on channel=0 note=67 velocity=80 time=241
|
83 |
-
note_on channel=0 note=67 velocity=0 time=239
|
84 |
-
note_on channel=0 note=59 velocity=0 time=168
|
85 |
-
MetaMessage('end_of_track', time=1)
|
86 |
-
```
|
87 |
-
### MIDI Text Format (MTF)
|
88 |
-
The MIDI Text Format (MTF) provides a structured, textual representation of MIDI data that preserves all original information without loss. Each MIDI message is accurately represented, allowing full reconstruction, ensuring no musical nuances are overlooked during conversion.
|
89 |
-
|
90 |
-
To generate MTF, the mido library reads raw MIDI messages from MIDI files. The output retains all essential information but can be lengthy and redundant. To simplify the representation, parameter values are read in a fixed order and separated by spaces. For example, the raw time signature message, which contains several parameters—numerator, denominator, clocks per click, notated 32nd notes per beat, and time—is represented in MTF as:
|
91 |
-
|
92 |
-
```
|
93 |
-
time_signature 3 4 24 8 0
|
94 |
-
```
|
95 |
-
|
96 |
-
Other messages, such as control changes and note events, follow a similar compact format while preserving all relevant musical details. This structured simplification improves computational performance and maintains precise control over musical elements, including timing and dynamics.
|
97 |
-
|
98 |
-
Example MTF representation:
|
99 |
-
```
|
100 |
-
ticks_per_beat 480
|
101 |
-
time_signature 3 4 24 8 0
|
102 |
-
key_signature G 0
|
103 |
-
set_tempo 500000 0
|
104 |
-
control_change 0 0 121 0
|
105 |
-
program_change 0 0 0
|
106 |
-
control_change 0 0 7 100
|
107 |
-
control_change 0 0 10 64
|
108 |
-
control_change 0 0 91 0
|
109 |
-
control_change 0 0 93 0
|
110 |
-
midi_port 0 0
|
111 |
-
note_on 0 0 74 80
|
112 |
-
key_signature G 0
|
113 |
-
midi_port 0 0
|
114 |
-
note_on 0 0 55 80
|
115 |
-
note_on 0 0 59 80
|
116 |
-
note_on 0 0 62 80
|
117 |
-
note_on 455 0 74 0
|
118 |
-
note_on 25 0 67 80
|
119 |
-
note_on 239 0 67 0
|
120 |
-
note_on 1 0 69 80
|
121 |
-
note_on 191 0 55 0
|
122 |
-
note_on 0 0 59 0
|
123 |
-
note_on 0 0 62 0
|
124 |
-
note_on 48 0 69 0
|
125 |
-
note_on 1 0 71 80
|
126 |
-
note_on 0 0 57 80
|
127 |
-
note_on 239 0 71 0
|
128 |
-
note_on 1 0 72 80
|
129 |
-
note_on 215 0 57 0
|
130 |
-
note_on 24 0 72 0
|
131 |
-
note_on 1 0 74 80
|
132 |
-
note_on 0 0 59 80
|
133 |
-
note_on 455 0 74 0
|
134 |
-
note_on 25 0 67 80
|
135 |
-
note_on 239 0 67 0
|
136 |
-
note_on 241 0 67 80
|
137 |
-
note_on 239 0 67 0
|
138 |
-
note_on 168 0 59 0
|
139 |
-
end_of_track 1
|
140 |
-
```
|
141 |
-
For simplicity, `ticks_per_beat`, though originally an attribute of MIDI objects in mido, is included as the first message at the beginning of the MTF representation.
|
142 |
-
|
143 |
-
### M3-Encoded MTF
|
144 |
-
When processed using M3 encoding, consecutive messages of the same type that fit within a 64-character limit (the patch size of M3) are combined into a single line. Only the first message in each group specifies the type, with subsequent messages listing only the parameter values separated by tabs. This further simplifies the representation and improves processing efficiency.
|
145 |
-
|
146 |
-
Below is the same data optimized with M3 encoding, where each line corresponds to a patch:
|
147 |
-
```
|
148 |
-
ticks_per_beat 480
|
149 |
-
time_signature 3 4 24 8 0
|
150 |
-
key_signature G 0
|
151 |
-
set_tempo 500000 0
|
152 |
-
control_change 0 0 121 0
|
153 |
-
program_change 0 0 0
|
154 |
-
control_change 0 0 7 100\t0 0 10 64\t0 0 91 0\t0 0 93 0
|
155 |
-
midi_port 0 0
|
156 |
-
note_on 0 0 74 80
|
157 |
-
key_signature G 0
|
158 |
-
midi_port 0 0
|
159 |
-
note_on 0 0 55 80\t0 0 59 80\t0 0 62 80\t455 0 74 0\t25 0 67 80
|
160 |
-
note_on 239 0 67 0\t1 0 69 80\t191 0 55 0\t0 0 59 0\t0 0 62 0
|
161 |
-
note_on 48 0 69 0\t1 0 71 80\t0 0 57 80\t239 0 71 0\t1 0 72 80
|
162 |
-
note_on 215 0 57 0\t24 0 72 0\t1 0 74 80\t0 0 59 80\t455 0 74 0
|
163 |
-
note_on 25 0 67 80\t239 0 67 0\t0 67 80\t239 0 67 0\t168 0 59 0
|
164 |
-
end_of_track 1
|
165 |
-
```
|
166 |
-
|
167 |
-
By reducing redundancy, M3 encoding ensures improved computational performance while maintaining precise timing and musical control, making it an ideal choice for efficient MIDI processing.
|
168 |
-
|
169 |
-
## Repository Structure
|
170 |
-
The `process_data/` folder includes the following scripts and utility files:
|
171 |
-
|
172 |
-
### 1. **Conversion Scripts**
|
173 |
-
|
174 |
-
#### `batch_abc2xml.py`
|
175 |
-
- **Purpose**: Converts ABC notation files into MusicXML format.
|
176 |
-
- **Input**: Directory of interleaved ABC files (modify the `input_dir` variable in the code).
|
177 |
-
- **Output**: MusicXML files saved in a newly created `_xml` directory.
|
178 |
-
- **Logging**: Errors are logged to `logs/abc2xml_error_log.txt`.
|
179 |
-
|
180 |
-
#### `batch_xml2abc.py`
|
181 |
-
- **Purpose**: Converts MusicXML files into standard ABC notation format.
|
182 |
-
- **Input**: Directory of MusicXML files (e.g., `.xml`, `.mxl`, `.musicxml`) (modify the `input_dir` variable in the code).
|
183 |
-
- **Output**: Standard ABC files saved in a newly created `_abc` directory.
|
184 |
-
- **Logging**: Errors are logged to `logs/xml2abc_error_log.txt`.
|
185 |
-
|
186 |
-
#### `batch_interleaved_abc.py`
|
187 |
-
- **Purpose**: Processes standard ABC notation files into interleaved ABC notation.
|
188 |
-
- **Input**: Directory of ABC files (modify the `input_dir` variable in the code).
|
189 |
-
- **Output**: Interleaved ABC files saved in a newly created `_interleaved` directory.
|
190 |
-
- **Logging**: Any processing errors are printed to the console.
|
191 |
-
|
192 |
-
#### `batch_midi2mtf.py`
|
193 |
-
- **Purpose**: Converts MIDI files into MIDI Text Format (MTF).
|
194 |
-
- **Input**: Directory of MIDI files (e.g., `.mid`, `.midi`) (modify the `input_dir` variable in the code).
|
195 |
-
- **Output**: MTF files saved in a newly created `_mtf` directory.
|
196 |
-
- **Logging**: Errors are logged to `logs/midi2mtf_error_log.txt`.
|
197 |
-
- **Note**: The script includes an `m3_compatible` variable, which is set to `True` by default. When `True`, the conversion omits messages whose parameters are strings or lists to eliminate potential natural language information. This ensures that the converted MTF files align with the data format used for training the M3 and CLaMP 2 pretrained weights.
|
198 |
-
|
199 |
-
#### `batch_mtf2midi.py`
|
200 |
-
- **Purpose**: Converts MTF files into MIDI format.
|
201 |
-
- **Input**: Directory of MTF files (modify the `input_dir` variable in the code).
|
202 |
-
- **Output**: MIDI files saved in a newly created `_midi` directory.
|
203 |
-
- **Logging**: Errors are logged to `logs/mtf2midi_error_log.txt`.
|
204 |
-
|
205 |
-
### 2. **Summarization Script**
|
206 |
-
|
207 |
-
#### `gpt4_summarize.py`
|
208 |
-
- **Purpose**: Utilizes the OpenAI GPT-4 API to generate concise summaries of music metadata in multiple languages. The script filters out any entries that lack sufficient musical information to ensure meaningful summaries are produced.
|
209 |
-
- **Input**: Directory of JSON files containing music metadata (modify the `input_dir` variable in the code). For any missing metadata fields, the corresponding keys can be set to `None`. Each JSON file corresponds to a single musical composition and can be linked to both ABC notation and MTF formats. Here’s an example of the required metadata format:
|
210 |
-
|
211 |
-
```json
|
212 |
-
{
|
213 |
-
"title": "Hard Times Come Again No More",
|
214 |
-
"composer": "Stephen Foster",
|
215 |
-
"genres": ["Children's Music", "Folk"],
|
216 |
-
"description": "\"Hard Times Come Again No More\" (sometimes referred to as \"Hard Times\") is an American parlor song written by Stephen Foster, reflecting themes of sorrow and hope.",
|
217 |
-
"lyrics": "Let us pause in life's pleasures and count its many tears,\nWhile we all sup sorrow with the poor;\nThere's a song that will linger forever in our ears;\nOh! Hard times come again no more.\n\nChorus:\n'Tis the song, the sigh of the weary,\nHard Times, hard times, come again no more.\nMany days you have lingered around my cabin door;\nOh! Hard times come again no more.\n\nWhile we seek mirth and beauty and music light and gay,\nThere are frail forms fainting at the door;\nThough their voices are silent, their pleading looks will say\nOh! Hard times come again no more.\nChorus\n\nThere's a pale weeping maiden who toils her life away,\nWith a worn heart whose better days are o'er:\nThough her voice would be merry, 'tis sighing all the day,\nOh! Hard times come again no more.\nChorus\n\n'Tis a sigh that is wafted across the troubled wave,\n'Tis a wail that is heard upon the shore\n'Tis a dirge that is murmured around the lowly grave\nOh! Hard times come again no more.\nChorus",
|
218 |
-
"tags": ["folk", "traditional", "bluegrass", "nostalgic", "heartfelt", "acoustic", "melancholic", "storytelling", "American roots", "resilience"],
|
219 |
-
"ensembles": ["Folk Ensemble"],
|
220 |
-
"instruments": ["Vocal", "Violin", "Tin whistle", "Guitar", "Banjo", "Tambourine"],
|
221 |
-
"filepaths": [
|
222 |
-
"abc/American_Music/Folk_Traditions/19th_Century/Stephen_Foster/Hard_Times_Come_Again_No_More.abc",
|
223 |
-
"mtf/American_Music/Folk_Traditions/19th_Century/Stephen_Foster/Hard_Times_Come_Again_No_More.mtf"
|
224 |
-
]
|
225 |
-
}
|
226 |
-
```
|
227 |
-
|
228 |
-
- **Output**: JSON files containing structured summaries in both English and a randomly selected non-English language, chosen from a selection of 100 different non-English languages (in this case, Simplified Chinese). Here’s an example of the expected output format:
|
229 |
-
|
230 |
-
```json
|
231 |
-
{
|
232 |
-
"title": "Hard Times Come Again No More",
|
233 |
-
"composer": "Stephen Foster",
|
234 |
-
"genres": ["Children's Music", "Folk"],
|
235 |
-
"description": "\"Hard Times Come Again No More\" (sometimes referred to as \"Hard Times\") is an American parlor song written by Stephen Foster, reflecting themes of sorrow and hope.",
|
236 |
-
"lyrics": "Let us pause in life's pleasures and count its many tears,\nWhile we all sup sorrow with the poor;\nThere's a song that will linger forever in our ears;\nOh! Hard times come again no more.\n\nChorus:\n'Tis the song, the sigh of the weary,\nHard Times, hard times, come again no more.\nMany days you have lingered around my cabin door;\nOh! Hard times come again no more.\n\nWhile we seek mirth and beauty and music light and gay,\nThere are frail forms fainting at the door;\nThough their voices are silent, their pleading looks will say\nOh! Hard times come again no more.\nChorus\n\nThere's a pale weeping maiden who toils her life away,\nWith a worn heart whose better days are o'er:\nThough her voice would be merry, 'tis sighing all the day,\nOh! Hard times come again no more.\nChorus\n\n'Tis a sigh that is wafted across the troubled wave,\n'Tis a wail that is heard upon the shore\n'Tis a dirge that is murmured around the lowly grave\nOh! Hard times come again no more.\nChorus",
|
237 |
-
"tags": ["folk", "traditional", "bluegrass", "nostalgic", "heartfelt", "acoustic", "melancholic", "storytelling", "American roots", "resilience"],
|
238 |
-
"ensembles": ["Folk Ensemble"],
|
239 |
-
"instruments": ["Vocal", "Violin", "Tin whistle", "Guitar", "Banjo", "Tambourine"],
|
240 |
-
"summary_en": "\"Hard Times Come Again No More,\" composed by Stephen Foster, is a poignant American parlor song that explores themes of sorrow and hope. The lyrics reflect on the contrast between life's pleasures and its hardships, inviting listeners to acknowledge both joy and suffering. With a heartfelt chorus that repeats the line \"Hard times come again no more,\" the song resonates with nostalgia and resilience. It is often performed by folk ensembles and features a variety of instruments, including vocals, violin, guitar, and banjo, encapsulating the spirit of American roots music.",
|
241 |
-
"summary_nen": {
|
242 |
-
"language": "Chinese (Simplified)",
|
243 |
-
"summary": "《艰难时光再无来临》是斯蒂芬·福斯特创作的一首感人至深的美国小歌厅歌曲,探讨了悲伤与希望的主题。歌词展现了生活的乐趣与艰辛之间的对比,邀请听众去感受快乐与痛苦的交织。歌曲中那句反复吟唱的“艰难时光再无来临”深情地表达了怀旧与坚韧。它常常由民谣乐队演奏,伴随着人声、小提琴、吉他和班卓琴等多种乐器,生动地展现了美国根源音乐的独特魅力。"
|
244 |
-
},
|
245 |
-
"filepaths": [
|
246 |
-
"abc/American_Music/Folk_Traditions/19th_Century/Stephen_Foster/Hard_Times_Come_Again_No_More.abc",
|
247 |
-
"mtf/American_Music/Folk_Traditions/19th_Century/Stephen_Foster/Hard_Times_Come_Again_No_More.mtf"
|
248 |
-
]
|
249 |
-
}
|
250 |
-
```
|
251 |
-
|
252 |
-
- **Logging**: Errors are logged to `logs/gpt4_summarize_error_log.txt`.
|
253 |
-
|
254 |
-
### 3. **Utilities**
|
255 |
-
- **`utils/`**: Contains utility files required for the conversion processes.
|
256 |
-
|
257 |
-
## Usage
|
258 |
-
To use the scripts, modify the `input_dir` variable in each script to point to the directory containing your input files. Then run the script from the command line. Below are example commands for each script:
|
259 |
-
|
260 |
-
### Example Commands
|
261 |
-
```bash
|
262 |
-
# Modify the input_dir variable in the script before running
|
263 |
-
python batch_abc2xml.py
|
264 |
-
python batch_xml2abc.py
|
265 |
-
python batch_interleaved_abc.py
|
266 |
-
python batch_midi2mtf.py
|
267 |
-
python batch_mtf2midi.py
|
268 |
-
python gpt4_summarize.py
|
269 |
-
```
|
270 |
-
|
271 |
-
### Execution Order
|
272 |
-
To achieve specific conversions, follow the order below:
|
273 |
-
|
274 |
-
1. **To obtain interleaved ABC notation**:
|
275 |
-
- First, run `batch_xml2abc.py` to convert MusicXML files to ABC notation.
|
276 |
-
- Then, run `batch_interleaved_abc.py` to process the ABC files into interleaved ABC notation.
|
277 |
-
|
278 |
-
2. **To obtain MTF**:
|
279 |
-
- Run `batch_midi2mtf.py` to convert MIDI files into MTF.
|
280 |
-
|
281 |
-
3. **To convert interleaved ABC back to XML**:
|
282 |
-
- Run `batch_xml2abc.py` on the interleaved ABC files to convert them back to MusicXML format.
|
283 |
-
|
284 |
-
4. **To convert MTF back to MIDI**:
|
285 |
-
- Run `batch_mtf2midi.py` to convert MTF files back to MIDI format.
|
286 |
-
|
287 |
-
5. **To summarize music metadata**:
|
288 |
-
- Run `gpt4_summarize.py` to generate summaries for the music metadata files in JSON format. This assumes you have a directory of JSON files that includes a `filepaths` key, which connects to the corresponding interleaved ABC and MTF files.
|
289 |
-
|
290 |
-
### Parameters
|
291 |
-
To run the scripts, you need to configure the following parameters:
|
292 |
-
|
293 |
-
- **`input_dir`**: This variable should be set to the directory containing the input files to be processed (such as ABC, MusicXML, MIDI, MTF, or JSON files), which is shared across all scripts.
|
294 |
-
|
295 |
-
In addition to **`input_dir`**, the following parameters are specific to certain scripts:
|
296 |
-
|
297 |
-
- **`m3_compatible`** (specific to `batch_midi2mtf.py`):
|
298 |
-
- Default is `True`, which omits messages with parameters that are strings or lists to avoid including potential natural language information.
|
299 |
-
- Setting this to `False` retains all MIDI messages, which is crucial for those planning to retrain models on custom datasets or needing precise MIDI reproduction.
|
300 |
-
|
301 |
-
For **`gpt4_summarize.py`**, you also need to configure these parameters:
|
302 |
-
|
303 |
-
1. **`base_url`**: The base URL for the OpenAI API, used to initialize the client.
|
304 |
-
2. **`api_key`**: Your API key for authenticating requests, required for client initialization.
|
305 |
-
3. **`model`**: The GPT-4 model to use, specified when generating summaries.
|
306 |
-
|
307 |
-
**Important**: When `m3_compatible` is set to `True`, the conversion back from MTF to MIDI using `batch_mtf2midi.py` may produce MIDI files that do not exactly match the original MIDI files. This discrepancy is unexpected; however, retraining both M3 and CLaMP 2 to address this issue would require approximately 6000 hours of H800 GPU hours. Considering that M3 and CLaMP 2 have already achieved state-of-the-art results on MIDI tasks, we have opted not to retrain. Therefore, if consistency with original MIDI files is critical for your application, it is advisable to set `m3_compatible` to `False`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
process_data/batch_abc2xml.py
DELETED
@@ -1,58 +0,0 @@
|
|
1 |
-
input_dir = "<path_to_your_interleaved_abc_files>" # Replace with the path to your folder containing interleaved ABC (.abc) files
|
2 |
-
|
3 |
-
import os
|
4 |
-
import math
|
5 |
-
import random
|
6 |
-
import subprocess
|
7 |
-
from tqdm import tqdm
|
8 |
-
from multiprocessing import Pool
|
9 |
-
|
10 |
-
def convert_abc2xml(file_list):
|
11 |
-
cmd = 'cmd /u /c python utils/abc2xml.py '
|
12 |
-
for file in tqdm(file_list):
|
13 |
-
filename = file.split('/')[-1] # Extract file name
|
14 |
-
output_dir = file.split('/')[:-1] # Extract directory path
|
15 |
-
output_dir[0] = output_dir[0] + '_xml' # Create new output folder
|
16 |
-
output_dir = '/'.join(output_dir)
|
17 |
-
os.makedirs(output_dir, exist_ok=True)
|
18 |
-
|
19 |
-
try:
|
20 |
-
p = subprocess.Popen(cmd + '"' + file + '"', stdout=subprocess.PIPE, shell=True)
|
21 |
-
result = p.communicate()
|
22 |
-
output = result[0].decode('utf-8')
|
23 |
-
|
24 |
-
if output == '':
|
25 |
-
with open("logs/abc2xml_error_log.txt", "a", encoding="utf-8") as f:
|
26 |
-
f.write(file + '\n')
|
27 |
-
continue
|
28 |
-
else:
|
29 |
-
output_path = f"{output_dir}/" + ".".join(filename.split(".")[:-1]) + ".xml"
|
30 |
-
with open(output_path, 'w', encoding='utf-8') as f:
|
31 |
-
f.write(output)
|
32 |
-
except Exception as e:
|
33 |
-
with open("logs/abc2xml_error_log.txt", "a", encoding="utf-8") as f:
|
34 |
-
f.write(file + ' ' + str(e) + '\n')
|
35 |
-
pass
|
36 |
-
|
37 |
-
if __name__ == '__main__':
|
38 |
-
file_list = []
|
39 |
-
os.makedirs("logs", exist_ok=True)
|
40 |
-
|
41 |
-
# Traverse the specified folder for ABC files
|
42 |
-
for root, dirs, files in os.walk(input_dir):
|
43 |
-
for file in files:
|
44 |
-
if not file.endswith(".abc"):
|
45 |
-
continue
|
46 |
-
filename = os.path.join(root, file).replace("\\", "/")
|
47 |
-
file_list.append(filename)
|
48 |
-
|
49 |
-
# Prepare for multiprocessing
|
50 |
-
file_lists = []
|
51 |
-
random.shuffle(file_list)
|
52 |
-
for i in range(os.cpu_count()):
|
53 |
-
start_idx = int(math.floor(i * len(file_list) / os.cpu_count()))
|
54 |
-
end_idx = int(math.floor((i + 1) * len(file_list) / os.cpu_count()))
|
55 |
-
file_lists.append(file_list[start_idx:end_idx])
|
56 |
-
|
57 |
-
pool = Pool(processes=os.cpu_count())
|
58 |
-
pool.map(convert_abc2xml, file_lists)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
process_data/batch_interleaved_abc.py
DELETED
@@ -1,117 +0,0 @@
|
|
1 |
-
input_dir = "<path_to_your_abc_files>" # Replace with the path to your folder containing standard ABC (.abc) files
|
2 |
-
|
3 |
-
import os
|
4 |
-
import re
|
5 |
-
import random
|
6 |
-
from multiprocessing import Pool
|
7 |
-
from tqdm import tqdm
|
8 |
-
from abctoolkit.utils import (
|
9 |
-
find_all_abc,
|
10 |
-
remove_information_field,
|
11 |
-
remove_bar_no_annotations,
|
12 |
-
Quote_re,
|
13 |
-
Barlines,
|
14 |
-
strip_empty_bars
|
15 |
-
)
|
16 |
-
from abctoolkit.rotate import rotate_abc
|
17 |
-
from abctoolkit.check import check_alignment_unrotated
|
18 |
-
|
19 |
-
def abc_pipeline(abc_path, input_dir, output_dir):
|
20 |
-
"""
|
21 |
-
Converts standard ABC notation to interleaved ABC notation.
|
22 |
-
"""
|
23 |
-
with open(abc_path, 'r', encoding='utf-8') as f:
|
24 |
-
abc_lines = f.readlines()
|
25 |
-
|
26 |
-
abc_lines = [line for line in abc_lines if line.strip() != '']
|
27 |
-
abc_lines = remove_information_field(
|
28 |
-
abc_lines=abc_lines,
|
29 |
-
info_fields=['X:', 'T:', 'C:', 'W:', 'w:', 'Z:', '%%MIDI']
|
30 |
-
)
|
31 |
-
abc_lines = remove_bar_no_annotations(abc_lines)
|
32 |
-
|
33 |
-
# Remove escaped quotes and clean up barlines inside quotes
|
34 |
-
for i, line in enumerate(abc_lines):
|
35 |
-
if not (re.search(r'^[A-Za-z]:', line) or line.startswith('%')):
|
36 |
-
abc_lines[i] = line.replace(r'\"', '')
|
37 |
-
quote_contents = re.findall(Quote_re, line)
|
38 |
-
for quote_content in quote_contents:
|
39 |
-
for barline in Barlines:
|
40 |
-
if barline in quote_content:
|
41 |
-
line = line.replace(quote_content, '')
|
42 |
-
abc_lines[i] = line
|
43 |
-
|
44 |
-
try:
|
45 |
-
stripped_abc_lines, bar_counts = strip_empty_bars(abc_lines)
|
46 |
-
except Exception as e:
|
47 |
-
print(abc_path, 'Error in stripping empty bars:', e)
|
48 |
-
return
|
49 |
-
|
50 |
-
if stripped_abc_lines is None:
|
51 |
-
print(abc_path, 'Failed to strip')
|
52 |
-
return
|
53 |
-
|
54 |
-
# Check alignment
|
55 |
-
_, bar_no_equal_flag, bar_dur_equal_flag = check_alignment_unrotated(stripped_abc_lines)
|
56 |
-
if not bar_no_equal_flag:
|
57 |
-
print(abc_path, 'Unequal bar number')
|
58 |
-
if not bar_dur_equal_flag:
|
59 |
-
print(abc_path, 'Unequal bar duration (unaligned)')
|
60 |
-
|
61 |
-
# Construct the output path, maintaining input folder structure
|
62 |
-
relative_path = os.path.relpath(abc_path, input_dir) # Get relative path from input dir
|
63 |
-
output_file_path = os.path.join(output_dir, relative_path) # Recreate output path
|
64 |
-
os.makedirs(os.path.dirname(output_file_path), exist_ok=True) # Ensure output folder exists
|
65 |
-
|
66 |
-
try:
|
67 |
-
rotated_abc_lines = rotate_abc(stripped_abc_lines)
|
68 |
-
except Exception as e:
|
69 |
-
print(abc_path, 'Error in rotating:', e)
|
70 |
-
return
|
71 |
-
|
72 |
-
if rotated_abc_lines is None:
|
73 |
-
print(abc_path, 'Failed to rotate')
|
74 |
-
return
|
75 |
-
|
76 |
-
with open(output_file_path, 'w', encoding='utf-8') as w:
|
77 |
-
w.writelines(rotated_abc_lines)
|
78 |
-
|
79 |
-
def abc_pipeline_list(abc_path_list, input_dir, output_dir):
|
80 |
-
for abc_path in tqdm(abc_path_list):
|
81 |
-
try:
|
82 |
-
abc_pipeline(abc_path, input_dir, output_dir)
|
83 |
-
except Exception as e:
|
84 |
-
print(abc_path, e)
|
85 |
-
pass
|
86 |
-
|
87 |
-
def batch_abc_pipeline(input_dir):
|
88 |
-
"""
|
89 |
-
Batch process all ABC files from `input_dir`, converting them to interleaved notation.
|
90 |
-
"""
|
91 |
-
output_dir = input_dir + "_interleaved"
|
92 |
-
if not os.path.exists(output_dir):
|
93 |
-
os.makedirs(output_dir, exist_ok=True)
|
94 |
-
|
95 |
-
abc_path_list = []
|
96 |
-
for abc_path in find_all_abc(input_dir):
|
97 |
-
if os.path.getsize(abc_path) > 0:
|
98 |
-
abc_path_list.append(abc_path)
|
99 |
-
random.shuffle(abc_path_list)
|
100 |
-
print(f"Found {len(abc_path_list)} ABC files.")
|
101 |
-
|
102 |
-
num_cpus = os.cpu_count()
|
103 |
-
split_lists = [[] for _ in range(num_cpus)]
|
104 |
-
index = 0
|
105 |
-
|
106 |
-
for abc_path in abc_path_list:
|
107 |
-
split_lists[index].append(abc_path)
|
108 |
-
index = (index + 1) % num_cpus
|
109 |
-
|
110 |
-
pool = Pool(processes=num_cpus)
|
111 |
-
pool.starmap(
|
112 |
-
abc_pipeline_list,
|
113 |
-
[(split, input_dir, output_dir) for split in split_lists]
|
114 |
-
)
|
115 |
-
|
116 |
-
if __name__ == '__main__':
|
117 |
-
batch_abc_pipeline(input_dir)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
process_data/batch_midi2mtf.py
DELETED
@@ -1,80 +0,0 @@
|
|
1 |
-
input_dir = "<path_to_your_midi_files>" # Replace with the path to your folder containing MIDI (.midi, .mid) files
|
2 |
-
m3_compatible = True # Set to True for M3 compatibility; set to False to retain all MIDI information during conversion.
|
3 |
-
|
4 |
-
import os
|
5 |
-
import math
|
6 |
-
import mido
|
7 |
-
import random
|
8 |
-
from tqdm import tqdm
|
9 |
-
from multiprocessing import Pool
|
10 |
-
|
11 |
-
def msg_to_str(msg):
|
12 |
-
str_msg = ""
|
13 |
-
for key, value in msg.dict().items():
|
14 |
-
str_msg += " " + str(value)
|
15 |
-
return str_msg.strip().encode('unicode_escape').decode('utf-8')
|
16 |
-
|
17 |
-
def load_midi(filename):
|
18 |
-
# Load a MIDI file
|
19 |
-
mid = mido.MidiFile(filename)
|
20 |
-
msg_list = ["ticks_per_beat " + str(mid.ticks_per_beat)]
|
21 |
-
|
22 |
-
# Traverse the MIDI file
|
23 |
-
for msg in mid.merged_track:
|
24 |
-
if m3_compatible:
|
25 |
-
if msg.is_meta:
|
26 |
-
if msg.type in ["text", "copyright", "track_name", "instrument_name",
|
27 |
-
"lyrics", "marker", "cue_marker", "device_name", "sequencer_specific"]:
|
28 |
-
continue
|
29 |
-
else:
|
30 |
-
if msg.type in ["sysex"]:
|
31 |
-
continue
|
32 |
-
str_msg = msg_to_str(msg)
|
33 |
-
msg_list.append(str_msg)
|
34 |
-
|
35 |
-
return "\n".join(msg_list)
|
36 |
-
|
37 |
-
def convert_midi2mtf(file_list):
|
38 |
-
for file in tqdm(file_list):
|
39 |
-
filename = file.split('/')[-1]
|
40 |
-
output_dir = file.split('/')[:-1]
|
41 |
-
output_dir[0] = output_dir[0] + '_mtf'
|
42 |
-
output_dir = '/'.join(output_dir)
|
43 |
-
os.makedirs(output_dir, exist_ok=True)
|
44 |
-
try:
|
45 |
-
output = load_midi(file)
|
46 |
-
|
47 |
-
if output == '':
|
48 |
-
with open('logs/midi2mtf_error_log.txt', 'a', encoding='utf-8') as f:
|
49 |
-
f.write(file + '\n')
|
50 |
-
continue
|
51 |
-
else:
|
52 |
-
with open(output_dir + "/" + ".".join(filename.split(".")[:-1]) + '.mtf', 'w', encoding='utf-8') as f:
|
53 |
-
f.write(output)
|
54 |
-
except Exception as e:
|
55 |
-
with open('logs/midi2mtf_error_log.txt', 'a', encoding='utf-8') as f:
|
56 |
-
f.write(file + " " + str(e) + '\n')
|
57 |
-
pass
|
58 |
-
|
59 |
-
if __name__ == '__main__':
|
60 |
-
file_list = []
|
61 |
-
os.makedirs("logs", exist_ok=True)
|
62 |
-
|
63 |
-
# Traverse the specified folder for MIDI files
|
64 |
-
for root, dirs, files in os.walk(input_dir):
|
65 |
-
for file in files:
|
66 |
-
if not file.endswith(".mid") and not file.endswith(".midi"):
|
67 |
-
continue
|
68 |
-
filename = os.path.join(root, file).replace("\\", "/")
|
69 |
-
file_list.append(filename)
|
70 |
-
|
71 |
-
# Prepare for multiprocessing
|
72 |
-
file_lists = []
|
73 |
-
random.shuffle(file_list)
|
74 |
-
for i in range(os.cpu_count()):
|
75 |
-
start_idx = int(math.floor(i * len(file_list) / os.cpu_count()))
|
76 |
-
end_idx = int(math.floor((i + 1) * len(file_list) / os.cpu_count()))
|
77 |
-
file_lists.append(file_list[start_idx:end_idx])
|
78 |
-
|
79 |
-
pool = Pool(processes=os.cpu_count())
|
80 |
-
pool.map(convert_midi2mtf, file_lists)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
process_data/batch_mtf2midi.py
DELETED
@@ -1,97 +0,0 @@
|
|
1 |
-
input_dir = "<path_to_your_mtf_files>" # Replace with the path to your folder containing MTF (.mtf) files
|
2 |
-
|
3 |
-
import os
|
4 |
-
import math
|
5 |
-
import mido
|
6 |
-
import random
|
7 |
-
from tqdm import tqdm
|
8 |
-
from multiprocessing import Pool
|
9 |
-
|
10 |
-
def str_to_msg(str_msg):
|
11 |
-
type = str_msg.split(" ")[0]
|
12 |
-
try:
|
13 |
-
msg = mido.Message(type)
|
14 |
-
except:
|
15 |
-
msg = mido.MetaMessage(type)
|
16 |
-
|
17 |
-
if type in ["text", "copyright", "track_name", "instrument_name",
|
18 |
-
"lyrics", "marker", "cue_marker", "device_name"]:
|
19 |
-
values = [type, " ".join(str_msg.split(" ")[1:-1]).encode('utf-8').decode('unicode_escape'), str_msg.split(" ")[-1]]
|
20 |
-
elif "[" in str_msg or "(" in str_msg:
|
21 |
-
is_bracket = "[" in str_msg
|
22 |
-
left_idx = str_msg.index("[") if is_bracket else str_msg.index("(")
|
23 |
-
right_idx = str_msg.index("]") if is_bracket else str_msg.index(")")
|
24 |
-
list_str = [int(num) for num in str_msg[left_idx+1:right_idx].split(", ")]
|
25 |
-
if not is_bracket:
|
26 |
-
list_str = tuple(list_str)
|
27 |
-
values = str_msg[:left_idx].split(" ") + [list_str] + str_msg[right_idx+1:].split(" ")
|
28 |
-
values = [value for value in values if value != ""]
|
29 |
-
else:
|
30 |
-
values = str_msg.split(" ")
|
31 |
-
|
32 |
-
if len(values) != 1:
|
33 |
-
for idx, (key, content) in enumerate(msg.__dict__.items()):
|
34 |
-
if key == "type":
|
35 |
-
continue
|
36 |
-
value = values[idx]
|
37 |
-
if isinstance(content, int) or isinstance(content, float):
|
38 |
-
float_value = float(value)
|
39 |
-
value = float_value
|
40 |
-
if value % 1 == 0:
|
41 |
-
value = int(value)
|
42 |
-
setattr(msg, key, value)
|
43 |
-
|
44 |
-
return msg
|
45 |
-
|
46 |
-
def convert_mtf2midi(file_list):
|
47 |
-
for file in tqdm(file_list):
|
48 |
-
filename = file.split('/')[-1]
|
49 |
-
output_dir = file.split('/')[:-1]
|
50 |
-
output_dir[0] = output_dir[0] + '_midi'
|
51 |
-
output_dir = '/'.join(output_dir)
|
52 |
-
os.makedirs(output_dir, exist_ok=True)
|
53 |
-
try:
|
54 |
-
with open(file, 'r', encoding='utf-8') as f:
|
55 |
-
msg_list = f.read().splitlines()
|
56 |
-
|
57 |
-
# Build a new MIDI file based on the MIDI messages
|
58 |
-
new_mid = mido.MidiFile()
|
59 |
-
new_mid.ticks_per_beat = int(msg_list[0].split(" ")[1])
|
60 |
-
|
61 |
-
track = mido.MidiTrack()
|
62 |
-
new_mid.tracks.append(track)
|
63 |
-
|
64 |
-
for msg in msg_list[1:]:
|
65 |
-
if "unknown_meta" in msg:
|
66 |
-
continue
|
67 |
-
new_msg = str_to_msg(msg)
|
68 |
-
track.append(new_msg)
|
69 |
-
|
70 |
-
output_file_path = os.path.join(output_dir, os.path.basename(file).replace('.mtf', '.mid'))
|
71 |
-
new_mid.save(output_file_path)
|
72 |
-
except Exception as e:
|
73 |
-
with open('logs/mtf2midi_error_log.txt', 'a', encoding='utf-8') as f:
|
74 |
-
f.write(f"Error processing {file}: {str(e)}\n")
|
75 |
-
|
76 |
-
if __name__ == '__main__':
|
77 |
-
file_list = []
|
78 |
-
os.makedirs("logs", exist_ok=True)
|
79 |
-
|
80 |
-
# Traverse the specified folder for MTF files
|
81 |
-
for root, dirs, files in os.walk(input_dir):
|
82 |
-
for file in files:
|
83 |
-
if not file.endswith(".mtf"):
|
84 |
-
continue
|
85 |
-
filename = os.path.join(root, file).replace("\\", "/")
|
86 |
-
file_list.append(filename)
|
87 |
-
|
88 |
-
# Prepare for multiprocessing
|
89 |
-
file_lists = []
|
90 |
-
random.shuffle(file_list)
|
91 |
-
for i in range(os.cpu_count()):
|
92 |
-
start_idx = int(math.floor(i * len(file_list) / os.cpu_count()))
|
93 |
-
end_idx = int(math.floor((i + 1) * len(file_list) / os.cpu_count()))
|
94 |
-
file_lists.append(file_list[start_idx:end_idx])
|
95 |
-
|
96 |
-
pool = Pool(processes=os.cpu_count())
|
97 |
-
pool.map(convert_mtf2midi, file_lists)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
process_data/batch_xml2abc.py
DELETED
@@ -1,57 +0,0 @@
|
|
1 |
-
input_dir = "<path_to_your_xml_files>" # Replace with the path to your folder containing XML (.xml, .mxl, .musicxml) files
|
2 |
-
|
3 |
-
import os
|
4 |
-
import math
|
5 |
-
import random
|
6 |
-
import subprocess
|
7 |
-
from tqdm import tqdm
|
8 |
-
from multiprocessing import Pool
|
9 |
-
|
10 |
-
def convert_xml2abc(file_list):
|
11 |
-
cmd = 'cmd /u /c python utils/xml2abc.py -d 8 -x '
|
12 |
-
for file in tqdm(file_list):
|
13 |
-
filename = file.split('/')[-1]
|
14 |
-
output_dir = file.split('/')[:-1]
|
15 |
-
output_dir[0] = output_dir[0] + '_abc'
|
16 |
-
output_dir = '/'.join(output_dir)
|
17 |
-
os.makedirs(output_dir, exist_ok=True)
|
18 |
-
|
19 |
-
try:
|
20 |
-
p = subprocess.Popen(cmd + '"' + file + '"', stdout=subprocess.PIPE, shell=True)
|
21 |
-
result = p.communicate()
|
22 |
-
output = result[0].decode('utf-8')
|
23 |
-
|
24 |
-
if output == '':
|
25 |
-
with open("logs/xml2abc_error_log.txt", "a", encoding="utf-8") as f:
|
26 |
-
f.write(file + '\n')
|
27 |
-
continue
|
28 |
-
else:
|
29 |
-
with open(output_dir + '/' + ".".join(filename.split(".")[:-1]) + '.abc', 'w', encoding='utf-8') as f:
|
30 |
-
f.write(output)
|
31 |
-
except Exception as e:
|
32 |
-
with open("logs/xml2abc_error_log.txt", "a", encoding="utf-8") as f:
|
33 |
-
f.write(file + ' ' + str(e) + '\n')
|
34 |
-
pass
|
35 |
-
|
36 |
-
if __name__ == '__main__':
|
37 |
-
file_list = []
|
38 |
-
os.makedirs("logs", exist_ok=True)
|
39 |
-
|
40 |
-
# Traverse the specified folder for XML/MXL files
|
41 |
-
for root, dirs, files in os.walk(input_dir):
|
42 |
-
for file in files:
|
43 |
-
if not file.endswith((".mxl", ".xml", ".musicxml")):
|
44 |
-
continue
|
45 |
-
filename = os.path.join(root, file).replace("\\", "/")
|
46 |
-
file_list.append(filename)
|
47 |
-
|
48 |
-
# Prepare for multiprocessing
|
49 |
-
file_lists = []
|
50 |
-
random.shuffle(file_list)
|
51 |
-
for i in range(os.cpu_count()):
|
52 |
-
start_idx = int(math.floor(i * len(file_list) / os.cpu_count()))
|
53 |
-
end_idx = int(math.floor((i + 1) * len(file_list) / os.cpu_count()))
|
54 |
-
file_lists.append(file_list[start_idx:end_idx])
|
55 |
-
|
56 |
-
pool = Pool(processes=os.cpu_count())
|
57 |
-
pool.map(convert_xml2abc, file_lists)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
process_data/gpt4_summarize.py
DELETED
@@ -1,250 +0,0 @@
|
|
1 |
-
input_dir = "<path_to_your_metadata_json_files>" # Replace with the path to your folder containing metadata (.json) files
|
2 |
-
base_url = "<your_base_url>" # Replace with the base URL for the API
|
3 |
-
api_key = "<your_api_key>" # Replace with your API key
|
4 |
-
model = "<your_model>" # Replace with your model name
|
5 |
-
|
6 |
-
import os
|
7 |
-
import json
|
8 |
-
import random
|
9 |
-
from openai import OpenAI
|
10 |
-
|
11 |
-
# Initialize the OpenAI client
|
12 |
-
client = OpenAI(base_url=base_url, api_key=api_key)
|
13 |
-
|
14 |
-
def log_error(file_path, error_message):
|
15 |
-
"""Logs error messages to a specified log file."""
|
16 |
-
os.makedirs("logs", exist_ok=True)
|
17 |
-
with open("logs/gpt4_summarize_error_log.txt", 'a', encoding='utf-8') as log_file:
|
18 |
-
log_file.write(f"Error processing {file_path}: {error_message}\n")
|
19 |
-
|
20 |
-
def process_json(metadata, language):
|
21 |
-
"""
|
22 |
-
Processes the given metadata of a music piece using GPT-4 API.
|
23 |
-
|
24 |
-
This function sends the metadata and target language to the GPT-4 model to generate
|
25 |
-
a structured summary. The summary is provided in both English and the specified
|
26 |
-
non-English language from the 'nen_language' field.
|
27 |
-
|
28 |
-
If the provided metadata lacks sufficient music-related details, the function returns `None`.
|
29 |
-
|
30 |
-
Parameters:
|
31 |
-
- metadata (dict): A dictionary containing the metadata of the music piece.
|
32 |
-
- language (str): The target non-English language for the summary.
|
33 |
-
|
34 |
-
Returns:
|
35 |
-
- str: A JSON-formatted string containing the English and non-English summaries,
|
36 |
-
or `None` if there is insufficient information.
|
37 |
-
"""
|
38 |
-
system = """Your task is to provide a concise, comprehensive, and coherent summary of the music piece using the provided metadata. Please write the summary in English first, and then write an equivalent summary in the specified non-English language from the "nen_language" field. Use this JSON format:
|
39 |
-
{
|
40 |
-
"summary_en": "Your English summary here.",
|
41 |
-
"summary_nen": {
|
42 |
-
"language": "Specified non-English language.",
|
43 |
-
"summary": "Your non-English summary here."
|
44 |
-
}
|
45 |
-
If there is not enough music-related information, return `None` instead.
|
46 |
-
}
|
47 |
-
"""
|
48 |
-
user1 = """{
|
49 |
-
"title": "Brejeiro",
|
50 |
-
"composer": "Ernesto Nazareth",
|
51 |
-
"genres": ["Choro", "Classical", "Instrumental"],
|
52 |
-
"description": "\"Brejeiro\" is in A major and 2/4 time. A joyful melody begins at bar six, and a lively tango rhythm starts at bar fourteen. It has a D.C. al Fine at bar fifty-three and ends on two quarter notes in bar thirty-seven. The piece, with its vibrant melodies and rhythms, reflects celebration and carefreeness, embodying the spirit of Brazilian music.",
|
53 |
-
"tags": ["Brazilian", "Choro", "Piano"],
|
54 |
-
"ensembles": ["Solo Piano", "Small Ensemble"],
|
55 |
-
"instruments": ["Piano"],
|
56 |
-
"nen_language": "Japanese"
|
57 |
-
}
|
58 |
-
"""
|
59 |
-
assistant1 = """{
|
60 |
-
"summary_en": "Brejeiro, composed by Ernesto Nazareth, is a lively choro piece in A major and 2/4 time. It features a joyful melody that begins at bar six and a vibrant tango rhythm introduced at bar fourteen. The piece includes a D.C. al Fine at bar fifty-three, concluding on two quarter notes in bar thirty-seven. With its themes of celebration and carefreeness, Brejeiro beautifully captures the essence of Brazilian music and is well-suited for solo piano and small ensembles.",
|
61 |
-
"summary_nen": {
|
62 |
-
"language": "Japanese",
|
63 |
-
"summary": "「ブレジェイロ」は、エルネスト・ナザレが作曲した活気あふれるショーロの作品で、イ長調の2/4拍子で書かれています。第6小節から始まる喜びに満ちたメロディーと、第14小節で導入される活気あるタンゴのリズムが特徴です。この曲には、第53小節でのD.C. al Fineが含まれ、また第37小節で二つの四分音符で締めくくられています。「ブレジェイロ」は、お祝いと無邪気さのテーマを持ち、ブラジル音楽の本質を美しく捉えており、ソロピアノや小編成のアンサンブルにぴったりの作品です。"
|
64 |
-
}
|
65 |
-
}
|
66 |
-
"""
|
67 |
-
user2 = """{
|
68 |
-
"title": "Untitled",
|
69 |
-
"composer": "Unknown",
|
70 |
-
"description": "This is a good song.",
|
71 |
-
"nen_language": "Russian"
|
72 |
-
}
|
73 |
-
"""
|
74 |
-
assistant2 = "None"
|
75 |
-
filepaths = metadata.pop('filepaths')
|
76 |
-
metadata = {k: v for k, v in metadata.items() if v is not None}
|
77 |
-
|
78 |
-
metadata["nen_language"] = language
|
79 |
-
metadata = json.dumps(metadata, ensure_ascii=False, indent=4)
|
80 |
-
summaries = client.chat.completions.create(
|
81 |
-
model=model,
|
82 |
-
messages=[
|
83 |
-
{"role": "system", "content": system},
|
84 |
-
{"role": "user", "content": user1},
|
85 |
-
{"role": "assistant", "content": assistant1},
|
86 |
-
{"role": "user", "content": user2},
|
87 |
-
{"role": "assistant", "content": assistant2},
|
88 |
-
{"role": "user", "content": metadata},
|
89 |
-
]
|
90 |
-
).choices[0].message.content
|
91 |
-
|
92 |
-
if summaries == "None":
|
93 |
-
raise ValueError("Received 'None' as summaries response")
|
94 |
-
|
95 |
-
metadata = json.loads(metadata)
|
96 |
-
summaries = json.loads(summaries)
|
97 |
-
|
98 |
-
if metadata["nen_language"] == summaries["summary_nen"]["language"]:
|
99 |
-
metadata.pop("nen_language")
|
100 |
-
metadata["summary_en"] = summaries["summary_en"]
|
101 |
-
metadata["summary_nen"] = summaries["summary_nen"]
|
102 |
-
metadata["filepaths"] = filepaths
|
103 |
-
return metadata
|
104 |
-
else:
|
105 |
-
raise ValueError("Language mismatch: nen_language does not match summary_nen language")
|
106 |
-
|
107 |
-
def process_files(input_dir):
|
108 |
-
# Create output directory with _summarized suffix
|
109 |
-
output_dir = input_dir + "_summarized"
|
110 |
-
|
111 |
-
# Define available languages
|
112 |
-
languages = """Afrikaans
|
113 |
-
Amharic
|
114 |
-
Arabic
|
115 |
-
Assamese
|
116 |
-
Azerbaijani
|
117 |
-
Belarusian
|
118 |
-
Bulgarian
|
119 |
-
Bengali
|
120 |
-
Bengali (Romanized)
|
121 |
-
Breton
|
122 |
-
Bosnian
|
123 |
-
Catalan
|
124 |
-
Czech
|
125 |
-
Welsh
|
126 |
-
Danish
|
127 |
-
German
|
128 |
-
Greek
|
129 |
-
Esperanto
|
130 |
-
Spanish
|
131 |
-
Estonian
|
132 |
-
Basque
|
133 |
-
Persian
|
134 |
-
Finnish
|
135 |
-
French
|
136 |
-
Western Frisian
|
137 |
-
Irish
|
138 |
-
Scottish Gaelic
|
139 |
-
Galician
|
140 |
-
Gujarati
|
141 |
-
Hausa
|
142 |
-
Hebrew
|
143 |
-
Hindi
|
144 |
-
Hindi (Romanized)
|
145 |
-
Croatian
|
146 |
-
Hungarian
|
147 |
-
Armenian
|
148 |
-
Indonesian
|
149 |
-
Icelandic
|
150 |
-
Italian
|
151 |
-
Japanese
|
152 |
-
Javanese
|
153 |
-
Georgian
|
154 |
-
Kazakh
|
155 |
-
Khmer
|
156 |
-
Kannada
|
157 |
-
Korean
|
158 |
-
Kurdish (Kurmanji)
|
159 |
-
Kyrgyz
|
160 |
-
Latin
|
161 |
-
Lao
|
162 |
-
Lithuanian
|
163 |
-
Latvian
|
164 |
-
Malagasy
|
165 |
-
Macedonian
|
166 |
-
Malayalam
|
167 |
-
Mongolian
|
168 |
-
Marathi
|
169 |
-
Malay
|
170 |
-
Burmese
|
171 |
-
Burmese (Romanized)
|
172 |
-
Nepali
|
173 |
-
Dutch
|
174 |
-
Norwegian
|
175 |
-
Oromo
|
176 |
-
Oriya
|
177 |
-
Punjabi
|
178 |
-
Polish
|
179 |
-
Pashto
|
180 |
-
Portuguese
|
181 |
-
Romanian
|
182 |
-
Russian
|
183 |
-
Sanskrit
|
184 |
-
Sindhi
|
185 |
-
Sinhala
|
186 |
-
Slovak
|
187 |
-
Slovenian
|
188 |
-
Somali
|
189 |
-
Albanian
|
190 |
-
Serbian
|
191 |
-
Sundanese
|
192 |
-
Swedish
|
193 |
-
Swahili
|
194 |
-
Tamil
|
195 |
-
Tamil (Romanized)
|
196 |
-
Telugu
|
197 |
-
Telugu (Romanized)
|
198 |
-
Thai
|
199 |
-
Filipino
|
200 |
-
Turkish
|
201 |
-
Uyghur
|
202 |
-
Ukrainian
|
203 |
-
Urdu
|
204 |
-
Urdu (Romanized)
|
205 |
-
Uzbek
|
206 |
-
Vietnamese
|
207 |
-
Xhosa
|
208 |
-
Yiddish
|
209 |
-
Chinese (Simplified)
|
210 |
-
Chinese (Traditional)
|
211 |
-
Cantonese"""
|
212 |
-
languages = [language.strip() for language in languages.split("\n")]
|
213 |
-
|
214 |
-
# Walk through the input directory
|
215 |
-
for root, _, files in os.walk(input_dir):
|
216 |
-
# Construct the corresponding path in the output folder
|
217 |
-
relative_path = os.path.relpath(root, input_dir)
|
218 |
-
output_path = os.path.join(output_dir, relative_path)
|
219 |
-
|
220 |
-
# Create the output directory if it doesn't exist
|
221 |
-
os.makedirs(output_path, exist_ok=True)
|
222 |
-
|
223 |
-
for file in files:
|
224 |
-
if file.endswith('.json'):
|
225 |
-
input_file = os.path.join(root, file)
|
226 |
-
output_file = os.path.join(output_path, file)
|
227 |
-
|
228 |
-
try:
|
229 |
-
# Read the JSON file
|
230 |
-
with open(input_file, 'r', encoding='utf-8') as f:
|
231 |
-
metadata = json.load(f)
|
232 |
-
|
233 |
-
# Randomly select a language from the list of languages
|
234 |
-
language = random.choice(languages)
|
235 |
-
|
236 |
-
# Process the JSON data
|
237 |
-
processed_metadata = process_json(metadata, language)
|
238 |
-
|
239 |
-
# Write the processed JSON to the output file
|
240 |
-
with open(output_file, 'w', encoding='utf-8') as f:
|
241 |
-
json.dump(processed_metadata, f, indent=4, ensure_ascii=False)
|
242 |
-
|
243 |
-
print(f"Processed: {input_file} -> {output_file}")
|
244 |
-
|
245 |
-
except Exception as e:
|
246 |
-
print(f"Failed to process {input_file}: {e}")
|
247 |
-
log_error(input_file, str(e))
|
248 |
-
|
249 |
-
if __name__ == "__main__":
|
250 |
-
process_files(input_dir)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
process_data/utils/abc2xml.py
DELETED
The diff for this file is too large to render.
See raw diff
|
|
process_data/utils/pyparsing.py
DELETED
The diff for this file is too large to render.
See raw diff
|
|
process_data/utils/xml2abc.py
DELETED
@@ -1,1582 +0,0 @@
|
|
1 |
-
#!/usr/bin/env python
|
2 |
-
# coding=latin-1
|
3 |
-
'''
|
4 |
-
Copyright (C) 2012-2018: W.G. Vree
|
5 |
-
Contributions: M. Tarenskeen, N. Liberg, Paul Villiger, Janus Meuris, Larry Myerscough,
|
6 |
-
Dick Jackson, Jan Wybren de Jong, Mark Zealey.
|
7 |
-
|
8 |
-
This program is free software; you can redistribute it and/or modify it under the terms of the
|
9 |
-
Lesser GNU General Public License as published by the Free Software Foundation;
|
10 |
-
|
11 |
-
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
12 |
-
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
13 |
-
See the Lesser GNU General Public License for more details. <http://www.gnu.org/licenses/lgpl.html>.
|
14 |
-
'''
|
15 |
-
|
16 |
-
try: import xml.etree.cElementTree as E
|
17 |
-
except: import xml.etree.ElementTree as E
|
18 |
-
import os, sys, types, re, math
|
19 |
-
|
20 |
-
VERSION = 143
|
21 |
-
|
22 |
-
python3 = sys.version_info.major > 2
|
23 |
-
if python3:
|
24 |
-
tupletype = tuple
|
25 |
-
listtype = list
|
26 |
-
max_int = sys.maxsize
|
27 |
-
else:
|
28 |
-
tupletype = types.TupleType
|
29 |
-
listtype = types.ListType
|
30 |
-
max_int = sys.maxint
|
31 |
-
|
32 |
-
note_ornamentation_map = { # for notations/, modified from EasyABC
|
33 |
-
'ornaments/trill-mark': 'T',
|
34 |
-
'ornaments/mordent': 'M',
|
35 |
-
'ornaments/inverted-mordent': 'P',
|
36 |
-
'ornaments/turn': '!turn!',
|
37 |
-
'ornaments/inverted-turn': '!invertedturn!',
|
38 |
-
'technical/up-bow': 'u',
|
39 |
-
'technical/down-bow': 'v',
|
40 |
-
'technical/harmonic': '!open!',
|
41 |
-
'technical/open-string': '!open!',
|
42 |
-
'technical/stopped': '!plus!',
|
43 |
-
'technical/snap-pizzicato': '!snap!',
|
44 |
-
'technical/thumb-position': '!thumb!',
|
45 |
-
'articulations/accent': '!>!',
|
46 |
-
'articulations/strong-accent':'!^!',
|
47 |
-
'articulations/staccato': '.',
|
48 |
-
'articulations/staccatissimo':'!wedge!',
|
49 |
-
'articulations/scoop': '!slide!',
|
50 |
-
'fermata': '!fermata!',
|
51 |
-
'arpeggiate': '!arpeggio!',
|
52 |
-
'articulations/tenuto': '!tenuto!',
|
53 |
-
'articulations/staccatissimo':'!wedge!', # not sure whether this is the right translation
|
54 |
-
'articulations/spiccato': '!wedge!', # not sure whether this is the right translation
|
55 |
-
'articulations/breath-mark': '!breath!', # this may need to be tested to make sure it appears on the right side of the note
|
56 |
-
'articulations/detached-legato': '!tenuto!.',
|
57 |
-
}
|
58 |
-
|
59 |
-
dynamics_map = { # for direction/direction-type/dynamics/
|
60 |
-
'p': '!p!',
|
61 |
-
'pp': '!pp!',
|
62 |
-
'ppp': '!ppp!',
|
63 |
-
'pppp': '!pppp!',
|
64 |
-
'f': '!f!',
|
65 |
-
'ff': '!ff!',
|
66 |
-
'fff': '!fff!',
|
67 |
-
'ffff': '!ffff!',
|
68 |
-
'mp': '!mp!',
|
69 |
-
'mf': '!mf!',
|
70 |
-
'sfz': '!sfz!',
|
71 |
-
}
|
72 |
-
|
73 |
-
percSvg = '''%%beginsvg
|
74 |
-
<defs>
|
75 |
-
<text id="x" x="-3" y="0"></text>
|
76 |
-
<text id="x-" x="-3" y="0"></text>
|
77 |
-
<text id="x+" x="-3" y="0"></text>
|
78 |
-
<text id="normal" x="-3.7" y="0"></text>
|
79 |
-
<text id="normal-" x="-3.7" y="0"></text>
|
80 |
-
<text id="normal+" x="-3.7" y="0"></text>
|
81 |
-
<g id="circle-x"><text x="-3" y="0"></text><circle r="4" class="stroke"></circle></g>
|
82 |
-
<g id="circle-x-"><text x="-3" y="0"></text><circle r="4" class="stroke"></circle></g>
|
83 |
-
<path id="triangle" d="m-4 -3.2l4 6.4 4 -6.4z" class="stroke" style="stroke-width:1.4"></path>
|
84 |
-
<path id="triangle-" d="m-4 -3.2l4 6.4 4 -6.4z" class="stroke" style="stroke-width:1.4"></path>
|
85 |
-
<path id="triangle+" d="m-4 -3.2l4 6.4 4 -6.4z" class="stroke" style="fill:#000"></path>
|
86 |
-
<path id="square" d="m-3.5 3l0 -6.2 7.2 0 0 6.2z" class="stroke" style="stroke-width:1.4"></path>
|
87 |
-
<path id="square-" d="m-3.5 3l0 -6.2 7.2 0 0 6.2z" class="stroke" style="stroke-width:1.4"></path>
|
88 |
-
<path id="square+" d="m-3.5 3l0 -6.2 7.2 0 0 6.2z" class="stroke" style="fill:#000"></path>
|
89 |
-
<path id="diamond" d="m0 -3l4.2 3.2 -4.2 3.2 -4.2 -3.2z" class="stroke" style="stroke-width:1.4"></path>
|
90 |
-
<path id="diamond-" d="m0 -3l4.2 3.2 -4.2 3.2 -4.2 -3.2z" class="stroke" style="stroke-width:1.4"></path>
|
91 |
-
<path id="diamond+" d="m0 -3l4.2 3.2 -4.2 3.2 -4.2 -3.2z" class="stroke" style="fill:#000"></path>
|
92 |
-
</defs>
|
93 |
-
%%endsvg'''
|
94 |
-
|
95 |
-
tabSvg = '''%%beginsvg
|
96 |
-
<style type="text/css">
|
97 |
-
.bf {font-family:sans-serif; font-size:7px}
|
98 |
-
</style>
|
99 |
-
<defs>
|
100 |
-
<rect id="clr" x="-3" y="-1" width="6" height="5" fill="white"></rect>
|
101 |
-
<rect id="clr2" x="-3" y="-1" width="11" height="5" fill="white"></rect>'''
|
102 |
-
|
103 |
-
kopSvg = '<g id="kop%s" class="bf"><use xlink:href="#clr"></use><text x="-2" y="3">%s</text></g>\n'
|
104 |
-
kopSvg2 = '<g id="kop%s" class="bf"><use xlink:href="#clr2"></use><text x="-2" y="3">%s</text></g>\n'
|
105 |
-
|
106 |
-
def info (s, warn=1): sys.stderr.write ((warn and '-- ' or '') + s + '\n')
|
107 |
-
|
108 |
-
#-------------------
|
109 |
-
# data abstractions
|
110 |
-
#-------------------
|
111 |
-
class Measure:
|
112 |
-
def __init__ (s, p):
|
113 |
-
s.reset ()
|
114 |
-
s.ixp = p # part number
|
115 |
-
s.ixm = 0 # measure number
|
116 |
-
s.mdur = 0 # measure duration (nominal metre value in divisions)
|
117 |
-
s.divs = 0 # number of divisions per 1/4
|
118 |
-
s.mtr = 4,4 # meter
|
119 |
-
|
120 |
-
def reset (s): # reset each measure
|
121 |
-
s.attr = '' # measure signatures, tempo
|
122 |
-
s.lline = '' # left barline, but only holds ':' at start of repeat, otherwise empty
|
123 |
-
s.rline = '|' # right barline
|
124 |
-
s.lnum = '' # (left) volta number
|
125 |
-
|
126 |
-
class Note:
|
127 |
-
def __init__ (s, dur=0, n=None):
|
128 |
-
s.tijd = 0 # the time in XML division units
|
129 |
-
s.dur = dur # duration of a note in XML divisions
|
130 |
-
s.fact = None # time modification for tuplet notes (num, div)
|
131 |
-
s.tup = [''] # start(s) and/or stop(s) of tuplet
|
132 |
-
s.tupabc = '' # abc tuplet string to issue before note
|
133 |
-
s.beam = 0 # 1 = beamed
|
134 |
-
s.grace = 0 # 1 = grace note
|
135 |
-
s.before = [] # abc string that goes before the note/chord
|
136 |
-
s.after = '' # the same after the note/chord
|
137 |
-
s.ns = n and [n] or [] # notes in the chord
|
138 |
-
s.lyrs = {} # {number -> syllabe}
|
139 |
-
s.tab = None # (string number, fret number)
|
140 |
-
s.ntdec = '' # !string!, !courtesy!
|
141 |
-
|
142 |
-
class Elem:
|
143 |
-
def __init__ (s, string):
|
144 |
-
s.tijd = 0 # the time in XML division units
|
145 |
-
s.str = string # any abc string that is not a note
|
146 |
-
|
147 |
-
class Counter:
|
148 |
-
def inc (s, key, voice): s.counters [key][voice] = s.counters [key].get (voice, 0) + 1
|
149 |
-
def clear (s, vnums): # reset all counters
|
150 |
-
tups = list( zip (vnums.keys (), len (vnums) * [0]))
|
151 |
-
s.counters = {'note': dict (tups), 'nopr': dict (tups), 'nopt': dict (tups)}
|
152 |
-
def getv (s, key, voice): return s.counters[key][voice]
|
153 |
-
def prcnt (s, ip): # print summary of all non zero counters
|
154 |
-
for iv in s.counters ['note']:
|
155 |
-
if s.getv ('nopr', iv) != 0:
|
156 |
-
info ( 'part %d, voice %d has %d skipped non printable notes' % (ip, iv, s.getv ('nopr', iv)))
|
157 |
-
if s.getv ('nopt', iv) != 0:
|
158 |
-
info ( 'part %d, voice %d has %d notes without pitch' % (ip, iv, s.getv ('nopt', iv)))
|
159 |
-
if s.getv ('note', iv) == 0: # no real notes counted in this voice
|
160 |
-
info ( 'part %d, skipped empty voice %d' % (ip, iv))
|
161 |
-
|
162 |
-
class Music:
|
163 |
-
def __init__(s, options):
|
164 |
-
s.tijd = 0 # the current time
|
165 |
-
s.maxtime = 0 # maximum time in a measure
|
166 |
-
s.gMaten = [] # [voices,.. for all measures in a part]
|
167 |
-
s.gLyrics = [] # [{num: (abc_lyric_string, melis)},.. for all measures in a part]
|
168 |
-
s.vnums = {} # all used voice id's in a part (xml voice id's == numbers)
|
169 |
-
s.cnt = Counter () # global counter object
|
170 |
-
s.vceCnt = 1 # the global voice count over all parts
|
171 |
-
s.lastnote = None # the last real note record inserted in s.voices
|
172 |
-
s.bpl = options.b # the max number of bars per line when writing abc
|
173 |
-
s.cpl = options.n # the number of chars per line when writing abc
|
174 |
-
s.repbra = 0 # true if volta is used somewhere
|
175 |
-
s.nvlt = options.v # no volta on higher voice numbers
|
176 |
-
s.jscript = options.j # compatibility with javascript version
|
177 |
-
|
178 |
-
def initVoices (s, newPart=0):
|
179 |
-
s.vtimes, s.voices, s.lyrics = {}, {}, {}
|
180 |
-
for v in s.vnums:
|
181 |
-
s.vtimes [v] = 0 # {voice: the end time of the last item in each voice}
|
182 |
-
s.voices [v] = [] # {voice: [Note|Elem, ..]}
|
183 |
-
s.lyrics [v] = [] # {voice: [{num: syl}, ..]}
|
184 |
-
if newPart: s.cnt.clear (s.vnums) # clear counters once per part
|
185 |
-
|
186 |
-
def incTime (s, dt):
|
187 |
-
s.tijd += dt
|
188 |
-
if s.tijd < 0: s.tijd = 0 # erroneous <backup> element
|
189 |
-
if s.tijd > s.maxtime: s.maxtime = s.tijd
|
190 |
-
|
191 |
-
def appendElemCv (s, voices, elem):
|
192 |
-
for v in voices:
|
193 |
-
s.appendElem (v, elem) # insert element in all voices
|
194 |
-
|
195 |
-
def insertElem (s, v, elem): # insert at the start of voice v in the current measure
|
196 |
-
obj = Elem (elem)
|
197 |
-
obj.tijd = 0 # because voice is sorted later
|
198 |
-
s.voices [v].insert (0, obj)
|
199 |
-
|
200 |
-
def appendObj (s, v, obj, dur):
|
201 |
-
obj.tijd = s.tijd
|
202 |
-
s.voices [v].append (obj)
|
203 |
-
s.incTime (dur)
|
204 |
-
if s.tijd > s.vtimes[v]: s.vtimes[v] = s.tijd # don't update for inserted earlier items
|
205 |
-
|
206 |
-
def appendElem (s, v, elem, tel=0):
|
207 |
-
s.appendObj (v, Elem (elem), 0)
|
208 |
-
if tel: s.cnt.inc ('note', v) # count number of certain elements in each voice (in addition to notes)
|
209 |
-
|
210 |
-
def appendElemT (s, v, elem, tijd): # insert element at specified time
|
211 |
-
obj = Elem (elem)
|
212 |
-
obj.tijd = tijd
|
213 |
-
s.voices [v].append (obj)
|
214 |
-
|
215 |
-
def appendNote (s, v, note, noot):
|
216 |
-
note.ns.append (note.ntdec + noot)
|
217 |
-
s.appendObj (v, note, int (note.dur))
|
218 |
-
s.lastnote = note # remember last note/rest for later modifications (chord, grace)
|
219 |
-
if noot != 'z' and noot != 'x': # real notes and grace notes
|
220 |
-
s.cnt.inc ('note', v) # count number of real notes in each voice
|
221 |
-
if not note.grace: # for every real note
|
222 |
-
s.lyrics[v].append (note.lyrs) # even when it has no lyrics
|
223 |
-
|
224 |
-
def getLastRec (s, voice):
|
225 |
-
if s.gMaten: return s.gMaten[-1][voice][-1] # the last record in the last measure
|
226 |
-
return None # no previous records in the first measure
|
227 |
-
|
228 |
-
def getLastMelis (s, voice, num): # get melisma of last measure
|
229 |
-
if s.gLyrics:
|
230 |
-
lyrdict = s.gLyrics[-1][voice] # the previous lyrics dict in this voice
|
231 |
-
if num in lyrdict: return lyrdict[num][1] # lyrdict = num -> (lyric string, melisma)
|
232 |
-
return 0 # no previous lyrics in voice or line number
|
233 |
-
|
234 |
-
def addChord (s, note, noot): # careful: we assume that chord notes follow immediately
|
235 |
-
for d in note.before: # put all decorations before chord
|
236 |
-
if d not in s.lastnote.before:
|
237 |
-
s.lastnote.before += [d]
|
238 |
-
s.lastnote.ns.append (note.ntdec + noot)
|
239 |
-
|
240 |
-
def addBar (s, lbrk, m): # linebreak, measure data
|
241 |
-
if m.mdur and s.maxtime > m.mdur: info ('measure %d in part %d longer than metre' % (m.ixm+1, m.ixp+1))
|
242 |
-
s.tijd = s.maxtime # the time of the bar lines inserted here
|
243 |
-
for v in s.vnums:
|
244 |
-
if m.lline or m.lnum: # if left barline or left volta number
|
245 |
-
p = s.getLastRec (v) # get the previous barline record
|
246 |
-
if p: # in measure 1 no previous measure is available
|
247 |
-
x = p.str # p.str is the ABC barline string
|
248 |
-
if m.lline: # append begin of repeat, m.lline == ':'
|
249 |
-
x = (x + m.lline).replace (':|:','::').replace ('||','|')
|
250 |
-
if s.nvlt == 3: # add volta number only to lowest voice in part 0
|
251 |
-
if m.ixp + v == min (s.vnums): x += m.lnum
|
252 |
-
elif m.lnum: # new behaviour with I:repbra 0
|
253 |
-
x += m.lnum # add volta number(s) or text to all voices
|
254 |
-
s.repbra = 1 # signal occurrence of a volta
|
255 |
-
p.str = x # modify previous right barline
|
256 |
-
elif m.lline: # begin of new part and left repeat bar is required
|
257 |
-
s.insertElem (v, '|:')
|
258 |
-
if lbrk:
|
259 |
-
p = s.getLastRec (v) # get the previous barline record
|
260 |
-
if p: p.str += lbrk # insert linebreak char after the barlines+volta
|
261 |
-
if m.attr: # insert signatures at front of buffer
|
262 |
-
s.insertElem (v, '%s' % m.attr)
|
263 |
-
s.appendElem (v, ' %s' % m.rline) # insert current barline record at time maxtime
|
264 |
-
s.voices[v] = sortMeasure (s.voices[v], m) # make all times consistent
|
265 |
-
lyrs = s.lyrics[v] # [{number: sylabe}, .. for all notes]
|
266 |
-
lyrdict = {} # {number: (abc_lyric_string, melis)} for this voice
|
267 |
-
nums = [num for d in lyrs for num in d.keys ()] # the lyrics numbers in this measure
|
268 |
-
maxNums = max (nums + [0]) # the highest lyrics number in this measure
|
269 |
-
for i in range (maxNums, 0, -1):
|
270 |
-
xs = [syldict.get (i, '') for syldict in lyrs] # collect the syllabi with number i
|
271 |
-
melis = s.getLastMelis (v, i) # get melisma from last measure
|
272 |
-
lyrdict [i] = abcLyr (xs, melis)
|
273 |
-
s.lyrics[v] = lyrdict # {number: (abc_lyric_string, melis)} for this measure
|
274 |
-
mkBroken (s.voices[v])
|
275 |
-
s.gMaten.append (s.voices)
|
276 |
-
s.gLyrics.append (s.lyrics)
|
277 |
-
s.tijd = s.maxtime = 0
|
278 |
-
s.initVoices ()
|
279 |
-
|
280 |
-
def outVoices (s, divs, ip, isSib): # output all voices of part ip
|
281 |
-
vvmap = {} # xml voice number -> abc voice number (one part)
|
282 |
-
vnum_keys = list (s.vnums.keys ())
|
283 |
-
if s.jscript or isSib: vnum_keys.sort ()
|
284 |
-
lvc = min (vnum_keys or [1]) # lowest xml voice number of this part
|
285 |
-
for iv in vnum_keys:
|
286 |
-
if s.cnt.getv ('note', iv) == 0: # no real notes counted in this voice
|
287 |
-
continue # skip empty voices
|
288 |
-
if abcOut.denL: unitL = abcOut.denL # take the unit length from the -d option
|
289 |
-
else: unitL = compUnitLength (iv, s.gMaten, divs) # compute the best unit length for this voice
|
290 |
-
abcOut.cmpL.append (unitL) # remember for header output
|
291 |
-
vn, vl = [], {} # for voice iv: collect all notes to vn and all lyric lines to vl
|
292 |
-
for im in range (len (s.gMaten)):
|
293 |
-
measure = s.gMaten [im][iv]
|
294 |
-
vn.append (outVoice (measure, divs [im], im, ip, unitL))
|
295 |
-
checkMelismas (s.gLyrics, s.gMaten, im, iv)
|
296 |
-
for n, (lyrstr, melis) in s.gLyrics [im][iv].items ():
|
297 |
-
if n in vl:
|
298 |
-
while len (vl[n]) < im: vl[n].append ('') # fill in skipped measures
|
299 |
-
vl[n].append (lyrstr)
|
300 |
-
else:
|
301 |
-
vl[n] = im * [''] + [lyrstr] # must skip im measures
|
302 |
-
for n, lyrs in vl.items (): # fill up possibly empty lyric measures at the end
|
303 |
-
mis = len (vn) - len (lyrs)
|
304 |
-
lyrs += mis * ['']
|
305 |
-
abcOut.add ('V:%d' % s.vceCnt)
|
306 |
-
if s.repbra:
|
307 |
-
if s.nvlt == 1 and s.vceCnt > 1: abcOut.add ('I:repbra 0') # only volta on first voice
|
308 |
-
if s.nvlt == 2 and iv > lvc: abcOut.add ('I:repbra 0') # only volta on first voice of each part
|
309 |
-
if s.cpl > 0: s.bpl = 0 # option -n (max chars per line) overrules -b (max bars per line)
|
310 |
-
elif s.bpl == 0: s.cpl = 100 # the default: 100 chars per line
|
311 |
-
bn = 0 # count bars
|
312 |
-
while vn: # while still measures available
|
313 |
-
ib = 1
|
314 |
-
chunk = vn [0]
|
315 |
-
while ib < len (vn):
|
316 |
-
if s.cpl > 0 and len (chunk) + len (vn [ib]) >= s.cpl: break # line full (number of chars)
|
317 |
-
if s.bpl > 0 and ib >= s.bpl: break # line full (number of bars)
|
318 |
-
chunk += vn [ib]
|
319 |
-
ib += 1
|
320 |
-
bn += ib
|
321 |
-
abcOut.add (chunk + ' %%%d' % bn) # line with barnumer
|
322 |
-
del vn[:ib] # chop ib bars
|
323 |
-
lyrlines = sorted (vl.items ()) # order the numbered lyric lines for output
|
324 |
-
for n, lyrs in lyrlines:
|
325 |
-
abcOut.add ('w: ' + '|'.join (lyrs[:ib]) + '|')
|
326 |
-
del lyrs[:ib]
|
327 |
-
vvmap [iv] = s.vceCnt # xml voice number -> abc voice number
|
328 |
-
s.vceCnt += 1 # count voices over all parts
|
329 |
-
s.gMaten = [] # reset the follwing instance vars for each part
|
330 |
-
s.gLyrics = []
|
331 |
-
s.cnt.prcnt (ip+1) # print summary of skipped items in this part
|
332 |
-
return vvmap
|
333 |
-
|
334 |
-
class ABCoutput:
|
335 |
-
pagekeys = 'scale,pageheight,pagewidth,leftmargin,rightmargin,topmargin,botmargin'.split (',')
|
336 |
-
def __init__ (s, fnmext, pad, X, options):
|
337 |
-
s.fnmext = fnmext
|
338 |
-
s.outlist = [] # list of ABC strings
|
339 |
-
s.title = 'T:Title'
|
340 |
-
s.key = 'none'
|
341 |
-
s.clefs = {} # clefs for all abc-voices
|
342 |
-
s.mtr = 'none'
|
343 |
-
s.tempo = 0 # 0 -> no tempo field
|
344 |
-
s.tempo_units = (1,4) # note type of tempo direction
|
345 |
-
s.pad = pad # the output path or none
|
346 |
-
s.X = X + 1 # the abc tune number
|
347 |
-
s.denL = options.d # denominator of the unit length (L:) from -d option
|
348 |
-
s.volpan = int (options.m) # 0 -> no %%MIDI, 1 -> only program, 2 -> all %%MIDI
|
349 |
-
s.cmpL = [] # computed optimal unit length for all voices
|
350 |
-
s.jscript = options.j # compatibility with javascript version
|
351 |
-
s.tstep = options.t # translate percmap to voicemap
|
352 |
-
s.stemless = 0 # use U:s=!stemless!
|
353 |
-
s.shiftStem = options.s # shift note heads 3 units left
|
354 |
-
if pad:
|
355 |
-
_, base_name = os.path.split (fnmext)
|
356 |
-
s.outfile = open (os.path.join (pad, base_name), 'w')
|
357 |
-
else: s.outfile = sys.stdout
|
358 |
-
if s.jscript: s.X = 1 # always X:1 in javascript version
|
359 |
-
s.pageFmt = {}
|
360 |
-
for k in s.pagekeys: s.pageFmt [k] = None
|
361 |
-
if len (options.p) == 7:
|
362 |
-
for k, v in zip (s.pagekeys, options.p):
|
363 |
-
try: s.pageFmt [k] = float (v)
|
364 |
-
except: info ('illegal float %s for %s', (k, v)); continue
|
365 |
-
|
366 |
-
def add (s, str):
|
367 |
-
s.outlist.append (str + '\n') # collect all ABC output
|
368 |
-
|
369 |
-
def mkHeader (s, stfmap, partlist, midimap, vmpdct, koppen): # stfmap = [parts], part = [staves], stave = [voices]
|
370 |
-
accVce, accStf, staffs = [], [], stfmap[:] # staffs is consumed
|
371 |
-
for x in partlist: # collect partnames into accVce and staff groups into accStf
|
372 |
-
try: prgroupelem (x, ('', ''), '', stfmap, accVce, accStf)
|
373 |
-
except: info ('lousy musicxml: error in part-list')
|
374 |
-
staves = ' '.join (accStf)
|
375 |
-
clfnms = {}
|
376 |
-
for part, (partname, partabbrv) in zip (staffs, accVce):
|
377 |
-
if not part: continue # skip empty part
|
378 |
-
firstVoice = part[0][0] # the first voice number in this part
|
379 |
-
nm = partname.replace ('\n','\\n').replace ('.:','.').strip (':')
|
380 |
-
snm = partabbrv.replace ('\n','\\n').replace ('.:','.').strip (':')
|
381 |
-
clfnms [firstVoice] = (nm and 'nm="%s"' % nm or '') + (snm and ' snm="%s"' % snm or '')
|
382 |
-
hd = ['X:%d\n%s\n' % (s.X, s.title)]
|
383 |
-
for i, k in enumerate (s.pagekeys):
|
384 |
-
if s.jscript and k in ['pageheight','topmargin', 'botmargin']: continue
|
385 |
-
if s.pageFmt [k] != None: hd.append ('%%%%%s %.2f%s\n' % (k, s.pageFmt [k], i > 0 and 'cm' or ''))
|
386 |
-
if staves and len (accStf) > 1: hd.append ('%%score ' + staves + '\n')
|
387 |
-
tempo = s.tempo and 'Q:%d/%d=%s\n' % (s.tempo_units [0], s.tempo_units [1], s.tempo) or '' # default no tempo field
|
388 |
-
d = {} # determine the most frequently occurring unit length over all voices
|
389 |
-
for x in s.cmpL: d[x] = d.get (x, 0) + 1
|
390 |
-
if s.jscript: defLs = sorted (d.items (), key=lambda x: (-x[1], x[0])) # when tie (1) sort on key (0)
|
391 |
-
else: defLs = sorted (d.items (), key=lambda x: -x[1])
|
392 |
-
defL = s.denL and s.denL or defLs [0][0] # override default unit length with -d option
|
393 |
-
hd.append ('L:1/%d\n%sM:%s\n' % (defL, tempo, s.mtr))
|
394 |
-
hd.append ('K:%s\n' % s.key)
|
395 |
-
if s.stemless: hd.append ('U:s=!stemless!\n')
|
396 |
-
vxs = sorted (vmpdct.keys ())
|
397 |
-
for vx in vxs: hd.extend (vmpdct [vx])
|
398 |
-
s.dojef = 0 # translate percmap to voicemap
|
399 |
-
for vnum, clef in s.clefs.items ():
|
400 |
-
ch, prg, vol, pan = midimap [vnum-1][:4]
|
401 |
-
dmap = midimap [vnum - 1][4:] # map of abc percussion notes to midi notes
|
402 |
-
if dmap and 'perc' not in clef: clef = (clef + ' map=perc').strip ();
|
403 |
-
hd.append ('V:%d %s %s\n' % (vnum, clef, clfnms.get (vnum, '')))
|
404 |
-
if vnum in vmpdct:
|
405 |
-
hd.append ('%%%%voicemap tab%d\n' % vnum)
|
406 |
-
hd.append ('K:none\nM:none\n%%clef none\n%%staffscale 1.6\n%%flatbeams true\n%%stemdir down\n')
|
407 |
-
if 'perc' in clef: hd.append ('K:none\n'); # no key for a perc voice
|
408 |
-
if s.volpan > 1: # option -m 2 -> output all recognized midi commands when needed and present in xml
|
409 |
-
if ch > 0 and ch != vnum: hd.append ('%%%%MIDI channel %d\n' % ch)
|
410 |
-
if prg > 0: hd.append ('%%%%MIDI program %d\n' % (prg - 1))
|
411 |
-
if vol >= 0: hd.append ('%%%%MIDI control 7 %.0f\n' % vol) # volume == 0 is possible ...
|
412 |
-
if pan >= 0: hd.append ('%%%%MIDI control 10 %.0f\n' % pan)
|
413 |
-
elif s.volpan > 0: # default -> only output midi program command when present in xml
|
414 |
-
if dmap and ch > 0: hd.append ('%%%%MIDI channel %d\n' % ch) # also channel if percussion part
|
415 |
-
if prg > 0: hd.append ('%%%%MIDI program %d\n' % (prg - 1))
|
416 |
-
for abcNote, step, midiNote, notehead in dmap:
|
417 |
-
if not notehead: notehead = 'normal'
|
418 |
-
if abcMid (abcNote) != midiNote or abcNote != step:
|
419 |
-
if s.volpan > 0: hd.append ('%%%%MIDI drummap %s %s\n' % (abcNote, midiNote))
|
420 |
-
hd.append ('I:percmap %s %s %s %s\n' % (abcNote, step, midiNote, notehead))
|
421 |
-
s.dojef = s.tstep
|
422 |
-
if defL != s.cmpL [vnum-1]: # only if computed unit length different from header
|
423 |
-
hd.append ('L:1/%d\n' % s.cmpL [vnum-1])
|
424 |
-
s.outlist = hd + s.outlist
|
425 |
-
if koppen: # output SVG stuff needed for tablature
|
426 |
-
k1 = kopSvg.replace ('-2','-5') if s.shiftStem else kopSvg # shift note heads 3 units left
|
427 |
-
k2 = kopSvg2.replace ('-2','-5') if s.shiftStem else kopSvg2
|
428 |
-
tb = tabSvg.replace ('-3','-6') if s.shiftStem else tabSvg
|
429 |
-
ks = sorted (koppen.keys ()) # javascript compatibility
|
430 |
-
ks = [k2 % (k, k) if len (k) == 2 else k1 % (k, k) for k in ks]
|
431 |
-
tbs = map (lambda x: x.strip () + '\n', tb.splitlines ()) # javascript compatibility
|
432 |
-
s.outlist = tbs + ks + ['</defs>\n%%endsvg\n'] + s.outlist
|
433 |
-
|
434 |
-
def writeall (s): # determine the required encoding of the entire ABC output
|
435 |
-
str = ''.join (s.outlist)
|
436 |
-
if s.dojef: str = perc2map (str)
|
437 |
-
if python3: s.outfile.write (str)
|
438 |
-
else: s.outfile.write (str.encode ('utf-8'))
|
439 |
-
if s.pad: s.outfile.close () # close each file with -o option
|
440 |
-
else: s.outfile.write ('\n') # add empty line between tunes on stdout
|
441 |
-
info ('%s written with %d voices' % (s.fnmext, len (s.clefs)), warn=0)
|
442 |
-
|
443 |
-
#----------------
|
444 |
-
# functions
|
445 |
-
#----------------
|
446 |
-
def abcLyr (xs, melis): # Convert list xs to abc lyrics.
|
447 |
-
if not ''.join (xs): return '', 0 # there is no lyrics in this measure
|
448 |
-
res = []
|
449 |
-
for x in xs: # xs has for every note a lyrics syllabe or an empty string
|
450 |
-
if x == '': # note without lyrics
|
451 |
-
if melis: x = '_' # set melisma
|
452 |
-
else: x = '*' # skip note
|
453 |
-
elif x.endswith ('_') and not x.endswith ('\_'): # start of new melisma
|
454 |
-
x = x.replace ('_', '') # remove and set melis boolean
|
455 |
-
melis = 1 # so next skips will become melisma
|
456 |
-
else: melis = 0 # melisma stops on first syllable
|
457 |
-
res.append (x)
|
458 |
-
return (' '.join (res), melis)
|
459 |
-
|
460 |
-
def simplify (a, b): # divide a and b by their greatest common divisor
|
461 |
-
x, y = a, b
|
462 |
-
while b: a, b = b, a % b
|
463 |
-
return x // a, y // a
|
464 |
-
|
465 |
-
def abcdur (nx, divs, uL): # convert an musicXML duration d to abc units with L:1/uL
|
466 |
-
if nx.dur == 0: return '' # when called for elements without duration
|
467 |
-
num, den = simplify (uL * nx.dur, divs * 4) # L=1/8 -> uL = 8 units
|
468 |
-
if nx.fact: # apply tuplet time modification
|
469 |
-
numfac, denfac = nx.fact
|
470 |
-
num, den = simplify (num * numfac, den * denfac)
|
471 |
-
if den > 64: # limit the denominator to a maximum of 64
|
472 |
-
x = float (num) / den; n = math.floor (x); # when just above an integer n
|
473 |
-
if x - n < 0.1 * x: num, den = n, 1; # round to n
|
474 |
-
num64 = 64. * num / den + 1.0e-15 # to get Python2 behaviour of round
|
475 |
-
num, den = simplify (int (round (num64)), 64)
|
476 |
-
if num == 1:
|
477 |
-
if den == 1: dabc = ''
|
478 |
-
elif den == 2: dabc = '/'
|
479 |
-
else: dabc = '/%d' % den
|
480 |
-
elif den == 1: dabc = '%d' % num
|
481 |
-
else: dabc = '%d/%d' % (num, den)
|
482 |
-
return dabc
|
483 |
-
|
484 |
-
def abcMid (note): # abc note -> midi pitch
|
485 |
-
r = re.search (r"([_^]*)([A-Ga-g])([',]*)", note)
|
486 |
-
if not r: return -1
|
487 |
-
acc, n, oct = r.groups ()
|
488 |
-
nUp = n.upper ()
|
489 |
-
p = 60 + [0,2,4,5,7,9,11]['CDEFGAB'.index (nUp)] + (12 if nUp != n else 0);
|
490 |
-
if acc: p += (1 if acc[0] == '^' else -1) * len (acc)
|
491 |
-
if oct: p += (12 if oct[0] == "'" else -12) * len (oct)
|
492 |
-
return p
|
493 |
-
|
494 |
-
def staffStep (ptc, o, clef, tstep):
|
495 |
-
ndif = 0
|
496 |
-
if 'stafflines=1' in clef: ndif += 4 # meaning of one line: E (xml) -> B (abc)
|
497 |
-
if not tstep and clef.startswith ('bass'): ndif += 12 # transpose bass -> treble (C3 -> A4)
|
498 |
-
if ndif: # diatonic transposition == addition modulo 7
|
499 |
-
nm7 = 'C,D,E,F,G,A,B'.split (',')
|
500 |
-
n = nm7.index (ptc) + ndif
|
501 |
-
ptc, o = nm7 [n % 7], o + n // 7
|
502 |
-
if o > 4: ptc = ptc.lower ()
|
503 |
-
if o > 5: ptc = ptc + (o-5) * "'"
|
504 |
-
if o < 4: ptc = ptc + (4-o) * ","
|
505 |
-
return ptc
|
506 |
-
|
507 |
-
def setKey (fifths, mode):
|
508 |
-
sharpness = ['Fb', 'Cb','Gb','Db','Ab','Eb','Bb','F','C','G','D','A', 'E', 'B', 'F#','C#','G#','D#','A#','E#','B#']
|
509 |
-
offTab = {'maj':8, 'ion':8, 'm':11, 'min':11, 'aeo':11, 'mix':9, 'dor':10, 'phr':12, 'lyd':7, 'loc':13, 'non':8}
|
510 |
-
mode = mode.lower ()[:3] # only first three chars, no case
|
511 |
-
key = sharpness [offTab [mode] + fifths] + (mode if offTab [mode] != 8 else '')
|
512 |
-
accs = ['F','C','G','D','A','E','B']
|
513 |
-
if fifths >= 0: msralts = dict (zip (accs[:fifths], fifths * [1]))
|
514 |
-
else: msralts = dict (zip (accs[fifths:], -fifths * [-1]))
|
515 |
-
return key, msralts
|
516 |
-
|
517 |
-
def insTup (ix, notes, fact): # read one nested tuplet
|
518 |
-
tupcnt = 0
|
519 |
-
nx = notes [ix]
|
520 |
-
if 'start' in nx.tup:
|
521 |
-
nx.tup.remove ('start') # do recursive calls when starts remain
|
522 |
-
tix = ix # index of first tuplet note
|
523 |
-
fn, fd = fact # xml time-mod of the higher level
|
524 |
-
fnum, fden = nx.fact # xml time-mod of the current level
|
525 |
-
tupfact = fnum//fn, fden//fd # abc time mod of this level
|
526 |
-
while ix < len (notes):
|
527 |
-
nx = notes [ix]
|
528 |
-
if isinstance (nx, Elem) or nx.grace:
|
529 |
-
ix += 1 # skip all non tuplet elements
|
530 |
-
continue
|
531 |
-
if 'start' in nx.tup: # more nested tuplets to start
|
532 |
-
ix, tupcntR = insTup (ix, notes, tupfact) # ix is on the stop note!
|
533 |
-
tupcnt += tupcntR
|
534 |
-
elif nx.fact:
|
535 |
-
tupcnt += 1 # count tuplet elements
|
536 |
-
if 'stop' in nx.tup:
|
537 |
-
nx.tup.remove ('stop')
|
538 |
-
break
|
539 |
-
if not nx.fact: # stop on first non tuplet note
|
540 |
-
ix = lastix # back to last tuplet note
|
541 |
-
break
|
542 |
-
lastix = ix
|
543 |
-
ix += 1
|
544 |
-
# put abc tuplet notation before the recursive ones
|
545 |
-
tup = (tupfact[0], tupfact[1], tupcnt)
|
546 |
-
if tup == (3, 2, 3): tupPrefix = '(3'
|
547 |
-
else: tupPrefix = '(%d:%d:%d' % tup
|
548 |
-
notes [tix].tupabc = tupPrefix + notes [tix].tupabc
|
549 |
-
return ix, tupcnt # ix is on the last tuplet note
|
550 |
-
|
551 |
-
def mkBroken (vs): # introduce broken rhythms (vs: one voice, one measure)
|
552 |
-
vs = [n for n in vs if isinstance (n, Note)]
|
553 |
-
i = 0
|
554 |
-
while i < len (vs) - 1:
|
555 |
-
n1, n2 = vs[i], vs[i+1] # scan all adjacent pairs
|
556 |
-
# skip if note in tuplet or has no duration or outside beam
|
557 |
-
if not n1.fact and not n2.fact and n1.dur > 0 and n2.beam:
|
558 |
-
if n1.dur * 3 == n2.dur:
|
559 |
-
n2.dur = (2 * n2.dur) // 3
|
560 |
-
n1.dur = n1.dur * 2
|
561 |
-
n1.after = '<' + n1.after
|
562 |
-
i += 1 # do not chain broken rhythms
|
563 |
-
elif n2.dur * 3 == n1.dur:
|
564 |
-
n1.dur = (2 * n1.dur) // 3
|
565 |
-
n2.dur = n2.dur * 2
|
566 |
-
n1.after = '>' + n1.after
|
567 |
-
i += 1 # do not chain broken rhythms
|
568 |
-
i += 1
|
569 |
-
|
570 |
-
def outVoice (measure, divs, im, ip, unitL): # note/elem objects of one measure in one voice
|
571 |
-
ix = 0
|
572 |
-
while ix < len (measure): # set all (nested) tuplet annotations
|
573 |
-
nx = measure [ix]
|
574 |
-
if isinstance (nx, Note) and nx.fact and not nx.grace:
|
575 |
-
ix, tupcnt = insTup (ix, measure, (1, 1)) # read one tuplet, insert annotation(s)
|
576 |
-
ix += 1
|
577 |
-
vs = []
|
578 |
-
for nx in measure:
|
579 |
-
if isinstance (nx, Note):
|
580 |
-
durstr = abcdur (nx, divs, unitL) # xml -> abc duration string
|
581 |
-
chord = len (nx.ns) > 1
|
582 |
-
cns = [nt[:-1] for nt in nx.ns if nt.endswith ('-')]
|
583 |
-
tie = ''
|
584 |
-
if chord and len (cns) == len (nx.ns): # all chord notes tied
|
585 |
-
nx.ns = cns # chord notes without tie
|
586 |
-
tie = '-' # one tie for whole chord
|
587 |
-
s = nx.tupabc + ''.join (nx.before)
|
588 |
-
if chord: s += '['
|
589 |
-
for nt in nx.ns: s += nt
|
590 |
-
if chord: s += ']' + tie
|
591 |
-
if s.endswith ('-'): s, tie = s[:-1], '-' # split off tie
|
592 |
-
s += durstr + tie # and put it back again
|
593 |
-
s += nx.after
|
594 |
-
nospace = nx.beam
|
595 |
-
else:
|
596 |
-
if isinstance (nx.str, listtype): nx.str = nx.str [0]
|
597 |
-
s = nx.str
|
598 |
-
nospace = 1
|
599 |
-
if nospace: vs.append (s)
|
600 |
-
else: vs.append (' ' + s)
|
601 |
-
vs = ''.join (vs) # ad hoc: remove multiple pedal directions
|
602 |
-
while vs.find ('!ped!!ped!') >= 0: vs = vs.replace ('!ped!!ped!','!ped!')
|
603 |
-
while vs.find ('!ped-up!!ped-up!') >= 0: vs = vs.replace ('!ped-up!!ped-up!','!ped-up!')
|
604 |
-
while vs.find ('!8va(!!8va)!') >= 0: vs = vs.replace ('!8va(!!8va)!','') # remove empty ottava's
|
605 |
-
return vs
|
606 |
-
|
607 |
-
def sortMeasure (voice, m):
|
608 |
-
voice.sort (key=lambda o: o.tijd) # sort on time
|
609 |
-
time = 0
|
610 |
-
v = []
|
611 |
-
rs = [] # holds rests in between notes
|
612 |
-
for i, nx in enumerate (voice): # establish sequentiality
|
613 |
-
if nx.tijd > time and chkbug (nx.tijd - time, m):
|
614 |
-
v.append (Note (nx.tijd - time, 'x')) # fill hole with invisble rest
|
615 |
-
rs.append (len (v) - 1)
|
616 |
-
if isinstance (nx, Elem):
|
617 |
-
if nx.tijd < time: nx.tijd = time # shift elems without duration to where they fit
|
618 |
-
v.append (nx)
|
619 |
-
time = nx.tijd
|
620 |
-
continue
|
621 |
-
if nx.tijd < time: # overlapping element
|
622 |
-
if nx.ns[0] == 'z': continue # discard overlapping rest
|
623 |
-
if v[-1].tijd <= nx.tijd: # we can do something
|
624 |
-
if v[-1].ns[0] == 'z': # shorten rest
|
625 |
-
v[-1].dur = nx.tijd - v[-1].tijd
|
626 |
-
if v[-1].dur == 0: del v[-1] # nothing left
|
627 |
-
info ('overlap in part %d, measure %d: rest shortened' % (m.ixp+1, m.ixm+1))
|
628 |
-
else: # make a chord of overlap
|
629 |
-
v[-1].ns += nx.ns
|
630 |
-
info ('overlap in part %d, measure %d: added chord' % (m.ixp+1, m.ixm+1))
|
631 |
-
nx.dur = (nx.tijd + nx.dur) - time # the remains
|
632 |
-
if nx.dur <= 0: continue # nothing left
|
633 |
-
nx.tijd = time # append remains
|
634 |
-
else: # give up
|
635 |
-
info ('overlapping notes in one voice! part %d, measure %d, note %s discarded' % (m.ixp+1, m.ixm+1, isinstance (nx, Note) and nx.ns or nx.str))
|
636 |
-
continue
|
637 |
-
v.append (nx)
|
638 |
-
if isinstance (nx, Note):
|
639 |
-
if nx.ns [0] in 'zx':
|
640 |
-
rs.append (len (v) - 1) # remember rests between notes
|
641 |
-
elif len (rs):
|
642 |
-
if nx.beam and not nx.grace: # copy beam into rests
|
643 |
-
for j in rs: v[j].beam = nx.beam
|
644 |
-
rs = [] # clear rests on each note
|
645 |
-
time = nx.tijd + nx.dur
|
646 |
-
# when a measure contains no elements and no forwards -> no incTime -> s.maxtime = 0 -> right barline
|
647 |
-
# is inserted at time == 0 (in addbar) and is only element in the voice when sortMeasure is called
|
648 |
-
if time == 0: info ('empty measure in part %d, measure %d, it should contain at least a rest to advance the time!' % (m.ixp+1, m.ixm+1))
|
649 |
-
return v
|
650 |
-
|
651 |
-
def getPartlist (ps): # correct part-list (from buggy xml-software)
|
652 |
-
xs = [] # the corrected part-list
|
653 |
-
e = [] # stack of opened part-groups
|
654 |
-
for x in list (ps): # insert missing stops, delete double starts
|
655 |
-
if x.tag == 'part-group':
|
656 |
-
num, type = x.get ('number'), x.get ('type')
|
657 |
-
if type == 'start':
|
658 |
-
if num in e: # missing stop: insert one
|
659 |
-
xs.append (E.Element ('part-group', number = num, type = 'stop'))
|
660 |
-
xs.append (x)
|
661 |
-
else: # normal start
|
662 |
-
xs.append (x)
|
663 |
-
e.append (num)
|
664 |
-
else:
|
665 |
-
if num in e: # normal stop
|
666 |
-
e.remove (num)
|
667 |
-
xs.append (x)
|
668 |
-
else: pass # double stop: skip it
|
669 |
-
else: xs.append (x)
|
670 |
-
for num in reversed (e): # fill missing stops at the end
|
671 |
-
xs.append (E.Element ('part-group', number = num, type = 'stop'))
|
672 |
-
return xs
|
673 |
-
|
674 |
-
def parseParts (xs, d, e): # -> [elems on current level], rest of xs
|
675 |
-
if not xs: return [],[]
|
676 |
-
x = xs.pop (0)
|
677 |
-
if x.tag == 'part-group':
|
678 |
-
num, type = x.get ('number'), x.get ('type')
|
679 |
-
if type == 'start': # go one level deeper
|
680 |
-
s = [x.findtext (n, '') for n in ['group-symbol','group-barline','group-name','group-abbreviation']]
|
681 |
-
d [num] = s # remember groupdata by group number
|
682 |
-
e.append (num) # make stack of open group numbers
|
683 |
-
elemsnext, rest1 = parseParts (xs, d, e) # parse one level deeper to next stop
|
684 |
-
elems, rest2 = parseParts (rest1, d, e) # parse the rest on this level
|
685 |
-
return [elemsnext] + elems, rest2
|
686 |
-
else: # stop: close level and return group-data
|
687 |
-
nums = e.pop () # last open group number in stack order
|
688 |
-
if xs and xs[0].get ('type') == 'stop': # two consequetive stops
|
689 |
-
if num != nums: # in the wrong order (tempory solution)
|
690 |
-
d[nums], d[num] = d[num], d[nums] # exchange values (only works for two stops!!!)
|
691 |
-
sym = d[num] # retrieve an return groupdata as last element of the group
|
692 |
-
return [sym], xs
|
693 |
-
else:
|
694 |
-
elems, rest = parseParts (xs, d, e) # parse remaining elements on current level
|
695 |
-
name = x.findtext ('part-name',''), x.findtext ('part-abbreviation','')
|
696 |
-
return [name] + elems, rest
|
697 |
-
|
698 |
-
def bracePart (part): # put a brace on multistaff part and group voices
|
699 |
-
if not part: return [] # empty part in the score
|
700 |
-
brace = []
|
701 |
-
for ivs in part:
|
702 |
-
if len (ivs) == 1: # stave with one voice
|
703 |
-
brace.append ('%s' % ivs[0])
|
704 |
-
else: # stave with multiple voices
|
705 |
-
brace += ['('] + ['%s' % iv for iv in ivs] + [')']
|
706 |
-
brace.append ('|')
|
707 |
-
del brace[-1] # no barline at the end
|
708 |
-
if len (part) > 1:
|
709 |
-
brace = ['{'] + brace + ['}']
|
710 |
-
return brace
|
711 |
-
|
712 |
-
def prgroupelem (x, gnm, bar, pmap, accVce, accStf): # collect partnames (accVce) and %%score map (accStf)
|
713 |
-
if type (x) == tupletype: # partname-tuple = (part-name, part-abbrev)
|
714 |
-
y = pmap.pop (0)
|
715 |
-
if gnm[0]: x = [n1 + ':' + n2 for n1, n2 in zip (gnm, x)] # put group-name before part-name
|
716 |
-
accVce.append (x)
|
717 |
-
accStf.extend (bracePart (y))
|
718 |
-
elif len (x) == 2 and type (x[0]) == tupletype: # misuse of group just to add extra name to stave
|
719 |
-
y = pmap.pop (0)
|
720 |
-
nms = [n1 + ':' + n2 for n1, n2 in zip (x[0], x[1][2:])] # x[0] = partname-tuple, x[1][2:] = groupname-tuple
|
721 |
-
accVce.append (nms)
|
722 |
-
accStf.extend (bracePart (y))
|
723 |
-
else:
|
724 |
-
prgrouplist (x, bar, pmap, accVce, accStf)
|
725 |
-
|
726 |
-
def prgrouplist (x, pbar, pmap, accVce, accStf): # collect partnames, scoremap for a part-group
|
727 |
-
sym, bar, gnm, gabbr = x[-1] # bracket symbol, continue barline, group-name-tuple
|
728 |
-
bar = bar == 'yes' or pbar # pbar -> the parent has bar
|
729 |
-
accStf.append (sym == 'brace' and '{' or '[')
|
730 |
-
for z in x[:-1]:
|
731 |
-
prgroupelem (z, (gnm, gabbr), bar, pmap, accVce, accStf)
|
732 |
-
if bar: accStf.append ('|')
|
733 |
-
if bar: del accStf [-1] # remove last one before close
|
734 |
-
accStf.append (sym == 'brace' and '}' or ']')
|
735 |
-
|
736 |
-
def compUnitLength (iv, maten, divs): # compute optimal unit length
|
737 |
-
uLmin, minLen = 0, max_int
|
738 |
-
for uL in [4,8,16]: # try 1/4, 1/8 and 1/16
|
739 |
-
vLen = 0 # total length of abc duration strings in this voice
|
740 |
-
for im, m in enumerate (maten): # all measures
|
741 |
-
for e in m[iv]: # all notes in voice iv
|
742 |
-
if isinstance (e, Elem) or e.dur == 0: continue # no real durations
|
743 |
-
vLen += len (abcdur (e, divs [im], uL)) # add len of duration string
|
744 |
-
if vLen < minLen: uLmin, minLen = uL, vLen # remember the smallest
|
745 |
-
return uLmin
|
746 |
-
|
747 |
-
def doSyllable (syl):
|
748 |
-
txt = ''
|
749 |
-
for e in syl:
|
750 |
-
if e.tag == 'elision': txt += '~'
|
751 |
-
elif e.tag == 'text': # escape - and space characters
|
752 |
-
txt += (e.text or '').replace ('_','\_').replace('-', r'\-').replace(' ', '~')
|
753 |
-
if not txt: return txt
|
754 |
-
if syl.findtext('syllabic') in ['begin', 'middle']: txt += '-'
|
755 |
-
if syl.find('extend') is not None: txt += '_'
|
756 |
-
return txt
|
757 |
-
|
758 |
-
def checkMelismas (lyrics, maten, im, iv):
|
759 |
-
if im == 0: return
|
760 |
-
maat = maten [im][iv] # notes of the current measure
|
761 |
-
curlyr = lyrics [im][iv] # lyrics dict of current measure
|
762 |
-
prvlyr = lyrics [im-1][iv] # lyrics dict of previous measure
|
763 |
-
for n, (lyrstr, melis) in prvlyr.items (): # all lyric numbers in the previous measure
|
764 |
-
if n not in curlyr and melis: # melisma required, but no lyrics present -> make one!
|
765 |
-
ms = getMelisma (maat) # get a melisma for the current measure
|
766 |
-
if ms: curlyr [n] = (ms, 0) # set melisma as the n-th lyrics of the current measure
|
767 |
-
|
768 |
-
def getMelisma (maat): # get melisma from notes in maat
|
769 |
-
ms = []
|
770 |
-
for note in maat: # every note should get an underscore
|
771 |
-
if not isinstance (note, Note): continue # skip Elem's
|
772 |
-
if note.grace: continue # skip grace notes
|
773 |
-
if note.ns [0] in 'zx': break # stop on first rest
|
774 |
-
ms.append ('_')
|
775 |
-
return ' '.join (ms)
|
776 |
-
|
777 |
-
def perc2map (abcIn):
|
778 |
-
fillmap = {'diamond':1, 'triangle':1, 'square':1, 'normal':1};
|
779 |
-
abc = map (lambda x: x.strip (), percSvg.splitlines ())
|
780 |
-
id='default'
|
781 |
-
maps = {'default': []};
|
782 |
-
dmaps = {'default': []}
|
783 |
-
r1 = re.compile (r'V:\s*(\S+)')
|
784 |
-
ls = abcIn.splitlines ()
|
785 |
-
for x in ls:
|
786 |
-
if 'I:percmap' in x:
|
787 |
-
noot, step, midi, kop = map (lambda x: x.strip (), x.split ()[1:])
|
788 |
-
if kop in fillmap: kop = kop + '+' + ',' + kop
|
789 |
-
x = '%%%%map perc%s %s print=%s midi=%s heads=%s' % (id, noot, step, midi, kop)
|
790 |
-
maps [id].append (x)
|
791 |
-
if '%%MIDI' in x: dmaps [id].append (x)
|
792 |
-
if 'V:' in x:
|
793 |
-
r = r1.match (x)
|
794 |
-
if r:
|
795 |
-
id = r.group (1);
|
796 |
-
if id not in maps: maps [id] = []; dmaps [id] = []
|
797 |
-
ids = sorted (maps.keys ())
|
798 |
-
for id in ids: abc += maps [id]
|
799 |
-
id='default'
|
800 |
-
for x in ls:
|
801 |
-
if 'I:percmap' in x: continue
|
802 |
-
if '%%MIDI' in x: continue
|
803 |
-
if 'V:' in x or 'K:' in x:
|
804 |
-
r = r1.match (x)
|
805 |
-
if r: id = r.group (1)
|
806 |
-
abc.append (x)
|
807 |
-
if id in dmaps and len (dmaps [id]) > 0: abc.extend (dmaps [id]); del dmaps [id]
|
808 |
-
if 'perc' in x and 'map=' not in x: x += ' map=perc';
|
809 |
-
if 'map=perc' in x and len (maps [id]) > 0: abc.append ('%%voicemap perc' + id);
|
810 |
-
if 'map=off' in x: abc.append ('%%voicemap');
|
811 |
-
else:
|
812 |
-
abc.append (x)
|
813 |
-
return '\n'.join (abc) + '\n'
|
814 |
-
|
815 |
-
def addoct (ptc, o): # xml staff step, xml octave number
|
816 |
-
p = ptc
|
817 |
-
if o > 4: p = ptc.lower ()
|
818 |
-
if o > 5: p = p + (o-5) * "'"
|
819 |
-
if o < 4: p = p + (4-o) * ","
|
820 |
-
return p # abc pitch == abc note without accidental
|
821 |
-
|
822 |
-
def chkbug (dt, m):
|
823 |
-
if dt > m.divs / 16: return 1 # duration should be > 1/64 note
|
824 |
-
info ('MuseScore bug: incorrect duration, smaller then 1/64! in measure %d, part %d' % (m.ixm, m.ixp))
|
825 |
-
return 0
|
826 |
-
|
827 |
-
#----------------
|
828 |
-
# parser
|
829 |
-
#----------------
|
830 |
-
class Parser:
|
831 |
-
note_alts = [ # 3 alternative notations of the same note for tablature mapping
|
832 |
-
[x.strip () for x in '=C, ^C, =D, ^D, =E, =F, ^F, =G, ^G, =A, ^A, =B'.split (',')],
|
833 |
-
[x.strip () for x in '^B, _D,^^C, _E, _F, ^E, _G,^^F, _A,^^G, _B, _C'.split (',')],
|
834 |
-
[x.strip () for x in '__D,^^B,__E,__F,^^D,__G,^^E,__A,_/A,__B,__C,^^A'.split (',')] ]
|
835 |
-
step_map = {'C':0,'D':2,'E':4,'F':5,'G':7,'A':9,'B':11}
|
836 |
-
def __init__ (s, options):
|
837 |
-
# unfold repeats, number of chars per line, credit filter level, volta option
|
838 |
-
s.slurBuf = {} # dict of open slurs keyed by slur number
|
839 |
-
s.dirStk = {} # {direction-type + number -> (type, voice | time)} dict for proper closing
|
840 |
-
s.ingrace = 0 # marks a sequence of grace notes
|
841 |
-
s.msc = Music (options) # global music data abstraction
|
842 |
-
s.unfold = options.u # turn unfolding repeats on
|
843 |
-
s.ctf = options.c # credit text filter level
|
844 |
-
s.gStfMap = [] # [[abc voice numbers] for all parts]
|
845 |
-
s.midiMap = [] # midi-settings for each abc voice, in order
|
846 |
-
s.drumInst = {} # inst_id -> midi pitch for channel 10 notes
|
847 |
-
s.drumNotes = {} # (xml voice, abc note) -> (midi note, note head)
|
848 |
-
s.instMid = [] # [{inst id -> midi-settings} for all parts]
|
849 |
-
s.midDflt = [-1,-1,-1,-91] # default midi settings for channel, program, volume, panning
|
850 |
-
s.msralts = {} # xml-notenames (without octave) with accidentals from the key
|
851 |
-
s.curalts = {} # abc-notenames (with voice number) with passing accidentals
|
852 |
-
s.stfMap = {} # xml staff number -> [xml voice number]
|
853 |
-
s.vce2stf = {} # xml voice number -> allocated staff number
|
854 |
-
s.clefMap = {} # xml staff number -> abc clef (for header only)
|
855 |
-
s.curClef = {} # xml staff number -> current abc clef
|
856 |
-
s.stemDir = {} # xml voice number -> current stem direction
|
857 |
-
s.clefOct = {} # xml staff number -> current clef-octave-change
|
858 |
-
s.curStf = {} # xml voice number -> current xml staff number
|
859 |
-
s.nolbrk = options.x; # generate no linebreaks ($)
|
860 |
-
s.jscript = options.j # compatibility with javascript version
|
861 |
-
s.ornaments = sorted (note_ornamentation_map.items ())
|
862 |
-
s.doPageFmt = len (options.p) == 1 # translate xml page format
|
863 |
-
s.tstep = options.t # clef determines step on staff (percussion)
|
864 |
-
s.dirtov1 = options.v1 # all directions to first voice of staff
|
865 |
-
s.ped = options.ped # render pedal directions
|
866 |
-
s.wstems = options.stm # translate stem elements
|
867 |
-
s.pedVce = None # voice for pedal directions
|
868 |
-
s.repeat_str = {} # staff number -> [measure number, repeat-text]
|
869 |
-
s.tabVceMap = {} # abc voice num -> [%%map ...] for tab voices
|
870 |
-
s.koppen = {} # noteheads needed for %%map
|
871 |
-
|
872 |
-
def matchSlur (s, type2, n, v2, note2, grace, stopgrace): # match slur number n in voice v2, add abc code to before/after
|
873 |
-
if type2 not in ['start', 'stop']: return # slur type continue has no abc equivalent
|
874 |
-
if n == None: n = '1'
|
875 |
-
if n in s.slurBuf:
|
876 |
-
type1, v1, note1, grace1 = s.slurBuf [n]
|
877 |
-
if type2 != type1: # slur complete, now check the voice
|
878 |
-
if v2 == v1: # begins and ends in the same voice: keep it
|
879 |
-
if type1 == 'start' and (not grace1 or not stopgrace): # normal slur: start before stop and no grace slur
|
880 |
-
note1.before = ['('] + note1.before # keep left-right order!
|
881 |
-
note2.after += ')'
|
882 |
-
# no else: don't bother with reversed stave spanning slurs
|
883 |
-
del s.slurBuf [n] # slur finished, remove from stack
|
884 |
-
else: # double definition, keep the last
|
885 |
-
info ('double slur numbers %s-%s in part %d, measure %d, voice %d note %s, first discarded' % (type2, n, s.msr.ixp+1, s.msr.ixm+1, v2, note2.ns))
|
886 |
-
s.slurBuf [n] = (type2, v2, note2, grace)
|
887 |
-
else: # unmatched slur, put in dict
|
888 |
-
s.slurBuf [n] = (type2, v2, note2, grace)
|
889 |
-
|
890 |
-
def doNotations (s, note, nttn, isTab):
|
891 |
-
for key, val in s.ornaments:
|
892 |
-
if nttn.find (key) != None: note.before += [val] # just concat all ornaments
|
893 |
-
trem = nttn.find ('ornaments/tremolo')
|
894 |
-
if trem != None:
|
895 |
-
type = trem.get ('type')
|
896 |
-
if type == 'single':
|
897 |
-
note.before.insert (0, '!%s!' % (int (trem.text) * '/'))
|
898 |
-
else:
|
899 |
-
note.fact = None # no time modification in ABC
|
900 |
-
if s.tstep: # abc2svg version
|
901 |
-
if type == 'stop': note.before.insert (0, '!trem%s!' % trem.text);
|
902 |
-
else: # abc2xml version
|
903 |
-
if type == 'start': note.before.insert (0, '!%s-!' % (int (trem.text) * '/'));
|
904 |
-
fingering = nttn.findall ('technical/fingering')
|
905 |
-
for finger in fingering: # handle multiple finger annotations
|
906 |
-
if not isTab: note.before += ['!%s!' % finger.text] # fingering goes before chord (addChord)
|
907 |
-
snaar = nttn.find ('technical/string')
|
908 |
-
if snaar != None and isTab:
|
909 |
-
if s.tstep:
|
910 |
-
fret = nttn.find ('technical/fret')
|
911 |
-
if fret != None: note.tab = (snaar.text, fret.text)
|
912 |
-
else:
|
913 |
-
deco = '!%s!' % snaar.text # no double string decos (bug in musescore)
|
914 |
-
if deco not in note.ntdec: note.ntdec += deco
|
915 |
-
wvln = nttn.find ('ornaments/wavy-line')
|
916 |
-
if wvln != None:
|
917 |
-
if wvln.get ('type') == 'start': note.before = ['!trill(!'] + note.before # keep left-right order!
|
918 |
-
elif wvln.get ('type') == 'stop': note.before = ['!trill)!'] + note.before
|
919 |
-
glis = nttn.find ('glissando')
|
920 |
-
if glis == None: glis = nttn.find ('slide') # treat slide as glissando
|
921 |
-
if glis != None:
|
922 |
-
lt = '~' if glis.get ('line-type') =='wavy' else '-'
|
923 |
-
if glis.get ('type') == 'start': note.before = ['!%s(!' % lt] + note.before # keep left-right order!
|
924 |
-
elif glis.get ('type') == 'stop': note.before = ['!%s)!' % lt] + note.before
|
925 |
-
|
926 |
-
def tabnote (s, alt, ptc, oct, v, ntrec):
|
927 |
-
p = s.step_map [ptc] + int (alt or '0') # p in -2 .. 13
|
928 |
-
if p > 11: oct += 1 # octave correction
|
929 |
-
if p < 0: oct -= 1
|
930 |
-
p = p % 12 # remap p into 0..11
|
931 |
-
snaar_nw, fret_nw = ntrec.tab # the computed/annotated allocation of nt
|
932 |
-
for i in range (4): # support same note on 4 strings
|
933 |
-
na = s.note_alts [i % 3] [p] # get alternative representation of same note
|
934 |
-
o = oct
|
935 |
-
if na in ['^B', '^^B']: o -= 1 # because in adjacent octave
|
936 |
-
if na in ['_C', '__C']: o += 1
|
937 |
-
if '/' in na or i == 3: o = 9 # emergency notation for 4th string case
|
938 |
-
nt = addoct (na, o)
|
939 |
-
snaar, fret = s.tabmap.get ((v, nt), ('', '')) # the current allocation of nt
|
940 |
-
if not snaar: break # note not yet allocated
|
941 |
-
if snaar_nw == snaar: return nt # use present allocation
|
942 |
-
if i == 3: # new allocaion needed but none is free
|
943 |
-
fmt = 'rejected: voice %d note %3s string %s fret %2s remains: string %s fret %s'
|
944 |
-
info (fmt % (v, nt, snaar_nw, fret_nw, snaar, fret), 1)
|
945 |
-
ntrec.tab = (snaar, fret)
|
946 |
-
s.tabmap [v, nt] = ntrec.tab # for tablature map (voice, note) -> (string, fret)
|
947 |
-
return nt # ABC code always in key C (with midi pitch alterations)
|
948 |
-
|
949 |
-
def ntAbc (s, ptc, oct, note, v, ntrec, isTab): # pitch, octave -> abc notation
|
950 |
-
acc2alt = {'double-flat':-2,'flat-flat':-2,'flat':-1,'natural':0,'sharp':1,'sharp-sharp':2,'double-sharp':2}
|
951 |
-
oct += s.clefOct.get (s.curStf [v], 0) # minus clef-octave-change value
|
952 |
-
acc = note.findtext ('accidental') # should be the notated accidental
|
953 |
-
alt = note.findtext ('pitch/alter') # pitch alteration (midi)
|
954 |
-
if ntrec.tab: return s.tabnote (alt, ptc, oct, v, ntrec) # implies s.tstep is true (options.t was given)
|
955 |
-
elif isTab and s.tstep:
|
956 |
-
nt = ['__','_','','^','^^'][int (alt or '0') + 2] + addoct (ptc, oct)
|
957 |
-
info ('no string notation found for note %s in voice %d' % (nt, v), 1)
|
958 |
-
p = addoct (ptc, oct)
|
959 |
-
if alt == None and s.msralts.get (ptc, 0): alt = 0 # no alt but key implies alt -> natural!!
|
960 |
-
if alt == None and (p, v) in s.curalts: alt = 0 # no alt but previous note had one -> natural!!
|
961 |
-
if acc == None and alt == None: return p # no acc, no alt
|
962 |
-
elif acc != None:
|
963 |
-
alt = acc2alt [acc] # acc takes precedence over the pitch here!
|
964 |
-
else: # now see if we really must add an accidental
|
965 |
-
alt = int (float (alt))
|
966 |
-
if (p, v) in s.curalts: # the note in this voice has been altered before
|
967 |
-
if alt == s.curalts [(p, v)]: return p # alteration still the same
|
968 |
-
elif alt == s.msralts.get (ptc, 0): return p # alteration implied by the key
|
969 |
-
tieElms = note.findall ('tie') + note.findall ('notations/tied') # in xml we have separate notated ties and playback ties
|
970 |
-
if 'stop' in [e.get ('type') for e in tieElms]: return p # don't alter tied notes
|
971 |
-
info ('accidental %d added in part %d, measure %d, voice %d note %s' % (alt, s.msr.ixp+1, s.msr.ixm+1, v+1, p))
|
972 |
-
s.curalts [(p, v)] = alt
|
973 |
-
p = ['__','_','=','^','^^'][alt+2] + p # and finally ... prepend the accidental
|
974 |
-
return p
|
975 |
-
|
976 |
-
def doNote (s, n): # parse a musicXML note tag
|
977 |
-
note = Note ()
|
978 |
-
v = int (n.findtext ('voice', '1'))
|
979 |
-
if s.isSib: v += 100 * int (n.findtext ('staff', '1')) # repair bug in Sibelius
|
980 |
-
chord = n.find ('chord') != None
|
981 |
-
p = n.findtext ('pitch/step') or n.findtext ('unpitched/display-step')
|
982 |
-
o = n.findtext ('pitch/octave') or n.findtext ('unpitched/display-octave')
|
983 |
-
r = n.find ('rest')
|
984 |
-
numer = n.findtext ('time-modification/actual-notes')
|
985 |
-
if numer:
|
986 |
-
denom = n.findtext ('time-modification/normal-notes')
|
987 |
-
note.fact = (int (numer), int (denom))
|
988 |
-
note.tup = [x.get ('type') for x in n.findall ('notations/tuplet')]
|
989 |
-
dur = n.findtext ('duration')
|
990 |
-
grc = n.find ('grace')
|
991 |
-
note.grace = grc != None
|
992 |
-
note.before, note.after = [], '' # strings with ABC stuff that goes before or after a note/chord
|
993 |
-
if note.grace and not s.ingrace: # open a grace sequence
|
994 |
-
s.ingrace = 1
|
995 |
-
note.before = ['{']
|
996 |
-
if grc.get ('slash') == 'yes': note.before += ['/'] # acciaccatura
|
997 |
-
stopgrace = not note.grace and s.ingrace
|
998 |
-
if stopgrace: # close the grace sequence
|
999 |
-
s.ingrace = 0
|
1000 |
-
s.msc.lastnote.after += '}' # close grace on lastenote.after
|
1001 |
-
if dur == None or note.grace: dur = 0
|
1002 |
-
if r == None and n.get ('print-object') == 'no':
|
1003 |
-
if chord: return
|
1004 |
-
r = 1 # turn invisible notes (that advance the time) into invisible rests
|
1005 |
-
note.dur = int (dur)
|
1006 |
-
if r == None and (not p or not o): # not a rest and no pitch
|
1007 |
-
s.msc.cnt.inc ('nopt', v) # count unpitched notes
|
1008 |
-
o, p = 5,'E' # make it an E5 ??
|
1009 |
-
isTab = s.curClef and s.curClef.get (s.curStf [v], '').startswith ('tab')
|
1010 |
-
nttn = n.find ('notations') # add ornaments
|
1011 |
-
if nttn != None: s.doNotations (note, nttn, isTab)
|
1012 |
-
e = n.find ('stem') if r == None else None # no !stemless! before rest
|
1013 |
-
if e != None and e.text == 'none' and (not isTab or v in s.hasStems or s.tstep):
|
1014 |
-
note.before += ['s']; abcOut.stemless = 1;
|
1015 |
-
e = n.find ('accidental')
|
1016 |
-
if e != None and e.get ('parentheses') == 'yes': note.ntdec += '!courtesy!'
|
1017 |
-
if r != None: noot = 'x' if n.get ('print-object') == 'no' or isTab else 'z'
|
1018 |
-
else: noot = s.ntAbc (p, int (o), n, v, note, isTab)
|
1019 |
-
if n.find ('unpitched') != None:
|
1020 |
-
clef = s.curClef [s.curStf [v]] # the current clef for this voice
|
1021 |
-
step = staffStep (p, int (o), clef, s.tstep) # (clef independent) step value of note on the staff
|
1022 |
-
instr = n.find ('instrument')
|
1023 |
-
instId = instr.get ('id') if instr != None else 'dummyId'
|
1024 |
-
midi = s.drumInst.get (instId, abcMid (noot))
|
1025 |
-
nh = n.findtext ('notehead', '').replace (' ','-') # replace spaces in xml notehead names for percmap
|
1026 |
-
if nh == 'x': noot = '^' + noot.replace ('^','').replace ('_','')
|
1027 |
-
if nh in ['circle-x','diamond','triangle']: noot = '_' + noot.replace ('^','').replace ('_','')
|
1028 |
-
if nh and n.find ('notehead').get ('filled','') == 'yes': nh += '+'
|
1029 |
-
if nh and n.find ('notehead').get ('filled','') == 'no': nh += '-'
|
1030 |
-
s.drumNotes [(v, noot)] = (step, midi, nh) # keep data for percussion map
|
1031 |
-
tieElms = n.findall ('tie') + n.findall ('notations/tied') # in xml we have separate notated ties and playback ties
|
1032 |
-
if 'start' in [e.get ('type') for e in tieElms]: # n can have stop and start tie
|
1033 |
-
noot = noot + '-'
|
1034 |
-
note.beam = sum ([1 for b in n.findall('beam') if b.text in ['continue', 'end']]) + int (note.grace)
|
1035 |
-
lyrlast = 0; rsib = re.compile (r'^.*verse')
|
1036 |
-
for e in n.findall ('lyric'):
|
1037 |
-
lyrnum = int (rsib.sub ('', e.get ('number', '1'))) # also do Sibelius numbers
|
1038 |
-
if lyrnum == 0: lyrnum = lyrlast + 1 # and correct Sibelius bugs
|
1039 |
-
else: lyrlast = lyrnum
|
1040 |
-
note.lyrs [lyrnum] = doSyllable (e)
|
1041 |
-
stemdir = n.findtext ('stem')
|
1042 |
-
if s.wstems and (stemdir == 'up' or stemdir == 'down'):
|
1043 |
-
if stemdir != s.stemDir.get (v, ''):
|
1044 |
-
s.stemDir [v] = stemdir
|
1045 |
-
s.msc.appendElem (v, '[I:stemdir %s]' % stemdir)
|
1046 |
-
if chord: s.msc.addChord (note, noot)
|
1047 |
-
else:
|
1048 |
-
xmlstaff = int (n.findtext ('staff', '1'))
|
1049 |
-
if s.curStf [v] != xmlstaff: # the note should go to another staff
|
1050 |
-
dstaff = xmlstaff - s.curStf [v] # relative new staff number
|
1051 |
-
s.curStf [v] = xmlstaff # remember the new staff for this voice
|
1052 |
-
s.msc.appendElem (v, '[I:staff %+d]' % dstaff) # insert a move before the note
|
1053 |
-
s.msc.appendNote (v, note, noot)
|
1054 |
-
for slur in n.findall ('notations/slur'): # s.msc.lastnote points to the last real note/chord inserted above
|
1055 |
-
s.matchSlur (slur.get ('type'), slur.get ('number'), v, s.msc.lastnote, note.grace, stopgrace) # match slur definitions
|
1056 |
-
|
1057 |
-
def doAttr (s, e): # parse a musicXML attribute tag
|
1058 |
-
teken = {'C1':'alto1','C2':'alto2','C3':'alto','C4':'tenor','F4':'bass','F3':'bass3','G2':'treble','TAB':'tab','percussion':'perc'}
|
1059 |
-
dvstxt = e.findtext ('divisions')
|
1060 |
-
if dvstxt: s.msr.divs = int (dvstxt)
|
1061 |
-
steps = int (e.findtext ('transpose/chromatic', '0')) # for transposing instrument
|
1062 |
-
fifths = e.findtext ('key/fifths')
|
1063 |
-
first = s.msc.tijd == 0 and s.msr.ixm == 0 # first attributes in first measure
|
1064 |
-
if fifths:
|
1065 |
-
key, s.msralts = setKey (int (fifths), e.findtext ('key/mode','major'))
|
1066 |
-
if first and not steps and abcOut.key == 'none':
|
1067 |
-
abcOut.key = key # first measure -> header, if not transposing instrument or percussion part!
|
1068 |
-
elif key != abcOut.key or not first:
|
1069 |
-
s.msr.attr += '[K:%s]' % key # otherwise -> voice
|
1070 |
-
beats = e.findtext ('time/beats')
|
1071 |
-
if beats:
|
1072 |
-
unit = e.findtext ('time/beat-type')
|
1073 |
-
mtr = beats + '/' + unit
|
1074 |
-
if first: abcOut.mtr = mtr # first measure -> header
|
1075 |
-
else: s.msr.attr += '[M:%s]' % mtr # otherwise -> voice
|
1076 |
-
s.msr.mtr = int (beats), int (unit)
|
1077 |
-
s.msr.mdur = (s.msr.divs * s.msr.mtr[0] * 4) // s.msr.mtr[1] # duration of measure in xml-divisions
|
1078 |
-
for ms in e.findall('measure-style'):
|
1079 |
-
n = int (ms.get ('number', '1')) # staff number
|
1080 |
-
voices = s.stfMap [n] # all voices of staff n
|
1081 |
-
for mr in ms.findall('measure-repeat'):
|
1082 |
-
ty = mr.get('type')
|
1083 |
-
if ty == 'start': # remember start measure number and text voor each staff
|
1084 |
-
s.repeat_str [n] = [s.msr.ixm, mr.text]
|
1085 |
-
for v in voices: # insert repeat into all voices, value will be overwritten at stop
|
1086 |
-
s.msc.insertElem (v, s.repeat_str [n])
|
1087 |
-
elif ty == 'stop': # calculate repeat measure count for this staff n
|
1088 |
-
start_ix, text_ = s.repeat_str [n]
|
1089 |
-
repeat_count = s.msr.ixm - start_ix
|
1090 |
-
if text_:
|
1091 |
-
mid_str = "%s " % text_
|
1092 |
-
repeat_count /= int (text_)
|
1093 |
-
else:
|
1094 |
-
mid_str = "" # overwrite repeat with final string
|
1095 |
-
s.repeat_str [n][0] = '[I:repeat %s%d]' % (mid_str, repeat_count)
|
1096 |
-
del s.repeat_str [n] # remove closed repeats
|
1097 |
-
toct = e.findtext ('transpose/octave-change', '')
|
1098 |
-
if toct: steps += 12 * int (toct) # extra transposition of toct octaves
|
1099 |
-
for clef in e.findall ('clef'): # a part can have multiple staves
|
1100 |
-
n = int (clef.get ('number', '1')) # local staff number for this clef
|
1101 |
-
sgn = clef.findtext ('sign')
|
1102 |
-
line = clef.findtext ('line', '') if sgn not in ['percussion','TAB'] else ''
|
1103 |
-
cs = teken.get (sgn + line, '')
|
1104 |
-
oct = clef.findtext ('clef-octave-change', '') or '0'
|
1105 |
-
if oct: cs += {-2:'-15', -1:'-8', 1:'+8', 2:'+15'}.get (int (oct), '')
|
1106 |
-
s.clefOct [n] = -int (oct); # xml playback pitch -> abc notation pitch
|
1107 |
-
if steps: cs += ' transpose=' + str (steps)
|
1108 |
-
stfdtl = e.find ('staff-details')
|
1109 |
-
if stfdtl and int (stfdtl.get ('number', '1')) == n:
|
1110 |
-
lines = stfdtl.findtext ('staff-lines')
|
1111 |
-
if lines:
|
1112 |
-
lns= '|||' if lines == '3' and sgn == 'TAB' else lines
|
1113 |
-
cs += ' stafflines=%s' % lns
|
1114 |
-
s.stafflines = int (lines) # remember for tab staves
|
1115 |
-
strings = stfdtl.findall ('staff-tuning')
|
1116 |
-
if strings:
|
1117 |
-
tuning = [st.findtext ('tuning-step') + st.findtext ('tuning-octave') for st in strings]
|
1118 |
-
cs += ' strings=%s' % ','.join (tuning)
|
1119 |
-
capo = stfdtl.findtext ('capo')
|
1120 |
-
if capo: cs += ' capo=%s' % capo
|
1121 |
-
s.curClef [n] = cs # keep track of current clef (for percmap)
|
1122 |
-
if first: s.clefMap [n] = cs # clef goes to header (where it is mapped to voices)
|
1123 |
-
else:
|
1124 |
-
voices = s.stfMap[n] # clef change to all voices of staff n
|
1125 |
-
for v in voices:
|
1126 |
-
if n != s.curStf [v]: # voice is not at its home staff n
|
1127 |
-
dstaff = n - s.curStf [v]
|
1128 |
-
s.curStf [v] = n # reset current staff at start of measure to home position
|
1129 |
-
s.msc.appendElem (v, '[I:staff %+d]' % dstaff)
|
1130 |
-
s.msc.appendElem (v, '[K:%s]' % cs)
|
1131 |
-
|
1132 |
-
def findVoice (s, i, es):
|
1133 |
-
stfnum = int (es[i].findtext ('staff',1)) # directions belong to a staff
|
1134 |
-
vs = s.stfMap [stfnum] # voices in this staff
|
1135 |
-
v1 = vs [0] if vs else 1 # directions to first voice of staff
|
1136 |
-
if s.dirtov1: return stfnum, v1, v1 # option --v1
|
1137 |
-
for e in es [i+1:]: # or to the voice of the next note
|
1138 |
-
if e.tag == 'note':
|
1139 |
-
v = int (e.findtext ('voice', '1'))
|
1140 |
-
if s.isSib: v += 100 * int (e.findtext ('staff', '1')) # repair bug in Sibelius
|
1141 |
-
stf = s.vce2stf [v] # use our own staff allocation
|
1142 |
-
return stf, v, v1 # voice of next note, first voice of staff
|
1143 |
-
if e.tag == 'backup': break
|
1144 |
-
return stfnum, v1, v1 # no note found, fall back to v1
|
1145 |
-
|
1146 |
-
def doDirection (s, e, i, es): # parse a musicXML direction tag
|
1147 |
-
def addDirection (x, vs, tijd, stfnum):
|
1148 |
-
if not x: return
|
1149 |
-
vs = s.stfMap [stfnum] if '!8v' in x else [vs] # ottava's go to all voices of staff
|
1150 |
-
for v in vs:
|
1151 |
-
if tijd != None: # insert at time of encounter
|
1152 |
-
s.msc.appendElemT (v, x.replace ('(',')').replace ('ped','ped-up'), tijd)
|
1153 |
-
else:
|
1154 |
-
s.msc.appendElem (v, x)
|
1155 |
-
def startStop (dtype, vs, stfnum=1):
|
1156 |
-
typmap = {'down':'!8va(!', 'up':'!8vb(!', 'crescendo':'!<(!', 'diminuendo':'!>(!', 'start':'!ped!'}
|
1157 |
-
type = t.get ('type', '')
|
1158 |
-
k = dtype + t.get ('number', '1') # key to match the closing direction
|
1159 |
-
if type in typmap: # opening the direction
|
1160 |
-
x = typmap [type]
|
1161 |
-
if k in s.dirStk: # closing direction already encountered
|
1162 |
-
stype, tijd = s.dirStk [k]; del s.dirStk [k]
|
1163 |
-
if stype == 'stop':
|
1164 |
-
addDirection (x, vs, tijd, stfnum)
|
1165 |
-
else:
|
1166 |
-
info ('%s direction %s has no stop in part %d, measure %d, voice %d' % (dtype, stype, s.msr.ixp+1, s.msr.ixm+1, vs+1))
|
1167 |
-
s.dirStk [k] = ((type , vs)) # remember voice and type for closing
|
1168 |
-
else:
|
1169 |
-
s.dirStk [k] = ((type , vs)) # remember voice and type for closing
|
1170 |
-
elif type == 'stop':
|
1171 |
-
if k in s.dirStk: # matching open direction found
|
1172 |
-
type, vs = s.dirStk [k]; del s.dirStk [k] # into the same voice
|
1173 |
-
if type == 'stop':
|
1174 |
-
info ('%s direction %s has double stop in part %d, measure %d, voice %d' % (dtype, type, s.msr.ixp+1, s.msr.ixm+1, vs+1))
|
1175 |
-
x = ''
|
1176 |
-
else:
|
1177 |
-
x = typmap [type].replace ('(',')').replace ('ped','ped-up')
|
1178 |
-
else: # closing direction found before opening
|
1179 |
-
s.dirStk [k] = ('stop', s.msc.tijd)
|
1180 |
-
x = '' # delay code generation until opening found
|
1181 |
-
else: raise ValueError ('wrong direction type')
|
1182 |
-
addDirection (x, vs, None, stfnum)
|
1183 |
-
tempo, wrdstxt = None, ''
|
1184 |
-
plcmnt = e.get ('placement')
|
1185 |
-
stf, vs, v1 = s.findVoice (i, es)
|
1186 |
-
jmp = '' # for jump sound elements: dacapo, dalsegno and family
|
1187 |
-
jmps = [('dacapo','D.C.'),('dalsegno','D.S.'),('tocoda','dacoda'),('fine','fine'),('coda','O'),('segno','S')]
|
1188 |
-
t = e.find ('sound') # there are many possible attributes for sound
|
1189 |
-
if t != None:
|
1190 |
-
minst = t.find ('midi-instrument')
|
1191 |
-
if minst:
|
1192 |
-
prg = t.findtext ('midi-instrument/midi-program')
|
1193 |
-
chn = t.findtext ('midi-instrument/midi-channel')
|
1194 |
-
vids = [v for v, id in s.vceInst.items () if id == minst.get ('id')]
|
1195 |
-
if vids: vs = vids [0] # direction for the indentified voice, not the staff
|
1196 |
-
parm, inst = ('program', str (int (prg) - 1)) if prg else ('channel', chn)
|
1197 |
-
if inst and abcOut.volpan > 0: s.msc.appendElem (vs, '[I:MIDI= %s %s]' % (parm, inst))
|
1198 |
-
tempo = t.get ('tempo') # look for tempo attribute
|
1199 |
-
if tempo:
|
1200 |
-
tempo = '%.0f' % float (tempo) # hope it is a number and insert in voice 1
|
1201 |
-
tempo_units = (1,4) # always 1/4 for sound elements!
|
1202 |
-
for r, v in jmps:
|
1203 |
-
if t.get (r, ''): jmp = v; break
|
1204 |
-
dirtypes = e.findall ('direction-type')
|
1205 |
-
for dirtyp in dirtypes:
|
1206 |
-
units = { 'whole': (1,1), 'half': (1,2), 'quarter': (1,4), 'eighth': (1,8) }
|
1207 |
-
metr = dirtyp.find ('metronome')
|
1208 |
-
if metr != None:
|
1209 |
-
t = metr.findtext ('beat-unit', '')
|
1210 |
-
if t in units: tempo_units = units [t]
|
1211 |
-
else: tempo_units = units ['quarter']
|
1212 |
-
if metr.find ('beat-unit-dot') != None:
|
1213 |
-
tempo_units = simplify (tempo_units [0] * 3, tempo_units [1] * 2)
|
1214 |
-
tmpro = re.search ('[.\d]+', metr.findtext ('per-minute')) # look for a number
|
1215 |
-
if tmpro: tempo = tmpro.group () # overwrites the value set by the sound element of this direction
|
1216 |
-
t = dirtyp.find ('wedge')
|
1217 |
-
if t != None: startStop ('wedge', vs)
|
1218 |
-
allwrds = dirtyp.findall ('words') # insert text annotations
|
1219 |
-
if not allwrds: allwrds = dirtyp.findall ('rehearsal') # treat rehearsal mark as text annotation
|
1220 |
-
for wrds in allwrds:
|
1221 |
-
if jmp: # ignore the words when a jump sound element is present in this direction
|
1222 |
-
s.msc.appendElem (vs, '!%s!' % jmp , 1) # to voice
|
1223 |
-
break
|
1224 |
-
plc = plcmnt == 'below' and '_' or '^'
|
1225 |
-
if float (wrds.get ('default-y', '0')) < 0: plc = '_'
|
1226 |
-
wrdstxt += (wrds.text or '').replace ('"','\\"').replace ('\n', '\\n')
|
1227 |
-
wrdstxt = wrdstxt.strip ()
|
1228 |
-
for key, val in dynamics_map.items ():
|
1229 |
-
if dirtyp.find ('dynamics/' + key) != None:
|
1230 |
-
s.msc.appendElem (vs, val, 1) # to voice
|
1231 |
-
if dirtyp.find ('coda') != None: s.msc.appendElem (vs, 'O', 1)
|
1232 |
-
if dirtyp.find ('segno') != None: s.msc.appendElem (vs, 'S', 1)
|
1233 |
-
t = dirtyp.find ('octave-shift')
|
1234 |
-
if t != None: startStop ('octave-shift', vs, stf) # assume size == 8 for the time being
|
1235 |
-
t = dirtyp.find ('pedal')
|
1236 |
-
if t != None and s.ped:
|
1237 |
-
if not s.pedVce: s.pedVce = vs
|
1238 |
-
startStop ('pedal', s.pedVce)
|
1239 |
-
if dirtyp.findtext ('other-direction') == 'diatonic fretting': s.diafret = 1;
|
1240 |
-
if tempo:
|
1241 |
-
tempo = '%.0f' % float (tempo) # hope it is a number and insert in voice 1
|
1242 |
-
if s.msc.tijd == 0 and s.msr.ixm == 0: # first measure -> header
|
1243 |
-
abcOut.tempo = tempo
|
1244 |
-
abcOut.tempo_units = tempo_units
|
1245 |
-
else:
|
1246 |
-
s.msc.appendElem (v1, '[Q:%d/%d=%s]' % (tempo_units [0], tempo_units [1], tempo)) # otherwise -> 1st voice
|
1247 |
-
if wrdstxt: s.msc.appendElem (vs, '"%s%s"' % (plc, wrdstxt), 1) # to voice, but after tempo
|
1248 |
-
|
1249 |
-
def doHarmony (s, e, i, es): # parse a musicXMl harmony tag
|
1250 |
-
_, vt, _ = s.findVoice (i, es)
|
1251 |
-
short = {'major':'', 'minor':'m', 'augmented':'+', 'diminished':'dim', 'dominant':'7', 'half-diminished':'m7b5'}
|
1252 |
-
accmap = {'major':'maj', 'dominant':'', 'minor':'m', 'diminished':'dim', 'augmented':'+', 'suspended':'sus'}
|
1253 |
-
modmap = {'second':'2', 'fourth':'4', 'seventh':'7', 'sixth':'6', 'ninth':'9', '11th':'11', '13th':'13'}
|
1254 |
-
altmap = {'1':'#', '0':'', '-1':'b'}
|
1255 |
-
root = e.findtext ('root/root-step','')
|
1256 |
-
alt = altmap.get (e.findtext ('root/root-alter'), '')
|
1257 |
-
sus = ''
|
1258 |
-
kind = e.findtext ('kind', '')
|
1259 |
-
if kind in short: kind = short [kind]
|
1260 |
-
elif '-' in kind: # xml chord names: <triad name>-<modification>
|
1261 |
-
triad, mod = kind.split ('-')
|
1262 |
-
kind = accmap.get (triad, '') + modmap.get (mod, '')
|
1263 |
-
if kind.startswith ('sus'): kind, sus = '', kind # sus-suffix goes to the end
|
1264 |
-
elif kind == 'none': kind = e.find ('kind').get ('text','')
|
1265 |
-
degrees = e.findall ('degree')
|
1266 |
-
for d in degrees: # chord alterations
|
1267 |
-
kind += altmap.get (d.findtext ('degree-alter'),'') + d.findtext ('degree-value','')
|
1268 |
-
kind = kind.replace ('79','9').replace ('713','13').replace ('maj6','6')
|
1269 |
-
bass = e.findtext ('bass/bass-step','') + altmap.get (e.findtext ('bass/bass-alter'),'')
|
1270 |
-
s.msc.appendElem (vt, '"%s%s%s%s%s"' % (root, alt, kind, sus, bass and '/' + bass), 1)
|
1271 |
-
|
1272 |
-
def doBarline (s, e): # 0 = no repeat, 1 = begin repeat, 2 = end repeat
|
1273 |
-
rep = e.find ('repeat')
|
1274 |
-
if rep != None: rep = rep.get ('direction')
|
1275 |
-
if s.unfold: # unfold repeat, don't translate barlines
|
1276 |
-
return rep and (rep == 'forward' and 1 or 2) or 0
|
1277 |
-
loc = e.get ('location', 'right') # right is the default
|
1278 |
-
if loc == 'right': # only change style for the right side
|
1279 |
-
style = e.findtext ('bar-style')
|
1280 |
-
if style == 'light-light': s.msr.rline = '||'
|
1281 |
-
elif style == 'light-heavy': s.msr.rline = '|]'
|
1282 |
-
if rep != None: # repeat found
|
1283 |
-
if rep == 'forward': s.msr.lline = ':'
|
1284 |
-
else: s.msr.rline = ':|' # override barline style
|
1285 |
-
end = e.find ('ending')
|
1286 |
-
if end != None:
|
1287 |
-
if end.get ('type') == 'start':
|
1288 |
-
n = end.get ('number', '1').replace ('.','').replace (' ','')
|
1289 |
-
try: list (map (int, n.split (','))) # should be a list of integers
|
1290 |
-
except: n = '"%s"' % n.strip () # illegal musicXML
|
1291 |
-
s.msr.lnum = n # assume a start is always at the beginning of a measure
|
1292 |
-
elif s.msr.rline == '|': # stop and discontinue the same in ABC ?
|
1293 |
-
s.msr.rline = '||' # to stop on a normal barline use || in ABC ?
|
1294 |
-
return 0
|
1295 |
-
|
1296 |
-
def doPrint (s, e): # print element, measure number -> insert a line break
|
1297 |
-
if e.get ('new-system') == 'yes' or e.get ('new-page') == 'yes':
|
1298 |
-
if not s.nolbrk: return '$' # a line break
|
1299 |
-
|
1300 |
-
def doPartList (s, e): # translate the start/stop-event-based xml-partlist into proper tree
|
1301 |
-
for sp in e.findall ('part-list/score-part'):
|
1302 |
-
midi = {}
|
1303 |
-
for m in sp.findall ('midi-instrument'):
|
1304 |
-
x = [m.findtext (p, s.midDflt [i]) for i,p in enumerate (['midi-channel','midi-program','volume','pan'])]
|
1305 |
-
pan = float (x[3])
|
1306 |
-
if pan >= -90 and pan <= 90: # would be better to map behind-pannings
|
1307 |
-
pan = (float (x[3]) + 90) / 180 * 127 # xml between -90 and +90
|
1308 |
-
midi [m.get ('id')] = [int (x[0]), int (x[1]), float (x[2]) * 1.27, pan] # volume 100 -> midi 127
|
1309 |
-
up = m.findtext ('midi-unpitched')
|
1310 |
-
if up: s.drumInst [m.get ('id')] = int (up) - 1 # store midi-pitch for channel 10 notes
|
1311 |
-
s.instMid.append (midi)
|
1312 |
-
ps = e.find ('part-list') # partlist = [groupelem]
|
1313 |
-
xs = getPartlist (ps) # groupelem = partname | grouplist
|
1314 |
-
partlist, _ = parseParts (xs, {}, []) # grouplist = [groupelem, ..., groupdata]
|
1315 |
-
return partlist # groupdata = [group-symbol, group-barline, group-name, group-abbrev]
|
1316 |
-
|
1317 |
-
def mkTitle (s, e):
|
1318 |
-
def filterCredits (y): # y == filter level, higher filters less
|
1319 |
-
cs = []
|
1320 |
-
for x in credits: # skip redundant credit lines
|
1321 |
-
if y < 6 and (x in title or x in mvttl): continue # sure skip
|
1322 |
-
if y < 5 and (x in composer or x in lyricist): continue # almost sure skip
|
1323 |
-
if y < 4 and ((title and title in x) or (mvttl and mvttl in x)): continue # may skip too much
|
1324 |
-
if y < 3 and ([1 for c in composer if c in x] or [1 for c in lyricist if c in x]): continue # skips too much
|
1325 |
-
if y < 2 and re.match (r'^[\d\W]*$', x): continue # line only contains numbers and punctuation
|
1326 |
-
cs.append (x)
|
1327 |
-
if y == 0 and (title + mvttl): cs = '' # default: only credit when no title set
|
1328 |
-
return cs
|
1329 |
-
title = e.findtext ('work/work-title', '').strip ()
|
1330 |
-
mvttl = e.findtext ('movement-title', '').strip ()
|
1331 |
-
composer, lyricist, credits = [], [], []
|
1332 |
-
for creator in e.findall ('identification/creator'):
|
1333 |
-
if creator.text:
|
1334 |
-
if creator.get ('type') == 'composer':
|
1335 |
-
composer += [line.strip () for line in creator.text.split ('\n')]
|
1336 |
-
elif creator.get ('type') in ('lyricist', 'transcriber'):
|
1337 |
-
lyricist += [line.strip () for line in creator.text.split ('\n')]
|
1338 |
-
for rights in e.findall ('identification/rights'):
|
1339 |
-
if rights.text:
|
1340 |
-
lyricist += [line.strip () for line in rights.text.split ('\n')]
|
1341 |
-
for credit in e.findall('credit'):
|
1342 |
-
cs = ''.join (e.text or '' for e in credit.findall('credit-words'))
|
1343 |
-
credits += [re.sub (r'\s*[\r\n]\s*', ' ', cs)]
|
1344 |
-
credits = filterCredits (s.ctf)
|
1345 |
-
if title: title = 'T:%s\n' % title.replace ('\n', '\nT:')
|
1346 |
-
if mvttl: title += 'T:%s\n' % mvttl.replace ('\n', '\nT:')
|
1347 |
-
if credits: title += '\n'.join (['T:%s' % c for c in credits]) + '\n'
|
1348 |
-
if composer: title += '\n'.join (['C:%s' % c for c in composer]) + '\n'
|
1349 |
-
if lyricist: title += '\n'.join (['Z:%s' % c for c in lyricist]) + '\n'
|
1350 |
-
if title: abcOut.title = title[:-1]
|
1351 |
-
s.isSib = 'Sibelius' in (e.findtext ('identification/encoding/software') or '')
|
1352 |
-
if s.isSib: info ('Sibelius MusicXMl is unreliable')
|
1353 |
-
|
1354 |
-
def doDefaults (s, e):
|
1355 |
-
if not s.doPageFmt: return # return if -pf option absent
|
1356 |
-
d = e.find ('defaults');
|
1357 |
-
if d == None: return;
|
1358 |
-
mils = d.findtext ('scaling/millimeters') # mills == staff height (mm)
|
1359 |
-
tenths = d.findtext ('scaling/tenths') # staff height in tenths
|
1360 |
-
if not mils or not tenths: return
|
1361 |
-
xmlScale = float (mils) / float (tenths) / 10 # tenths -> mm
|
1362 |
-
space = 10 * xmlScale # space between staff lines == 10 tenths
|
1363 |
-
abcScale = space / 0.2117 # 0.2117 cm = 6pt = space between staff lines for scale = 1.0 in abcm2ps
|
1364 |
-
abcOut.pageFmt ['scale'] = abcScale
|
1365 |
-
eks = 2 * ['page-layout/'] + 4 * ['page-layout/page-margins/']
|
1366 |
-
eks = [a+b for a,b in zip (eks, 'page-height,page-width,left-margin,right-margin,top-margin,bottom-margin'.split (','))]
|
1367 |
-
for i in range (6):
|
1368 |
-
v = d.findtext (eks [i])
|
1369 |
-
k = abcOut.pagekeys [i+1] # pagekeys [0] == scale already done, skip it
|
1370 |
-
if not abcOut.pageFmt [k] and v:
|
1371 |
-
try: abcOut.pageFmt [k] = float (v) * xmlScale # -> cm
|
1372 |
-
except: info ('illegal value %s for xml element %s', (v, eks [i])); continue # just skip illegal values
|
1373 |
-
|
1374 |
-
def locStaffMap (s, part, maten): # map voice to staff with majority voting
|
1375 |
-
vmap = {} # {voice -> {staff -> n}} count occurrences of voice in staff
|
1376 |
-
s.vceInst = {} # {voice -> instrument id} for this part
|
1377 |
-
s.msc.vnums = {} # voice id's
|
1378 |
-
s.hasStems = {} # XML voice nums with at least one note with a stem (for tab key)
|
1379 |
-
s.stfMap, s.clefMap = {}, {} # staff -> [voices], staff -> clef
|
1380 |
-
ns = part.findall ('measure/note')
|
1381 |
-
for n in ns: # count staff allocations for all notes
|
1382 |
-
v = int (n.findtext ('voice', '1'))
|
1383 |
-
if s.isSib: v += 100 * int (n.findtext ('staff', '1')) # repair bug in Sibelius
|
1384 |
-
s.msc.vnums [v] = 1 # collect all used voice id's in this part
|
1385 |
-
sn = int (n.findtext ('staff', '1'))
|
1386 |
-
s.stfMap [sn] = []
|
1387 |
-
if v not in vmap:
|
1388 |
-
vmap [v] = {sn:1}
|
1389 |
-
else:
|
1390 |
-
d = vmap[v] # counter for voice v
|
1391 |
-
d[sn] = d.get (sn, 0) + 1 # ++ number of allocations for staff sn
|
1392 |
-
x = n.find ('instrument')
|
1393 |
-
if x != None: s.vceInst [v] = x.get ('id')
|
1394 |
-
x, noRest = n.findtext ('stem'), n.find ('rest') == None
|
1395 |
-
if noRest and (not x or x != 'none'): s.hasStems [v] = 1 # XML voice v has at least one stem
|
1396 |
-
vks = list (vmap.keys ())
|
1397 |
-
if s.jscript or s.isSib: vks.sort ()
|
1398 |
-
for v in vks: # choose staff with most allocations for each voice
|
1399 |
-
xs = [(n, sn) for sn, n in vmap[v].items ()]
|
1400 |
-
xs.sort ()
|
1401 |
-
stf = xs[-1][1] # the winner: staff with most notes of voice v
|
1402 |
-
s.stfMap [stf].append (v)
|
1403 |
-
s.vce2stf [v] = stf # reverse map
|
1404 |
-
s.curStf [v] = stf # current staff of XML voice v
|
1405 |
-
|
1406 |
-
def addStaffMap (s, vvmap): # vvmap: xml voice number -> global abc voice number
|
1407 |
-
part = [] # default: brace on staffs of one part
|
1408 |
-
for stf, voices in sorted (s.stfMap.items ()): # s.stfMap has xml staff and voice numbers
|
1409 |
-
locmap = [vvmap [iv] for iv in voices if iv in vvmap]
|
1410 |
-
nostem = [(iv not in s.hasStems) for iv in voices if iv in vvmap] # same order as locmap
|
1411 |
-
if locmap: # abc voice number of staff stf
|
1412 |
-
part.append (locmap)
|
1413 |
-
clef = s.clefMap.get (stf, 'treble') # {xml staff number -> clef}
|
1414 |
-
for i, iv in enumerate (locmap):
|
1415 |
-
clef_attr = ''
|
1416 |
-
if clef.startswith ('tab'):
|
1417 |
-
if nostem [i] and 'nostems' not in clef: clef_attr = ' nostems'
|
1418 |
-
if s.diafret and 'diafret' not in clef: clef_attr += ' diafret' # for all voices in the part
|
1419 |
-
abcOut.clefs [iv] = clef + clef_attr # add nostems when all notes of voice had no stem
|
1420 |
-
s.gStfMap.append (part)
|
1421 |
-
|
1422 |
-
def addMidiMap (s, ip, vvmap): # map abc voices to midi settings
|
1423 |
-
instr = s.instMid [ip] # get the midi settings for this part
|
1424 |
-
if instr.values (): defInstr = list(instr.values ())[0] # default settings = first instrument
|
1425 |
-
else: defInstr = s.midDflt # no instruments defined
|
1426 |
-
xs = []
|
1427 |
-
for v, vabc in vvmap.items (): # xml voice num, abc voice num
|
1428 |
-
ks = sorted (s.drumNotes.items ())
|
1429 |
-
ds = [(nt, step, midi, head) for (vd, nt), (step, midi, head) in ks if v == vd] # map perc notes
|
1430 |
-
id = s.vceInst.get (v, '') # get the instrument-id for part with multiple instruments
|
1431 |
-
if id in instr: # id is defined as midi-instrument in part-list
|
1432 |
-
xs.append ((vabc, instr [id] + ds)) # get midi settings for id
|
1433 |
-
else: xs.append ((vabc, defInstr + ds)) # only one instrument for this part
|
1434 |
-
xs.sort () # put abc voices in order
|
1435 |
-
s.midiMap.extend ([midi for v, midi in xs])
|
1436 |
-
snaarmap = ['E','G','B','d', 'f', 'a', "c'", "e'"]
|
1437 |
-
diamap = '0,1-,1,1+,2,3,3,4,4,5,6,6+,7,8-,8,8+,9,10,10,11,11,12,13,13+,14'.split (',')
|
1438 |
-
for k in sorted (s.tabmap.keys ()): # add %%map's for all tab voices
|
1439 |
-
v, noot = k;
|
1440 |
-
snaar, fret = s.tabmap [k];
|
1441 |
-
if s.diafret: fret = diamap [int (fret)]
|
1442 |
-
vabc = vvmap [v]
|
1443 |
-
snaar = s.stafflines - int (snaar)
|
1444 |
-
xs = s.tabVceMap.get (vabc, [])
|
1445 |
-
xs.append ('%%%%map tab%d %s print=%s heads=kop%s\n' % (vabc, noot, snaarmap [snaar], fret))
|
1446 |
-
s.tabVceMap [vabc] = xs
|
1447 |
-
s.koppen [fret] = 1 # collect noteheads for SVG defs
|
1448 |
-
|
1449 |
-
def parse (s, fobj):
|
1450 |
-
vvmapAll = {} # collect xml->abc voice maps (vvmap) of all parts
|
1451 |
-
e = E.parse (fobj)
|
1452 |
-
s.mkTitle (e)
|
1453 |
-
s.doDefaults (e)
|
1454 |
-
partlist = s.doPartList (e)
|
1455 |
-
parts = e.findall ('part')
|
1456 |
-
for ip, p in enumerate (parts):
|
1457 |
-
maten = p.findall ('measure')
|
1458 |
-
s.locStaffMap (p, maten) # {voice -> staff} for this part
|
1459 |
-
s.drumNotes = {} # (xml voice, abc note) -> (midi note, note head)
|
1460 |
-
s.clefOct = {} # xml staff number -> current clef-octave-change
|
1461 |
-
s.curClef = {} # xml staff number -> current abc clef
|
1462 |
-
s.stemDir = {} # xml voice number -> current stem direction
|
1463 |
-
s.tabmap = {} # (xml voice, abc note) -> (string, fret)
|
1464 |
-
s.diafret = 0 # use diatonic fretting
|
1465 |
-
s.stafflines = 5
|
1466 |
-
s.msc.initVoices (newPart = 1) # create all voices
|
1467 |
-
aantalHerhaald = 0 # keep track of number of repititions
|
1468 |
-
herhaalMaat = 0 # target measure of the repitition
|
1469 |
-
divisions = [] # current value of <divisions> for each measure
|
1470 |
-
s.msr = Measure (ip) # various measure data
|
1471 |
-
while s.msr.ixm < len (maten):
|
1472 |
-
maat = maten [s.msr.ixm]
|
1473 |
-
herhaal, lbrk = 0, ''
|
1474 |
-
s.msr.reset ()
|
1475 |
-
s.curalts = {} # passing accidentals are reset each measure
|
1476 |
-
es = list (maat)
|
1477 |
-
for i, e in enumerate (es):
|
1478 |
-
if e.tag == 'note': s.doNote (e)
|
1479 |
-
elif e.tag == 'attributes': s.doAttr (e)
|
1480 |
-
elif e.tag == 'direction': s.doDirection (e, i, es)
|
1481 |
-
elif e.tag == 'sound': s.doDirection (maat, i, es) # sound element directly in measure!
|
1482 |
-
elif e.tag == 'harmony': s.doHarmony (e, i, es)
|
1483 |
-
elif e.tag == 'barline': herhaal = s.doBarline (e)
|
1484 |
-
elif e.tag == 'backup':
|
1485 |
-
dt = int (e.findtext ('duration'))
|
1486 |
-
if chkbug (dt, s.msr): s.msc.incTime (-dt)
|
1487 |
-
elif e.tag == 'forward':
|
1488 |
-
dt = int (e.findtext ('duration'))
|
1489 |
-
if chkbug (dt, s.msr): s.msc.incTime (dt)
|
1490 |
-
elif e.tag == 'print': lbrk = s.doPrint (e)
|
1491 |
-
s.msc.addBar (lbrk, s.msr)
|
1492 |
-
divisions.append (s.msr.divs)
|
1493 |
-
if herhaal == 1:
|
1494 |
-
herhaalMaat = s.msr.ixm
|
1495 |
-
s.msr.ixm += 1
|
1496 |
-
elif herhaal == 2:
|
1497 |
-
if aantalHerhaald < 1: # jump
|
1498 |
-
s.msr.ixm = herhaalMaat
|
1499 |
-
aantalHerhaald += 1
|
1500 |
-
else:
|
1501 |
-
aantalHerhaald = 0 # reset
|
1502 |
-
s.msr.ixm += 1 # just continue
|
1503 |
-
else: s.msr.ixm += 1 # on to the next measure
|
1504 |
-
for rv in s.repeat_str.values (): # close hanging measure-repeats without stop
|
1505 |
-
rv [0] = '[I:repeat %s %d]' % (rv [1], 1)
|
1506 |
-
vvmap = s.msc.outVoices (divisions, ip, s.isSib)
|
1507 |
-
s.addStaffMap (vvmap) # update global staff map
|
1508 |
-
s.addMidiMap (ip, vvmap)
|
1509 |
-
vvmapAll.update (vvmap)
|
1510 |
-
if vvmapAll: # skip output if no part has any notes
|
1511 |
-
abcOut.mkHeader (s.gStfMap, partlist, s.midiMap, s.tabVceMap, s.koppen)
|
1512 |
-
abcOut.writeall ()
|
1513 |
-
else: info ('nothing written, %s has no notes ...' % abcOut.fnmext)
|
1514 |
-
|
1515 |
-
#----------------
|
1516 |
-
# Main Program
|
1517 |
-
#----------------
|
1518 |
-
if __name__ == '__main__':
|
1519 |
-
from optparse import OptionParser
|
1520 |
-
from glob import glob
|
1521 |
-
from zipfile import ZipFile
|
1522 |
-
ustr = '%prog [-h] [-u] [-m] [-c C] [-d D] [-n CPL] [-b BPL] [-o DIR] [-v V]\n'
|
1523 |
-
ustr += '[-x] [-p PFMT] [-t] [-s] [-i] [--v1] [--noped] [--stems] <file1> [<file2> ...]'
|
1524 |
-
parser = OptionParser (usage=ustr, version=str(VERSION))
|
1525 |
-
parser.add_option ("-u", action="store_true", help="unfold simple repeats")
|
1526 |
-
parser.add_option ("-m", action="store", help="0 -> no %%MIDI, 1 -> minimal %%MIDI, 2-> all %%MIDI", default=0)
|
1527 |
-
parser.add_option ("-c", action="store", type="int", help="set credit text filter to C", default=0, metavar='C')
|
1528 |
-
parser.add_option ("-d", action="store", type="int", help="set L:1/D", default=0, metavar='D')
|
1529 |
-
parser.add_option ("-n", action="store", type="int", help="CPL: max number of characters per line (default 100)", default=0, metavar='CPL')
|
1530 |
-
parser.add_option ("-b", action="store", type="int", help="BPL: max number of bars per line", default=0, metavar='BPL')
|
1531 |
-
parser.add_option ("-o", action="store", help="store abc files in DIR", default='', metavar='DIR')
|
1532 |
-
parser.add_option ("-v", action="store", type="int", help="set volta typesetting behaviour to V", default=0, metavar='V')
|
1533 |
-
parser.add_option ("-x", action="store_true", help="output no line breaks")
|
1534 |
-
parser.add_option ("-p", action="store", help="pageformat PFMT (cm) = scale, pageheight, pagewidth, leftmargin, rightmargin, topmargin, botmargin", default='', metavar='PFMT')
|
1535 |
-
parser.add_option ("-j", action="store_true", help="switch for compatibility with javascript version")
|
1536 |
-
parser.add_option ("-t", action="store_true", help="translate perc- and tab-staff to ABC code with %%map, %%voicemap")
|
1537 |
-
parser.add_option ("-s", action="store_true", help="shift node heads 3 units left in a tab staff")
|
1538 |
-
parser.add_option ("--v1", action="store_true", help="start-stop directions allways to first voice of staff")
|
1539 |
-
parser.add_option ("--noped", action="store_false", help="skip all pedal directions", dest='ped', default=True)
|
1540 |
-
parser.add_option ("--stems", action="store_true", help="translate stem directions", dest='stm', default=False)
|
1541 |
-
parser.add_option ("-i", action="store_true", help="read xml file from standard input")
|
1542 |
-
options, args = parser.parse_args ()
|
1543 |
-
if options.n < 0: parser.error ('only values >= 0')
|
1544 |
-
if options.b < 0: parser.error ('only values >= 0')
|
1545 |
-
if options.d and options.d not in [2**n for n in range (10)]:
|
1546 |
-
parser.error ('D should be on of %s' % ','.join ([str(2**n) for n in range (10)]))
|
1547 |
-
options.p = options.p and options.p.split (',') or [] # ==> [] | [string]
|
1548 |
-
if len (args) == 0 and not options.i: parser.error ('no input file given')
|
1549 |
-
pad = options.o
|
1550 |
-
if pad:
|
1551 |
-
if not os.path.exists (pad): os.mkdir (pad)
|
1552 |
-
if not os.path.isdir (pad): parser.error ('%s is not a directory' % pad)
|
1553 |
-
fnmext_list = []
|
1554 |
-
for i in args: fnmext_list += glob (i)
|
1555 |
-
if options.i: fnmext_list = ['stdin.xml']
|
1556 |
-
if not fnmext_list: parser.error ('none of the input files exist')
|
1557 |
-
for X, fnmext in enumerate (fnmext_list):
|
1558 |
-
fnm, ext = os.path.splitext (fnmext)
|
1559 |
-
if ext.lower () not in ('.xml','.mxl','.musicxml'):
|
1560 |
-
info ('skipped input file %s, it should have extension .xml or .mxl' % fnmext)
|
1561 |
-
continue
|
1562 |
-
if os.path.isdir (fnmext):
|
1563 |
-
info ('skipped directory %s. Only files are accepted' % fnmext)
|
1564 |
-
continue
|
1565 |
-
if fnmext == 'stdin.xml':
|
1566 |
-
fobj = sys.stdin
|
1567 |
-
elif ext.lower () == '.mxl': # extract .xml file from .mxl file
|
1568 |
-
z = ZipFile(fnmext)
|
1569 |
-
for n in z.namelist(): # assume there is always an xml file in a mxl archive !!
|
1570 |
-
if (n[:4] != 'META') and (n[-4:].lower() == '.xml'):
|
1571 |
-
fobj = z.open (n)
|
1572 |
-
break # assume only one MusicXML file per archive
|
1573 |
-
else:
|
1574 |
-
fobj = open (fnmext, 'rb') # open regular xml file
|
1575 |
-
|
1576 |
-
abcOut = ABCoutput (fnm + '.abc', pad, X, options) # create global ABC output object
|
1577 |
-
psr = Parser (options) # xml parser
|
1578 |
-
try:
|
1579 |
-
psr.parse (fobj) # parse file fobj and write abc to <fnm>.abc
|
1580 |
-
except:
|
1581 |
-
etype, value, traceback = sys.exc_info () # works in python 2 & 3
|
1582 |
-
info ('** %s occurred: %s in %s' % (etype, value, fnmext), 0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|