sander-wood commited on
Commit
e3b6675
·
verified ·
1 Parent(s): bbaca30

Delete process_data

Browse files
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">&#xe263;</text>
76
- <text id="x-" x="-3" y="0">&#xe263;</text>
77
- <text id="x+" x="-3" y="0">&#xe263;</text>
78
- <text id="normal" x="-3.7" y="0">&#xe0a3;</text>
79
- <text id="normal-" x="-3.7" y="0">&#xe0a3;</text>
80
- <text id="normal+" x="-3.7" y="0">&#xe0a4;</text>
81
- <g id="circle-x"><text x="-3" y="0">&#xe263;</text><circle r="4" class="stroke"></circle></g>
82
- <g id="circle-x-"><text x="-3" y="0">&#xe263;</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)