avans06 commited on
Commit
adcbc9f
·
1 Parent(s): d371a2d

Integrated and Enhanced: Audio-to-MIDI and Advanced MIDI Renderer

Browse files

- **Integration of asigalov61's Projects:**
Merges the `ByteDance-Solo-Piano-Audio-to-MIDI-Transcription` and `Advanced-MIDI-Renderer` projects into a single, cohesive application. This forms the foundational workflow for MIDI processing and rendering.

- **Addition of General-Purpose Transcription:**
Implements Spotify's `basic-pitch` library as a new transcription option. This allows for the effective transcription of non-piano music, including songs with vocals and multiple instruments.

- **Dynamic UI for Method Selection:**
A new UI control allows users to choose between the "General Purpose" (basic-pitch) and "Piano-Specific" (ByteDance) transcription models, with the UI dynamically showing relevant settings for the selected method.

- **Refactor: Extract shared MIDI module to reduce duplication:**
The `midi_to_colab_audio.py` and `TMIDIX.py` scripts both contained a large, duplicated block of code for MIDI processing.

- **Improve FluidSynth rendering speed in midi_to_colab_audio:**
Optimized the audio synthesis process to fix a major performance issue with long MIDI files. The previous method of using `np.concatenate` in a loop was very slow.
The new implementation now collects audio chunks in a list and merges them all at once at the end, making the rendering process significantly faster.
This change reduces the time complexity from O(n^2) to O(n), resulting in a significant performance improvement for long MIDI files.

.gitignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .vs
2
+ .vscode
3
+ # Byte-compiled / optimized / DLL files
4
+ __pycache__/
5
+
6
+ venv/
7
+ tmp/
8
+ sf2/
9
+ models/
10
+ output/
11
+ rendered_midi/
12
+ transcribed_/
KBH-Real-Choir-V2.5.sf2 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:04848e9c6cb7e27f2131156dd2ecedc283c1805bd326b5ea64ed2b6da23e106a
3
- size 17360134
 
 
 
 
Live HQ Natural SoundFont GM.sf2 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:5ed1b6a205686e43ead7386560c6610406b3cf4a0dfda230b89b8403dcf5efb7
3
- size 836038682
 
 
 
 
Nice-Strings-PlusOrchestra-v1.6.sf2 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:215e41f740172ddf72bf729c7df2f38356866f84a99fac39f84f861a02eddddd
3
- size 442272364
 
 
 
 
Orpheus_18.06.2020.sf2 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:759fb756dcc8560c46f7e911ce981c69690e1d4aa3a634c537dbf658ccd11615
3
- size 1288303498
 
 
 
 
ProtoSquare.sf2 DELETED
Binary file (364 kB)
 
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Advanced MIDI Renderer
3
  emoji: 🎹
4
  colorFrom: purple
5
  colorTo: green
@@ -13,8 +13,8 @@ tags:
13
  - renderer
14
  - MIDI rendering
15
  - MIDI renderer
16
- short_description: Transform and render any MIDI
17
- sdk_version: 5.39.0
18
  thumbnail: >-
19
  https://cdn-uploads.huggingface.co/production/uploads/5f57ea2d3f32f12a3c0692e6/SvsnExU8EVOdm-Ol32RIn.png
20
  ---
 
1
  ---
2
+ title: Audio To MIDI And Advanced Renderer
3
  emoji: 🎹
4
  colorFrom: purple
5
  colorTo: green
 
13
  - renderer
14
  - MIDI rendering
15
  - MIDI renderer
16
+ short_description: Audio to MIDI Transcription and Advanced render
17
+ sdk_version: 5.41.1
18
  thumbnail: >-
19
  https://cdn-uploads.huggingface.co/production/uploads/5f57ea2d3f32f12a3c0692e6/SvsnExU8EVOdm-Ol32RIn.png
20
  ---
SGM-v2.01-YamahaGrand-Guit-Bass-v2.7.sf2 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:cd41a4639c9e7a96413b4b22540d48e6741e24bcdabcb2eff22cd65929df3cfa
3
- size 553961496
 
 
 
 
SuperGameBoy.sf2 DELETED
Binary file (112 kB)
 
TCUPY.py DELETED
@@ -1,1093 +0,0 @@
1
- #! /usr/bin/python3
2
-
3
- r'''############################################################################
4
- ################################################################################
5
- #
6
- #
7
- # Tegridy Cupy Python Module (TCUPY)
8
- # Version 1.0
9
- #
10
- # Project Los Angeles
11
- #
12
- # Tegridy Code 2025
13
- #
14
- # https://github.com/asigalov61/tegridy-tools
15
- #
16
- #
17
- ################################################################################
18
- #
19
- # Copyright 2024 Project Los Angeles / Tegridy Code
20
- #
21
- # Licensed under the Apache License, Version 2.0 (the "License");
22
- # you may not use this file except in compliance with the License.
23
- # You may obtain a copy of the License at
24
- #
25
- # http://www.apache.org/licenses/LICENSE-2.0
26
- #
27
- # Unless required by applicable law or agreed to in writing, software
28
- # distributed under the License is distributed on an "AS IS" BASIS,
29
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
30
- # See the License for the specific language governing permissions and
31
- # limitations under the License.
32
- #
33
- ################################################################################
34
- ################################################################################
35
- #
36
- # Critical dependencies
37
- #
38
- # !pip install cupy-cuda12x
39
- # !pip install numpy==1.24.4
40
- #
41
- ################################################################################
42
- '''
43
-
44
- ################################################################################
45
-
46
- print('=' * 70)
47
- print('Loading module...')
48
- print('Please wait...')
49
- print('=' * 70)
50
-
51
- ################################################################################
52
-
53
- import sys
54
- import os
55
-
56
- ################################################################################
57
-
58
- try:
59
- import cupy as cp
60
- import cupy as np
61
- print('=' * 70)
62
- print('CuPy is found!')
63
- print('Will use CuPy and GPU for processing!')
64
- print('=' * 70)
65
-
66
- except ImportError as e:
67
- print(f"Error: Could not import CuPy. Details: {e}")
68
- # Handle the error, such as providing a fallback or exiting the program
69
- # For example:
70
- print("Please make sure CuPy is installed.")
71
- print('=' * 70)
72
-
73
- raise RuntimeError("CuPy could not be loaded!") from e
74
-
75
- ################################################################################
76
-
77
- from collections import defaultdict, deque
78
- from typing import Optional, Tuple, Dict, Any, List
79
-
80
- ################################################################################
81
-
82
- # Constants
83
- MEMORY_LEN = 12 # Autoregressive context length
84
- SEQUENCE_LENGTH = 32 # Each sequence has 24 triplets
85
-
86
- # Baseline penalty values:
87
- REPETITION_PENALTY = (1.0, 1.0, 1.0) # base repetition penalty per element
88
- SPIKE_PENALTY_STRENGTH = (1.0, 1.0, 1.0) # base spike penalty strength per element
89
- SPIKE_SIGMA = (1.0, 1.0, 1.0) # baseline sigma value per element (minimum allowed)
90
-
91
- ###################################################################################
92
-
93
- def find_numpy_array(src_array, trg_array):
94
-
95
- """
96
- Finds 1D numpy array in 2D numpy array
97
- """
98
-
99
- match_mask = np.all(src_array == trg_array, axis=1)
100
-
101
- return np.where(match_mask)[0]
102
-
103
- ###################################################################################
104
-
105
- def vertical_list_search(src_list, trg_list):
106
-
107
- """
108
- For each vertical window of consecutive rows of height len(trg_list) in src_list,
109
- this function checks whether for every offset j (0 <= j < len(trg_list)) the row
110
- at index (window_start + j) contains trg_list[j].
111
-
112
- It returns a list of windows (each a list of consecutive row indices) that meet this condition.
113
- """
114
-
115
- if not src_list or not trg_list:
116
- return []
117
-
118
- n = len(src_list)
119
- k = len(trg_list)
120
-
121
- num_windows = n - k + 1
122
-
123
- if num_windows <= 0:
124
- return []
125
-
126
- # Determine the maximum row length.
127
- max_len = max(len(row) for row in src_list)
128
-
129
- # Determine a fill value guaranteed to be less than any valid value.
130
- global_min = min(min(row) for row in src_list if row)
131
- fill_value = global_min - 1
132
-
133
- # Build a padded 2D array A (shape n x max_len) from src_list.
134
- A = np.full((n, max_len), fill_value, dtype=np.int64)
135
- for i, row in enumerate(src_list):
136
- L = len(row)
137
- A[i, :L] = row
138
-
139
- # For each unique target in trg_list, compute a Boolean vector of length n.
140
- # present[t][i] will be True if A[i, :] contains t, else False.
141
- unique_targets = set(trg_list)
142
-
143
- present_dict = {}
144
-
145
- for t in unique_targets:
146
- # Compute along axis=1 so that for each row we see if any element equals t.
147
- present_dict[t] = np.any(A == t, axis=1)
148
-
149
- # Build a Boolean array B of shape (k, num_windows) where for each offset j,
150
- # B[j, s] = present_dict[ trg_list[j] ][s + j] for each window starting index s.
151
- B = np.empty((k, num_windows), dtype=bool)
152
-
153
- for j in range(k):
154
- t = trg_list[j]
155
- # For a vertical window starting at s, row s+j should contain t.
156
- B[j, :] = present_dict[t][j: j + num_windows]
157
-
158
- # A window is valid if all k rows in that window contain the required target.
159
- valid_windows_mask = np.all(B, axis=0)
160
- valid_starts = np.nonzero(valid_windows_mask)[0]
161
-
162
- # Create output windows (each as a list of consecutive row indices).
163
- result = [list(range(s, s + k)) for s in valid_starts]
164
-
165
- return result
166
-
167
-
168
- ###################################################################################
169
-
170
- def pack_sequences(train_data, pad_val=-1):
171
- """
172
- Packs a list of variable-length token sequences into a 2D CuPy array.
173
-
174
- This version computes lengths and builds the padded array and mask entirely on GPU.
175
- It converts each sequence into a CuPy array, concatenates them, and assigns tokens in one shot.
176
-
177
- Returns:
178
- batch: a CuPy array of shape (n, max_len)
179
- lengths: a CuPy array of shape (n,) containing each sequence's length.
180
- """
181
- n = len(train_data)
182
- # Compute lengths of each sequence and convert to a CuPy array.
183
- lengths = cp.array([len(seq) for seq in train_data], dtype=cp.int64)
184
- max_len_val = int(cp.max(lengths).get())
185
- # Allocate the padded 2D array filled with pad_val.
186
- batch = cp.full((n, max_len_val), pad_val, dtype=cp.int64)
187
- # Create a boolean mask: for each row, positions less than the sequence length are valid.
188
- mask = cp.arange(max_len_val).reshape(1, max_len_val) < lengths.reshape(n, 1)
189
- # Convert each sequence to a CuPy array and concatenate them.
190
- sequences = [cp.array(seq, dtype=cp.int64) for seq in train_data]
191
- flat = cp.concatenate(sequences)
192
- # Fill in the valid positions.
193
- batch[mask] = flat
194
- return batch, lengths
195
-
196
- ###################################################################################
197
-
198
- def count_best_pair_gpu(batch, lengths, factor, pad_val=-1):
199
- """
200
- Given the entire GPU-resident packed data, compute the most frequent
201
- adjacent pair (encoded as: pair_val = first * factor + second) on GPU.
202
- """
203
- n, L = batch.shape
204
- cols = cp.arange(L - 1, dtype=cp.int64)
205
- cols_expanded = cp.broadcast_to(cols, (n, L - 1))
206
- valid_mask = cols_expanded < cp.reshape(lengths, (n, 1)) - 1
207
-
208
- first_tokens = batch[:, :L - 1]
209
- second_tokens = batch[:, 1:L]
210
- valid_first = first_tokens[valid_mask]
211
- valid_second = second_tokens[valid_mask]
212
-
213
- pairs = valid_first * factor + valid_second
214
- if pairs.size == 0:
215
- return None
216
-
217
- sorted_pairs = cp.sort(pairs)
218
- diff = cp.diff(sorted_pairs)
219
- boundaries = cp.nonzero(diff)[0] + 1
220
- group_starts = cp.concatenate([cp.array([0], dtype=cp.int64), boundaries])
221
- group_ends = cp.concatenate([boundaries, cp.array([sorted_pairs.size], dtype=cp.int64)])
222
- group_counts = group_ends - group_starts
223
-
224
- max_idx = int(cp.argmax(group_counts))
225
- best_pair_enc = int(sorted_pairs[group_starts[max_idx]])
226
- best_freq = int(group_counts[max_idx])
227
- first = best_pair_enc // factor
228
- second = best_pair_enc % factor
229
- return (first, second, best_freq)
230
-
231
- ###################################################################################
232
-
233
- merge_kernel_code = r'''
234
- extern "C" __global__
235
- void merge_pair_kernel(const long* input, long* output,
236
- const long* input_lengths, long* output_lengths,
237
- const long num_rows, const long num_cols,
238
- const long a, const long b, const long new_token,
239
- const long pad_val) {
240
- int row = blockIdx.x * blockDim.x + threadIdx.x;
241
- if (row >= num_rows) return;
242
- long in_length = input_lengths[row];
243
- long out_idx = 0;
244
- bool skip_next = false;
245
- for (long i = 0; i < in_length; i++) {
246
- if (skip_next) {
247
- skip_next = false;
248
- continue;
249
- }
250
- long token = input[row * num_cols + i];
251
- if (i < in_length - 1 && token == a && input[row * num_cols + i + 1] == b) {
252
- output[row * num_cols + out_idx] = new_token;
253
- out_idx++;
254
- skip_next = true;
255
- } else {
256
- output[row * num_cols + out_idx] = token;
257
- out_idx++;
258
- }
259
- }
260
- output_lengths[row] = out_idx;
261
- for (long j = out_idx; j < num_cols; j++) {
262
- output[row * num_cols + j] = pad_val;
263
- }
264
- }
265
- '''
266
- merge_kernel = cp.RawKernel(merge_kernel_code, 'merge_pair_kernel')
267
-
268
- ###################################################################################
269
-
270
- def learn_bpe_codes_gpu(train_data, vocab_size=4096, max_merges=None, pad_val=-1):
271
- """
272
- Learn BPE merge rules completely on GPU.
273
-
274
- The training data is packed once (using the vectorized pack_sequences).
275
- On each merge iteration, the best adjacent pair is computed on GPU and then merged
276
- into a new token via a custom merge kernel (with double-buffering).
277
-
278
- Returns:
279
- codes: a list of merge rules as ((first, second), new_token)
280
- final_data: the merged training data (list of sequences)
281
- """
282
- # Pack the entire dataset onto GPU.
283
- batch, lengths = pack_sequences(train_data, pad_val)
284
- n, L = batch.shape
285
-
286
- # Initialize vocabulary and the next available token.
287
- initial_vocab = {token for seq in train_data for token in seq}
288
- next_token = max(initial_vocab) + 1
289
- codes = []
290
- merge_count = 0
291
- pbar = tqdm.tqdm(total=max_merges if max_merges is not None else None,
292
- desc="Learning BPE Codes (GPU)", leave=True)
293
-
294
- # Preallocate buffers for double-buffering.
295
- work_batch = cp.empty_like(batch)
296
- work_lengths = cp.empty_like(lengths)
297
- input_batch = batch
298
- input_lengths = lengths
299
-
300
- threads_per_block = 128
301
- blocks = (n + threads_per_block - 1) // threads_per_block
302
-
303
- while next_token < vocab_size and (max_merges is None or merge_count < max_merges):
304
- # Early stop if all sequences have collapsed (checked on GPU).
305
- if bool(cp.all(input_lengths == 1)):
306
- pbar.write("All sequences have collapsed; stopping early.")
307
- break
308
-
309
- factor = next_token # by construction, every token is < next_token
310
- best = count_best_pair_gpu(input_batch, input_lengths, factor, pad_val)
311
- if best is None:
312
- pbar.write("No mergeable pairs found; stopping early.")
313
- break
314
-
315
- best_pair = (best[0], best[1])
316
- best_freq = best[2]
317
- if best_freq < 2:
318
- pbar.write("Best pair frequency is less than 2; stopping early.")
319
- break
320
-
321
- codes.append((best_pair, next_token))
322
-
323
- # Launch the merge kernel.
324
- merge_kernel((blocks,), (threads_per_block,),
325
- (input_batch,
326
- work_batch,
327
- input_lengths,
328
- work_lengths,
329
- cp.int64(n),
330
- cp.int64(L),
331
- cp.int64(best_pair[0]),
332
- cp.int64(best_pair[1]),
333
- cp.int64(next_token),
334
- cp.int64(pad_val)))
335
- # Swap buffers for double-buffering.
336
- input_batch, work_batch = work_batch, input_batch
337
- input_lengths, work_lengths = work_lengths, input_lengths
338
-
339
- next_token += 1
340
- merge_count += 1
341
- pbar.update(1)
342
- pbar.close()
343
-
344
- final_batch = cp.asnumpy(input_batch)
345
- final_lengths = cp.asnumpy(input_lengths)
346
- final_data = [final_batch[i, :final_lengths[i]].tolist() for i in range(n)]
347
- return codes, final_data
348
-
349
- ###################################################################################
350
-
351
- fused_merge_kernel_code = r'''
352
- extern "C" __global__
353
- void fused_merge_kernel(long* data_in, long* data_out, long* lengths, const long pad_val,
354
- const long num_rows, const long max_len, const long num_merges, const long* merge_rules) {
355
- int row = blockIdx.x * blockDim.x + threadIdx.x;
356
- if (row >= num_rows) return;
357
- long base = row * max_len;
358
- long cur_len = lengths[row];
359
- long* cur = data_in + base;
360
- long* other = data_out + base;
361
- // Process each merge rule sequentially.
362
- for (int m = 0; m < num_merges; m++) {
363
- long a = merge_rules[3 * m];
364
- long b = merge_rules[3 * m + 1];
365
- long new_token = merge_rules[3 * m + 2];
366
- long out_idx = 0;
367
- for (int i = 0; i < cur_len; i++) {
368
- if (i < cur_len - 1 && cur[i] == a && cur[i+1] == b) {
369
- other[out_idx] = new_token;
370
- out_idx++;
371
- i++; // Skip the next token.
372
- } else {
373
- other[out_idx] = cur[i];
374
- out_idx++;
375
- }
376
- }
377
- cur_len = out_idx;
378
- // Swap pointers for the next merge.
379
- long* temp = cur;
380
- cur = other;
381
- other = temp;
382
- }
383
- lengths[row] = cur_len;
384
- // Pad the remaining positions with pad_val.
385
- for (int i = cur_len; i < max_len; i++) {
386
- cur[i] = pad_val;
387
- }
388
- // If the final result is not in data_in, copy back.
389
- if (cur != data_in + base) {
390
- for (int i = 0; i < cur_len; i++) {
391
- data_in[base + i] = cur[i];
392
- }
393
- }
394
- }
395
- '''
396
- fused_kernel = cp.RawKernel(fused_merge_kernel_code, 'fused_merge_kernel')
397
-
398
- ###################################################################################
399
-
400
- def retokenize_train_data_fused_gpu(train_data, codes, pad_val=-1):
401
- """
402
- Retokenize training data using the fully fused GPU kernel.
403
-
404
- The entire training dataset is first packed into GPU memory (using pack_sequences).
405
- All learned merge rules (provided in 'codes') are applied via a single kernel launch.
406
- Each GPU thread processes one sequence by applying all merge rules sequentially.
407
-
408
- Returns:
409
- tokenized_data: list of retokenized sequences.
410
- """
411
- # Pack the data.
412
- batch, lengths = pack_sequences(train_data, pad_val)
413
- n, max_len = batch.shape
414
- # Build a flattened merge_rules array using CuPy.
415
- if len(codes) > 0:
416
- merge_rules_list = [[rule[0][0], rule[0][1], rule[1]] for rule in codes]
417
- merge_rules_gpu = cp.array(merge_rules_list, dtype=cp.int64)
418
- merge_rules_gpu = merge_rules_gpu.reshape(-1)
419
- else:
420
- merge_rules_gpu = cp.empty((0,), dtype=cp.int64)
421
- num_merges = merge_rules_gpu.shape[0] // 3
422
- # Preallocate a scratch buffer.
423
- scratch = cp.empty_like(batch)
424
- threads_per_block = 128
425
- blocks = (n + threads_per_block - 1) // threads_per_block
426
- # Launch the fused kernel.
427
- fused_kernel((blocks,), (threads_per_block,),
428
- (batch, scratch, lengths, cp.int64(pad_val),
429
- cp.int64(n), cp.int64(max_len), cp.int64(num_merges), merge_rules_gpu))
430
- final_batch = cp.asnumpy(batch)
431
- final_lengths = cp.asnumpy(lengths)
432
- tokenized_data = [final_batch[i, :final_lengths[i]].tolist() for i in range(n)]
433
- return tokenized_data
434
-
435
- ###################################################################################
436
-
437
- def bpe_encode(seq, codes):
438
- """
439
- Iteratively encodes a sequence using BPE merge rules provided in a dictionary.
440
-
441
- Args:
442
- seq (list): A list of tokens (e.g. integers) representing the input sequence.
443
- codes (dict): A dictionary mapping token pairs (a tuple of two tokens)
444
- to a merged token. For example:
445
- { (1, 2): 100, (100, 3): 101 }
446
-
447
- Returns:
448
- list: The encoded sequence after applying all possible merges.
449
-
450
- The function repeatedly scans the entire sequence from left to right;
451
- whenever it finds a contiguous token pair that exists as a key in the
452
- codes dict, it replaces that pair with the merged token. This pass is
453
- repeated until no more merges are possible.
454
- """
455
-
456
- if type(codes) == list:
457
- codes = dict(codes)
458
-
459
- encoded_seq = seq.copy() # work on a copy so as not to modify the original
460
- done = False
461
- while not done:
462
- new_seq = []
463
- i = 0
464
- changed = False
465
- while i < len(encoded_seq):
466
- # If a merge is possible, merge the two tokens.
467
- if i < len(encoded_seq) - 1 and (encoded_seq[i], encoded_seq[i + 1]) in codes:
468
- new_seq.append(codes[(encoded_seq[i], encoded_seq[i + 1])])
469
- i += 2 # Skip the next token as it was merged.
470
- changed = True
471
- else:
472
- new_seq.append(encoded_seq[i])
473
- i += 1
474
- # If no merges occurred in this pass, exit the loop.
475
- if not changed:
476
- done = True
477
- encoded_seq = new_seq
478
- return encoded_seq
479
-
480
- ###################################################################################
481
-
482
- def bpe_decode(seq, codes):
483
- """
484
- Decodes a sequence encoded with BPE merge rules defined in a codes dictionary.
485
-
486
- Args:
487
- seq (list): The encoded sequence (a list of tokens).
488
- codes (dict): A dictionary mapping token pairs to the merged token, used during encoding.
489
-
490
- Returns:
491
- list: The fully decoded sequence, with all merged tokens recursively expanded.
492
-
493
- The function constructs a reverse mapping that converts a merged token back into
494
- its constituent pair. Each token in the sequence is then recursively expanded.
495
- """
496
-
497
- if type(codes) == list:
498
- codes = dict(codes)
499
-
500
- # Build the reverse mapping: key = merged token, value = tuple (original token pair)
501
- reverse_mapping = {merged: pair for pair, merged in codes.items()}
502
-
503
- def recursive_expand(token):
504
- # If the token is a merged token, expand it recursively.
505
- if token in reverse_mapping:
506
- a, b = reverse_mapping[token]
507
- return recursive_expand(a) + recursive_expand(b)
508
- else:
509
- return [token]
510
-
511
- decoded_seq = []
512
- for token in seq:
513
- decoded_seq.extend(recursive_expand(token))
514
- return decoded_seq
515
-
516
- ###################################################################################
517
-
518
- def ensure_triplet(val: Any, name: str = "") -> Tuple[float, float, float]:
519
- """
520
- Ensure the given parameter is returned as a triplet.
521
- If provided as a scalar, promote it to a triplet.
522
- """
523
- if np.isscalar(val):
524
- return (float(val), float(val), float(val))
525
- elif isinstance(val, (list, tuple)) and len(val) == 3:
526
- return tuple(float(x) for x in val)
527
- else:
528
- raise ValueError(f"{name} must be a scalar or a sequence of 3 numbers.")
529
-
530
- ###################################################################################
531
-
532
- REP_PENALTY = ensure_triplet(REPETITION_PENALTY, "REPETITION_PENALTY")
533
- SPIKE_STRENGTH = ensure_triplet(SPIKE_PENALTY_STRENGTH, "SPIKE_PENALTY_STRENGTH")
534
- SPIKE_SIG = ensure_triplet(SPIKE_SIGMA, "SPIKE_SIGMA")
535
-
536
- ###################################################################################
537
-
538
- def sliding_window_view_alternative(a: np.ndarray, window_length: int) -> np.ndarray:
539
- """
540
- Create a sliding-window view (without copying) of an array.
541
- Expected input shape: (n, L, d) and returns: (n, L - window_length + 1, window_length, d)
542
- """
543
- n, L, d = a.shape
544
- new_shape = (n, L - window_length + 1, window_length, d)
545
- new_strides = (a.strides[0], a.strides[1], a.strides[1], a.strides[2])
546
- return np.lib.stride_tricks.as_strided(a, shape=new_shape, strides=new_strides)
547
-
548
- ###################################################################################
549
-
550
- def build_ngram_mapping(data: np.ndarray, memory_len: int) -> Dict[Any, Dict[Any, int]]:
551
- """
552
- Build an n-gram mapping from a context (a sequence of triplets) to candidate triplets with frequencies.
553
- """
554
- n, L, d = data.shape
555
- window_length = memory_len + 1 # context (memory) + candidate
556
- windows = sliding_window_view_alternative(data, window_length)
557
- # windows shape: (n, L - window_length + 1, window_length, d)
558
-
559
- # Split windows into context (first memory_len triplets) and candidates (last triplet)
560
- contexts = windows[:, :, :memory_len, :] # shape: (n, num_windows, memory_len, d)
561
- candidates = windows[:, :, memory_len, :] # shape: (n, num_windows, d)
562
-
563
- # Flatten the batch and window dimensions.
564
- contexts_flat = contexts.reshape(-1, memory_len, d)
565
- candidates_flat = candidates.reshape(-1, d)
566
-
567
- mapping = defaultdict(lambda: defaultdict(int))
568
- total_windows = contexts_flat.shape[0]
569
- for context_arr, candidate_arr in tqdm.tqdm(
570
- zip(contexts_flat, candidates_flat),
571
- total=total_windows,
572
- desc="Building n-gram mapping"):
573
- context_key = tuple(map(tuple, context_arr)) # use a tuple of triplets as the key
574
- candidate_val = tuple(candidate_arr)
575
- mapping[context_key][candidate_val] += 1
576
-
577
- return {context: dict(candidates) for context, candidates in mapping.items()}
578
-
579
- ###################################################################################
580
-
581
- def precompute_mapping_lookup(mapping: Dict[Any, Dict[Any, int]]) -> Dict[Any, Tuple[Tuple[Any, ...], np.ndarray]]:
582
- """
583
- Converts the mapping into a lookup table: context -> (tuple(candidates), frequencies_array).
584
- """
585
- mapping_lookup = {}
586
- for context, candidate_dict in tqdm.tqdm(mapping.items(), desc="Precomputing lookup"):
587
- candidates = tuple(candidate_dict.keys())
588
- frequencies = np.array(list(candidate_dict.values()), dtype=np.float64)
589
- mapping_lookup[context] = (candidates, frequencies)
590
- return mapping_lookup
591
-
592
- ###################################################################################
593
-
594
- def build_training_sequences_set(data: np.ndarray) -> set:
595
- """
596
- Build a set of training sequences (each as a tuple of triplets) for uniqueness checking.
597
- """
598
- return {tuple(map(tuple, seq)) for seq in data}
599
-
600
- ###################################################################################
601
-
602
- def generate_sequence_optimized(mapping_lookup: Dict[Any, Tuple[Tuple[Any, ...], np.ndarray]],
603
- training_set: set,
604
- memory_len: int,
605
- sequence_length: int = 24,
606
- max_attempts: int = 1000) -> Optional[Tuple[Tuple[float, float, float], ...]]:
607
- """
608
- Autoregressively generate a new, unique sequence using the precomputed mapping lookup.
609
- The invariant maintained is: the second element of one triplet is never greater than the first element
610
- of the following triplet.
611
-
612
- Two dynamic adjustments are applied for candidate selection:
613
-
614
- 1. **Dynamic Repetition Penalty:**
615
- For each candidate, count the occurrences of each element in the generated sequence.
616
- Rather than a fixed penalty, this repetition penalty scales with the ratio
617
- (current_length / sequence_length). In log-space, it subtracts:
618
- (current_length / sequence_length) * sum_k(count[k] * log(REP_PENALTY[k])
619
- 2. **Dynamic Spike (Variance) Penalty:**
620
- For each candidate, compute the squared difference from the running average for each element.
621
- Use a dynamic sigma that is the maximum between the running standard deviation and the baseline.
622
- The penalty term for each element is:
623
- SPIKE_STRENGTH[k] * ((cand[k] - running_avg[k])^2) / (2 * dynamic_sigma[k]^2)
624
- The overall spike penalty is the sum of the three terms and is subtracted from the candidate’s log frequency.
625
-
626
- The resulting candidate log score is computed as:
627
- log(candidate_frequency) - rep_penalty_component - spike_penalty_component
628
- A numerical stable softmax is then applied over these scores to determine the probability for drawing a candidate.
629
-
630
- If no candidate passing the invariant is found, the attempt is aborted.
631
-
632
- Parameters:
633
- mapping_lookup: Precomputed lookup mapping (context → (candidates, frequencies)).
634
- training_set: Set of training sequences to ensure uniqueness.
635
- memory_len: Number of triplets used as context.
636
- sequence_length: Desired length of the generated sequence.
637
- max_attempts: Maximum number of generation attempts.
638
-
639
- Returns:
640
- A new unique sequence (tuple of triplets) that respects the invariant, or None if not found.
641
- """
642
- mapping_keys = list(mapping_lookup.keys())
643
- num_keys = len(mapping_keys)
644
-
645
- for attempt in range(max_attempts):
646
- # Select a seed context randomly (from training data so that the invariant holds).
647
- seed = mapping_keys[np.random.randint(0, num_keys)]
648
- generated_sequence: List[Tuple[float, float, float]] = list(seed)
649
- valid_generation = True
650
-
651
- while len(generated_sequence) < sequence_length:
652
- last_triplet = generated_sequence[-1]
653
- current_context = tuple(generated_sequence[-memory_len:]) # context as tuple of triplets
654
- candidate_found = False
655
-
656
- if current_context in mapping_lookup:
657
- candidates, frequencies = mapping_lookup[current_context]
658
- # Filter candidates by invariant:
659
- # Candidate's first element must be >= last triplet's second element.
660
- valid_indices = [i for i, cand in enumerate(candidates) if cand[0] >= last_triplet[1]]
661
- if valid_indices:
662
- # Filter candidates and their associated frequencies.
663
- filtered_freqs = frequencies[valid_indices]
664
- filtered_candidates = [candidates[i] for i in valid_indices]
665
-
666
- # Convert candidates into a NumPy array for vectorized operations.
667
- candidate_array = np.array(filtered_candidates, dtype=np.float64) # shape: (n_candidates, 3)
668
-
669
- # Prepare generation history as array.
670
- generated_array = np.array(generated_sequence, dtype=np.float64) # shape: (T, 3)
671
- current_length = generated_array.shape[0]
672
-
673
- # Running average and standard deviation for dynamic spike adjustment.
674
- running_avg = np.mean(generated_array, axis=0) # shape: (3,)
675
- running_std = np.std(generated_array, axis=0) # shape: (3,)
676
- # Dynamic sigma: ensure a minimum sigma value.
677
- dynamic_sigma = np.maximum(running_std, np.array(SPIKE_SIG))
678
-
679
- # --- Compute Repetition Penalty ---
680
- # For each candidate, count the number of occurrences for each element along the corresponding column.
681
- rep_counts = np.array([
682
- [np.sum(generated_array[:, k] == candidate_array[i, k]) for k in range(3)]
683
- for i in range(candidate_array.shape[0])
684
- ]) # shape: (n_candidates, 3)
685
- # The repetition penalty in log-space.
686
- rep_penalty_term = np.sum(rep_counts * np.log(np.array(REP_PENALTY)) *
687
- (current_length / sequence_length), axis=1) # shape: (n_candidates,)
688
-
689
- # --- Compute Spike (Variance) Penalty ---
690
- # Compute the difference per candidate from the running average.
691
- diff = candidate_array - running_avg # shape: (n_candidates, 3)
692
- spike_penalty_term = np.sum(np.array(SPIKE_STRENGTH) * (diff**2) / (2 * (dynamic_sigma**2)),
693
- axis=1) # shape: (n_candidates,)
694
-
695
- # --- Compute Candidate Log-Scores ---
696
- # Use np.log on frequencies (they are positive by construction).
697
- log_freq = np.log(filtered_freqs)
698
- log_scores = log_freq - rep_penalty_term - spike_penalty_term
699
-
700
- # --- Softmax in Log-space (stable computation) ---
701
- max_log = np.max(log_scores)
702
- exp_scores = np.exp(log_scores - max_log)
703
- probabilities = exp_scores / np.sum(exp_scores)
704
-
705
- # Choose the next candidate using advanced probabilities.
706
- chosen_idx = np.random.choice(len(filtered_candidates), p=probabilities)
707
- next_triplet = filtered_candidates[chosen_idx]
708
- candidate_found = True
709
-
710
- if not candidate_found:
711
- # Abort this generation attempt if no valid candidate is available.
712
- valid_generation = False
713
- break
714
-
715
- generated_sequence.append(next_triplet)
716
-
717
- # Ensure the final sequence meets the invariant and is unique.
718
- if valid_generation and len(generated_sequence) == sequence_length:
719
- new_sequence = tuple(generated_sequence)
720
- invariant_ok = all(a[1] <= b[0] for a, b in zip(new_sequence, new_sequence[1:]))
721
- if invariant_ok and new_sequence not in training_set:
722
- return new_sequence
723
-
724
- return None
725
-
726
- ###################################################################################
727
-
728
- def analyze_generated_sequence(sequence: tuple, mapping_lookup: dict, memory_len: int) -> tuple:
729
- """
730
- Analyze the generated sequence and return several useful statistics
731
- as both a dictionary and as a nicely formatted string report.
732
-
733
- Statistics Computed:
734
- - unigram_diversity: Ratio of unique triplets to total triplets.
735
- - repetition_rate: Fraction of repeated triplets.
736
- - bigram_diversity: Ratio of unique consecutive pairs to total pairs.
737
- - max_consecutive_repetitions: Maximum number of identical consecutive triplets.
738
- - avg_candidate_probability (overfit rate): For the transitions (using a sliding window of size
739
- MEMORY_LEN as context followed by candidate), the average probability of the chosen candidate
740
- as per the training mapping.
741
-
742
- Additional Analytics:
743
- - element_stats: For each element (index 0, 1, 2) in a triplet, includes:
744
- * mean, standard deviation, minimum, maximum, and average consecutive absolute difference.
745
- - avg_transition_entropy: The average entropy of the candidate distributions (from mapping_lookup)
746
- for each transition context.
747
- - context_coverage: The fraction of transitions (based on context of length MEMORY_LEN) that are found
748
- in the mapping_lookup.
749
-
750
- Parameters:
751
- sequence: Generated sequence (tuple of triplets).
752
- mapping_lookup: Precomputed mapping lookup.
753
- memory_len: The context length used.
754
-
755
- Returns:
756
- A tuple containing:
757
- (stats_dict, stats_report_string)
758
- """
759
- stats = {}
760
- seq_len = len(sequence)
761
-
762
- # --- Basic Statistics ---
763
-
764
- # Unigram.
765
- unique_triplets = len(set(sequence))
766
- stats["unigram_diversity"] = unique_triplets / seq_len
767
- stats["repetition_rate"] = 1 - (unique_triplets / seq_len)
768
-
769
- # Bigram.
770
- bigrams = [(sequence[i], sequence[i+1]) for i in range(seq_len - 1)]
771
- unique_bigrams = len(set(bigrams))
772
- stats["bigram_diversity"] = unique_bigrams / (seq_len - 1)
773
-
774
- # Maximum consecutive repetitions.
775
- max_consecutive = 1
776
- current_consecutive = 1
777
- for i in range(1, seq_len):
778
- if sequence[i] == sequence[i-1]:
779
- current_consecutive += 1
780
- if current_consecutive > max_consecutive:
781
- max_consecutive = current_consecutive
782
- else:
783
- current_consecutive = 1
784
- stats["max_consecutive_repetitions"] = max_consecutive
785
-
786
- # Avg Candidate Probability (Overfit Rate)
787
- overfit_probs = []
788
- for i in range(memory_len, seq_len):
789
- context = tuple(sequence[i - memory_len: i])
790
- candidate = sequence[i]
791
- if context in mapping_lookup:
792
- candidates, frequencies = mapping_lookup[context]
793
- total_freq = np.sum(frequencies)
794
- try:
795
- idx = candidates.index(candidate)
796
- cand_prob = frequencies[idx] / total_freq
797
- overfit_probs.append(cand_prob)
798
- except ValueError:
799
- pass
800
- stats["avg_candidate_probability"] = np.mean(overfit_probs) if overfit_probs else None
801
-
802
- # --- Additional Analytics ---
803
-
804
- # 1. Element-Level Statistics.
805
- seq_arr = np.array(sequence) # shape: (seq_len, 3)
806
- element_stats = {}
807
- for dim in range(seq_arr.shape[1]):
808
- values = seq_arr[:, dim]
809
- mean_val = np.mean(values)
810
- std_val = np.std(values)
811
- min_val = np.min(values)
812
- max_val = np.max(values)
813
- # Calculate average absolute difference between consecutive values:
814
- diffs = np.abs(np.diff(values))
815
- avg_diff = np.mean(diffs) if diffs.size > 0 else 0
816
- element_stats[f"element_{dim}"] = {
817
- "mean": mean_val,
818
- "std": std_val,
819
- "min": min_val,
820
- "max": max_val,
821
- "avg_consecutive_diff": avg_diff,
822
- }
823
- stats["element_stats"] = element_stats
824
-
825
- # 2. Transition Entropy:
826
- entropies = []
827
- valid_transitions = 0
828
- for i in range(memory_len, seq_len):
829
- context = tuple(sequence[i - memory_len: i])
830
- if context in mapping_lookup:
831
- candidates, freqs = mapping_lookup[context]
832
- total_freq = np.sum(freqs)
833
- if total_freq > 0:
834
- probs = freqs / total_freq
835
- # Add a very small constant to avoid log(0)
836
- epsilon = 1e-10
837
- entropy = -np.sum(probs * np.log(probs + epsilon))
838
- entropies.append(entropy)
839
- valid_transitions += 1
840
- stats["avg_transition_entropy"] = np.mean(entropies) if entropies else None
841
-
842
- # 3. Context Coverage:
843
- total_transitions = seq_len - memory_len
844
- stats["context_coverage"] = (valid_transitions / total_transitions) if total_transitions > 0 else None
845
-
846
- # --- Build a Pretty Report String ---
847
- sep_line = "-" * 60
848
- lines = []
849
- lines.append(sep_line)
850
- lines.append("Sequence Analytics Report:")
851
- lines.append(sep_line)
852
- lines.append("Overall Statistics:")
853
- lines.append(f" Unigram Diversity : {stats['unigram_diversity']:.3f}")
854
- lines.append(f" Repetition Rate : {stats['repetition_rate']:.3f}")
855
- lines.append(f" Bigram Diversity : {stats['bigram_diversity']:.3f}")
856
- lines.append(f" Max Consecutive Repetitions: {stats['max_consecutive_repetitions']}")
857
- cand_prob = stats["avg_candidate_probability"]
858
- cand_prob_str = f"{cand_prob:.3f}" if cand_prob is not None else "N/A"
859
- lines.append(f" Avg Candidate Probability : {cand_prob_str}")
860
- lines.append("")
861
-
862
- lines.append("Element-Level Statistics:")
863
- for dim in sorted(element_stats.keys()):
864
- ed = element_stats[dim]
865
- lines.append(f" {dim.capitalize()}:")
866
- lines.append(f" Mean : {ed['mean']:.3f}")
867
- lines.append(f" Std Dev : {ed['std']:.3f}")
868
- lines.append(f" Min : {ed['min']:.3f}")
869
- lines.append(f" Max : {ed['max']:.3f}")
870
- lines.append(f" Avg Consecutive Diff : {ed['avg_consecutive_diff']:.3f}")
871
- lines.append("")
872
-
873
- lines.append("Transition Statistics:")
874
- avg_entropy = stats["avg_transition_entropy"]
875
- entropy_str = f"{avg_entropy:.3f}" if avg_entropy is not None else "N/A"
876
- lines.append(f" Average Transition Entropy: {entropy_str}")
877
- cc = stats["context_coverage"]
878
- cc_str = f"{cc:.3f}" if cc is not None else "N/A"
879
- lines.append(f" Context Coverage : {cc_str}")
880
- lines.append(sep_line)
881
-
882
- stats_report = "\n".join(lines)
883
-
884
- # Return both the dictionary and the formatted report string.
885
- return stats, stats_report
886
-
887
- ###################################################################################
888
-
889
- def autoregressive_generate(start_seq, mel_tones, trg_array, trg_matches_array, num_new_tokens, chunk_len=5):
890
-
891
- # Convert sequences to NumPy arrays.
892
- current_seq = np.array(start_seq, dtype=int) # Shape: (num_tokens, token_dim)
893
- trg_array = np.array(trg_array, dtype=int) # Shape: (num_candidates, 2, token_dim)
894
- start_len = len(start_seq)
895
-
896
- midx = start_len-1
897
-
898
- # Deque for sliding memory of candidate pairs (immutable tuples).
899
- recent_candidates = deque(maxlen=5)
900
-
901
- while (len(current_seq) - start_len) < num_new_tokens:
902
-
903
- midx += 1
904
-
905
- # Get the last two tokens as context.
906
- context = current_seq[-(chunk_len-1):] # Shape: (2, token_dim)
907
-
908
- sli = 0
909
- msize = 0
910
-
911
- ctx = context[:, :-1].reshape(1, -1)
912
- trg_mat_arr = trg_matches_array
913
-
914
- while msize < 8:
915
-
916
- print('=== Slice', sli)
917
-
918
- # Compare context with candidates in trg_array.
919
- match_mask = np.all(ctx == trg_mat_arr, axis=1)
920
- match_indices = np.where(match_mask)[0]
921
-
922
- msize = match_indices.size
923
-
924
- if msize < 8:
925
- sli += 1
926
- ctx = context[:, :-1].reshape(1, -1)[:, sli:]
927
- trg_mat_arr = trg_matches_array[:, :-sli]
928
-
929
- if match_indices.size == 0:
930
- if len(current_seq) > start_len:
931
-
932
- #tones_chord = sorted([mel_tones[midx], (mel_tones[midx]+7) % 12])
933
- tones_chord = sorted([mel_tones[midx]])
934
- new_tuple = [[mel_tones[midx], TMIDIX.ALL_CHORDS_SORTED.index(tones_chord)]]
935
- current_seq = np.concatenate((current_seq, new_tuple), axis=0)
936
- print('Subbed', midx)
937
- continue
938
-
939
- # From the matching candidates, filter out those whose candidate pair is in recent memory.
940
- available_candidates = []
941
- cseen = []
942
- for idx in match_indices:
943
-
944
- if idx not in recent_candidates:
945
- # Convert candidate pair to an immutable tuple
946
- candidate_pair = tuple(trg_array[idx].tolist())
947
- if candidate_pair[-1][0] == mel_tones[midx] and candidate_pair[-1][1] not in cseen:
948
- available_candidates.append((idx, candidate_pair))
949
- cseen.append(candidate_pair[-1][1])
950
-
951
- # If all candidates have recently been used, backtrack.
952
- if len(available_candidates) < 3:
953
- if len(current_seq) >= start_len:
954
- #tones_chord = sorted([mel_tones[midx], (mel_tones[midx]+7) % 12])
955
- tones_chord = sorted([mel_tones[midx]])
956
- new_tuple = [[mel_tones[midx], TMIDIX.ALL_CHORDS_SORTED.index(tones_chord)]]
957
- current_seq = np.concatenate((current_seq, new_tuple), axis=0)
958
- #rev_val = random.choice([-1, -2])
959
- #current_seq = current_seq[:rev_val]
960
- #print(midx)
961
- #midx = len(current_seq)
962
- #print('Reverted', midx, len(current_seq))
963
- continue
964
-
965
- else:
966
- print(len(available_candidates))
967
- # Choose one available candidate at random.
968
- chosen_idx, chosen_pair = available_candidates[np.random.choice(len(available_candidates))]
969
- new_token = trg_array[chosen_idx][-1] # The second token of the candidate pair.
970
-
971
-
972
- # Append the new token to the sequence.
973
- current_seq = np.concatenate((current_seq, new_token[None, :]), axis=0)
974
-
975
- recent_candidates.append(chosen_idx)
976
-
977
- print('Gen seq len', len(current_seq))
978
-
979
- return current_seq
980
-
981
- ###################################################################################
982
-
983
- def minkowski_distance_vector_to_matrix(x: cp.ndarray, X: cp.ndarray, p: float = 3) -> cp.ndarray:
984
-
985
- """
986
- Computes the Minkowski distance between a 1D CuPy array 'x' and each row of a 2D CuPy array 'X'.
987
-
988
- Parameters:
989
- x (cp.ndarray): A 1D array with shape (n_features,) representing a single vector.
990
- X (cp.ndarray): A 2D array with shape (n_samples, n_features) where each row is a vector.
991
- p (float): The order of the Minkowski distance.
992
- For instance:
993
- - p=1 yields the Manhattan distance,
994
- - p=2 yields the Euclidean distance,
995
- - p=3 yields the Minkowski distance and will use the cube-root implementation,
996
- - p=∞ (or cp.inf) gives the Chebyshev distance.
997
-
998
- Returns:
999
- cp.ndarray: A 1D array of length n_samples containing the Minkowski distance between 'x'
1000
- and the corresponding row in 'X'.
1001
- """
1002
-
1003
- # Compute the element-wise absolute differences between x and every row in X.
1004
- # Broadcasting x over the rows of X results in an array of shape (n_samples, n_features).
1005
- diff = cp.abs(X - x)
1006
-
1007
- if p == float('inf') or p == cp.inf:
1008
- # For the Chebyshev distance, use the maximum absolute difference along the feature axis.
1009
- distances = cp.max(diff, axis=1)
1010
- elif p == 3:
1011
- # Instead of using the generic power operation (sum(diff**3) ** (1/3)),
1012
- # we use cp.cbrt for cube-root calculation when p is exactly 3.
1013
- distances = cp.cbrt(cp.sum(diff ** 3, axis=1))
1014
- else:
1015
- # For general Minkowski distance with finite p,
1016
- # compute the p-th power of differences, sum them, then take the p-th root.
1017
- distances = cp.sum(diff ** p, axis=1) ** (1.0 / p)
1018
-
1019
- return distances
1020
-
1021
- ###################################################################################
1022
-
1023
- def pairwise_minkowski_distance(X: cp.ndarray, p: float = 2) -> cp.ndarray:
1024
-
1025
- """
1026
- Computes pairwise Minkowski distances for a 2D CuPy array.
1027
-
1028
- Parameters:
1029
- X (cp.ndarray): A 2D array of shape (n_samples, n_features), where each row represents a vector.
1030
- p (float): The order of the Minkowski distance.
1031
- For example:
1032
- - p=1 is the Manhattan distance,
1033
- - p=2 is the Euclidean distance,
1034
- - p=∞ (e.g., float('inf') or cp.inf) is the Chebyshev distance.
1035
-
1036
- Returns:
1037
- cp.ndarray: A 2D array of shape (n_samples, n_samples) containing the pairwise Minkowski distances.
1038
- """
1039
-
1040
- # Use broadcasting to compute the absolute difference between every pair of vectors.
1041
- # The result of X[:, None, :] - X[None, :, :] will have shape (n_samples, n_samples, n_features).
1042
- if p == float('inf') or p == cp.inf:
1043
- # For the Chebyshev distance, take the maximum absolute difference along the feature axis.
1044
- return cp.max(cp.abs(X[:, None, :] - X[None, :, :]), axis=-1)
1045
- else:
1046
- # Raise the absolute differences to the power p.
1047
- diff_powered = cp.abs(X[:, None, :] - X[None, :, :]) ** p
1048
- # Sum over the features for each pair (i, j) and then take the p-th root.
1049
- distances = cp.sum(diff_powered, axis=-1) ** (1.0 / p)
1050
-
1051
- return distances
1052
-
1053
- ###################################################################################
1054
-
1055
- def pairwise_cosine_similarity(X: cp.ndarray, eps: float = 1e-10) -> cp.ndarray:
1056
-
1057
- """
1058
- Computes the pairwise cosine similarity for a 2D CuPy array.
1059
-
1060
- Parameters:
1061
- X (cp.ndarray): A 2D array of shape (n_samples, n_features) where each row represents a vector.
1062
- eps (float): A small constant added to the denominator to prevent division by zero.
1063
-
1064
- Returns:
1065
- cp.ndarray: A 2D array of shape (n_samples, n_samples) containing the pairwise cosine similarities.
1066
- """
1067
-
1068
- # Compute the dot product between every pair of rows.
1069
- # This results in a matrix where element (i, j) is the dot product of X[i] and X[j].
1070
- dot_product = cp.dot(X, X.T)
1071
-
1072
- # Compute the L2 norm (Euclidean norm) for each row vector.
1073
- norms = cp.linalg.norm(X, axis=1)
1074
-
1075
- # Compute the outer product of the norms to form the denominator.
1076
- # The element (i, j) in this matrix is norms[i] * norms[j].
1077
- norm_matrix = cp.outer(norms, norms)
1078
-
1079
- # Compute the cosine similarity matrix.
1080
- # Adding a small epsilon (eps) to the denominator prevents division by zero.
1081
- cosine_similarity = dot_product / (norm_matrix + eps)
1082
-
1083
- return cosine_similarity
1084
-
1085
- ###################################################################################
1086
-
1087
- print('Module is loaded!')
1088
- print('Enjoy! :)')
1089
- print('=' * 70)
1090
-
1091
- ###################################################################################
1092
- # This is the end of the TCUPY Python module
1093
- ###################################################################################
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -1,53 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  #================================================================
 
 
2
  # https://huggingface.co/spaces/asigalov61/Advanced-MIDI-Renderer
3
  #================================================================
4
  # Packages:
5
  #
6
  # sudo apt install fluidsynth
7
  #
8
- #================================================================
9
  # Requirements:
10
- #
11
- # pip install gradio
12
- # pip install numpy
13
- # pip install scipy
14
- # pip install matplotlib
15
- # pip install networkx
16
- # pip install scikit-learn
17
  #
18
- #================================================================
19
- # Core modules:
 
20
  #
21
- # git clone --depth 1 https://github.com/asigalov61/tegridy-tools
 
22
  #
23
- # import TMIDIX
24
- # import TPLOTS
25
- # import midi_to_colab_audio
26
  #
27
- #================================================================
28
 
29
  import os
30
  import hashlib
31
-
32
- import time
33
- import datetime
34
- from pytz import timezone
35
-
36
  import copy
37
- from collections import Counter
38
- import random
39
- import statistics
40
 
 
41
  import gradio as gr
42
 
43
- import TMIDIX
44
- import TPLOTS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
- from midi_to_colab_audio import midi_to_colab_audio
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
- #==========================================================================================================
 
 
49
 
50
- def Render_MIDI(input_midi,
51
  render_type,
52
  soundfont_bank,
53
  render_sample_rate,
@@ -58,70 +329,83 @@ def Render_MIDI(input_midi,
58
  render_transpose_value,
59
  render_transpose_to_C4,
60
  render_output_as_solo_piano,
61
- render_remove_drums
 
 
 
62
  ):
63
-
 
 
 
 
 
 
 
 
 
 
64
  print('*' * 70)
65
- print('Req start time: {:%Y-%m-%d %H:%M:%S}'.format(datetime.datetime.now(PDT)))
66
- start_time = time.time()
67
-
68
- print('=' * 70)
69
- print('Loading MIDI...')
70
 
71
- fn = os.path.basename(input_midi)
 
72
  fn1 = fn.split('.')[0]
73
-
74
- fdata = open(input_midi, 'rb').read()
75
-
76
- input_midi_md5hash = hashlib.md5(fdata).hexdigest()
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  print('=' * 70)
79
  print('Requested settings:')
80
- print('=' * 70)
81
- print('Input MIDI file name:', fn)
82
- print('Input MIDI md5 hash', input_midi_md5hash)
83
  print('-' * 70)
84
- print('Render type:', render_type)
85
- print('Soudnfont bank', soundfont_bank)
86
- print('Audio render sample rate', render_sample_rate)
87
-
88
- if render_type != 'Render as-is':
89
- print('Render with sustains:', render_with_sustains)
90
- print('Merge misaligned notes:', merge_misaligned_notes)
91
- print('Custom MIDI render patch', custom_render_patch)
92
- print('Align to bars:', render_align)
93
- print('Transpose value:', render_transpose_value)
94
- print('Transpose to C4', render_transpose_to_C4)
95
- print('Output as Solo Piano', render_output_as_solo_piano)
96
- print('Remove drums:', render_remove_drums)
97
-
98
  print('=' * 70)
99
- print('Processing MIDI...Please wait...')
100
-
101
- #=======================================================
102
- # START PROCESSING
103
-
104
- raw_score = TMIDIX.midi2single_track_ms_score(fdata)
105
 
 
 
 
106
  escore = TMIDIX.advanced_score_processor(raw_score,
107
  return_enhanced_score_notes=True,
108
  apply_sustain=render_with_sustains
109
  )[0]
110
 
 
 
 
 
 
 
111
  if merge_misaligned_notes > 0:
112
  escore = TMIDIX.merge_escore_notes(escore, merge_threshold=merge_misaligned_notes)
113
-
114
  escore = TMIDIX.augment_enhanced_score_notes(escore, timings_divider=1)
115
-
116
- first_note_index = [e[0] for e in raw_score[1]].index('note')
117
 
 
118
  cscore = TMIDIX.chordify_score([1000, escore])
119
 
120
  meta_data = raw_score[1][:first_note_index] + [escore[0]] + [escore[-1]] + [raw_score[1][-1]]
121
 
122
  aux_escore_notes = TMIDIX.augment_enhanced_score_notes(escore, sort_drums_last=True)
123
  song_description = TMIDIX.escore_notes_to_text_description(aux_escore_notes)
124
-
125
  print('Done!')
126
  print('=' * 70)
127
  print('Input MIDI metadata:', meta_data[:5])
@@ -130,28 +414,24 @@ def Render_MIDI(input_midi,
130
  print('=' * 70)
131
  print('Processing...Please wait...')
132
 
 
133
  output_score = copy.deepcopy(escore)
134
 
 
135
  if render_type == "Extract melody":
136
  output_score = TMIDIX.add_melody_to_enhanced_score_notes(escore, return_melody=True)
137
  output_score = TMIDIX.recalculate_score_timings(output_score)
138
-
139
  elif render_type == "Flip":
140
  output_score = TMIDIX.flip_enhanced_score_notes(escore)
141
-
142
  elif render_type == "Reverse":
143
  output_score = TMIDIX.reverse_enhanced_score_notes(escore)
144
-
145
  elif render_type == 'Repair Durations':
146
  output_score = TMIDIX.fix_escore_notes_durations(escore, min_notes_gap=0)
147
-
148
  elif render_type == 'Repair Chords':
149
  fixed_cscore = TMIDIX.advanced_check_and_fix_chords_in_chordified_score(cscore)[0]
150
  output_score = TMIDIX.flatten(fixed_cscore)
151
-
152
  elif render_type == 'Remove Duplicate Pitches':
153
  output_score = TMIDIX.remove_duplicate_pitches_from_escore_notes(escore)
154
-
155
  elif render_type == "Add Drum Track":
156
  nd_escore = [e for e in escore if e[3] != 9]
157
  nd_escore = TMIDIX.augment_enhanced_score_notes(nd_escore)
@@ -161,38 +441,27 @@ def Render_MIDI(input_midi,
161
  e[1] *= 16
162
  e[2] *= 16
163
 
164
- print('Done processing!')
165
- print('=' * 70)
166
-
167
- print('Repatching if needed...')
168
- print('=' * 70)
169
-
170
- if -1 < custom_render_patch < 128:
171
- for e in output_score:
172
- if e[3] != 9:
173
- e[6] = custom_render_patch
174
-
175
- print('Done repatching!')
176
- print('=' * 70)
177
-
178
- print('Sample output events', output_score[:5])
179
  print('=' * 70)
180
- print('Final processing...')
181
-
182
- new_fn = fn1+'.mid'
183
 
 
184
  if render_type != "Render as-is":
185
-
 
 
 
 
 
186
  if render_transpose_value != 0:
187
  output_score = TMIDIX.transpose_escore_notes(output_score, render_transpose_value)
188
 
189
  if render_transpose_to_C4:
190
- output_score = TMIDIX.transpose_escore_notes_to_pitch(output_score)
191
 
192
  if render_align == "Start Times":
193
  output_score = TMIDIX.recalculate_score_timings(output_score)
194
  output_score = TMIDIX.align_escore_notes_to_bars(output_score)
195
-
196
  elif render_align == "Start Times and Durations":
197
  output_score = TMIDIX.recalculate_score_timings(output_score)
198
  output_score = TMIDIX.align_escore_notes_to_bars(output_score, trim_durations=True)
@@ -204,38 +473,38 @@ def Render_MIDI(input_midi,
204
  if render_type == "Longest Repeating Phrase":
205
  zscore = TMIDIX.recalculate_score_timings(output_score)
206
  lrno_score = TMIDIX.escore_notes_lrno_pattern_fast(zscore)
207
-
208
  if lrno_score is not None:
209
  output_score = lrno_score
210
-
211
  else:
212
  output_score = TMIDIX.recalculate_score_timings(TMIDIX.escore_notes_middle(output_score, 50))
213
-
214
  if render_type == "Multi-Instrumental Summary":
215
  zscore = TMIDIX.recalculate_score_timings(output_score)
216
  c_escore_notes = TMIDIX.compress_patches_in_escore_notes_chords(zscore)
217
-
218
  if len(c_escore_notes) > 128:
219
  cmatrix = TMIDIX.escore_notes_to_image_matrix(c_escore_notes, filter_out_zero_rows=True, filter_out_duplicate_rows=True)
220
  smatrix = TPLOTS.square_image_matrix(cmatrix, num_pca_components=max(1, min(5, len(c_escore_notes) // 128)))
221
  output_score = TMIDIX.image_matrix_to_original_escore_notes(smatrix)
222
-
223
  for o in output_score:
224
  o[1] *= 250
225
- o[2] *= 250
226
 
227
  if render_output_as_solo_piano:
228
- output_score = TMIDIX.solo_piano_escore_notes(output_score, keep_drums=True)
229
-
230
- if render_remove_drums:
231
  output_score = TMIDIX.strip_drums_from_escore_notes(output_score)
232
-
233
  if render_type == "Solo Piano Summary":
234
  sp_escore_notes = TMIDIX.solo_piano_escore_notes(output_score, keep_drums=False)
235
  zscore = TMIDIX.recalculate_score_timings(sp_escore_notes)
236
-
237
  if len(zscore) > 128:
238
-
239
  bmatrix = TMIDIX.escore_notes_to_binary_matrix(zscore)
240
  cmatrix = TMIDIX.compress_binary_matrix(bmatrix, only_compress_zeros=True)
241
  smatrix = TPLOTS.square_binary_matrix(cmatrix, interpolation_order=max(1, min(5, len(zscore) // 128)))
@@ -244,221 +513,338 @@ def Render_MIDI(input_midi,
244
  for o in output_score:
245
  o[1] *= 200
246
  o[2] *= 200
247
-
248
- SONG, patches, overflow_patches = TMIDIX.patch_enhanced_score_notes(output_score)
249
-
250
- detailed_stats = TMIDIX.Tegridy_ms_SONG_to_MIDI_Converter(SONG,
251
- output_signature = 'Advanced MIDI Renderer',
252
- output_file_name = fn1,
253
- track_name='Project Los Angeles',
254
- list_of_MIDI_patches=patches
255
- )
256
 
257
- else:
258
- with open(new_fn, 'wb') as f:
259
- f.write(fdata)
260
- f.close()
261
-
262
- if soundfont_bank in ["Super GM",
263
- "Orpheus GM",
264
- "Live HQ GM",
265
- "Nice Strings + Orchestra",
266
- "Real Choir",
267
- "Super Game Boy",
268
- "Proto Square"
269
- ]:
270
 
271
- sf2bank = ["Super GM",
272
- "Orpheus GM",
273
- "Live HQ GM",
274
- "Nice Strings + Orchestra",
275
- "Real Choir",
276
- "Super Game Boy",
277
- "Proto Square"
278
- ].index(soundfont_bank)
279
-
 
 
 
 
 
 
280
  else:
281
- sf2bank = 0
 
 
 
 
 
 
282
 
283
- if render_sample_rate in ["16000", "32000", "44100"]:
284
- srate = int(render_sample_rate)
285
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  else:
287
- srate = 16000
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
 
289
- print('-' * 70)
290
- print('Generating audio with SF2 bank', sf2bank, 'and', srate, 'Hz sample rate')
291
-
292
- audio = midi_to_colab_audio(new_fn,
293
- soundfont_path=soundfonts[sf2bank],
294
- sample_rate=srate,
295
- output_for_gradio=True
296
- )
297
 
298
- print('-' * 70)
299
-
300
- new_md5_hash = hashlib.md5(open(new_fn,'rb').read()).hexdigest()
 
 
301
 
302
- print('Done!')
303
  print('=' * 70)
304
 
305
- #========================================================
 
 
 
306
 
307
- output_midi_md5 = str(new_md5_hash)
308
- output_midi_title = str(fn1)
309
  output_midi_summary = str(meta_data)
310
- output_midi = str(new_fn)
311
- output_audio = (srate, audio)
312
 
313
- output_plot = TMIDIX.plot_ms_SONG(output_score, plot_title=output_midi, return_plt=True)
314
-
315
- print('Output MIDI file name:', output_midi)
316
- print('Output MIDI title:', output_midi_title)
317
- print('Output MIDI hash:', output_midi_md5)
318
- print('Output MIDI summary:', output_midi_summary[:5])
319
- print('=' * 70)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
 
321
- #========================================================
322
-
323
- print('Req end time: {:%Y-%m-%d %H:%M:%S}'.format(datetime.datetime.now(PDT)))
324
- print('-' * 70)
325
- print('Req execution time:', (time.time() - start_time), 'sec')
326
  print('*' * 70)
327
 
328
- #========================================================
329
-
330
- return output_midi_md5, output_midi_title, output_midi_summary, output_midi, output_audio, output_plot, song_description
 
 
 
 
 
 
 
 
 
331
 
332
- #==========================================================================================================
 
 
 
333
 
334
  if __name__ == "__main__":
335
-
336
- PDT = timezone('US/Pacific')
 
337
 
338
- print('=' * 70)
339
- print('App start time: {:%Y-%m-%d %H:%M:%S}'.format(datetime.datetime.now(PDT)))
340
- print('=' * 70)
 
 
341
 
342
- soundfonts = ["SGM-v2.01-YamahaGrand-Guit-Bass-v2.7.sf2",
343
- "Orpheus_18.06.2020.sf2",
344
- "Live HQ Natural SoundFont GM.sf2",
345
- "Nice-Strings-PlusOrchestra-v1.6.sf2",
346
- "KBH-Real-Choir-V2.5.sf2",
347
- "SuperGameBoy.sf2",
348
- "ProtoSquare.sf2"
349
- ]
350
 
351
- app = gr.Blocks()
352
 
353
  with app:
 
 
 
 
 
 
354
 
355
- gr.Markdown("<h1 style='text-align: center; margin-bottom: 1rem'>Advanced MIDI Renderer</h1>")
356
- gr.Markdown("<h1 style='text-align: center; margin-bottom: 1rem'>Transform and render any MIDI</h1>")
357
-
358
- gr.Markdown("![Visitors](https://api.visitorbadge.io/api/visitors?path=asigalov61.Advanced-MIDI-Renderer&style=flat)\n\n"
359
- "This is a demo for tegridy-tools\n\n"
360
- "Please see [tegridy-tools](https://github.com/asigalov61/tegridy-tools) GitHub repo for more information\n\n"
361
- )
362
-
363
- gr.Markdown("## Upload your MIDI")
364
-
365
- input_midi = gr.File(label="Input MIDI", file_types=[".midi", ".mid", ".kar"], type="filepath")
366
-
367
- gr.Markdown("## Select desired Sound Font bank and render sample rate")
368
-
369
- soundfont_bank = gr.Radio(["Super GM",
370
- "Orpheus GM",
371
- "Live HQ GM",
372
- "Nice Strings + Orchestra",
373
- "Real Choir",
374
- "Super Game Boy",
375
- "Proto Square"
376
- ],
377
- label="SoundFont bank",
378
- value="Super GM"
379
- )
380
-
381
- render_sample_rate = gr.Radio(["16000",
382
- "32000",
383
- "44100"
384
- ],
385
- label="MIDI audio render sample rate",
386
- value="16000"
387
- )
388
-
389
- gr.Markdown("## Select desired render type")
390
-
391
- render_type = gr.Radio(["Render as-is",
392
- "Custom render",
393
- "Extract melody",
394
- "Flip",
395
- "Reverse",
396
- "Repair Durations",
397
- "Repair Chords",
398
- "Remove Duplicate Pitches",
399
- "Longest Repeating Phrase",
400
- "Multi-Instrumental Summary",
401
- "Solo Piano Summary",
402
- "Add Drum Track"
403
- ],
404
- label="Render type",
405
- value="Render as-is"
406
- )
407
-
408
- gr.Markdown("## Select custom render options")
409
-
410
- render_with_sustains = gr.Checkbox(label="Render with sustains (if present)", value=True)
411
- merge_misaligned_notes = gr.Slider(-1, 127, value=-1, label="Merge misaligned notes")
412
- custom_render_patch = gr.Slider(-1, 127, value=-1, label="Custom render MIDI patch")
413
-
414
- render_align = gr.Radio(["Do not align",
415
- "Start Times",
416
- "Start Times and Durations",
417
- "Start Times and Split Durations"
418
- ],
419
- label="Align output to bars",
420
- value="Do not align"
421
- )
422
-
423
- render_transpose_value = gr.Slider(-12, 12, value=0, step=1, label="Transpose value")
424
- render_transpose_to_C4 = gr.Checkbox(label="Transpose to C4", value=False)
425
-
426
-
427
- render_output_as_solo_piano = gr.Checkbox(label="Output as Solo Piano", value=False)
428
- render_remove_drums = gr.Checkbox(label="Remove drums", value=False)
429
-
430
- submit = gr.Button("Render MIDI", variant="primary")
431
-
432
- gr.Markdown("## Render results")
433
-
434
- output_midi_md5 = gr.Textbox(label="Output MIDI md5 hash")
435
- output_midi_title = gr.Textbox(label="Output MIDI title")
436
- output_song_description = gr.Textbox(label="Output MIDI description")
437
- output_midi_summary = gr.Textbox(label="Output MIDI summary")
438
- output_audio = gr.Audio(label="Output MIDI audio", format="wav", elem_id="midi_audio")
439
- output_plot = gr.Plot(label="Output MIDI score plot")
440
- output_midi = gr.File(label="Output MIDI file", file_types=[".mid"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
 
442
- run_event = submit.click(Render_MIDI, [input_midi,
443
- render_type,
444
- soundfont_bank,
445
- render_sample_rate,
446
- render_with_sustains,
447
- merge_misaligned_notes,
448
- custom_render_patch,
449
- render_align,
450
- render_transpose_value,
451
- render_transpose_to_C4,
452
- render_output_as_solo_piano,
453
- render_remove_drums
454
- ],
455
- [output_midi_md5,
456
- output_midi_title,
457
- output_midi_summary,
458
- output_midi,
459
- output_audio,
460
- output_plot,
461
- output_song_description
462
- ])
463
 
464
- app.queue().launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =================================================================
2
+ #
3
+ # Merged and Integrated Script for Audio/MIDI Processing and Rendering
4
+ #
5
+ # This script combines two functionalities:
6
+ # 1. Transcribing audio (WAV/MP3) to MIDI using two methods:
7
+ # a) A general-purpose model (basic-pitch by Spotify).
8
+ # b) A model specialized for solo piano (ByteDance).
9
+ # 2. Applying advanced transformations and re-rendering MIDI files using:
10
+ # a) Standard SoundFonts via FluidSynth.
11
+ # b) A custom 8-bit style synthesizer for a chiptune sound.
12
+ #
13
+ # The user can upload a WAV, MP3, or MIDI file.
14
+ # - If an audio file is uploaded, it is first transcribed to MIDI using the selected method.
15
+ # - The resulting MIDI (or an uploaded MIDI) can then be processed
16
+ # with various effects and rendered into audio.
17
+ #
18
  #================================================================
19
+ # Original sources:
20
+ # https://huggingface.co/spaces/asigalov61/ByteDance-Solo-Piano-Audio-to-MIDI-Transcription
21
  # https://huggingface.co/spaces/asigalov61/Advanced-MIDI-Renderer
22
  #================================================================
23
  # Packages:
24
  #
25
  # sudo apt install fluidsynth
26
  #
27
+ # =================================================================
28
  # Requirements:
 
 
 
 
 
 
 
29
  #
30
+ # pip install gradio torch pytz numpy scipy matplotlib networkx scikit-learn
31
+ # pip install piano_transcription_inference huggingface_hub
32
+ # pip install basic-pitch pretty_midi librosa
33
  #
34
+ # =================================================================
35
+ # Core modules:
36
  #
37
+ # git clone --depth 1 https://github.com/asigalov61/tegridy-tools
 
 
38
  #
39
+ # =================================================================
40
 
41
  import os
42
  import hashlib
43
+ import time as reqtime
 
 
 
 
44
  import copy
 
 
 
45
 
46
+ import torch
47
  import gradio as gr
48
 
49
+ from src.piano_transcription.utils import initialize_app
50
+
51
+ from piano_transcription_inference import PianoTranscription, utilities, sample_rate as transcription_sample_rate
52
+
53
+ # --- Import core transcription and MIDI processing libraries ---
54
+ from src import TMIDIX, TPLOTS
55
+ from src import MIDI
56
+ from src.midi_to_colab_audio import midi_to_colab_audio
57
+
58
+ # --- Imports for General Purpose Transcription (basic-pitch) ---
59
+ import basic_pitch
60
+ from basic_pitch.inference import predict
61
+ from basic_pitch import ICASSP_2022_MODEL_PATH
62
+
63
+ # --- Imports for 8-bit Synthesizer ---
64
+ import pretty_midi
65
+ import numpy as np
66
+ from scipy import signal
67
+
68
+ # =================================================================================================
69
+ # === Hugging Face SoundFont Downloader ===
70
+ # =================================================================================================
71
+ from huggingface_hub import hf_hub_download
72
+ import glob
73
+
74
+ # --- Define a constant for the 8-bit synthesizer option ---
75
+ SYNTH_8_BIT_LABEL = "None (8-bit Synthesizer)"
76
+
77
+ def prepare_soundfonts():
78
+ """
79
+ Ensures a default set of SoundFonts are downloaded, then scans the 'src/sf2'
80
+ directory recursively for all .sf2 files.
81
+ Returns a dictionary mapping a user-friendly name to its full file path, with
82
+ default soundfonts listed first in their specified order.
83
+
84
+ Downloads soundfont files from the specified Hugging Face Space repository
85
+ to a local 'src/sf2' directory if they don't already exist.
86
+ Returns a list of local paths to the soundfont files.
87
+ """
88
+ SF2_REPO_ID = "asigalov61/Advanced-MIDI-Renderer"
89
+ SF2_DIR = "src/sf2"
90
+ # This list is now just for ensuring default files exist
91
+ # {"Super GM": 0, "Orpheus GM": 1, "Live HQ GM": 2, "Nice Strings + Orchestra": 3, "Real Choir": 4, "Super Game Boy": 5, "Proto Square": 6}
92
+ DEFAULT_SF2_FILENAMES = [
93
+ "SGM-v2.01-YamahaGrand-Guit-Bass-v2.7.sf2",
94
+ "Orpheus_18.06.2020.sf2",
95
+ "Live HQ Natural SoundFont GM.sf2",
96
+ "Nice-Strings-PlusOrchestra-v1.6.sf2",
97
+ "KBH-Real-Choir-V2.5.sf2",
98
+ "SuperGameBoy.sf2",
99
+ "ProtoSquare.sf2"
100
+ ]
101
+
102
+ # Create the target directory if it doesn't exist
103
+ os.makedirs(SF2_DIR, exist_ok=True)
104
+
105
+ # --- Step 1: Ensure default SoundFonts are available ---
106
+ print("Checking for SoundFont files...")
107
+ for filename in DEFAULT_SF2_FILENAMES:
108
+ local_path = os.path.join(SF2_DIR, filename)
109
+
110
+ # Check if the file already exists locally to avoid re-downloading
111
+ if not os.path.exists(local_path):
112
+ print(f"Downloading '{filename}' from Hugging Face Hub...")
113
+ try:
114
+ # Use hf_hub_download to get the file
115
+ # It will be downloaded to the specified local directory
116
+ hf_hub_download(
117
+ repo_id=SF2_REPO_ID,
118
+ repo_type='space', # Specify that the repository is a Space
119
+ filename=f"{filename}", # The path to the file within the repository
120
+ local_dir=SF2_DIR,
121
+ # local_dir_use_symlinks=False # Copy file to the dir for a clean folder structure
122
+ )
123
+ print(f"'{filename}' downloaded successfully.")
124
+ except Exception as e:
125
+ print(f"Error downloading {filename}: {e}")
126
+ # If download fails, we might not be able to use this soundfont
127
+
128
+ # --- Step 2: Scan the entire directory for all .sf2 files ---
129
+ print(f"Scanning '{SF2_DIR}' for all .sf2 files...")
130
+ all_sfs_map = {}
131
+ # Use glob with recursive=True to find all .sf2 files in subdirectories
132
+ search_pattern = os.path.join(SF2_DIR, '**', '*.sf2')
133
+ for full_path in glob.glob(search_pattern, recursive=True):
134
+ # Create a user-friendly display name, including subfolder if it exists
135
+ relative_path = os.path.relpath(full_path, SF2_DIR)
136
+ display_name = os.path.splitext(relative_path)[0].replace("\\", "/") # Use forward slashes for consistency
137
+ all_sfs_map[display_name] = full_path
138
+
139
+ # --- Step 3: Create the final ordered dictionary based on priority ---
140
+ ordered_soundfont_map = {}
141
+
142
+ # Create display names for default files (filename without extension)
143
+ default_display_names = [os.path.splitext(f)[0] for f in DEFAULT_SF2_FILENAMES]
144
+
145
+ # Separate other files from the default ones
146
+ other_display_names = [name for name in all_sfs_map.keys() if name not in default_display_names]
147
+ other_display_names.sort() # Sort the rest alphabetically
148
+
149
+ # Add default soundfonts first, maintaining the order from DEFAULT_SF2_FILENAMES
150
+ for name in default_display_names:
151
+ if name in all_sfs_map: # Check if the file was actually found by the scanner
152
+ ordered_soundfont_map[name] = all_sfs_map[name]
153
+
154
+ # Add all other soundfonts after the default ones
155
+ for name in other_display_names:
156
+ ordered_soundfont_map[name] = all_sfs_map[name]
157
+
158
+ return ordered_soundfont_map
159
+
160
+ # =================================================================================================
161
+ # === 8-bit Style Synthesizer ===
162
+ # =================================================================================================
163
+ def synthesize_8bit_style(midi_data, waveform_type, envelope_type, decay_time_s, pulse_width, vibrato_rate, vibrato_depth, fs=44100):
164
+ """
165
+ Synthesizes an 8-bit style audio waveform from a PrettyMIDI object.
166
+ This function generates waveforms manually instead of using a synthesizer like FluidSynth.
167
+ """
168
+ total_duration = midi_data.get_end_time()
169
+ waveform = np.zeros(int(total_duration * fs) + fs)
170
+
171
+ for instrument in midi_data.instruments:
172
+ for note in instrument.notes:
173
+ freq = pretty_midi.note_number_to_hz(note.pitch)
174
+ note_duration = note.end - note.start
175
+ num_samples = int(note_duration * fs)
176
+ if num_samples == 0:
177
+ continue
178
+
179
+ t = np.linspace(0., note_duration, num_samples, endpoint=False)
180
+
181
+ # --- Vibrato LFO ---
182
+ vibrato_lfo = vibrato_depth * np.sin(2 * np.pi * vibrato_rate * t)
183
+
184
+ # --- Waveform Generation ---
185
+ if waveform_type == 'Square':
186
+ note_waveform = signal.square(2 * np.pi * (freq + vibrato_lfo) * t, duty=pulse_width)
187
+ elif waveform_type == 'Sawtooth':
188
+ note_waveform = signal.sawtooth(2 * np.pi * (freq + vibrato_lfo) * t)
189
+ elif waveform_type == 'Triangle':
190
+ note_waveform = signal.sawtooth(2 * np.pi * (freq + vibrato_lfo) * t, width=0.5)
191
+
192
+ # --- ADSR Envelope ---
193
+ start_amp = note.velocity / 127.0
194
+ envelope = np.zeros(num_samples)
195
+
196
+ if envelope_type == 'Plucky (AD Envelope)' and num_samples > 0:
197
+ attack_time_s = 0.005
198
+ attack_samples = min(int(attack_time_s * fs), num_samples)
199
+ decay_samples = min(int(decay_time_s * fs), num_samples - attack_samples)
200
+
201
+ envelope[:attack_samples] = np.linspace(0, start_amp, attack_samples)
202
+ if decay_samples > 0:
203
+ envelope[attack_samples:attack_samples+decay_samples] = np.linspace(start_amp, 0, decay_samples)
204
+ elif envelope_type == 'Sustained (Full Decay)' and num_samples > 0:
205
+ envelope = np.linspace(start_amp, 0, num_samples)
206
+
207
+ note_waveform *= envelope
208
+
209
+ start_sample = int(note.start * fs)
210
+ end_sample = start_sample + num_samples
211
+ if end_sample > len(waveform):
212
+ end_sample = len(waveform)
213
+ note_waveform = note_waveform[:end_sample-start_sample]
214
+
215
+ waveform[start_sample:end_sample] += note_waveform
216
+
217
+ return waveform
218
+
219
+ # =================================================================================================
220
+ # === Stage 1: Audio to MIDI Transcription Functions ===
221
+ # =================================================================================================
222
+
223
+ def TranscribePianoAudio(input_file):
224
+ """
225
+ Transcribes a WAV or MP3 audio file of a SOLO PIANO performance into a MIDI file.
226
+ This uses the ByteDance model.
227
+ Args:
228
+ input_file_path (str): The path to the input audio file.
229
+ Returns:
230
+ str: The file path of the generated MIDI file.
231
+ """
232
+ print('=' * 70)
233
+ print('STAGE 1: Starting Piano-Specific Transcription')
234
+ print('=' * 70)
235
+
236
+ # Generate a unique output filename for the MIDI
237
+ fn = os.path.basename(input_file)
238
+ fn1 = fn.split('.')[0]
239
+
240
+ # Use os.path.join to create a platform-independent directory path
241
+ output_dir = os.path.join("output", "transcribed_piano_")
242
+ out_mid_path = os.path.join(output_dir, fn1 + '.mid')
243
+
244
+ # Check for the directory's existence and create it if necessary
245
+ if not os.path.exists(output_dir):
246
+ os.makedirs(output_dir)
247
+
248
+ print('-' * 70)
249
+ print(f'Input file name: {fn}')
250
+ print(f'Output MIDI path: {out_mid_path}')
251
+ print('-' * 70)
252
+
253
+ # Load audio using the utility function
254
+ print('Loading audio...')
255
+ (audio, _) = utilities.load_audio(input_file, sr=transcription_sample_rate, mono=True)
256
+ print('Audio loaded successfully.')
257
+ print('-' * 70)
258
+
259
+ # Initialize the transcription model
260
+ # Use 'cuda' if a GPU is available and configured, otherwise 'cpu'
261
+ device = 'cuda' if torch.cuda.is_available() else 'cpu'
262
+ print(f'Loading transcriptor model... device= {device}')
263
+ transcriptor = PianoTranscription(device=device, checkpoint_path="src/models/CRNN_note_F1=0.9677_pedal_F1=0.9186.pth")
264
+ print('Transcriptor loaded.')
265
+ print('-' * 70)
266
+
267
+ # Perform transcription
268
+ print('Transcribing audio to MIDI (Piano-Specific)...')
269
+ # This function call saves the MIDI file to the specified path
270
+ transcriptor.transcribe(audio, out_mid_path)
271
+ print('Piano transcription complete.')
272
+ print('=' * 70)
273
+
274
+ # Return the path to the newly created MIDI file
275
+ return out_mid_path
276
+
277
+ def TranscribeGeneralAudio(input_file, onset_thresh, frame_thresh, min_note_len, min_freq, max_freq, infer_onsets_bool, melodia_trick_bool, multiple_bends_bool):
278
+ """
279
+ Transcribes a general audio file (WAV/MP3) into a MIDI file using basic-pitch.
280
+ This is suitable for various instruments and vocals.
281
+ """
282
+ print('=' * 70)
283
+ print('STAGE 1: Starting General Purpose Transcription')
284
+ print('=' * 70)
285
+
286
+ fn = os.path.basename(input_file)
287
+ fn1 = fn.split('.')[0]
288
+ output_dir = os.path.join("output", "transcribed_general_")
289
+ out_mid_path = os.path.join(output_dir, fn1 + '.mid')
290
+ os.makedirs(output_dir, exist_ok=True)
291
 
292
+ print(f'Input file: {fn}\nOutput MIDI: {out_mid_path}')
293
+
294
+ # --- Perform transcription using basic-pitch ---
295
+ print('Transcribing audio to MIDI (General Purpose)...')
296
+ # The predict function handles audio loading internally
297
+ model_output, midi_data, note_events = basic_pitch.inference.predict(
298
+ audio_path=input_file,
299
+ model_or_model_path=ICASSP_2022_MODEL_PATH,
300
+ onset_threshold=onset_thresh,
301
+ frame_threshold=frame_thresh,
302
+ minimum_note_length=min_note_len,
303
+ minimum_frequency=min_freq,
304
+ maximum_frequency=max_freq,
305
+ infer_onsets=infer_onsets_bool,
306
+ melodia_trick=melodia_trick_bool,
307
+ multiple_pitch_bends=multiple_bends_bool
308
+ )
309
+
310
+ # --- Save the MIDI file ---
311
+ midi_data.write(out_mid_path)
312
+ print('General transcription complete.')
313
+ print('=' * 70)
314
+
315
+ return out_mid_path
316
 
317
+ # =================================================================================================
318
+ # === Stage 2: MIDI Transformation and Rendering Function ===
319
+ # =================================================================================================
320
 
321
+ def Render_MIDI(input_midi_path,
322
  render_type,
323
  soundfont_bank,
324
  render_sample_rate,
 
329
  render_transpose_value,
330
  render_transpose_to_C4,
331
  render_output_as_solo_piano,
332
+ render_remove_drums,
333
+ # --- 8-bit synth params ---
334
+ s8bit_waveform_type, s8bit_envelope_type, s8bit_decay_time_s,
335
+ s8bit_pulse_width, s8bit_vibrato_rate, s8bit_vibrato_depth
336
  ):
337
+ """
338
+ Processes and renders a MIDI file according to user-defined settings.
339
+ Can render using SoundFonts or a custom 8-bit synthesizer.
340
+ Args:
341
+ input_midi_path (str): The path to the input MIDI file.
342
+ All other arguments are rendering options from the Gradio UI.
343
+ Returns:
344
+ A tuple containing all the output elements for the Gradio UI.
345
+ """
346
+ print('*' * 70)
347
+ print('STAGE 2: Starting MIDI Rendering')
348
  print('*' * 70)
 
 
 
 
 
349
 
350
+ # --- File and Settings Setup ---
351
+ fn = os.path.basename(input_midi_path)
352
  fn1 = fn.split('.')[0]
 
 
 
 
353
 
354
+ # Use os.path.join to create a platform-independent directory path
355
+ output_dir = os.path.join("output", "rendered_midi")
356
+ if not os.path.exists(output_dir):
357
+ os.makedirs(output_dir)
358
+
359
+ # Now, join the clean directory path with the filename
360
+ new_fn_path = os.path.join(output_dir, fn1 + '_rendered.mid')
361
+
362
+ try:
363
+ with open(input_midi_path, 'rb') as f:
364
+ fdata = f.read()
365
+ input_midi_md5hash = hashlib.md5(fdata).hexdigest()
366
+ except FileNotFoundError:
367
+ # Handle cases where the input file might not exist
368
+ print(f"Error: Input MIDI file not found at {input_midi_path}")
369
+ return [None] * 7 # Return empty values for all outputs
370
+
371
  print('=' * 70)
372
  print('Requested settings:')
373
+ print(f'Input MIDI file name: {fn}')
374
+ print(f'Input MIDI md5 hash: {input_midi_md5hash}')
 
375
  print('-' * 70)
376
+ print(f'Render type: {render_type}')
377
+ print(f'Soundfont bank: {soundfont_bank}')
378
+ print(f'Audio render sample rate: {render_sample_rate}')
379
+ # ... (add other print statements for settings if needed)
 
 
 
 
 
 
 
 
 
 
380
  print('=' * 70)
 
 
 
 
 
 
381
 
382
+ # --- MIDI Processing using TMIDIX ---
383
+ print('Processing MIDI... Please wait...')
384
+ raw_score = MIDI.midi2single_track_ms_score(fdata)
385
  escore = TMIDIX.advanced_score_processor(raw_score,
386
  return_enhanced_score_notes=True,
387
  apply_sustain=render_with_sustains
388
  )[0]
389
 
390
+ # Handle cases where the MIDI might not contain any notes
391
+ if not escore:
392
+ print("Warning: MIDI file contains no processable notes.")
393
+ return ("N/A", fn1, "MIDI file contains no notes.",None, None, None, "No notes found.")
394
+
395
+ # This line will now work correctly because merge_misaligned_notes is guaranteed to be an integer.
396
  if merge_misaligned_notes > 0:
397
  escore = TMIDIX.merge_escore_notes(escore, merge_threshold=merge_misaligned_notes)
398
+
399
  escore = TMIDIX.augment_enhanced_score_notes(escore, timings_divider=1)
 
 
400
 
401
+ first_note_index = [e[0] for e in raw_score[1]].index('note')
402
  cscore = TMIDIX.chordify_score([1000, escore])
403
 
404
  meta_data = raw_score[1][:first_note_index] + [escore[0]] + [escore[-1]] + [raw_score[1][-1]]
405
 
406
  aux_escore_notes = TMIDIX.augment_enhanced_score_notes(escore, sort_drums_last=True)
407
  song_description = TMIDIX.escore_notes_to_text_description(aux_escore_notes)
408
+
409
  print('Done!')
410
  print('=' * 70)
411
  print('Input MIDI metadata:', meta_data[:5])
 
414
  print('=' * 70)
415
  print('Processing...Please wait...')
416
 
417
+ # A deep copy of the score to be modified
418
  output_score = copy.deepcopy(escore)
419
 
420
+ # Apply transformations based on render_type
421
  if render_type == "Extract melody":
422
  output_score = TMIDIX.add_melody_to_enhanced_score_notes(escore, return_melody=True)
423
  output_score = TMIDIX.recalculate_score_timings(output_score)
 
424
  elif render_type == "Flip":
425
  output_score = TMIDIX.flip_enhanced_score_notes(escore)
 
426
  elif render_type == "Reverse":
427
  output_score = TMIDIX.reverse_enhanced_score_notes(escore)
 
428
  elif render_type == 'Repair Durations':
429
  output_score = TMIDIX.fix_escore_notes_durations(escore, min_notes_gap=0)
 
430
  elif render_type == 'Repair Chords':
431
  fixed_cscore = TMIDIX.advanced_check_and_fix_chords_in_chordified_score(cscore)[0]
432
  output_score = TMIDIX.flatten(fixed_cscore)
 
433
  elif render_type == 'Remove Duplicate Pitches':
434
  output_score = TMIDIX.remove_duplicate_pitches_from_escore_notes(escore)
 
435
  elif render_type == "Add Drum Track":
436
  nd_escore = [e for e in escore if e[3] != 9]
437
  nd_escore = TMIDIX.augment_enhanced_score_notes(nd_escore)
 
441
  e[1] *= 16
442
  e[2] *= 16
443
 
444
+ print('MIDI processing complete.')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
  print('=' * 70)
 
 
 
446
 
447
+ # --- Final Processing and Patching ---
448
  if render_type != "Render as-is":
449
+ print('Applying final adjustments (transpose, align, patch)...')
450
+ if custom_render_patch != -1: # -1 indicates no change
451
+ for e in output_score:
452
+ if e[3] != 9: # not a drum channel
453
+ e[6] = custom_render_patch
454
+
455
  if render_transpose_value != 0:
456
  output_score = TMIDIX.transpose_escore_notes(output_score, render_transpose_value)
457
 
458
  if render_transpose_to_C4:
459
+ output_score = TMIDIX.transpose_escore_notes_to_pitch(output_score, 60) # C4 is MIDI pitch 60
460
 
461
  if render_align == "Start Times":
462
  output_score = TMIDIX.recalculate_score_timings(output_score)
463
  output_score = TMIDIX.align_escore_notes_to_bars(output_score)
464
+
465
  elif render_align == "Start Times and Durations":
466
  output_score = TMIDIX.recalculate_score_timings(output_score)
467
  output_score = TMIDIX.align_escore_notes_to_bars(output_score, trim_durations=True)
 
473
  if render_type == "Longest Repeating Phrase":
474
  zscore = TMIDIX.recalculate_score_timings(output_score)
475
  lrno_score = TMIDIX.escore_notes_lrno_pattern_fast(zscore)
476
+
477
  if lrno_score is not None:
478
  output_score = lrno_score
479
+
480
  else:
481
  output_score = TMIDIX.recalculate_score_timings(TMIDIX.escore_notes_middle(output_score, 50))
482
+
483
  if render_type == "Multi-Instrumental Summary":
484
  zscore = TMIDIX.recalculate_score_timings(output_score)
485
  c_escore_notes = TMIDIX.compress_patches_in_escore_notes_chords(zscore)
486
+
487
  if len(c_escore_notes) > 128:
488
  cmatrix = TMIDIX.escore_notes_to_image_matrix(c_escore_notes, filter_out_zero_rows=True, filter_out_duplicate_rows=True)
489
  smatrix = TPLOTS.square_image_matrix(cmatrix, num_pca_components=max(1, min(5, len(c_escore_notes) // 128)))
490
  output_score = TMIDIX.image_matrix_to_original_escore_notes(smatrix)
491
+
492
  for o in output_score:
493
  o[1] *= 250
494
+ o[2] *= 250
495
 
496
  if render_output_as_solo_piano:
497
+ output_score = TMIDIX.solo_piano_escore_notes(output_score, keep_drums=(not render_remove_drums))
498
+
499
+ if render_remove_drums and not render_output_as_solo_piano:
500
  output_score = TMIDIX.strip_drums_from_escore_notes(output_score)
501
+
502
  if render_type == "Solo Piano Summary":
503
  sp_escore_notes = TMIDIX.solo_piano_escore_notes(output_score, keep_drums=False)
504
  zscore = TMIDIX.recalculate_score_timings(sp_escore_notes)
505
+
506
  if len(zscore) > 128:
507
+
508
  bmatrix = TMIDIX.escore_notes_to_binary_matrix(zscore)
509
  cmatrix = TMIDIX.compress_binary_matrix(bmatrix, only_compress_zeros=True)
510
  smatrix = TPLOTS.square_binary_matrix(cmatrix, interpolation_order=max(1, min(5, len(zscore) // 128)))
 
513
  for o in output_score:
514
  o[1] *= 200
515
  o[2] *= 200
 
 
 
 
 
 
 
 
 
516
 
517
+ print('Final adjustments complete.')
518
+ print('=' * 70)
 
 
 
 
 
 
 
 
 
 
 
519
 
520
+ # --- Saving Processed MIDI File ---
521
+ # Save the transformed MIDI data
522
+ SONG, patches, _ = TMIDIX.patch_enhanced_score_notes(output_score)
523
+
524
+ # The underlying function mistakenly adds a '.mid' extension.
525
+ # We must pass the path without the extension to compensate.
526
+ path_without_ext = new_fn_path.rsplit('.mid', 1)[0]
527
+
528
+ TMIDIX.Tegridy_ms_SONG_to_MIDI_Converter(SONG,
529
+ output_signature = 'Integrated-MIDI-Processor',
530
+ output_file_name = path_without_ext,
531
+ track_name='Processed Track',
532
+ list_of_MIDI_patches=patches
533
+ )
534
+ midi_to_render_path = new_fn_path
535
  else:
536
+ # If "Render as-is", use the original MIDI data
537
+ with open(new_fn_path, 'wb') as f:
538
+ f.write(fdata)
539
+ midi_to_render_path = new_fn_path
540
+
541
+ # --- Audio Rendering ---
542
+ print('Rendering final audio...')
543
 
544
+ # Select sample rate
545
+ srate = int(render_sample_rate)
546
 
547
+ # --- Conditional Rendering Logic ---
548
+ if soundfont_bank == SYNTH_8_BIT_LABEL:
549
+ print("Using 8-bit style synthesizer...")
550
+ try:
551
+ # Load the MIDI file with pretty_midi for manual synthesis
552
+ midi_data_for_synth = pretty_midi.PrettyMIDI(midi_to_render_path)
553
+ # Synthesize the waveform
554
+ audio = synthesize_8bit_style(
555
+ midi_data_for_synth,
556
+ s8bit_waveform_type, s8bit_envelope_type, s8bit_decay_time_s,
557
+ s8bit_pulse_width, s8bit_vibrato_rate, s8bit_vibrato_depth,
558
+ fs=srate
559
+ )
560
+ # Normalize audio
561
+ peak_val = np.max(np.abs(audio))
562
+ if peak_val > 0:
563
+ audio /= peak_val
564
+ audio = (audio * 32767).astype(np.int16)
565
+ except Exception as e:
566
+ print(f"Error during 8-bit synthesis: {e}")
567
+ return [None] * 7
568
  else:
569
+ print(f"Using SoundFont: {soundfont_bank}")
570
+ # Get the full path from the global dictionary
571
+ soundfont_path = soundfonts_dict.get(soundfont_bank)
572
+
573
+ # Select soundfont
574
+ if not soundfont_path or not os.path.exists(soundfont_path):
575
+ # Error handling in case the selected file is not found
576
+ error_msg = f"SoundFont '{soundfont_bank}' not found!"
577
+ print(f"ERROR: {error_msg}")
578
+ # Fallback to the first available soundfont if possible
579
+ if soundfonts_dict:
580
+ fallback_key = list(soundfonts_dict.keys())[0]
581
+ soundfont_path = soundfonts_dict[fallback_key]
582
+ print(f"Falling back to '{fallback_key}'.")
583
+ else:
584
+ # If no soundfonts are available at all, raise an error
585
+ raise gr.Error("No SoundFonts are available for rendering!")
586
 
587
+ with open(midi_to_render_path, 'rb') as f:
588
+ midi_file_content = f.read()
 
 
 
 
 
 
589
 
590
+ audio = midi_to_colab_audio(midi_file_content,
591
+ soundfont_path=soundfont_path, # Use the dynamically found path
592
+ sample_rate=srate,
593
+ output_for_gradio=True
594
+ )
595
 
596
+ print('Audio rendering complete.')
597
  print('=' * 70)
598
 
599
+ # --- Preparing Outputs for Gradio ---
600
+ with open(midi_to_render_path, 'rb') as f:
601
+ new_md5_hash = hashlib.md5(f.read()).hexdigest()
602
+ output_plot = TPLOTS.plot_ms_SONG(output_score, plot_title=f"Score of {fn1}", return_plt=True)
603
 
 
 
604
  output_midi_summary = str(meta_data)
 
 
605
 
606
+ return new_md5_hash, fn1, output_midi_summary, midi_to_render_path, (srate, audio), output_plot, song_description
607
+
608
+ # =================================================================================================
609
+ # === Main Application Logic ===
610
+ # =================================================================================================
611
+
612
+ def process_and_render_file(input_file,
613
+ # --- Transcription params ---
614
+ transcription_method,
615
+ onset_thresh, frame_thresh, min_note_len, min_freq, max_freq, infer_onsets_bool, melodia_trick_bool, multiple_bends_bool,
616
+ # --- MIDI rendering params ---
617
+ render_type, soundfont_bank, render_sample_rate,
618
+ render_with_sustains, merge_misaligned_notes, custom_render_patch, render_align,
619
+ render_transpose_value, render_transpose_to_C4, render_output_as_solo_piano, render_remove_drums,
620
+ # --- 8-bit synth params ---
621
+ s8bit_waveform_type, s8bit_envelope_type, s8bit_decay_time_s,
622
+ s8bit_pulse_width, s8bit_vibrato_rate, s8bit_vibrato_depth
623
+ ):
624
+ """
625
+ Main function to handle file processing. It determines the file type and calls the
626
+ appropriate functions for transcription and/or rendering based on user selections.
627
+ """
628
+ start_time = reqtime.time()
629
+ if input_file is None:
630
+ # Return a list of updates to clear all output fields
631
+ num_outputs = 7
632
+ return [gr.update(value=None)] * num_outputs
633
+
634
+ # The input_file from gr.Audio(type="filepath") is now the direct path (a string),
635
+ # not a temporary file object. We no longer need to access the .name attribute.
636
+ input_file_path = input_file
637
+ filename = os.path.basename(input_file_path)
638
+ print(f"Processing new file: {filename}")
639
+
640
+ # --- Step 1: Check file type and transcribe if necessary ---
641
+ if filename.lower().endswith(('.mid', '.midi', '.kar')):
642
+ print("MIDI file detected. Proceeding directly to rendering.")
643
+ midi_path_for_rendering = input_file_path
644
+ else: #if filename.lower().endswith(('.wav', '.mp3'))
645
+ print("Audio file detected. Starting transcription...")
646
+ try:
647
+ if transcription_method == "General Purpose":
648
+ midi_path_for_rendering = TranscribeGeneralAudio(
649
+ input_file_path, onset_thresh, frame_thresh, min_note_len,
650
+ min_freq, max_freq, infer_onsets_bool, melodia_trick_bool, multiple_bends_bool
651
+ )
652
+ else: # Piano-Specific
653
+ midi_path_for_rendering = TranscribePianoAudio(input_file_path)
654
+ except Exception as e:
655
+ print(f"An error occurred during transcription: {e}")
656
+ raise gr.Error(f"Transcription Failed: {e}")
657
+
658
+ # --- Step 2: Render the MIDI file with selected options ---
659
+ print(f"Proceeding to render MIDI file: {os.path.basename(midi_path_for_rendering)}")
660
+ results = Render_MIDI(midi_path_for_rendering,
661
+ render_type, soundfont_bank, render_sample_rate,
662
+ render_with_sustains, merge_misaligned_notes, custom_render_patch, render_align,
663
+ render_transpose_value, render_transpose_to_C4, render_output_as_solo_piano, render_remove_drums,
664
+ s8bit_waveform_type, s8bit_envelope_type, s8bit_decay_time_s,
665
+ s8bit_pulse_width, s8bit_vibrato_rate, s8bit_vibrato_depth)
666
 
667
+ print(f'Total processing time: {(reqtime.time() - start_time):.2f} sec')
 
 
 
 
668
  print('*' * 70)
669
 
670
+ return results
671
+
672
+ # =================================================================================================
673
+ # === Gradio UI Setup ===
674
+ # =================================================================================================
675
+
676
+ def update_ui_visibility(transcription_method, soundfont_choice):
677
+ """
678
+ Dynamically updates the visibility of UI components based on user selections.
679
+ """
680
+ is_general = (transcription_method == "General Purpose")
681
+ is_8bit = (soundfont_choice == SYNTH_8_BIT_LABEL)
682
 
683
+ return {
684
+ general_transcription_settings: gr.update(visible=is_general),
685
+ synth_8bit_settings: gr.update(visible=is_8bit),
686
+ }
687
 
688
  if __name__ == "__main__":
689
+ # Initialize the app: download model (if needed) and apply patches
690
+ # Set to False if you don't have 'requests' or 'tqdm' installed
691
+ initialize_app()
692
 
693
+ # --- Prepare soundfonts and make the map globally accessible ---
694
+ global soundfonts_dict
695
+ # On application start, download SoundFonts from Hugging Face Hub if they don't exist.
696
+ soundfonts_dict = prepare_soundfonts()
697
+ print(f"Found {len(soundfonts_dict)} local SoundFonts.")
698
 
699
+ if not soundfonts_dict:
700
+ print("\nWARNING: No SoundFonts were found or could be downloaded.")
701
+ print("Rendering with SoundFonts will fail. Only the 8-bit synthesizer will be available.")
 
 
 
 
 
702
 
703
+ app = gr.Blocks(theme=gr.themes.Base())
704
 
705
  with app:
706
+ gr.Markdown("<h1 style='text-align: center; margin-bottom: 1rem'>Audio-to-MIDI & Advanced Renderer</h1>")
707
+ gr.Markdown(
708
+ "**Upload a WAV/MP3 for transcription-then-rendering, or a MIDI for rendering-only.**\n\n"
709
+ "This application combines piano audio transcription with a powerful MIDI transformation and rendering toolkit. "
710
+ "Based on the work of [asigalov61](https://github.com/asigalov61)."
711
+ )
712
 
713
+ with gr.Row():
714
+ waveform_options = gr.WaveformOptions(show_recording_waveform=False)
715
+ with gr.Column(scale=1):
716
+ # --- INPUT COLUMN ---
717
+ gr.Markdown("## 1. Upload File")
718
+
719
+ # Changed from gr.File to gr.Audio to allow for audio preview.
720
+ # type="filepath" ensures the component returns a string path to the uploaded file.
721
+ # The component will show a player for supported audio types (e.g., WAV, MP3).
722
+ input_file = gr.Audio(
723
+ label="Input Audio (WAV, MP3) or MIDI File",
724
+ type="filepath",
725
+ sources=["upload"], waveform_options=waveform_options
726
+ )
727
+
728
+ gr.Markdown("## 2. Configure Processing")
729
+
730
+ # --- Transcription Method Selector ---
731
+ transcription_method = gr.Radio(
732
+ ["General Purpose", "Piano-Specific"],
733
+ label="Audio Transcription Method",
734
+ value="General Purpose",
735
+ info="Choose 'General Purpose' for most music (vocals, etc.). Choose 'Piano-Specific' only for solo piano recordings."
736
+ )
737
+
738
+ # --- General Purpose (basic-pitch) Settings ---
739
+ with gr.Accordion("General Purpose Transcription Settings", open=True) as general_transcription_settings:
740
+ onset_threshold = gr.Slider(0.0, 1.0, value=0.5, step=0.05, label="On-set Threshold", info="Sensitivity for detecting note beginnings. Higher is stricter.")
741
+ frame_threshold = gr.Slider(0.0, 1.0, value=0.3, step=0.05, label="Frame Threshold", info="Sensitivity for detecting active notes. Higher is stricter.")
742
+ minimum_note_length = gr.Slider(10, 500, value=128, step=1, label="Minimum Note Length (ms)", info="Filters out very short, noisy notes.")
743
+ minimum_frequency = gr.Slider(0, 500, value=60, step=5, label="Minimum Frequency (Hz)", info="Ignores pitches below this frequency.")
744
+ maximum_frequency = gr.Slider(501, 10000, value=4000, step=10, label="Maximum Frequency (Hz)", info="Ignores pitches above this frequency.")
745
+ infer_onsets = gr.Checkbox(value=True, label="Infer Onsets (Boost Onsets)")
746
+ melodia_trick = gr.Checkbox(value=True, label="Melodia Trick (Contour Optimization)")
747
+ multiple_pitch_bends = gr.Checkbox(value=False, label="Allow Multiple Pitch Bends")
748
+
749
+ # --- Rendering Settings ---
750
+ render_type = gr.Radio(
751
+ ["Render as-is", "Custom render", "Extract melody", "Flip", "Reverse", "Repair Durations", "Repair Chords", "Remove Duplicate Pitches", "Longest Repeating Phrase", "Multi-Instrumental Summary", "Solo Piano Summary", "Add Drum Track"],
752
+ label="MIDI Transformation Render Type",
753
+ value="Render as-is",
754
+ info="Apply transformations to the MIDI before rendering. Select 'Render as-is' for basic rendering or other options for transformations."
755
+ )
756
+
757
+ # --- SoundFont Bank with 8-bit option ---
758
+ # --- Dynamically create the list of choices ---
759
+ soundfont_choices = [SYNTH_8_BIT_LABEL] + list(soundfonts_dict.keys())
760
+ # Set a safe default value
761
+ default_sf_choice = "SGM-v2.01-YamahaGrand-Guit-Bass-v2.7" if "SGM-v2.01-YamahaGrand-Guit-Bass-v2.7" in soundfonts_dict else soundfont_choices[0]
762
+
763
+ soundfont_bank = gr.Dropdown(
764
+ soundfont_choices,
765
+ label="SoundFont / Synthesizer",
766
+ value=default_sf_choice
767
+ )
768
+
769
+ render_sample_rate = gr.Radio(
770
+ ["16000", "32000", "44100"],
771
+ label="Audio Sample Rate",
772
+ value="44100"
773
+ )
774
+
775
+ # --- NEW: 8-bit Synthesizer Settings ---
776
+ with gr.Accordion("8-bit Synthesizer Settings", open=False, visible=False) as synth_8bit_settings:
777
+ s8bit_waveform_type = gr.Dropdown(['Square', 'Sawtooth', 'Triangle'], value='Square', label="Waveform Type")
778
+ s8bit_envelope_type = gr.Dropdown(['Plucky (AD Envelope)', 'Sustained (Full Decay)'], value='Plucky (AD Envelope)', label="Envelope Type")
779
+ s8bit_decay_time_s = gr.Slider(0.01, 0.5, value=0.1, step=0.01, label="Decay Time (s)")
780
+ s8bit_pulse_width = gr.Slider(0.01, 0.99, value=0.5, step=0.01, label="Pulse Width")
781
+ s8bit_vibrato_rate = gr.Slider(0, 20, value=5, label="Vibrato Rate (Hz)")
782
+ s8bit_vibrato_depth = gr.Slider(0, 50, value=0, label="Vibrato Depth (Hz)")
783
+
784
+ # --- Original Advanced Options (Now tied to Piano-Specific) ---
785
+ with gr.Accordion("Advanced MIDI Rendering Options", open=False) as advanced_rendering_options:
786
+ render_with_sustains = gr.Checkbox(label="Apply sustain pedal effects (if present)", value=True)
787
+ render_output_as_solo_piano = gr.Checkbox(label="Convert to Solo Piano (Grand Piano patch)", value=False)
788
+ render_remove_drums = gr.Checkbox(label="Remove drum track", value=False)
789
+ render_transpose_to_C4 = gr.Checkbox(label="Transpose entire score to center around C4", value=False)
790
+ render_transpose_value = gr.Slider(-12, 12, value=0, step=1, label="Transpose (semitones)")
791
+ custom_render_patch = gr.Slider(-1, 127, value=-1, step=1, label="Force MIDI Patch (-1 to disable)")
792
+ merge_misaligned_notes = gr.Slider(-1, 127, value=-1, info="Time to merge notes in ms (-1 to disable)")
793
+ render_align = gr.Radio(
794
+ ["Do not align", "Start Times", "Start Times and Durations", "Start Times and Split Durations"],
795
+ label="Align notes to musical bars",
796
+ value="Do not align"
797
+ )
798
+
799
+ submit_btn = gr.Button("Process and Render", variant="primary")
800
+
801
+ with gr.Column(scale=2):
802
+ # --- OUTPUT COLUMN ---
803
+ gr.Markdown("## 3. Results")
804
+ output_midi_title = gr.Textbox(label="MIDI Title")
805
+ output_song_description = gr.Textbox(label="MIDI Description", lines=3)
806
+ output_audio = gr.Audio(label="Rendered Audio Output", format="wav", waveform_options=waveform_options)
807
+ output_plot = gr.Plot(label="MIDI Score Plot")
808
+ with gr.Row():
809
+ output_midi = gr.File(label="Download Processed MIDI File", file_types=[".mid"])
810
+ output_midi_md5 = gr.Textbox(label="Output MIDI MD5 Hash")
811
+ output_midi_summary = gr.Textbox(label="MIDI metadata summary", lines=4)
812
+
813
+ # --- Define all input components for the click event ---
814
+ all_inputs = [
815
+ input_file,
816
+ transcription_method,
817
+ onset_threshold, frame_threshold, minimum_note_length, minimum_frequency, maximum_frequency,
818
+ infer_onsets, melodia_trick, multiple_pitch_bends,
819
+ render_type, soundfont_bank, render_sample_rate,
820
+ render_with_sustains, merge_misaligned_notes, custom_render_patch, render_align,
821
+ render_transpose_value, render_transpose_to_C4, render_output_as_solo_piano, render_remove_drums,
822
+ s8bit_waveform_type, s8bit_envelope_type, s8bit_decay_time_s,
823
+ s8bit_pulse_width, s8bit_vibrato_rate, s8bit_vibrato_depth
824
+ ]
825
+ all_outputs = [
826
+ output_midi_md5, output_midi_title, output_midi_summary,
827
+ output_midi, output_audio, output_plot, output_song_description
828
+ ]
829
 
830
+ # --- Event Handling ---
831
+ submit_btn.click(
832
+ process_and_render_file,
833
+ inputs=all_inputs,
834
+ outputs=all_outputs
835
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
836
 
837
+ # --- Listeners for dynamic UI updates ---
838
+ transcription_method.change(
839
+ fn=update_ui_visibility,
840
+ inputs=[transcription_method, soundfont_bank],
841
+ outputs=[general_transcription_settings, synth_8bit_settings]
842
+ )
843
+ soundfont_bank.change(
844
+ fn=update_ui_visibility,
845
+ inputs=[transcription_method, soundfont_bank],
846
+ outputs=[general_transcription_settings, synth_8bit_settings]
847
+ )
848
+
849
+ # Launch the Gradio app
850
+ app.queue().launch(inbrowser=True, debug=True)
midi_to_colab_audio.py DELETED
The diff for this file is too large to render. See raw diff
 
packages.txt CHANGED
@@ -1 +1,3 @@
1
- fluidsynth
 
 
 
1
+ fluidsynth
2
+ portaudio19-dev
3
+ libportaudio2
requirements.txt CHANGED
@@ -1,6 +1,24 @@
1
- gradio
 
 
2
  numpy
3
- scipy
 
 
 
 
4
  matplotlib
 
 
 
 
5
  networkx
6
- scikit-learn
 
 
 
 
 
 
 
 
 
1
+ --extra-index-url https://download.pytorch.org/whl/cu128
2
+
3
+ torch
4
  numpy
5
+ gradio
6
+ mido
7
+ librosa
8
+ torchlibrosa
9
+ resampy
10
  matplotlib
11
+
12
+ huggingface_hub
13
+
14
+ scipy
15
  networkx
16
+ scikit-learn
17
+ psutil
18
+ pretty_midi
19
+ piano_transcription_inference
20
+
21
+ basic-pitch @ git+https://github.com/avan06/basic-pitch; sys_platform != 'linux'
22
+ basic-pitch[tf] @ git+https://github.com/avan06/basic-pitch; sys_platform == 'linux'
23
+
24
+ git+https://github.com/avan06/pyfluidsynth
MIDI.py → src/MIDI.py RENAMED
@@ -1,4 +1,52 @@
1
  #! /usr/bin/python3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  # unsupported 20091104 ...
3
  # ['set_sequence_number', dtime, sequence]
4
  # ['raw_data', dtime, raw]
@@ -12,7 +60,6 @@
12
  # could break compatiblity, but there's not much else you can do to fix the bug
13
  # https://en.wikipedia.org/wiki/Shift_JIS
14
 
15
- r'''
16
  This module offers functions: concatenate_scores(), grep(),
17
  merge_scores(), mix_scores(), midi2opus(), midi2score(), opus2midi(),
18
  opus2score(), play_score(), score2midi(), score2opus(), score2stats(),
@@ -121,8 +168,20 @@ event, with a duration:
121
 
122
  '''
123
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  import sys, struct, copy
125
- # sys.stdout = os.fdopen(sys.stdout.fileno(), 'wb')
126
  Version = '6.7'
127
  VersionDate = '20201120'
128
  # 20201120 6.7 call to bytest() removed, and protect _unshift_ber_int
@@ -179,9 +238,11 @@ VersionDate = '20201120'
179
 
180
  _previous_warning = '' # 5.4
181
  _previous_times = 0 # 5.4
 
 
182
  #------------------------------- Encoding stuff --------------------------
183
 
184
- def opus2midi(opus=[]):
185
  r'''The argument is a list: the first item in the list is the "ticks"
186
  parameter, the others are the tracks. Each track is a list
187
  of midi-events, and each event is itself a list; see above.
@@ -214,13 +275,13 @@ sys.stdout.buffer.write(my_midi)
214
 
215
  my_midi = b"MThd\x00\x00\x00\x06"+struct.pack('>HHH',format,ntracks,ticks)
216
  for track in tracks:
217
- events = _encode(track)
218
  my_midi += b'MTrk' + struct.pack('>I',len(events)) + events
219
  _clean_up_warnings()
220
  return my_midi
221
 
222
 
223
- def score2opus(score=None):
224
  r'''
225
  The argument is a list: the first item in the list is the "ticks"
226
  parameter, the others are the tracks. Each track is a list
@@ -289,15 +350,15 @@ my_opus = score2opus(my_score)
289
  _clean_up_warnings()
290
  return opus_tracks
291
 
292
- def score2midi(score=None):
293
  r'''
294
  Translates a "score" into MIDI, using score2opus() then opus2midi()
295
  '''
296
- return opus2midi(score2opus(score))
297
 
298
  #--------------------------- Decoding stuff ------------------------
299
 
300
- def midi2opus(midi=b''):
301
  r'''Translates MIDI into a "opus". For a description of the
302
  "opus" format, see opus2midi()
303
  '''
@@ -309,7 +370,8 @@ def midi2opus(midi=b''):
309
  if id != b'MThd':
310
  _warn("midi2opus: midi starts with "+str(id)+" instead of 'MThd'")
311
  _clean_up_warnings()
312
- return [1000,[],]
 
313
  [length, format, tracks_expected, ticks] = struct.unpack(
314
  '>IHHH', bytes(my_midi[4:14]))
315
  if length != 6:
@@ -322,7 +384,8 @@ def midi2opus(midi=b''):
322
  while len(my_midi) >= 8:
323
  track_type = bytes(my_midi[0:4])
324
  if track_type != b'MTrk':
325
- _warn('midi2opus: Warning: track #'+str(track_num)+' type is '+str(track_type)+" instead of b'MTrk'")
 
326
  [track_length] = struct.unpack('>I', my_midi[4:8])
327
  my_midi = my_midi[8:]
328
  if track_length > len(my_midi):
@@ -388,36 +451,114 @@ see opus2midi() and score2opus().
388
  _clean_up_warnings()
389
  return score
390
 
391
- def midi2score(midi=b''):
392
  r'''
393
  Translates MIDI into a "score", using midi2opus() then opus2score()
394
  '''
395
- return opus2score(midi2opus(midi))
396
 
397
- def midi2ms_score(midi=b''):
398
  r'''
399
  Translates MIDI into a "score" with one beat per second and one
400
  tick per millisecond, using midi2opus() then to_millisecs()
401
  then opus2score()
402
  '''
403
- return opus2score(to_millisecs(midi2opus(midi)))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
 
405
  #------------------------ Other Transformations ---------------------
406
 
407
- def to_millisecs(old_opus=None):
408
  r'''Recallibrates all the times in an "opus" to use one beat
409
  per second and one tick per millisecond. This makes it
410
  hard to retrieve any information about beats or barlines,
411
  but it does make it easy to mix different scores together.
412
  '''
413
  if old_opus == None:
414
- return [1000,[],]
415
  try:
416
  old_tpq = int(old_opus[0])
417
  except IndexError: # 5.0
418
  _warn('to_millisecs: the opus '+str(type(old_opus))+' has no elements')
419
- return [1000,[],]
420
- new_opus = [1000,]
421
  # 6.7 first go through building a table of set_tempos by absolute-tick
422
  ticks2tempo = {}
423
  itrack = 1
@@ -439,30 +580,44 @@ but it does make it easy to mix different scores together.
439
  # set_tempo lies before the next track-event, and using it if so.
440
  itrack = 1
441
  while itrack < len(old_opus):
442
- ms_per_old_tick = 500.0 / old_tpq # float: will round later 6.3
443
  i_tempo_ticks = 0
444
  ticks_so_far = 0
445
  ms_so_far = 0.0
446
  previous_ms_so_far = 0.0
447
- new_track = [['set_tempo',0,1000000],] # new "crochet" is 1 sec
 
 
 
 
448
  for old_event in old_opus[itrack]:
449
  # detect if ticks2tempo has something before this event
450
  # 20160702 if ticks2tempo is at the same time, leave it
451
- event_delta_ticks = old_event[1]
452
  if (i_tempo_ticks < len(tempo_ticks) and
453
- tempo_ticks[i_tempo_ticks] < (ticks_so_far + old_event[1])):
454
  delta_ticks = tempo_ticks[i_tempo_ticks] - ticks_so_far
455
- ms_so_far += (ms_per_old_tick * delta_ticks)
456
  ticks_so_far = tempo_ticks[i_tempo_ticks]
457
- ms_per_old_tick = ticks2tempo[ticks_so_far] / (1000.0*old_tpq)
458
  i_tempo_ticks += 1
459
  event_delta_ticks -= delta_ticks
460
  new_event = copy.deepcopy(old_event) # now handle the new event
461
- ms_so_far += (ms_per_old_tick * old_event[1])
462
  new_event[1] = round(ms_so_far - previous_ms_so_far)
463
- if old_event[0] != 'set_tempo':
464
- previous_ms_so_far = ms_so_far
465
- new_track.append(new_event)
 
 
 
 
 
 
 
 
 
 
466
  ticks_so_far += event_delta_ticks
467
  new_opus.append(new_track)
468
  itrack += 1
@@ -500,285 +655,6 @@ def grep(score=None, channels=None):
500
  itrack += 1
501
  return new_score
502
 
503
- def play_score(score=None):
504
- r'''Converts the "score" to midi, and feeds it into 'aplaymidi -'
505
- '''
506
- if score == None:
507
- return
508
- import subprocess
509
- pipe = subprocess.Popen(['aplaymidi','-'], stdin=subprocess.PIPE)
510
- if score_type(score) == 'opus':
511
- pipe.stdin.write(opus2midi(score))
512
- else:
513
- pipe.stdin.write(score2midi(score))
514
- pipe.stdin.close()
515
-
516
- def timeshift(score=None, shift=None, start_time=None, from_time=0, tracks={0,1,2,3,4,5,6,7,8,10,12,13,14,15}):
517
- r'''Returns a "score" shifted in time by "shift" ticks, or shifted
518
- so that the first event starts at "start_time" ticks.
519
-
520
- If "from_time" is specified, only those events in the score
521
- that begin after it are shifted. If "start_time" is less than
522
- "from_time" (or "shift" is negative), then the intermediate
523
- notes are deleted, though patch-change events are preserved.
524
-
525
- If "tracks" are specified, then only those tracks get shifted.
526
- "tracks" can be a list, tuple or set; it gets converted to set
527
- internally.
528
-
529
- It is deprecated to specify both "shift" and "start_time".
530
- If this does happen, timeshift() will print a warning to
531
- stderr and ignore the "shift" argument.
532
-
533
- If "shift" is negative and sufficiently large that it would
534
- leave some event with a negative tick-value, then the score
535
- is shifted so that the first event occurs at time 0. This
536
- also occurs if "start_time" is negative, and is also the
537
- default if neither "shift" nor "start_time" are specified.
538
- '''
539
- #_warn('tracks='+str(tracks))
540
- if score == None or len(score) < 2:
541
- return [1000, [],]
542
- new_score = [score[0],]
543
- my_type = score_type(score)
544
- if my_type == '':
545
- return new_score
546
- if my_type == 'opus':
547
- _warn("timeshift: opus format is not supported\n")
548
- # _clean_up_scores() 6.2; doesn't exist! what was it supposed to do?
549
- return new_score
550
- if not (shift == None) and not (start_time == None):
551
- _warn("timeshift: shift and start_time specified: ignoring shift\n")
552
- shift = None
553
- if shift == None:
554
- if (start_time == None) or (start_time < 0):
555
- start_time = 0
556
- # shift = start_time - from_time
557
-
558
- i = 1 # ignore first element (ticks)
559
- tracks = set(tracks) # defend against tuples and lists
560
- earliest = 1000000000
561
- if not (start_time == None) or shift < 0: # first find the earliest event
562
- while i < len(score):
563
- if len(tracks) and not ((i-1) in tracks):
564
- i += 1
565
- continue
566
- for event in score[i]:
567
- if event[1] < from_time:
568
- continue # just inspect the to_be_shifted events
569
- if event[1] < earliest:
570
- earliest = event[1]
571
- i += 1
572
- if earliest > 999999999:
573
- earliest = 0
574
- if shift == None:
575
- shift = start_time - earliest
576
- elif (earliest + shift) < 0:
577
- start_time = 0
578
- shift = 0 - earliest
579
-
580
- i = 1 # ignore first element (ticks)
581
- while i < len(score):
582
- if len(tracks) == 0 or not ((i-1) in tracks): # 3.8
583
- new_score.append(score[i])
584
- i += 1
585
- continue
586
- new_track = []
587
- for event in score[i]:
588
- new_event = list(event)
589
- #if new_event[1] == 0 and shift > 0 and new_event[0] != 'note':
590
- # pass
591
- #elif new_event[1] >= from_time:
592
- if new_event[1] >= from_time:
593
- # 4.1 must not rightshift set_tempo
594
- if new_event[0] != 'set_tempo' or shift<0:
595
- new_event[1] += shift
596
- elif (shift < 0) and (new_event[1] >= (from_time+shift)):
597
- continue
598
- new_track.append(new_event)
599
- if len(new_track) > 0:
600
- new_score.append(new_track)
601
- i += 1
602
- _clean_up_warnings()
603
- return new_score
604
-
605
- def segment(score=None, start_time=None, end_time=None, start=0, end=100000000,
606
- tracks={0,1,2,3,4,5,6,7,8,10,11,12,13,14,15}):
607
- r'''Returns a "score" which is a segment of the one supplied
608
- as the argument, beginning at "start_time" ticks and ending
609
- at "end_time" ticks (or at the end if "end_time" is not supplied).
610
- If the set "tracks" is specified, only those tracks will
611
- be returned.
612
- '''
613
- if score == None or len(score) < 2:
614
- return [1000, [],]
615
- if start_time == None: # as of 4.2 start_time is recommended
616
- start_time = start # start is legacy usage
617
- if end_time == None: # likewise
618
- end_time = end
619
- new_score = [score[0],]
620
- my_type = score_type(score)
621
- if my_type == '':
622
- return new_score
623
- if my_type == 'opus':
624
- # more difficult (disconnecting note_on's from their note_off's)...
625
- _warn("segment: opus format is not supported\n")
626
- _clean_up_warnings()
627
- return new_score
628
- i = 1 # ignore first element (ticks); we count in ticks anyway
629
- tracks = set(tracks) # defend against tuples and lists
630
- while i < len(score):
631
- if len(tracks) and not ((i-1) in tracks):
632
- i += 1
633
- continue
634
- new_track = []
635
- channel2cc_num = {} # most recent controller change before start
636
- channel2cc_val = {}
637
- channel2cc_time = {}
638
- channel2patch_num = {} # keep most recent patch change before start
639
- channel2patch_time = {}
640
- set_tempo_num = 500000 # most recent tempo change before start 6.3
641
- set_tempo_time = 0
642
- earliest_note_time = end_time
643
- for event in score[i]:
644
- if event[0] == 'control_change': # 6.5
645
- cc_time = channel2cc_time.get(event[2]) or 0
646
- if (event[1] <= start_time) and (event[1] >= cc_time):
647
- channel2cc_num[event[2]] = event[3]
648
- channel2cc_val[event[2]] = event[4]
649
- channel2cc_time[event[2]] = event[1]
650
- elif event[0] == 'patch_change':
651
- patch_time = channel2patch_time.get(event[2]) or 0
652
- if (event[1]<=start_time) and (event[1] >= patch_time): # 2.0
653
- channel2patch_num[event[2]] = event[3]
654
- channel2patch_time[event[2]] = event[1]
655
- elif event[0] == 'set_tempo':
656
- if (event[1]<=start_time) and (event[1]>=set_tempo_time): #6.4
657
- set_tempo_num = event[2]
658
- set_tempo_time = event[1]
659
- if (event[1] >= start_time) and (event[1] <= end_time):
660
- new_track.append(event)
661
- if (event[0] == 'note') and (event[1] < earliest_note_time):
662
- earliest_note_time = event[1]
663
- if len(new_track) > 0:
664
- new_track.append(['set_tempo', start_time, set_tempo_num])
665
- for c in channel2patch_num:
666
- new_track.append(['patch_change',start_time,c,channel2patch_num[c]],)
667
- for c in channel2cc_num: # 6.5
668
- new_track.append(['control_change',start_time,c,channel2cc_num[c],channel2cc_val[c]])
669
- new_score.append(new_track)
670
- i += 1
671
- _clean_up_warnings()
672
- return new_score
673
-
674
- def score_type(opus_or_score=None):
675
- r'''Returns a string, either 'opus' or 'score' or ''
676
- '''
677
- if opus_or_score == None or str(type(opus_or_score)).find('list')<0 or len(opus_or_score) < 2:
678
- return ''
679
- i = 1 # ignore first element
680
- while i < len(opus_or_score):
681
- for event in opus_or_score[i]:
682
- if event[0] == 'note':
683
- return 'score'
684
- elif event[0] == 'note_on':
685
- return 'opus'
686
- i += 1
687
- return ''
688
-
689
- def concatenate_scores(scores):
690
- r'''Concatenates a list of scores into one score.
691
- If the scores differ in their "ticks" parameter,
692
- they will all get converted to millisecond-tick format.
693
- '''
694
- # the deepcopys are needed if the input_score's are refs to the same obj
695
- # e.g. if invoked by midisox's repeat()
696
- input_scores = _consistentise_ticks(scores) # 3.7
697
- output_score = copy.deepcopy(input_scores[0])
698
- for input_score in input_scores[1:]:
699
- output_stats = score2stats(output_score)
700
- delta_ticks = output_stats['nticks']
701
- itrack = 1
702
- while itrack < len(input_score):
703
- if itrack >= len(output_score): # new output track if doesn't exist
704
- output_score.append([])
705
- for event in input_score[itrack]:
706
- output_score[itrack].append(copy.deepcopy(event))
707
- output_score[itrack][-1][1] += delta_ticks
708
- itrack += 1
709
- return output_score
710
-
711
- def merge_scores(scores):
712
- r'''Merges a list of scores into one score. A merged score comprises
713
- all of the tracks from all of the input scores; un-merging is possible
714
- by selecting just some of the tracks. If the scores differ in their
715
- "ticks" parameter, they will all get converted to millisecond-tick
716
- format. merge_scores attempts to resolve channel-conflicts,
717
- but there are of course only 15 available channels...
718
- '''
719
- input_scores = _consistentise_ticks(scores) # 3.6
720
- output_score = [1000]
721
- channels_so_far = set()
722
- all_channels = {0,1,2,3,4,5,6,7,8,10,11,12,13,14,15}
723
- global Event2channelindex
724
- for input_score in input_scores:
725
- new_channels = set(score2stats(input_score).get('channels_total', []))
726
- new_channels.discard(9) # 2.8 cha9 must remain cha9 (in GM)
727
- for channel in channels_so_far & new_channels:
728
- # consistently choose lowest avaiable, to ease testing
729
- free_channels = list(all_channels - (channels_so_far|new_channels))
730
- if len(free_channels) > 0:
731
- free_channels.sort()
732
- free_channel = free_channels[0]
733
- else:
734
- free_channel = None
735
- break
736
- itrack = 1
737
- while itrack < len(input_score):
738
- for input_event in input_score[itrack]:
739
- channel_index=Event2channelindex.get(input_event[0],False)
740
- if channel_index and input_event[channel_index]==channel:
741
- input_event[channel_index] = free_channel
742
- itrack += 1
743
- channels_so_far.add(free_channel)
744
-
745
- channels_so_far |= new_channels
746
- output_score.extend(input_score[1:])
747
- return output_score
748
-
749
- def _ticks(event):
750
- return event[1]
751
- def mix_opus_tracks(input_tracks): # 5.5
752
- r'''Mixes an array of tracks into one track. A mixed track
753
- cannot be un-mixed. It is assumed that the tracks share the same
754
- ticks parameter and the same tempo.
755
- Mixing score-tracks is trivial (just insert all events into one array).
756
- Mixing opus-tracks is only slightly harder, but it's common enough
757
- that a dedicated function is useful.
758
- '''
759
- output_score = [1000, []]
760
- for input_track in input_tracks: # 5.8
761
- input_score = opus2score([1000, input_track])
762
- for event in input_score[1]:
763
- output_score[1].append(event)
764
- output_score[1].sort(key=_ticks)
765
- output_opus = score2opus(output_score)
766
- return output_opus[1]
767
-
768
- def mix_scores(scores):
769
- r'''Mixes a list of scores into one one-track score.
770
- A mixed score cannot be un-mixed. Hopefully the scores
771
- have no undesirable channel-conflicts between them.
772
- If the scores differ in their "ticks" parameter,
773
- they will all get converted to millisecond-tick format.
774
- '''
775
- input_scores = _consistentise_ticks(scores) # 3.6
776
- output_score = [1000, []]
777
- for input_score in input_scores:
778
- for input_track in input_score[1:]:
779
- output_score[1].extend(input_track)
780
- return output_score
781
-
782
  def score2stats(opus_or_score=None):
783
  r'''Returns a dict of some basic stats about the score, like
784
  bank_select (list of tuples (msb,lsb)),
@@ -1153,10 +1029,11 @@ def _unshift_ber_int(ba):
1153
  r'''Given a bytearray, returns a tuple of (the ber-integer at the
1154
  start, and the remainder of the bytearray).
1155
  '''
1156
- if not len(ba): # 6.7
1157
  _warn('_unshift_ber_int: no integer found')
1158
  return ((0, b""))
1159
- byte = ba.pop(0)
 
1160
  integer = 0
1161
  while True:
1162
  integer += (byte & 0x7F)
@@ -1165,13 +1042,17 @@ start, and the remainder of the bytearray).
1165
  if not len(ba):
1166
  _warn('_unshift_ber_int: no end-of-integer found')
1167
  return ((0, ba))
1168
- byte = ba.pop(0)
 
1169
  integer <<= 7
1170
 
 
1171
  def _clean_up_warnings(): # 5.4
1172
  # Call this before returning from any publicly callable function
1173
  # whenever there's a possibility that a warning might have been printed
1174
  # by the function, or by any private functions it might have called.
 
 
1175
  global _previous_times
1176
  global _previous_warning
1177
  if _previous_times > 1:
@@ -1184,27 +1065,32 @@ def _clean_up_warnings(): # 5.4
1184
  _previous_times = 0
1185
  _previous_warning = ''
1186
 
 
1187
  def _warn(s=''):
 
 
1188
  global _previous_times
1189
  global _previous_warning
1190
  if s == _previous_warning: # 5.4
1191
  _previous_times = _previous_times + 1
1192
  else:
1193
  _clean_up_warnings()
1194
- sys.stderr.write(str(s)+"\n")
1195
  _previous_warning = s
1196
 
1197
- def _some_text_event(which_kind=0x01, text=b'some_text'):
1198
- if str(type(text)).find("'str'") >= 0: # 6.4 test for back-compatibility
1199
- data = bytes(text, encoding='ISO-8859-1')
 
1200
  else:
1201
  data = bytes(text)
1202
- return b'\xFF'+bytes((which_kind,))+_ber_compressed_int(len(data))+data
 
1203
 
1204
  def _consistentise_ticks(scores): # 3.6
1205
  # used by mix_scores, merge_scores, concatenate_scores
1206
  if len(scores) == 1:
1207
- return copy.deepcopy(scores)
1208
  are_consistent = True
1209
  ticks = scores[0][0]
1210
  iscore = 1
@@ -1225,9 +1111,8 @@ def _consistentise_ticks(scores): # 3.6
1225
 
1226
 
1227
  ###########################################################################
1228
-
1229
  def _decode(trackdata=b'', exclude=None, include=None,
1230
- event_callback=None, exclusive_event_callback=None, no_eot_magic=False):
1231
  r'''Decodes MIDI track data into an opus-style list of events.
1232
  The options:
1233
  'exclude' is a list of event types which will be ignored SHOULD BE A SET
@@ -1247,24 +1132,24 @@ The options:
1247
  exclude = set(exclude)
1248
 
1249
  # Pointer = 0; not used here; we eat through the bytearray instead.
1250
- event_code = -1; # used for running status
1251
  event_count = 0;
1252
  events = []
1253
 
1254
- while(len(trackdata)):
1255
  # loop while there's anything to analyze ...
1256
- eot = False # When True, the event registrar aborts this loop
1257
  event_count += 1
1258
 
1259
  E = []
1260
  # E for events - we'll feed it to the event registrar at the end.
1261
 
1262
  # Slice off the delta time code, and analyze it
1263
- [time, remainder] = _unshift_ber_int(trackdata)
1264
 
1265
  # Now let's see what we can make of the command
1266
- first_byte = trackdata.pop(0) & 0xFF
1267
-
1268
  if (first_byte < 0xF0): # It's a MIDI event
1269
  if (first_byte & 0x80):
1270
  event_code = first_byte
@@ -1278,17 +1163,19 @@ The options:
1278
  command = event_code & 0xF0
1279
  channel = event_code & 0x0F
1280
 
1281
- if (command == 0xF6): # 0-byte argument
1282
  pass
1283
- elif (command == 0xC0 or command == 0xD0): # 1-byte argument
1284
- parameter = trackdata.pop(0) # could be B
1285
- else: # 2-byte argument could be BB or 14-bit
1286
- parameter = (trackdata.pop(0), trackdata.pop(0))
 
 
1287
 
1288
  #################################################################
1289
  # MIDI events
1290
 
1291
- if (command == 0x80):
1292
  if 'note_off' in exclude:
1293
  continue
1294
  E = ['note_off', time, channel, parameter[0], parameter[1]]
@@ -1299,11 +1186,11 @@ The options:
1299
  elif (command == 0xA0):
1300
  if 'key_after_touch' in exclude:
1301
  continue
1302
- E = ['key_after_touch',time,channel,parameter[0],parameter[1]]
1303
  elif (command == 0xB0):
1304
  if 'control_change' in exclude:
1305
  continue
1306
- E = ['control_change',time,channel,parameter[0],parameter[1]]
1307
  elif (command == 0xC0):
1308
  if 'patch_change' in exclude:
1309
  continue
@@ -1316,93 +1203,94 @@ The options:
1316
  if 'pitch_wheel_change' in exclude:
1317
  continue
1318
  E = ['pitch_wheel_change', time, channel,
1319
- _read_14_bit(parameter)-0x2000]
1320
  else:
1321
- _warn("Shouldn't get here; command="+hex(command))
1322
 
1323
  elif (first_byte == 0xFF): # It's a Meta-Event! ##################
1324
- #[command, length, remainder] =
1325
  # unpack("xCwa*", substr(trackdata, $Pointer, 6));
1326
- #Pointer += 6 - len(remainder);
1327
  # # Move past JUST the length-encoded.
1328
- command = trackdata.pop(0) & 0xFF
 
1329
  [length, trackdata] = _unshift_ber_int(trackdata)
1330
- if (command == 0x00):
1331
- if (length == 2):
1332
- E = ['set_sequence_number',time,_twobytes2int(trackdata)]
1333
- else:
1334
- _warn('set_sequence_number: length must be 2, not '+str(length))
1335
- E = ['set_sequence_number', time, 0]
1336
-
1337
- elif command >= 0x01 and command <= 0x0f: # Text events
1338
  # 6.2 take it in bytes; let the user get the right encoding.
1339
  # text_str = trackdata[0:length].decode('ascii','ignore')
1340
  # text_str = trackdata[0:length].decode('ISO-8859-1')
1341
  # 6.4 take it in bytes; let the user get the right encoding.
1342
- text_data = bytes(trackdata[0:length]) # 6.4
1343
  # Defined text events
1344
  if (command == 0x01):
1345
- E = ['text_event', time, text_data]
1346
  elif (command == 0x02):
1347
- E = ['copyright_text_event', time, text_data]
1348
  elif (command == 0x03):
1349
- E = ['track_name', time, text_data]
1350
  elif (command == 0x04):
1351
- E = ['instrument_name', time, text_data]
1352
  elif (command == 0x05):
1353
- E = ['lyric', time, text_data]
1354
  elif (command == 0x06):
1355
- E = ['marker', time, text_data]
1356
  elif (command == 0x07):
1357
- E = ['cue_point', time, text_data]
1358
  # Reserved but apparently unassigned text events
1359
  elif (command == 0x08):
1360
- E = ['text_event_08', time, text_data]
1361
  elif (command == 0x09):
1362
- E = ['text_event_09', time, text_data]
1363
  elif (command == 0x0a):
1364
- E = ['text_event_0a', time, text_data]
1365
  elif (command == 0x0b):
1366
- E = ['text_event_0b', time, text_data]
1367
  elif (command == 0x0c):
1368
- E = ['text_event_0c', time, text_data]
1369
  elif (command == 0x0d):
1370
- E = ['text_event_0d', time, text_data]
1371
  elif (command == 0x0e):
1372
- E = ['text_event_0e', time, text_data]
1373
  elif (command == 0x0f):
1374
- E = ['text_event_0f', time, text_data]
1375
 
1376
  # Now the sticky events -------------------------------------
1377
  elif (command == 0x2F):
1378
- E = ['end_track', time]
1379
- # The code for handling this, oddly, comes LATER,
1380
- # in the event registrar.
1381
- elif (command == 0x51): # DTime, Microseconds/Crochet
1382
- if length != 3:
1383
- _warn('set_tempo event, but length='+str(length))
1384
- E = ['set_tempo', time,
1385
- struct.unpack(">I", b'\x00'+trackdata[0:3])[0]]
1386
  elif (command == 0x54):
1387
- if length != 5: # DTime, HR, MN, SE, FR, FF
1388
- _warn('smpte_offset event, but length='+str(length))
1389
- E = ['smpte_offset',time] + list(struct.unpack(">BBBBB",trackdata[0:5]))
1390
  elif (command == 0x58):
1391
- if length != 4: # DTime, NN, DD, CC, BB
1392
- _warn('time_signature event, but length='+str(length))
1393
- E = ['time_signature', time]+list(trackdata[0:4])
1394
  elif (command == 0x59):
1395
- if length != 2: # DTime, SF(signed), MI
1396
- _warn('key_signature event, but length='+str(length))
1397
- E = ['key_signature',time] + list(struct.unpack(">bB",trackdata[0:2]))
1398
- elif (command == 0x7F): # 6.4
1399
- E = ['sequencer_specific',time, bytes(trackdata[0:length])]
1400
  else:
1401
- E = ['raw_meta_event', time, command,
1402
- bytes(trackdata[0:length])] # 6.0
1403
- #"[uninterpretable meta-event command of length length]"
1404
- # DTime, Command, Binary Data
1405
- # It's uninterpretable; record it as raw_data.
1406
 
1407
  # Pointer += length; # Now move Pointer
1408
  trackdata = trackdata[length:]
@@ -1419,7 +1307,7 @@ The options:
1419
  # is omitted if this is a non-final block in a multiblock sysex;
1420
  # but the F7 (if there) is counted in the message's declared
1421
  # length, so we don't have to think about it anyway.)
1422
- #command = trackdata.pop(0)
1423
  [length, trackdata] = _unshift_ber_int(trackdata)
1424
  if first_byte == 0xF0:
1425
  # 20091008 added ISO-8859-1 to get an 8-bit str
@@ -1443,32 +1331,32 @@ The options:
1443
  # from the MIDI file spec. So, I'm going to assume that
1444
  # they CAN, in practice, occur. I don't know whether it's
1445
  # proper for you to actually emit these into a MIDI file.
1446
-
1447
- elif (first_byte == 0xF2): # DTime, Beats
1448
  # <song position msg> ::= F2 <data pair>
1449
  E = ['song_position', time, _read_14_bit(trackdata[:2])]
1450
  trackdata = trackdata[2:]
1451
 
1452
- elif (first_byte == 0xF3): # <song select msg> ::= F3 <data singlet>
1453
  # E = ['song_select', time, struct.unpack('>B',trackdata.pop(0))[0]]
1454
  E = ['song_select', time, trackdata[0]]
1455
  trackdata = trackdata[1:]
1456
  # DTime, Thing (what?! song number? whatever ...)
1457
 
1458
- elif (first_byte == 0xF6): # DTime
1459
  E = ['tune_request', time]
1460
  # What would a tune request be doing in a MIDI /file/?
1461
 
1462
- #########################################################
1463
- # ADD MORE META-EVENTS HERE. TODO:
1464
- # f1 -- MTC Quarter Frame Message. One data byte follows
1465
- # the Status; it's the time code value, from 0 to 127.
1466
- # f8 -- MIDI clock. no data.
1467
- # fa -- MIDI start. no data.
1468
- # fb -- MIDI continue. no data.
1469
- # fc -- MIDI stop. no data.
1470
- # fe -- Active sense. no data.
1471
- # f4 f5 f9 fd -- unallocated
1472
 
1473
  r'''
1474
  elif (first_byte > 0xF0) { # Some unknown kinda F-series event ####
@@ -1483,31 +1371,30 @@ The options:
1483
  elif first_byte > 0xF0: # Some unknown F-series event
1484
  # Here we only produce a one-byte piece of raw data.
1485
  # E = ['raw_data', time, bytest(trackdata[0])] # 6.4
1486
- E = ['raw_data', time, trackdata[0]] # 6.4 6.7
1487
  trackdata = trackdata[1:]
1488
  else: # Fallthru.
1489
- _warn("Aborting track. Command-byte first_byte="+hex(first_byte))
1490
  break
1491
  # End of the big if-group
1492
 
1493
-
1494
  ######################################################################
1495
  # THE EVENT REGISTRAR...
1496
- if E and (E[0] == 'end_track'):
1497
  # This is the code for exceptional handling of the EOT event.
1498
  eot = True
1499
  if not no_eot_magic:
1500
  if E[1] > 0: # a null text-event to carry the delta-time
1501
  E = ['text_event', E[1], '']
1502
  else:
1503
- E = [] # EOT with a delta-time of 0; ignore it.
1504
-
1505
  if E and not (E[0] in exclude):
1506
- #if ( $exclusive_event_callback ):
1507
  # &{ $exclusive_event_callback }( @E );
1508
- #else:
1509
  # &{ $event_callback }( @E ) if $event_callback;
1510
- events.append(E)
1511
  if eot:
1512
  break
1513
 
@@ -1518,7 +1405,7 @@ The options:
1518
 
1519
  ###########################################################################
1520
  def _encode(events_lol, unknown_callback=None, never_add_eot=False,
1521
- no_eot_magic=False, no_running_status=False):
1522
  # encode an event structure, presumably for writing to a file
1523
  # Calling format:
1524
  # $data_r = MIDI::Event::encode( \@event_lol, { options } );
@@ -1630,42 +1517,42 @@ def _encode(events_lol, unknown_callback=None, never_add_eot=False,
1630
  last_status = -1
1631
 
1632
  if event == 'raw_meta_event':
1633
- event_data = _some_text_event(int(E[0]), E[1])
1634
  elif (event == 'set_sequence_number'): # 3.9
1635
  event_data = b'\xFF\x00\x02'+_int2twobytes(E[0])
1636
 
1637
  # Text meta-events...
1638
  # a case for a dict, I think (pjb) ...
1639
  elif (event == 'text_event'):
1640
- event_data = _some_text_event(0x01, E[0])
1641
  elif (event == 'copyright_text_event'):
1642
- event_data = _some_text_event(0x02, E[0])
1643
  elif (event == 'track_name'):
1644
- event_data = _some_text_event(0x03, E[0])
1645
  elif (event == 'instrument_name'):
1646
- event_data = _some_text_event(0x04, E[0])
1647
  elif (event == 'lyric'):
1648
- event_data = _some_text_event(0x05, E[0])
1649
  elif (event == 'marker'):
1650
- event_data = _some_text_event(0x06, E[0])
1651
  elif (event == 'cue_point'):
1652
- event_data = _some_text_event(0x07, E[0])
1653
  elif (event == 'text_event_08'):
1654
- event_data = _some_text_event(0x08, E[0])
1655
  elif (event == 'text_event_09'):
1656
- event_data = _some_text_event(0x09, E[0])
1657
  elif (event == 'text_event_0a'):
1658
- event_data = _some_text_event(0x0A, E[0])
1659
  elif (event == 'text_event_0b'):
1660
- event_data = _some_text_event(0x0B, E[0])
1661
  elif (event == 'text_event_0c'):
1662
- event_data = _some_text_event(0x0C, E[0])
1663
  elif (event == 'text_event_0d'):
1664
- event_data = _some_text_event(0x0D, E[0])
1665
  elif (event == 'text_event_0e'):
1666
- event_data = _some_text_event(0x0E, E[0])
1667
  elif (event == 'text_event_0f'):
1668
- event_data = _some_text_event(0x0F, E[0])
1669
  # End of text meta-events
1670
 
1671
  elif (event == 'end_track'):
@@ -1685,7 +1572,7 @@ def _encode(events_lol, unknown_callback=None, never_add_eot=False,
1685
  event_data = struct.pack(">BBBbB", 0xFF, 0x59, 0x02, E[0],E[1])
1686
  elif (event == 'sequencer_specific'):
1687
  # event_data = struct.pack(">BBwa*", 0xFF,0x7F, len(E[0]), E[0])
1688
- event_data = _some_text_event(0x7F, E[0])
1689
  # End of Meta-events
1690
 
1691
  # Other Things...
@@ -1730,3 +1617,517 @@ def _encode(events_lol, unknown_callback=None, never_add_eot=False,
1730
 
1731
  return b''.join(data)
1732
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  #! /usr/bin/python3
2
+
3
+ r'''###############################################################################
4
+ ###################################################################################
5
+ #
6
+ #
7
+ # Tegridy MIDI X Module (TMIDI X / tee-midi eks)
8
+ #
9
+ # NOTE: TMIDI X Module starts after the partial MIDI.py module @ line 1450
10
+ #
11
+ # Based upon MIDI.py module v.6.7. by Peter Billam / pjb.com.au
12
+ #
13
+ # Project Los Angeles
14
+ #
15
+ # Tegridy Code 2025
16
+ #
17
+ # https://github.com/Tegridy-Code/Project-Los-Angeles
18
+ #
19
+ #
20
+ ###################################################################################
21
+ ###################################################################################
22
+ # Copyright 2025 Project Los Angeles / Tegridy Code
23
+ #
24
+ # Licensed under the Apache License, Version 2.0 (the "License");
25
+ # you may not use this file except in compliance with the License.
26
+ # You may obtain a copy of the License at
27
+ #
28
+ # http://www.apache.org/licenses/LICENSE-2.0
29
+ #
30
+ # Unless required by applicable law or agreed to in writing, software
31
+ # distributed under the License is distributed on an "AS IS" BASIS,
32
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
33
+ # See the License for the specific language governing permissions and
34
+ # limitations under the License.
35
+ ###################################################################################
36
+ ###################################################################################
37
+ #
38
+ # PARTIAL MIDI.py Module v.6.7. by Peter Billam
39
+ # Please see TMIDI 2.3/tegridy-tools repo for full MIDI.py module code
40
+ #
41
+ # Or you can always download the latest full version from:
42
+ #
43
+ # https://pjb.com.au/
44
+ # https://peterbillam.gitlab.io/miditools/
45
+ #
46
+ # Copyright 2020 Peter Billam
47
+ #
48
+ ###################################################################################
49
+ ###################################################################################
50
  # unsupported 20091104 ...
51
  # ['set_sequence_number', dtime, sequence]
52
  # ['raw_data', dtime, raw]
 
60
  # could break compatiblity, but there's not much else you can do to fix the bug
61
  # https://en.wikipedia.org/wiki/Shift_JIS
62
 
 
63
  This module offers functions: concatenate_scores(), grep(),
64
  merge_scores(), mix_scores(), midi2opus(), midi2score(), opus2midi(),
65
  opus2score(), play_score(), score2midi(), score2opus(), score2stats(),
 
168
 
169
  '''
170
 
171
+ ###################################################################################
172
+
173
+ __version__ = "25.7.8"
174
+
175
+ print('=' * 70)
176
+ print('TMIDIX Python module')
177
+ print('Version:', __version__)
178
+ print('=' * 70)
179
+ print('Loading module...')
180
+
181
+ ###################################################################################
182
+
183
  import sys, struct, copy
184
+
185
  Version = '6.7'
186
  VersionDate = '20201120'
187
  # 20201120 6.7 call to bytest() removed, and protect _unshift_ber_int
 
238
 
239
  _previous_warning = '' # 5.4
240
  _previous_times = 0 # 5.4
241
+ _no_warning = False
242
+
243
  #------------------------------- Encoding stuff --------------------------
244
 
245
+ def opus2midi(opus=[], text_encoding='ISO-8859-1'):
246
  r'''The argument is a list: the first item in the list is the "ticks"
247
  parameter, the others are the tracks. Each track is a list
248
  of midi-events, and each event is itself a list; see above.
 
275
 
276
  my_midi = b"MThd\x00\x00\x00\x06"+struct.pack('>HHH',format,ntracks,ticks)
277
  for track in tracks:
278
+ events = _encode(track, text_encoding=text_encoding)
279
  my_midi += b'MTrk' + struct.pack('>I',len(events)) + events
280
  _clean_up_warnings()
281
  return my_midi
282
 
283
 
284
+ def score2opus(score=None, text_encoding='ISO-8859-1'):
285
  r'''
286
  The argument is a list: the first item in the list is the "ticks"
287
  parameter, the others are the tracks. Each track is a list
 
350
  _clean_up_warnings()
351
  return opus_tracks
352
 
353
+ def score2midi(score=None, text_encoding='ISO-8859-1'):
354
  r'''
355
  Translates a "score" into MIDI, using score2opus() then opus2midi()
356
  '''
357
+ return opus2midi(score2opus(score, text_encoding), text_encoding)
358
 
359
  #--------------------------- Decoding stuff ------------------------
360
 
361
+ def midi2opus(midi=b'', do_not_check_MIDI_signature=False):
362
  r'''Translates MIDI into a "opus". For a description of the
363
  "opus" format, see opus2midi()
364
  '''
 
370
  if id != b'MThd':
371
  _warn("midi2opus: midi starts with "+str(id)+" instead of 'MThd'")
372
  _clean_up_warnings()
373
+ if do_not_check_MIDI_signature == False:
374
+ return [1000,[],]
375
  [length, format, tracks_expected, ticks] = struct.unpack(
376
  '>IHHH', bytes(my_midi[4:14]))
377
  if length != 6:
 
384
  while len(my_midi) >= 8:
385
  track_type = bytes(my_midi[0:4])
386
  if track_type != b'MTrk':
387
+ #_warn('midi2opus: Warning: track #'+str(track_num)+' type is '+str(track_type)+" instead of b'MTrk'")
388
+ pass
389
  [track_length] = struct.unpack('>I', my_midi[4:8])
390
  my_midi = my_midi[8:]
391
  if track_length > len(my_midi):
 
451
  _clean_up_warnings()
452
  return score
453
 
454
+ def midi2score(midi=b'', do_not_check_MIDI_signature=False):
455
  r'''
456
  Translates MIDI into a "score", using midi2opus() then opus2score()
457
  '''
458
+ return opus2score(midi2opus(midi, do_not_check_MIDI_signature))
459
 
460
+ def midi2ms_score(midi=b'', do_not_check_MIDI_signature=False):
461
  r'''
462
  Translates MIDI into a "score" with one beat per second and one
463
  tick per millisecond, using midi2opus() then to_millisecs()
464
  then opus2score()
465
  '''
466
+ return opus2score(to_millisecs(midi2opus(midi, do_not_check_MIDI_signature)))
467
+
468
+ def midi2single_track_ms_score(midi_path_or_bytes,
469
+ recalculate_channels = False,
470
+ pass_old_timings_events= False,
471
+ verbose = False,
472
+ do_not_check_MIDI_signature=False
473
+ ):
474
+ r'''
475
+ Translates MIDI into a single track "score" with 16 instruments and one beat per second and one
476
+ tick per millisecond
477
+ '''
478
+
479
+ if type(midi_path_or_bytes) == bytes:
480
+ midi_data = midi_path_or_bytes
481
+
482
+ elif type(midi_path_or_bytes) == str:
483
+ midi_data = open(midi_path_or_bytes, 'rb').read()
484
+
485
+ score = midi2score(midi_data, do_not_check_MIDI_signature)
486
+
487
+ if recalculate_channels:
488
+
489
+ events_matrixes = []
490
+
491
+ itrack = 1
492
+ events_matrixes_channels = []
493
+ while itrack < len(score):
494
+ events_matrix = []
495
+ for event in score[itrack]:
496
+ if event[0] == 'note' and event[3] != 9:
497
+ event[3] = (16 * (itrack-1)) + event[3]
498
+ if event[3] not in events_matrixes_channels:
499
+ events_matrixes_channels.append(event[3])
500
+
501
+ events_matrix.append(event)
502
+ events_matrixes.append(events_matrix)
503
+ itrack += 1
504
+
505
+ events_matrix1 = []
506
+ for e in events_matrixes:
507
+ events_matrix1.extend(e)
508
+
509
+ if verbose:
510
+ if len(events_matrixes_channels) > 16:
511
+ print('MIDI has', len(events_matrixes_channels), 'instruments!', len(events_matrixes_channels) - 16, 'instrument(s) will be removed!')
512
+
513
+ for e in events_matrix1:
514
+ if e[0] == 'note' and e[3] != 9:
515
+ if e[3] in events_matrixes_channels[:15]:
516
+ if events_matrixes_channels[:15].index(e[3]) < 9:
517
+ e[3] = events_matrixes_channels[:15].index(e[3])
518
+ else:
519
+ e[3] = events_matrixes_channels[:15].index(e[3])+1
520
+ else:
521
+ events_matrix1.remove(e)
522
+
523
+ if e[0] in ['patch_change', 'control_change', 'channel_after_touch', 'key_after_touch', 'pitch_wheel_change'] and e[2] != 9:
524
+ if e[2] in [e % 16 for e in events_matrixes_channels[:15]]:
525
+ if [e % 16 for e in events_matrixes_channels[:15]].index(e[2]) < 9:
526
+ e[2] = [e % 16 for e in events_matrixes_channels[:15]].index(e[2])
527
+ else:
528
+ e[2] = [e % 16 for e in events_matrixes_channels[:15]].index(e[2])+1
529
+ else:
530
+ events_matrix1.remove(e)
531
+
532
+ else:
533
+ events_matrix1 = []
534
+ itrack = 1
535
+
536
+ while itrack < len(score):
537
+ for event in score[itrack]:
538
+ events_matrix1.append(event)
539
+ itrack += 1
540
+
541
+ opus = score2opus([score[0], events_matrix1])
542
+ ms_score = opus2score(to_millisecs(opus, pass_old_timings_events=pass_old_timings_events))
543
+
544
+ return ms_score
545
 
546
  #------------------------ Other Transformations ---------------------
547
 
548
+ def to_millisecs(old_opus=None, desired_time_in_ms=1, pass_old_timings_events = False):
549
  r'''Recallibrates all the times in an "opus" to use one beat
550
  per second and one tick per millisecond. This makes it
551
  hard to retrieve any information about beats or barlines,
552
  but it does make it easy to mix different scores together.
553
  '''
554
  if old_opus == None:
555
+ return [1000 * desired_time_in_ms,[],]
556
  try:
557
  old_tpq = int(old_opus[0])
558
  except IndexError: # 5.0
559
  _warn('to_millisecs: the opus '+str(type(old_opus))+' has no elements')
560
+ return [1000 * desired_time_in_ms,[],]
561
+ new_opus = [1000 * desired_time_in_ms,]
562
  # 6.7 first go through building a table of set_tempos by absolute-tick
563
  ticks2tempo = {}
564
  itrack = 1
 
580
  # set_tempo lies before the next track-event, and using it if so.
581
  itrack = 1
582
  while itrack < len(old_opus):
583
+ ms_per_old_tick = 400 / old_tpq # float: will round later 6.3
584
  i_tempo_ticks = 0
585
  ticks_so_far = 0
586
  ms_so_far = 0.0
587
  previous_ms_so_far = 0.0
588
+
589
+ if pass_old_timings_events:
590
+ new_track = [['set_tempo',0,1000000 * desired_time_in_ms],['old_tpq', 0, old_tpq]] # new "crochet" is 1 sec
591
+ else:
592
+ new_track = [['set_tempo',0,1000000 * desired_time_in_ms],] # new "crochet" is 1 sec
593
  for old_event in old_opus[itrack]:
594
  # detect if ticks2tempo has something before this event
595
  # 20160702 if ticks2tempo is at the same time, leave it
596
+ event_delta_ticks = old_event[1] * desired_time_in_ms
597
  if (i_tempo_ticks < len(tempo_ticks) and
598
+ tempo_ticks[i_tempo_ticks] < (ticks_so_far + old_event[1]) * desired_time_in_ms):
599
  delta_ticks = tempo_ticks[i_tempo_ticks] - ticks_so_far
600
+ ms_so_far += (ms_per_old_tick * delta_ticks * desired_time_in_ms)
601
  ticks_so_far = tempo_ticks[i_tempo_ticks]
602
+ ms_per_old_tick = ticks2tempo[ticks_so_far] / (1000.0*old_tpq * desired_time_in_ms)
603
  i_tempo_ticks += 1
604
  event_delta_ticks -= delta_ticks
605
  new_event = copy.deepcopy(old_event) # now handle the new event
606
+ ms_so_far += (ms_per_old_tick * old_event[1] * desired_time_in_ms)
607
  new_event[1] = round(ms_so_far - previous_ms_so_far)
608
+
609
+ if pass_old_timings_events:
610
+ if old_event[0] != 'set_tempo':
611
+ previous_ms_so_far = ms_so_far
612
+ new_track.append(new_event)
613
+ else:
614
+ new_event[0] = 'old_set_tempo'
615
+ previous_ms_so_far = ms_so_far
616
+ new_track.append(new_event)
617
+ else:
618
+ if old_event[0] != 'set_tempo':
619
+ previous_ms_so_far = ms_so_far
620
+ new_track.append(new_event)
621
  ticks_so_far += event_delta_ticks
622
  new_opus.append(new_track)
623
  itrack += 1
 
655
  itrack += 1
656
  return new_score
657
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
  def score2stats(opus_or_score=None):
659
  r'''Returns a dict of some basic stats about the score, like
660
  bank_select (list of tuples (msb,lsb)),
 
1029
  r'''Given a bytearray, returns a tuple of (the ber-integer at the
1030
  start, and the remainder of the bytearray).
1031
  '''
1032
+ if not len(ba): # 6.7
1033
  _warn('_unshift_ber_int: no integer found')
1034
  return ((0, b""))
1035
+ byte = ba[0]
1036
+ ba = ba[1:]
1037
  integer = 0
1038
  while True:
1039
  integer += (byte & 0x7F)
 
1042
  if not len(ba):
1043
  _warn('_unshift_ber_int: no end-of-integer found')
1044
  return ((0, ba))
1045
+ byte = ba[0]
1046
+ ba = ba[1:]
1047
  integer <<= 7
1048
 
1049
+
1050
  def _clean_up_warnings(): # 5.4
1051
  # Call this before returning from any publicly callable function
1052
  # whenever there's a possibility that a warning might have been printed
1053
  # by the function, or by any private functions it might have called.
1054
+ if _no_warning:
1055
+ return
1056
  global _previous_times
1057
  global _previous_warning
1058
  if _previous_times > 1:
 
1065
  _previous_times = 0
1066
  _previous_warning = ''
1067
 
1068
+
1069
  def _warn(s=''):
1070
+ if _no_warning:
1071
+ return
1072
  global _previous_times
1073
  global _previous_warning
1074
  if s == _previous_warning: # 5.4
1075
  _previous_times = _previous_times + 1
1076
  else:
1077
  _clean_up_warnings()
1078
+ sys.stderr.write(str(s) + "\n")
1079
  _previous_warning = s
1080
 
1081
+
1082
+ def _some_text_event(which_kind=0x01, text=b'some_text', text_encoding='ISO-8859-1'):
1083
+ if str(type(text)).find("'str'") >= 0: # 6.4 test for back-compatibility
1084
+ data = bytes(text, encoding=text_encoding)
1085
  else:
1086
  data = bytes(text)
1087
+ return b'\xFF' + bytes((which_kind,)) + _ber_compressed_int(len(data)) + data
1088
+
1089
 
1090
  def _consistentise_ticks(scores): # 3.6
1091
  # used by mix_scores, merge_scores, concatenate_scores
1092
  if len(scores) == 1:
1093
+ return copy.deepcopy(scores)
1094
  are_consistent = True
1095
  ticks = scores[0][0]
1096
  iscore = 1
 
1111
 
1112
 
1113
  ###########################################################################
 
1114
  def _decode(trackdata=b'', exclude=None, include=None,
1115
+ event_callback=None, exclusive_event_callback=None, no_eot_magic=False):
1116
  r'''Decodes MIDI track data into an opus-style list of events.
1117
  The options:
1118
  'exclude' is a list of event types which will be ignored SHOULD BE A SET
 
1132
  exclude = set(exclude)
1133
 
1134
  # Pointer = 0; not used here; we eat through the bytearray instead.
1135
+ event_code = -1; # used for running status
1136
  event_count = 0;
1137
  events = []
1138
 
1139
+ while (len(trackdata)):
1140
  # loop while there's anything to analyze ...
1141
+ eot = False # When True, the event registrar aborts this loop
1142
  event_count += 1
1143
 
1144
  E = []
1145
  # E for events - we'll feed it to the event registrar at the end.
1146
 
1147
  # Slice off the delta time code, and analyze it
1148
+ [time, trackdata] = _unshift_ber_int(trackdata)
1149
 
1150
  # Now let's see what we can make of the command
1151
+ first_byte = trackdata[0] & 0xFF
1152
+ trackdata = trackdata[1:]
1153
  if (first_byte < 0xF0): # It's a MIDI event
1154
  if (first_byte & 0x80):
1155
  event_code = first_byte
 
1163
  command = event_code & 0xF0
1164
  channel = event_code & 0x0F
1165
 
1166
+ if (command == 0xF6): # 0-byte argument
1167
  pass
1168
+ elif (command == 0xC0 or command == 0xD0): # 1-byte argument
1169
+ parameter = trackdata[0] # could be B
1170
+ trackdata = trackdata[1:]
1171
+ else: # 2-byte argument could be BB or 14-bit
1172
+ parameter = (trackdata[0], trackdata[1])
1173
+ trackdata = trackdata[2:]
1174
 
1175
  #################################################################
1176
  # MIDI events
1177
 
1178
+ if (command == 0x80):
1179
  if 'note_off' in exclude:
1180
  continue
1181
  E = ['note_off', time, channel, parameter[0], parameter[1]]
 
1186
  elif (command == 0xA0):
1187
  if 'key_after_touch' in exclude:
1188
  continue
1189
+ E = ['key_after_touch', time, channel, parameter[0], parameter[1]]
1190
  elif (command == 0xB0):
1191
  if 'control_change' in exclude:
1192
  continue
1193
+ E = ['control_change', time, channel, parameter[0], parameter[1]]
1194
  elif (command == 0xC0):
1195
  if 'patch_change' in exclude:
1196
  continue
 
1203
  if 'pitch_wheel_change' in exclude:
1204
  continue
1205
  E = ['pitch_wheel_change', time, channel,
1206
+ _read_14_bit(parameter) - 0x2000]
1207
  else:
1208
+ _warn("Shouldn't get here; command=" + hex(command))
1209
 
1210
  elif (first_byte == 0xFF): # It's a Meta-Event! ##################
1211
+ # [command, length, remainder] =
1212
  # unpack("xCwa*", substr(trackdata, $Pointer, 6));
1213
+ # Pointer += 6 - len(remainder);
1214
  # # Move past JUST the length-encoded.
1215
+ command = trackdata[0] & 0xFF
1216
+ trackdata = trackdata[1:]
1217
  [length, trackdata] = _unshift_ber_int(trackdata)
1218
+ if (command == 0x00):
1219
+ if (length == 2):
1220
+ E = ['set_sequence_number', time, _twobytes2int(trackdata)]
1221
+ else:
1222
+ _warn('set_sequence_number: length must be 2, not ' + str(length))
1223
+ E = ['set_sequence_number', time, 0]
1224
+
1225
+ elif command >= 0x01 and command <= 0x0f: # Text events
1226
  # 6.2 take it in bytes; let the user get the right encoding.
1227
  # text_str = trackdata[0:length].decode('ascii','ignore')
1228
  # text_str = trackdata[0:length].decode('ISO-8859-1')
1229
  # 6.4 take it in bytes; let the user get the right encoding.
1230
+ text_data = bytes(trackdata[0:length]) # 6.4
1231
  # Defined text events
1232
  if (command == 0x01):
1233
+ E = ['text_event', time, text_data]
1234
  elif (command == 0x02):
1235
+ E = ['copyright_text_event', time, text_data]
1236
  elif (command == 0x03):
1237
+ E = ['track_name', time, text_data]
1238
  elif (command == 0x04):
1239
+ E = ['instrument_name', time, text_data]
1240
  elif (command == 0x05):
1241
+ E = ['lyric', time, text_data]
1242
  elif (command == 0x06):
1243
+ E = ['marker', time, text_data]
1244
  elif (command == 0x07):
1245
+ E = ['cue_point', time, text_data]
1246
  # Reserved but apparently unassigned text events
1247
  elif (command == 0x08):
1248
+ E = ['text_event_08', time, text_data]
1249
  elif (command == 0x09):
1250
+ E = ['text_event_09', time, text_data]
1251
  elif (command == 0x0a):
1252
+ E = ['text_event_0a', time, text_data]
1253
  elif (command == 0x0b):
1254
+ E = ['text_event_0b', time, text_data]
1255
  elif (command == 0x0c):
1256
+ E = ['text_event_0c', time, text_data]
1257
  elif (command == 0x0d):
1258
+ E = ['text_event_0d', time, text_data]
1259
  elif (command == 0x0e):
1260
+ E = ['text_event_0e', time, text_data]
1261
  elif (command == 0x0f):
1262
+ E = ['text_event_0f', time, text_data]
1263
 
1264
  # Now the sticky events -------------------------------------
1265
  elif (command == 0x2F):
1266
+ E = ['end_track', time]
1267
+ # The code for handling this, oddly, comes LATER,
1268
+ # in the event registrar.
1269
+ elif (command == 0x51): # DTime, Microseconds/Crochet
1270
+ if length != 3:
1271
+ _warn('set_tempo event, but length=' + str(length))
1272
+ E = ['set_tempo', time,
1273
+ struct.unpack(">I", b'\x00' + trackdata[0:3])[0]]
1274
  elif (command == 0x54):
1275
+ if length != 5: # DTime, HR, MN, SE, FR, FF
1276
+ _warn('smpte_offset event, but length=' + str(length))
1277
+ E = ['smpte_offset', time] + list(struct.unpack(">BBBBB", trackdata[0:5]))
1278
  elif (command == 0x58):
1279
+ if length != 4: # DTime, NN, DD, CC, BB
1280
+ _warn('time_signature event, but length=' + str(length))
1281
+ E = ['time_signature', time] + list(trackdata[0:4])
1282
  elif (command == 0x59):
1283
+ if length != 2: # DTime, SF(signed), MI
1284
+ _warn('key_signature event, but length=' + str(length))
1285
+ E = ['key_signature', time] + list(struct.unpack(">bB", trackdata[0:2]))
1286
+ elif (command == 0x7F): # 6.4
1287
+ E = ['sequencer_specific', time, bytes(trackdata[0:length])]
1288
  else:
1289
+ E = ['raw_meta_event', time, command,
1290
+ bytes(trackdata[0:length])] # 6.0
1291
+ # "[uninterpretable meta-event command of length length]"
1292
+ # DTime, Command, Binary Data
1293
+ # It's uninterpretable; record it as raw_data.
1294
 
1295
  # Pointer += length; # Now move Pointer
1296
  trackdata = trackdata[length:]
 
1307
  # is omitted if this is a non-final block in a multiblock sysex;
1308
  # but the F7 (if there) is counted in the message's declared
1309
  # length, so we don't have to think about it anyway.)
1310
+ # command = trackdata.pop(0)
1311
  [length, trackdata] = _unshift_ber_int(trackdata)
1312
  if first_byte == 0xF0:
1313
  # 20091008 added ISO-8859-1 to get an 8-bit str
 
1331
  # from the MIDI file spec. So, I'm going to assume that
1332
  # they CAN, in practice, occur. I don't know whether it's
1333
  # proper for you to actually emit these into a MIDI file.
1334
+
1335
+ elif (first_byte == 0xF2): # DTime, Beats
1336
  # <song position msg> ::= F2 <data pair>
1337
  E = ['song_position', time, _read_14_bit(trackdata[:2])]
1338
  trackdata = trackdata[2:]
1339
 
1340
+ elif (first_byte == 0xF3): # <song select msg> ::= F3 <data singlet>
1341
  # E = ['song_select', time, struct.unpack('>B',trackdata.pop(0))[0]]
1342
  E = ['song_select', time, trackdata[0]]
1343
  trackdata = trackdata[1:]
1344
  # DTime, Thing (what?! song number? whatever ...)
1345
 
1346
+ elif (first_byte == 0xF6): # DTime
1347
  E = ['tune_request', time]
1348
  # What would a tune request be doing in a MIDI /file/?
1349
 
1350
+ #########################################################
1351
+ # ADD MORE META-EVENTS HERE. TODO:
1352
+ # f1 -- MTC Quarter Frame Message. One data byte follows
1353
+ # the Status; it's the time code value, from 0 to 127.
1354
+ # f8 -- MIDI clock. no data.
1355
+ # fa -- MIDI start. no data.
1356
+ # fb -- MIDI continue. no data.
1357
+ # fc -- MIDI stop. no data.
1358
+ # fe -- Active sense. no data.
1359
+ # f4 f5 f9 fd -- unallocated
1360
 
1361
  r'''
1362
  elif (first_byte > 0xF0) { # Some unknown kinda F-series event ####
 
1371
  elif first_byte > 0xF0: # Some unknown F-series event
1372
  # Here we only produce a one-byte piece of raw data.
1373
  # E = ['raw_data', time, bytest(trackdata[0])] # 6.4
1374
+ E = ['raw_data', time, trackdata[0]] # 6.4 6.7
1375
  trackdata = trackdata[1:]
1376
  else: # Fallthru.
1377
+ _warn("Aborting track. Command-byte first_byte=" + hex(first_byte))
1378
  break
1379
  # End of the big if-group
1380
 
 
1381
  ######################################################################
1382
  # THE EVENT REGISTRAR...
1383
+ if E and (E[0] == 'end_track'):
1384
  # This is the code for exceptional handling of the EOT event.
1385
  eot = True
1386
  if not no_eot_magic:
1387
  if E[1] > 0: # a null text-event to carry the delta-time
1388
  E = ['text_event', E[1], '']
1389
  else:
1390
+ E = [] # EOT with a delta-time of 0; ignore it.
1391
+
1392
  if E and not (E[0] in exclude):
1393
+ # if ( $exclusive_event_callback ):
1394
  # &{ $exclusive_event_callback }( @E );
1395
+ # else:
1396
  # &{ $event_callback }( @E ) if $event_callback;
1397
+ events.append(E)
1398
  if eot:
1399
  break
1400
 
 
1405
 
1406
  ###########################################################################
1407
  def _encode(events_lol, unknown_callback=None, never_add_eot=False,
1408
+ no_eot_magic=False, no_running_status=False, text_encoding='ISO-8859-1'):
1409
  # encode an event structure, presumably for writing to a file
1410
  # Calling format:
1411
  # $data_r = MIDI::Event::encode( \@event_lol, { options } );
 
1517
  last_status = -1
1518
 
1519
  if event == 'raw_meta_event':
1520
+ event_data = _some_text_event(int(E[0]), E[1], text_encoding)
1521
  elif (event == 'set_sequence_number'): # 3.9
1522
  event_data = b'\xFF\x00\x02'+_int2twobytes(E[0])
1523
 
1524
  # Text meta-events...
1525
  # a case for a dict, I think (pjb) ...
1526
  elif (event == 'text_event'):
1527
+ event_data = _some_text_event(0x01, E[0], text_encoding)
1528
  elif (event == 'copyright_text_event'):
1529
+ event_data = _some_text_event(0x02, E[0], text_encoding)
1530
  elif (event == 'track_name'):
1531
+ event_data = _some_text_event(0x03, E[0], text_encoding)
1532
  elif (event == 'instrument_name'):
1533
+ event_data = _some_text_event(0x04, E[0], text_encoding)
1534
  elif (event == 'lyric'):
1535
+ event_data = _some_text_event(0x05, E[0], text_encoding)
1536
  elif (event == 'marker'):
1537
+ event_data = _some_text_event(0x06, E[0], text_encoding)
1538
  elif (event == 'cue_point'):
1539
+ event_data = _some_text_event(0x07, E[0], text_encoding)
1540
  elif (event == 'text_event_08'):
1541
+ event_data = _some_text_event(0x08, E[0], text_encoding)
1542
  elif (event == 'text_event_09'):
1543
+ event_data = _some_text_event(0x09, E[0], text_encoding)
1544
  elif (event == 'text_event_0a'):
1545
+ event_data = _some_text_event(0x0A, E[0], text_encoding)
1546
  elif (event == 'text_event_0b'):
1547
+ event_data = _some_text_event(0x0B, E[0], text_encoding)
1548
  elif (event == 'text_event_0c'):
1549
+ event_data = _some_text_event(0x0C, E[0], text_encoding)
1550
  elif (event == 'text_event_0d'):
1551
+ event_data = _some_text_event(0x0D, E[0], text_encoding)
1552
  elif (event == 'text_event_0e'):
1553
+ event_data = _some_text_event(0x0E, E[0], text_encoding)
1554
  elif (event == 'text_event_0f'):
1555
+ event_data = _some_text_event(0x0F, E[0], text_encoding)
1556
  # End of text meta-events
1557
 
1558
  elif (event == 'end_track'):
 
1572
  event_data = struct.pack(">BBBbB", 0xFF, 0x59, 0x02, E[0],E[1])
1573
  elif (event == 'sequencer_specific'):
1574
  # event_data = struct.pack(">BBwa*", 0xFF,0x7F, len(E[0]), E[0])
1575
+ event_data = _some_text_event(0x7F, E[0], text_encoding)
1576
  # End of Meta-events
1577
 
1578
  # Other Things...
 
1617
 
1618
  return b''.join(data)
1619
 
1620
+ ###################################################################################
1621
+ ###################################################################################
1622
+ ###################################################################################
1623
+ #
1624
+ # Tegridy MIDI X Module (TMIDI X / tee-midi eks)
1625
+ #
1626
+ # Based upon and includes the amazing MIDI.py module v.6.7. by Peter Billam
1627
+ # pjb.com.au
1628
+ #
1629
+ # Project Los Angeles
1630
+ # Tegridy Code 2025
1631
+ #
1632
+ # https://github.com/Tegridy-Code/Project-Los-Angeles
1633
+ #
1634
+ ###################################################################################
1635
+ ###################################################################################
1636
+ ###################################################################################
1637
+
1638
+ import os
1639
+
1640
+ import datetime
1641
+
1642
+ from datetime import datetime
1643
+
1644
+ import pickle
1645
+
1646
+ import matplotlib.pyplot as plt
1647
+
1648
+ ###################################################################################
1649
+ #
1650
+ # Original TMIDI Tegridy helper functions
1651
+ #
1652
+ ###################################################################################
1653
+
1654
+ def Tegridy_TXT_to_INT_Converter(input_TXT_string, line_by_line_INT_string=True, max_INT = 0):
1655
+
1656
+ '''Tegridy TXT to Intergers Converter
1657
+
1658
+ Input: Input TXT string in the TMIDI-TXT format
1659
+
1660
+ Type of output TXT INT string: line-by-line or one long string
1661
+
1662
+ Maximum absolute integer to process. Maximum is inclusive
1663
+ Default = process all integers. This helps to remove outliers/unwanted ints
1664
+
1665
+ Output: List of pure intergers
1666
+ String of intergers in the specified format: line-by-line or one long string
1667
+ Number of processed integers
1668
+ Number of skipped integers
1669
+
1670
+ Project Los Angeles
1671
+ Tegridy Code 2021'''
1672
+
1673
+ print('Tegridy TXT to Intergers Converter')
1674
+
1675
+ output_INT_list = []
1676
+
1677
+ npi = 0
1678
+ nsi = 0
1679
+
1680
+ TXT_List = list(input_TXT_string)
1681
+ for char in TXT_List:
1682
+ if max_INT != 0:
1683
+ if abs(ord(char)) <= max_INT:
1684
+ output_INT_list.append(ord(char))
1685
+ npi += 1
1686
+ else:
1687
+ nsi += 1
1688
+ else:
1689
+ output_INT_list.append(ord(char))
1690
+ npi += 1
1691
+
1692
+ if line_by_line_INT_string:
1693
+ output_INT_string = '\n'.join([str(elem) for elem in output_INT_list])
1694
+ else:
1695
+ output_INT_string = ' '.join([str(elem) for elem in output_INT_list])
1696
+
1697
+ print('Converted TXT to INTs:', npi, ' / ', nsi)
1698
+
1699
+ return output_INT_list, output_INT_string, npi, nsi
1700
+
1701
+ ###################################################################################
1702
+
1703
+ def Tegridy_INT_to_TXT_Converter(input_INT_list):
1704
+
1705
+ '''Tegridy Intergers to TXT Converter
1706
+
1707
+ Input: List of intergers in TMIDI-TXT-INT format
1708
+ Output: Decoded TXT string in TMIDI-TXT format
1709
+ Project Los Angeles
1710
+ Tegridy Code 2020'''
1711
+
1712
+ output_TXT_string = ''
1713
+
1714
+ for i in input_INT_list:
1715
+ output_TXT_string += chr(int(i))
1716
+
1717
+ return output_TXT_string
1718
+
1719
+ ###################################################################################
1720
+
1721
+ def Tegridy_INT_String_to_TXT_Converter(input_INT_String, line_by_line_input=True):
1722
+
1723
+ '''Tegridy Intergers String to TXT Converter
1724
+
1725
+ Input: List of intergers in TMIDI-TXT-INT-String format
1726
+ Output: Decoded TXT string in TMIDI-TXT format
1727
+ Project Los Angeles
1728
+ Tegridy Code 2020'''
1729
+
1730
+ print('Tegridy Intergers String to TXT Converter')
1731
+
1732
+ if line_by_line_input:
1733
+ input_string = input_INT_String.split('\n')
1734
+ else:
1735
+ input_string = input_INT_String.split(' ')
1736
+
1737
+ output_TXT_string = ''
1738
+
1739
+ for i in input_string:
1740
+ try:
1741
+ output_TXT_string += chr(abs(int(i)))
1742
+ except:
1743
+ print('Bad note:', i)
1744
+ continue
1745
+
1746
+ print('Done!')
1747
+
1748
+ return output_TXT_string
1749
+
1750
+ ###################################################################################
1751
+
1752
+ def Tegridy_SONG_to_MIDI_Converter(SONG,
1753
+ output_signature = 'Tegridy TMIDI Module',
1754
+ track_name = 'Composition Track',
1755
+ number_of_ticks_per_quarter = 425,
1756
+ list_of_MIDI_patches = [0, 24, 32, 40, 42, 46, 56, 71, 73, 0, 0, 0, 0, 0, 0, 0],
1757
+ output_file_name = 'TMIDI-Composition',
1758
+ text_encoding='ISO-8859-1',
1759
+ verbose=True):
1760
+
1761
+ '''Tegridy SONG to MIDI Converter
1762
+
1763
+ Input: Input SONG in TMIDI SONG/MIDI.py Score format
1764
+ Output MIDI Track 0 name / MIDI Signature
1765
+ Output MIDI Track 1 name / Composition track name
1766
+ Number of ticks per quarter for the output MIDI
1767
+ List of 16 MIDI patch numbers for output MIDI. Def. is MuseNet compatible patches.
1768
+ Output file name w/o .mid extension.
1769
+ Optional text encoding if you are working with text_events/lyrics. This is especially useful for Karaoke. Please note that anything but ISO-8859-1 is a non-standard way of encoding text_events according to MIDI specs.
1770
+
1771
+ Output: MIDI File
1772
+ Detailed MIDI stats
1773
+
1774
+ Project Los Angeles
1775
+ Tegridy Code 2020'''
1776
+
1777
+ if verbose:
1778
+ print('Converting to MIDI. Please stand-by...')
1779
+
1780
+ output_header = [number_of_ticks_per_quarter,
1781
+ [['track_name', 0, bytes(output_signature, text_encoding)]]]
1782
+
1783
+ patch_list = [['patch_change', 0, 0, list_of_MIDI_patches[0]],
1784
+ ['patch_change', 0, 1, list_of_MIDI_patches[1]],
1785
+ ['patch_change', 0, 2, list_of_MIDI_patches[2]],
1786
+ ['patch_change', 0, 3, list_of_MIDI_patches[3]],
1787
+ ['patch_change', 0, 4, list_of_MIDI_patches[4]],
1788
+ ['patch_change', 0, 5, list_of_MIDI_patches[5]],
1789
+ ['patch_change', 0, 6, list_of_MIDI_patches[6]],
1790
+ ['patch_change', 0, 7, list_of_MIDI_patches[7]],
1791
+ ['patch_change', 0, 8, list_of_MIDI_patches[8]],
1792
+ ['patch_change', 0, 9, list_of_MIDI_patches[9]],
1793
+ ['patch_change', 0, 10, list_of_MIDI_patches[10]],
1794
+ ['patch_change', 0, 11, list_of_MIDI_patches[11]],
1795
+ ['patch_change', 0, 12, list_of_MIDI_patches[12]],
1796
+ ['patch_change', 0, 13, list_of_MIDI_patches[13]],
1797
+ ['patch_change', 0, 14, list_of_MIDI_patches[14]],
1798
+ ['patch_change', 0, 15, list_of_MIDI_patches[15]],
1799
+ ['track_name', 0, bytes(track_name, text_encoding)]]
1800
+
1801
+ output = output_header + [patch_list + SONG]
1802
+
1803
+ midi_data = score2midi(output, text_encoding)
1804
+ detailed_MIDI_stats = score2stats(output)
1805
+
1806
+ with open(output_file_name + '.mid', 'wb') as midi_file:
1807
+ midi_file.write(midi_data)
1808
+ midi_file.close()
1809
+
1810
+ if verbose:
1811
+ print('Done! Enjoy! :)')
1812
+
1813
+ return detailed_MIDI_stats
1814
+
1815
+ ###################################################################################
1816
+
1817
+ def Tegridy_ms_SONG_to_MIDI_Converter(ms_SONG,
1818
+ output_signature = 'Tegridy TMIDI Module',
1819
+ track_name = 'Composition Track',
1820
+ list_of_MIDI_patches = [0, 24, 32, 40, 42, 46, 56, 71, 73, 0, 0, 0, 0, 0, 0, 0],
1821
+ output_file_name = 'TMIDI-Composition',
1822
+ text_encoding='ISO-8859-1',
1823
+ timings_multiplier=1,
1824
+ verbose=True
1825
+ ):
1826
+
1827
+ '''Tegridy milisecond SONG to MIDI Converter
1828
+
1829
+ Input: Input ms SONG in TMIDI ms SONG/MIDI.py ms Score format
1830
+ Output MIDI Track 0 name / MIDI Signature
1831
+ Output MIDI Track 1 name / Composition track name
1832
+ List of 16 MIDI patch numbers for output MIDI. Def. is MuseNet compatible patches.
1833
+ Output file name w/o .mid extension.
1834
+ Optional text encoding if you are working with text_events/lyrics. This is especially useful for Karaoke. Please note that anything but ISO-8859-1 is a non-standard way of encoding text_events according to MIDI specs.
1835
+ Optional timings multiplier
1836
+ Optional verbose output
1837
+
1838
+ Output: MIDI File
1839
+ Detailed MIDI stats
1840
+
1841
+ Project Los Angeles
1842
+ Tegridy Code 2024'''
1843
+
1844
+ if verbose:
1845
+ print('Converting to MIDI. Please stand-by...')
1846
+
1847
+ output_header = [1000,
1848
+ [['set_tempo', 0, 1000000],
1849
+ ['time_signature', 0, 4, 2, 24, 8],
1850
+ ['track_name', 0, bytes(output_signature, text_encoding)]]]
1851
+
1852
+ patch_list = [['patch_change', 0, 0, list_of_MIDI_patches[0]],
1853
+ ['patch_change', 0, 1, list_of_MIDI_patches[1]],
1854
+ ['patch_change', 0, 2, list_of_MIDI_patches[2]],
1855
+ ['patch_change', 0, 3, list_of_MIDI_patches[3]],
1856
+ ['patch_change', 0, 4, list_of_MIDI_patches[4]],
1857
+ ['patch_change', 0, 5, list_of_MIDI_patches[5]],
1858
+ ['patch_change', 0, 6, list_of_MIDI_patches[6]],
1859
+ ['patch_change', 0, 7, list_of_MIDI_patches[7]],
1860
+ ['patch_change', 0, 8, list_of_MIDI_patches[8]],
1861
+ ['patch_change', 0, 9, list_of_MIDI_patches[9]],
1862
+ ['patch_change', 0, 10, list_of_MIDI_patches[10]],
1863
+ ['patch_change', 0, 11, list_of_MIDI_patches[11]],
1864
+ ['patch_change', 0, 12, list_of_MIDI_patches[12]],
1865
+ ['patch_change', 0, 13, list_of_MIDI_patches[13]],
1866
+ ['patch_change', 0, 14, list_of_MIDI_patches[14]],
1867
+ ['patch_change', 0, 15, list_of_MIDI_patches[15]],
1868
+ ['track_name', 0, bytes(track_name, text_encoding)]]
1869
+
1870
+ SONG = copy.deepcopy(ms_SONG)
1871
+
1872
+ if timings_multiplier != 1:
1873
+ for S in SONG:
1874
+ S[1] = S[1] * timings_multiplier
1875
+ if S[0] == 'note':
1876
+ S[2] = S[2] * timings_multiplier
1877
+
1878
+ output = output_header + [patch_list + SONG]
1879
+
1880
+ midi_data = score2midi(output, text_encoding)
1881
+ detailed_MIDI_stats = score2stats(output)
1882
+
1883
+ with open(output_file_name + '.mid', 'wb') as midi_file:
1884
+ midi_file.write(midi_data)
1885
+ midi_file.close()
1886
+
1887
+ if verbose:
1888
+ print('Done! Enjoy! :)')
1889
+
1890
+ return detailed_MIDI_stats
1891
+
1892
+ ###################################################################################
1893
+
1894
+ def hsv_to_rgb(h, s, v):
1895
+ if s == 0.0:
1896
+ return v, v, v
1897
+ i = int(h*6.0)
1898
+ f = (h*6.0) - i
1899
+ p = v*(1.0 - s)
1900
+ q = v*(1.0 - s*f)
1901
+ t = v*(1.0 - s*(1.0-f))
1902
+ i = i%6
1903
+ return [(v, t, p), (q, v, p), (p, v, t), (p, q, v), (t, p, v), (v, p, q)][i]
1904
+
1905
+ def generate_colors(n):
1906
+ return [hsv_to_rgb(i/n, 1, 1) for i in range(n)]
1907
+
1908
+ def add_arrays(a, b):
1909
+ return [sum(pair) for pair in zip(a, b)]
1910
+
1911
+ #-------------------------------------------------------------------------------
1912
+
1913
+ def plot_ms_SONG(ms_song,
1914
+ preview_length_in_notes=0,
1915
+ block_lines_times_list = None,
1916
+ plot_title='ms Song',
1917
+ max_num_colors=129,
1918
+ drums_color_num=128,
1919
+ plot_size=(11,4),
1920
+ note_height = 0.75,
1921
+ show_grid_lines=False,
1922
+ return_plt = False,
1923
+ timings_multiplier=1,
1924
+ save_plt='',
1925
+ save_only_plt_image=True,
1926
+ save_transparent=False
1927
+ ):
1928
+
1929
+ '''Tegridy ms SONG plotter/vizualizer'''
1930
+
1931
+ notes = [s for s in ms_song if s[0] == 'note']
1932
+
1933
+ if (len(max(notes, key=len)) != 7) and (len(min(notes, key=len)) != 7):
1934
+ print('The song notes do not have patches information')
1935
+ print('Ploease add patches to the notes in the song')
1936
+
1937
+ else:
1938
+
1939
+ start_times = [(s[1] * timings_multiplier) / 1000 for s in notes]
1940
+ durations = [(s[2] * timings_multiplier) / 1000 for s in notes]
1941
+ pitches = [s[4] for s in notes]
1942
+ patches = [s[6] for s in notes]
1943
+
1944
+ colors = generate_colors(max_num_colors)
1945
+ colors[drums_color_num] = (1, 1, 1)
1946
+
1947
+ pbl = (notes[preview_length_in_notes][1] * timings_multiplier) / 1000
1948
+
1949
+ fig, ax = plt.subplots(figsize=plot_size)
1950
+ #fig, ax = plt.subplots()
1951
+
1952
+ # Create a rectangle for each note with color based on patch number
1953
+ for start, duration, pitch, patch in zip(start_times, durations, pitches, patches):
1954
+ rect = plt.Rectangle((start, pitch), duration, note_height, facecolor=colors[patch])
1955
+ ax.add_patch(rect)
1956
+
1957
+ # Set the limits of the plot
1958
+ ax.set_xlim([min(start_times), max(add_arrays(start_times, durations))])
1959
+ ax.set_ylim([min(pitches)-1, max(pitches)+1])
1960
+
1961
+ # Set the background color to black
1962
+ ax.set_facecolor('black')
1963
+ fig.patch.set_facecolor('white')
1964
+
1965
+ if preview_length_in_notes > 0:
1966
+ ax.axvline(x=pbl, c='white')
1967
+
1968
+ if block_lines_times_list:
1969
+ for bl in block_lines_times_list:
1970
+ ax.axvline(x=bl, c='white')
1971
+
1972
+ if show_grid_lines:
1973
+ ax.grid(color='white')
1974
+
1975
+ plt.xlabel('Time (s)', c='black')
1976
+ plt.ylabel('MIDI Pitch', c='black')
1977
+
1978
+ plt.title(plot_title)
1979
+
1980
+ if save_plt != '':
1981
+ if save_only_plt_image:
1982
+ plt.axis('off')
1983
+ plt.title('')
1984
+ plt.savefig(save_plt, transparent=save_transparent, bbox_inches='tight', pad_inches=0, facecolor='black')
1985
+ plt.close()
1986
+
1987
+ else:
1988
+ plt.savefig(save_plt)
1989
+ plt.close()
1990
+
1991
+ if return_plt:
1992
+ plt.close(fig)
1993
+ return fig
1994
+
1995
+ plt.show()
1996
+ plt.close()
1997
+
1998
+ ###################################################################################
1999
+
2000
+ def Tegridy_SONG_to_Full_MIDI_Converter(SONG,
2001
+ output_signature = 'Tegridy TMIDI Module',
2002
+ track_name = 'Composition Track',
2003
+ number_of_ticks_per_quarter = 1000,
2004
+ output_file_name = 'TMIDI-Composition',
2005
+ text_encoding='ISO-8859-1',
2006
+ verbose=True):
2007
+
2008
+ '''Tegridy SONG to Full MIDI Converter
2009
+
2010
+ Input: Input SONG in Full TMIDI SONG/MIDI.py Score format
2011
+ Output MIDI Track 0 name / MIDI Signature
2012
+ Output MIDI Track 1 name / Composition track name
2013
+ Number of ticks per quarter for the output MIDI
2014
+ Output file name w/o .mid extension.
2015
+ Optional text encoding if you are working with text_events/lyrics. This is especially useful for Karaoke. Please note that anything but ISO-8859-1 is a non-standard way of encoding text_events according to MIDI specs.
2016
+
2017
+ Output: MIDI File
2018
+ Detailed MIDI stats
2019
+
2020
+ Project Los Angeles
2021
+ Tegridy Code 2023'''
2022
+
2023
+ if verbose:
2024
+ print('Converting to MIDI. Please stand-by...')
2025
+
2026
+ output_header = [number_of_ticks_per_quarter,
2027
+ [['set_tempo', 0, 1000000],
2028
+ ['track_name', 0, bytes(output_signature, text_encoding)]]]
2029
+
2030
+ song_track = [['track_name', 0, bytes(track_name, text_encoding)]]
2031
+
2032
+ output = output_header + [song_track + SONG]
2033
+
2034
+ midi_data = score2midi(output, text_encoding)
2035
+ detailed_MIDI_stats = score2stats(output)
2036
+
2037
+ with open(output_file_name + '.mid', 'wb') as midi_file:
2038
+ midi_file.write(midi_data)
2039
+ midi_file.close()
2040
+
2041
+ if verbose:
2042
+ print('Done! Enjoy! :)')
2043
+
2044
+ return detailed_MIDI_stats
2045
+
2046
+ ###################################################################################
2047
+
2048
+ def Tegridy_File_Time_Stamp(input_file_name='File_Created_on_', ext = ''):
2049
+
2050
+ '''Tegridy File Time Stamp
2051
+
2052
+ Input: Full path and file name without extention
2053
+ File extension
2054
+
2055
+ Output: File name string with time-stamp and extension (time-stamped file name)
2056
+
2057
+ Project Los Angeles
2058
+ Tegridy Code 2021'''
2059
+
2060
+ print('Time-stamping output file...')
2061
+
2062
+ now = ''
2063
+ now_n = str(datetime.now())
2064
+ now_n = now_n.replace(' ', '_')
2065
+ now_n = now_n.replace(':', '_')
2066
+ now = now_n.replace('.', '_')
2067
+
2068
+ fname = input_file_name + str(now) + ext
2069
+
2070
+ return(fname)
2071
+
2072
+ ###################################################################################
2073
+
2074
+ def Tegridy_Any_Pickle_File_Writer(Data, input_file_name='TMIDI_Pickle_File'):
2075
+
2076
+ '''Tegridy Pickle File Writer
2077
+
2078
+ Input: Data to write (I.e. a list)
2079
+ Full path and file name without extention
2080
+
2081
+ Output: Named Pickle file
2082
+
2083
+ Project Los Angeles
2084
+ Tegridy Code 2021'''
2085
+
2086
+ print('Tegridy Pickle File Writer')
2087
+
2088
+ full_path_to_output_dataset_to = input_file_name + '.pickle'
2089
+
2090
+ if os.path.exists(full_path_to_output_dataset_to):
2091
+ os.remove(full_path_to_output_dataset_to)
2092
+ print('Removing old Dataset...')
2093
+ else:
2094
+ print("Creating new Dataset file...")
2095
+
2096
+ with open(full_path_to_output_dataset_to, 'wb') as filehandle:
2097
+ # store the data as binary data stream
2098
+ pickle.dump(Data, filehandle, protocol=pickle.HIGHEST_PROTOCOL)
2099
+
2100
+ print('Dataset was saved as:', full_path_to_output_dataset_to)
2101
+ print('Task complete. Enjoy! :)')
2102
+
2103
+ ###################################################################################
2104
+
2105
+ def Tegridy_Any_Pickle_File_Reader(input_file_name='TMIDI_Pickle_File', ext='.pickle', verbose=True):
2106
+
2107
+ '''Tegridy Pickle File Loader
2108
+
2109
+ Input: Full path and file name with or without extention
2110
+ File extension if different from default .pickle
2111
+
2112
+ Output: Standard Python 3 unpickled data object
2113
+
2114
+ Project Los Angeles
2115
+ Tegridy Code 2021'''
2116
+
2117
+ if verbose:
2118
+ print('Tegridy Pickle File Loader')
2119
+ print('Loading the pickle file. Please wait...')
2120
+
2121
+ if os.path.basename(input_file_name).endswith(ext):
2122
+ fname = input_file_name
2123
+
2124
+ else:
2125
+ fname = input_file_name + ext
2126
+
2127
+ with open(fname, 'rb') as pickle_file:
2128
+ content = pickle.load(pickle_file)
2129
+
2130
+ if verbose:
2131
+ print('Done!')
2132
+
2133
+ return content
TMIDIX.py → src/TMIDIX.py RENAMED
@@ -1,2007 +1,29 @@
1
  #! /usr/bin/python3
2
 
3
- r'''###############################################################################
4
- ###################################################################################
5
- #
6
- #
7
- # Tegridy MIDI X Module (TMIDI X / tee-midi eks)
8
- #
9
- # NOTE: TMIDI X Module starts after the partial MIDI.py module @ line 1450
10
- #
11
- # Based upon MIDI.py module v.6.7. by Peter Billam / pjb.com.au
12
- #
13
- # Project Los Angeles
14
- #
15
- # Tegridy Code 2025
16
- #
17
- # https://github.com/Tegridy-Code/Project-Los-Angeles
18
- #
19
- #
20
- ###################################################################################
21
- ###################################################################################
22
- # Copyright 2025 Project Los Angeles / Tegridy Code
23
- #
24
- # Licensed under the Apache License, Version 2.0 (the "License");
25
- # you may not use this file except in compliance with the License.
26
- # You may obtain a copy of the License at
27
- #
28
- # http://www.apache.org/licenses/LICENSE-2.0
29
- #
30
- # Unless required by applicable law or agreed to in writing, software
31
- # distributed under the License is distributed on an "AS IS" BASIS,
32
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
33
- # See the License for the specific language governing permissions and
34
- # limitations under the License.
35
- ###################################################################################
36
- ###################################################################################
37
- #
38
- # PARTIAL MIDI.py Module v.6.7. by Peter Billam
39
- # Please see TMIDI 2.3/tegridy-tools repo for full MIDI.py module code
40
- #
41
- # Or you can always download the latest full version from:
42
- #
43
- # https://pjb.com.au/
44
- # https://peterbillam.gitlab.io/miditools/
45
- #
46
- # Copyright 2020 Peter Billam
47
- #
48
- ###################################################################################
49
- ###################################################################################
50
- '''
51
-
52
- ###################################################################################
53
-
54
- __version__ = "25.7.8"
55
-
56
- print('=' * 70)
57
- print('TMIDIX Python module')
58
- print('Version:', __version__)
59
- print('=' * 70)
60
- print('Loading module...')
61
-
62
- ###################################################################################
63
-
64
- import sys, struct, copy
65
-
66
- Version = '6.7'
67
- VersionDate = '20201120'
68
-
69
- _previous_warning = '' # 5.4
70
- _previous_times = 0 # 5.4
71
- _no_warning = False
72
-
73
- #------------------------------- Encoding stuff --------------------------
74
-
75
- def opus2midi(opus=[], text_encoding='ISO-8859-1'):
76
- r'''The argument is a list: the first item in the list is the "ticks"
77
- parameter, the others are the tracks. Each track is a list
78
- of midi-events, and each event is itself a list; see above.
79
- opus2midi() returns a bytestring of the MIDI, which can then be
80
- written either to a file opened in binary mode (mode='wb'),
81
- or to stdout by means of: sys.stdout.buffer.write()
82
-
83
- my_opus = [
84
- 96,
85
- [ # track 0:
86
- ['patch_change', 0, 1, 8], # and these are the events...
87
- ['note_on', 5, 1, 25, 96],
88
- ['note_off', 96, 1, 25, 0],
89
- ['note_on', 0, 1, 29, 96],
90
- ['note_off', 96, 1, 29, 0],
91
- ], # end of track 0
92
- ]
93
- my_midi = opus2midi(my_opus)
94
- sys.stdout.buffer.write(my_midi)
95
- '''
96
- if len(opus) < 2:
97
- opus=[1000, [],]
98
- tracks = copy.deepcopy(opus)
99
- ticks = int(tracks.pop(0))
100
- ntracks = len(tracks)
101
- if ntracks == 1:
102
- format = 0
103
- else:
104
- format = 1
105
-
106
- my_midi = b"MThd\x00\x00\x00\x06"+struct.pack('>HHH',format,ntracks,ticks)
107
- for track in tracks:
108
- events = _encode(track, text_encoding=text_encoding)
109
- my_midi += b'MTrk' + struct.pack('>I',len(events)) + events
110
- _clean_up_warnings()
111
- return my_midi
112
-
113
-
114
- def score2opus(score=None, text_encoding='ISO-8859-1'):
115
- r'''
116
- The argument is a list: the first item in the list is the "ticks"
117
- parameter, the others are the tracks. Each track is a list
118
- of score-events, and each event is itself a list. A score-event
119
- is similar to an opus-event (see above), except that in a score:
120
- 1) the times are expressed as an absolute number of ticks
121
- from the track's start time
122
- 2) the pairs of 'note_on' and 'note_off' events in an "opus"
123
- are abstracted into a single 'note' event in a "score":
124
- ['note', start_time, duration, channel, pitch, velocity]
125
- score2opus() returns a list specifying the equivalent "opus".
126
-
127
- my_score = [
128
- 96,
129
- [ # track 0:
130
- ['patch_change', 0, 1, 8],
131
- ['note', 5, 96, 1, 25, 96],
132
- ['note', 101, 96, 1, 29, 96]
133
- ], # end of track 0
134
- ]
135
- my_opus = score2opus(my_score)
136
- '''
137
- if len(score) < 2:
138
- score=[1000, [],]
139
- tracks = copy.deepcopy(score)
140
- ticks = int(tracks.pop(0))
141
- opus_tracks = []
142
- for scoretrack in tracks:
143
- time2events = dict([])
144
- for scoreevent in scoretrack:
145
- if scoreevent[0] == 'note':
146
- note_on_event = ['note_on',scoreevent[1],
147
- scoreevent[3],scoreevent[4],scoreevent[5]]
148
- note_off_event = ['note_off',scoreevent[1]+scoreevent[2],
149
- scoreevent[3],scoreevent[4],scoreevent[5]]
150
- if time2events.get(note_on_event[1]):
151
- time2events[note_on_event[1]].append(note_on_event)
152
- else:
153
- time2events[note_on_event[1]] = [note_on_event,]
154
- if time2events.get(note_off_event[1]):
155
- time2events[note_off_event[1]].append(note_off_event)
156
- else:
157
- time2events[note_off_event[1]] = [note_off_event,]
158
- continue
159
- if time2events.get(scoreevent[1]):
160
- time2events[scoreevent[1]].append(scoreevent)
161
- else:
162
- time2events[scoreevent[1]] = [scoreevent,]
163
-
164
- sorted_times = [] # list of keys
165
- for k in time2events.keys():
166
- sorted_times.append(k)
167
- sorted_times.sort()
168
-
169
- sorted_events = [] # once-flattened list of values sorted by key
170
- for time in sorted_times:
171
- sorted_events.extend(time2events[time])
172
-
173
- abs_time = 0
174
- for event in sorted_events: # convert abs times => delta times
175
- delta_time = event[1] - abs_time
176
- abs_time = event[1]
177
- event[1] = delta_time
178
- opus_tracks.append(sorted_events)
179
- opus_tracks.insert(0,ticks)
180
- _clean_up_warnings()
181
- return opus_tracks
182
-
183
- def score2midi(score=None, text_encoding='ISO-8859-1'):
184
- r'''
185
- Translates a "score" into MIDI, using score2opus() then opus2midi()
186
- '''
187
- return opus2midi(score2opus(score, text_encoding), text_encoding)
188
-
189
- #--------------------------- Decoding stuff ------------------------
190
-
191
- def midi2opus(midi=b'', do_not_check_MIDI_signature=False):
192
- r'''Translates MIDI into a "opus". For a description of the
193
- "opus" format, see opus2midi()
194
- '''
195
- my_midi=bytearray(midi)
196
- if len(my_midi) < 4:
197
- _clean_up_warnings()
198
- return [1000,[],]
199
- id = bytes(my_midi[0:4])
200
- if id != b'MThd':
201
- _warn("midi2opus: midi starts with "+str(id)+" instead of 'MThd'")
202
- _clean_up_warnings()
203
- if do_not_check_MIDI_signature == False:
204
- return [1000,[],]
205
- [length, format, tracks_expected, ticks] = struct.unpack(
206
- '>IHHH', bytes(my_midi[4:14]))
207
- if length != 6:
208
- _warn("midi2opus: midi header length was "+str(length)+" instead of 6")
209
- _clean_up_warnings()
210
- return [1000,[],]
211
- my_opus = [ticks,]
212
- my_midi = my_midi[14:]
213
- track_num = 1 # 5.1
214
- while len(my_midi) >= 8:
215
- track_type = bytes(my_midi[0:4])
216
- if track_type != b'MTrk':
217
- #_warn('midi2opus: Warning: track #'+str(track_num)+' type is '+str(track_type)+" instead of b'MTrk'")
218
- pass
219
- [track_length] = struct.unpack('>I', my_midi[4:8])
220
- my_midi = my_midi[8:]
221
- if track_length > len(my_midi):
222
- _warn('midi2opus: track #'+str(track_num)+' length '+str(track_length)+' is too large')
223
- _clean_up_warnings()
224
- return my_opus # 5.0
225
- my_midi_track = my_midi[0:track_length]
226
- my_track = _decode(my_midi_track)
227
- my_opus.append(my_track)
228
- my_midi = my_midi[track_length:]
229
- track_num += 1 # 5.1
230
- _clean_up_warnings()
231
- return my_opus
232
-
233
- def opus2score(opus=[]):
234
- r'''For a description of the "opus" and "score" formats,
235
- see opus2midi() and score2opus().
236
- '''
237
- if len(opus) < 2:
238
- _clean_up_warnings()
239
- return [1000,[],]
240
- tracks = copy.deepcopy(opus) # couple of slices probably quicker...
241
- ticks = int(tracks.pop(0))
242
- score = [ticks,]
243
- for opus_track in tracks:
244
- ticks_so_far = 0
245
- score_track = []
246
- chapitch2note_on_events = dict([]) # 4.0
247
- for opus_event in opus_track:
248
- ticks_so_far += opus_event[1]
249
- if opus_event[0] == 'note_off' or (opus_event[0] == 'note_on' and opus_event[4] == 0): # 4.8
250
- cha = opus_event[2]
251
- pitch = opus_event[3]
252
- key = cha*128 + pitch
253
- if chapitch2note_on_events.get(key):
254
- new_event = chapitch2note_on_events[key].pop(0)
255
- new_event[2] = ticks_so_far - new_event[1]
256
- score_track.append(new_event)
257
- elif pitch > 127:
258
- pass #_warn('opus2score: note_off with no note_on, bad pitch='+str(pitch))
259
- else:
260
- pass #_warn('opus2score: note_off with no note_on cha='+str(cha)+' pitch='+str(pitch))
261
- elif opus_event[0] == 'note_on':
262
- cha = opus_event[2]
263
- pitch = opus_event[3]
264
- key = cha*128 + pitch
265
- new_event = ['note',ticks_so_far,0,cha,pitch, opus_event[4]]
266
- if chapitch2note_on_events.get(key):
267
- chapitch2note_on_events[key].append(new_event)
268
- else:
269
- chapitch2note_on_events[key] = [new_event,]
270
- else:
271
- opus_event[1] = ticks_so_far
272
- score_track.append(opus_event)
273
- # check for unterminated notes (Oisín) -- 5.2
274
- for chapitch in chapitch2note_on_events:
275
- note_on_events = chapitch2note_on_events[chapitch]
276
- for new_e in note_on_events:
277
- new_e[2] = ticks_so_far - new_e[1]
278
- score_track.append(new_e)
279
- pass #_warn("opus2score: note_on with no note_off cha="+str(new_e[3])+' pitch='+str(new_e[4])+'; adding note_off at end')
280
- score.append(score_track)
281
- _clean_up_warnings()
282
- return score
283
-
284
- def midi2score(midi=b'', do_not_check_MIDI_signature=False):
285
- r'''
286
- Translates MIDI into a "score", using midi2opus() then opus2score()
287
- '''
288
- return opus2score(midi2opus(midi, do_not_check_MIDI_signature))
289
-
290
- def midi2ms_score(midi=b'', do_not_check_MIDI_signature=False):
291
- r'''
292
- Translates MIDI into a "score" with one beat per second and one
293
- tick per millisecond, using midi2opus() then to_millisecs()
294
- then opus2score()
295
- '''
296
- return opus2score(to_millisecs(midi2opus(midi, do_not_check_MIDI_signature)))
297
-
298
- def midi2single_track_ms_score(midi_path_or_bytes,
299
- recalculate_channels = False,
300
- pass_old_timings_events= False,
301
- verbose = False,
302
- do_not_check_MIDI_signature=False
303
- ):
304
- r'''
305
- Translates MIDI into a single track "score" with 16 instruments and one beat per second and one
306
- tick per millisecond
307
- '''
308
-
309
- if type(midi_path_or_bytes) == bytes:
310
- midi_data = midi_path_or_bytes
311
-
312
- elif type(midi_path_or_bytes) == str:
313
- midi_data = open(midi_path_or_bytes, 'rb').read()
314
-
315
- score = midi2score(midi_data, do_not_check_MIDI_signature)
316
-
317
- if recalculate_channels:
318
-
319
- events_matrixes = []
320
-
321
- itrack = 1
322
- events_matrixes_channels = []
323
- while itrack < len(score):
324
- events_matrix = []
325
- for event in score[itrack]:
326
- if event[0] == 'note' and event[3] != 9:
327
- event[3] = (16 * (itrack-1)) + event[3]
328
- if event[3] not in events_matrixes_channels:
329
- events_matrixes_channels.append(event[3])
330
-
331
- events_matrix.append(event)
332
- events_matrixes.append(events_matrix)
333
- itrack += 1
334
-
335
- events_matrix1 = []
336
- for e in events_matrixes:
337
- events_matrix1.extend(e)
338
-
339
- if verbose:
340
- if len(events_matrixes_channels) > 16:
341
- print('MIDI has', len(events_matrixes_channels), 'instruments!', len(events_matrixes_channels) - 16, 'instrument(s) will be removed!')
342
-
343
- for e in events_matrix1:
344
- if e[0] == 'note' and e[3] != 9:
345
- if e[3] in events_matrixes_channels[:15]:
346
- if events_matrixes_channels[:15].index(e[3]) < 9:
347
- e[3] = events_matrixes_channels[:15].index(e[3])
348
- else:
349
- e[3] = events_matrixes_channels[:15].index(e[3])+1
350
- else:
351
- events_matrix1.remove(e)
352
-
353
- if e[0] in ['patch_change', 'control_change', 'channel_after_touch', 'key_after_touch', 'pitch_wheel_change'] and e[2] != 9:
354
- if e[2] in [e % 16 for e in events_matrixes_channels[:15]]:
355
- if [e % 16 for e in events_matrixes_channels[:15]].index(e[2]) < 9:
356
- e[2] = [e % 16 for e in events_matrixes_channels[:15]].index(e[2])
357
- else:
358
- e[2] = [e % 16 for e in events_matrixes_channels[:15]].index(e[2])+1
359
- else:
360
- events_matrix1.remove(e)
361
-
362
- else:
363
- events_matrix1 = []
364
- itrack = 1
365
-
366
- while itrack < len(score):
367
- for event in score[itrack]:
368
- events_matrix1.append(event)
369
- itrack += 1
370
-
371
- opus = score2opus([score[0], events_matrix1])
372
- ms_score = opus2score(to_millisecs(opus, pass_old_timings_events=pass_old_timings_events))
373
-
374
- return ms_score
375
-
376
- #------------------------ Other Transformations ---------------------
377
-
378
- def to_millisecs(old_opus=None, desired_time_in_ms=1, pass_old_timings_events = False):
379
- r'''Recallibrates all the times in an "opus" to use one beat
380
- per second and one tick per millisecond. This makes it
381
- hard to retrieve any information about beats or barlines,
382
- but it does make it easy to mix different scores together.
383
- '''
384
- if old_opus == None:
385
- return [1000 * desired_time_in_ms,[],]
386
- try:
387
- old_tpq = int(old_opus[0])
388
- except IndexError: # 5.0
389
- _warn('to_millisecs: the opus '+str(type(old_opus))+' has no elements')
390
- return [1000 * desired_time_in_ms,[],]
391
- new_opus = [1000 * desired_time_in_ms,]
392
- # 6.7 first go through building a table of set_tempos by absolute-tick
393
- ticks2tempo = {}
394
- itrack = 1
395
- while itrack < len(old_opus):
396
- ticks_so_far = 0
397
- for old_event in old_opus[itrack]:
398
- if old_event[0] == 'note':
399
- raise TypeError('to_millisecs needs an opus, not a score')
400
- ticks_so_far += old_event[1]
401
- if old_event[0] == 'set_tempo':
402
- ticks2tempo[ticks_so_far] = old_event[2]
403
- itrack += 1
404
- # then get the sorted-array of their keys
405
- tempo_ticks = [] # list of keys
406
- for k in ticks2tempo.keys():
407
- tempo_ticks.append(k)
408
- tempo_ticks.sort()
409
- # then go through converting to millisec, testing if the next
410
- # set_tempo lies before the next track-event, and using it if so.
411
- itrack = 1
412
- while itrack < len(old_opus):
413
- ms_per_old_tick = 400 / old_tpq # float: will round later 6.3
414
- i_tempo_ticks = 0
415
- ticks_so_far = 0
416
- ms_so_far = 0.0
417
- previous_ms_so_far = 0.0
418
-
419
- if pass_old_timings_events:
420
- new_track = [['set_tempo',0,1000000 * desired_time_in_ms],['old_tpq', 0, old_tpq]] # new "crochet" is 1 sec
421
- else:
422
- new_track = [['set_tempo',0,1000000 * desired_time_in_ms],] # new "crochet" is 1 sec
423
- for old_event in old_opus[itrack]:
424
- # detect if ticks2tempo has something before this event
425
- # 20160702 if ticks2tempo is at the same time, leave it
426
- event_delta_ticks = old_event[1] * desired_time_in_ms
427
- if (i_tempo_ticks < len(tempo_ticks) and
428
- tempo_ticks[i_tempo_ticks] < (ticks_so_far + old_event[1]) * desired_time_in_ms):
429
- delta_ticks = tempo_ticks[i_tempo_ticks] - ticks_so_far
430
- ms_so_far += (ms_per_old_tick * delta_ticks * desired_time_in_ms)
431
- ticks_so_far = tempo_ticks[i_tempo_ticks]
432
- ms_per_old_tick = ticks2tempo[ticks_so_far] / (1000.0*old_tpq * desired_time_in_ms)
433
- i_tempo_ticks += 1
434
- event_delta_ticks -= delta_ticks
435
- new_event = copy.deepcopy(old_event) # now handle the new event
436
- ms_so_far += (ms_per_old_tick * old_event[1] * desired_time_in_ms)
437
- new_event[1] = round(ms_so_far - previous_ms_so_far)
438
-
439
- if pass_old_timings_events:
440
- if old_event[0] != 'set_tempo':
441
- previous_ms_so_far = ms_so_far
442
- new_track.append(new_event)
443
- else:
444
- new_event[0] = 'old_set_tempo'
445
- previous_ms_so_far = ms_so_far
446
- new_track.append(new_event)
447
- else:
448
- if old_event[0] != 'set_tempo':
449
- previous_ms_so_far = ms_so_far
450
- new_track.append(new_event)
451
- ticks_so_far += event_delta_ticks
452
- new_opus.append(new_track)
453
- itrack += 1
454
- _clean_up_warnings()
455
- return new_opus
456
-
457
- def event2alsaseq(event=None): # 5.5
458
- r'''Converts an event into the format needed by the alsaseq module,
459
- http://pp.com.mx/python/alsaseq
460
- The type of track (opus or score) is autodetected.
461
- '''
462
- pass
463
-
464
- def grep(score=None, channels=None):
465
- r'''Returns a "score" containing only the channels specified
466
- '''
467
- if score == None:
468
- return [1000,[],]
469
- ticks = score[0]
470
- new_score = [ticks,]
471
- if channels == None:
472
- return new_score
473
- channels = set(channels)
474
- global Event2channelindex
475
- itrack = 1
476
- while itrack < len(score):
477
- new_score.append([])
478
- for event in score[itrack]:
479
- channel_index = Event2channelindex.get(event[0], False)
480
- if channel_index:
481
- if event[channel_index] in channels:
482
- new_score[itrack].append(event)
483
- else:
484
- new_score[itrack].append(event)
485
- itrack += 1
486
- return new_score
487
-
488
- def score2stats(opus_or_score=None):
489
- r'''Returns a dict of some basic stats about the score, like
490
- bank_select (list of tuples (msb,lsb)),
491
- channels_by_track (list of lists), channels_total (set),
492
- general_midi_mode (list),
493
- ntracks, nticks, patch_changes_by_track (list of dicts),
494
- num_notes_by_channel (list of numbers),
495
- patch_changes_total (set),
496
- percussion (dict histogram of channel 9 events),
497
- pitches (dict histogram of pitches on channels other than 9),
498
- pitch_range_by_track (list, by track, of two-member-tuples),
499
- pitch_range_sum (sum over tracks of the pitch_ranges),
500
- '''
501
- bank_select_msb = -1
502
- bank_select_lsb = -1
503
- bank_select = []
504
- channels_by_track = []
505
- channels_total = set([])
506
- general_midi_mode = []
507
- num_notes_by_channel = dict([])
508
- patches_used_by_track = []
509
- patches_used_total = set([])
510
- patch_changes_by_track = []
511
- patch_changes_total = set([])
512
- percussion = dict([]) # histogram of channel 9 "pitches"
513
- pitches = dict([]) # histogram of pitch-occurrences channels 0-8,10-15
514
- pitch_range_sum = 0 # u pitch-ranges of each track
515
- pitch_range_by_track = []
516
- is_a_score = True
517
- if opus_or_score == None:
518
- return {'bank_select':[], 'channels_by_track':[], 'channels_total':[],
519
- 'general_midi_mode':[], 'ntracks':0, 'nticks':0,
520
- 'num_notes_by_channel':dict([]),
521
- 'patch_changes_by_track':[], 'patch_changes_total':[],
522
- 'percussion':{}, 'pitches':{}, 'pitch_range_by_track':[],
523
- 'ticks_per_quarter':0, 'pitch_range_sum':0}
524
- ticks_per_quarter = opus_or_score[0]
525
- i = 1 # ignore first element, which is ticks
526
- nticks = 0
527
- while i < len(opus_or_score):
528
- highest_pitch = 0
529
- lowest_pitch = 128
530
- channels_this_track = set([])
531
- patch_changes_this_track = dict({})
532
- for event in opus_or_score[i]:
533
- if event[0] == 'note':
534
- num_notes_by_channel[event[3]] = num_notes_by_channel.get(event[3],0) + 1
535
- if event[3] == 9:
536
- percussion[event[4]] = percussion.get(event[4],0) + 1
537
- else:
538
- pitches[event[4]] = pitches.get(event[4],0) + 1
539
- if event[4] > highest_pitch:
540
- highest_pitch = event[4]
541
- if event[4] < lowest_pitch:
542
- lowest_pitch = event[4]
543
- channels_this_track.add(event[3])
544
- channels_total.add(event[3])
545
- finish_time = event[1] + event[2]
546
- if finish_time > nticks:
547
- nticks = finish_time
548
- elif event[0] == 'note_off' or (event[0] == 'note_on' and event[4] == 0): # 4.8
549
- finish_time = event[1]
550
- if finish_time > nticks:
551
- nticks = finish_time
552
- elif event[0] == 'note_on':
553
- is_a_score = False
554
- num_notes_by_channel[event[2]] = num_notes_by_channel.get(event[2],0) + 1
555
- if event[2] == 9:
556
- percussion[event[3]] = percussion.get(event[3],0) + 1
557
- else:
558
- pitches[event[3]] = pitches.get(event[3],0) + 1
559
- if event[3] > highest_pitch:
560
- highest_pitch = event[3]
561
- if event[3] < lowest_pitch:
562
- lowest_pitch = event[3]
563
- channels_this_track.add(event[2])
564
- channels_total.add(event[2])
565
- elif event[0] == 'patch_change':
566
- patch_changes_this_track[event[2]] = event[3]
567
- patch_changes_total.add(event[3])
568
- elif event[0] == 'control_change':
569
- if event[3] == 0: # bank select MSB
570
- bank_select_msb = event[4]
571
- elif event[3] == 32: # bank select LSB
572
- bank_select_lsb = event[4]
573
- if bank_select_msb >= 0 and bank_select_lsb >= 0:
574
- bank_select.append((bank_select_msb,bank_select_lsb))
575
- bank_select_msb = -1
576
- bank_select_lsb = -1
577
- elif event[0] == 'sysex_f0':
578
- if _sysex2midimode.get(event[2], -1) >= 0:
579
- general_midi_mode.append(_sysex2midimode.get(event[2]))
580
- if is_a_score:
581
- if event[1] > nticks:
582
- nticks = event[1]
583
- else:
584
- nticks += event[1]
585
- if lowest_pitch == 128:
586
- lowest_pitch = 0
587
- channels_by_track.append(channels_this_track)
588
- patch_changes_by_track.append(patch_changes_this_track)
589
- pitch_range_by_track.append((lowest_pitch,highest_pitch))
590
- pitch_range_sum += (highest_pitch-lowest_pitch)
591
- i += 1
592
-
593
- return {'bank_select':bank_select,
594
- 'channels_by_track':channels_by_track,
595
- 'channels_total':channels_total,
596
- 'general_midi_mode':general_midi_mode,
597
- 'ntracks':len(opus_or_score)-1,
598
- 'nticks':nticks,
599
- 'num_notes_by_channel':num_notes_by_channel,
600
- 'patch_changes_by_track':patch_changes_by_track,
601
- 'patch_changes_total':patch_changes_total,
602
- 'percussion':percussion,
603
- 'pitches':pitches,
604
- 'pitch_range_by_track':pitch_range_by_track,
605
- 'pitch_range_sum':pitch_range_sum,
606
- 'ticks_per_quarter':ticks_per_quarter}
607
-
608
- #----------------------------- Event stuff --------------------------
609
-
610
- _sysex2midimode = {
611
- "\x7E\x7F\x09\x01\xF7": 1,
612
- "\x7E\x7F\x09\x02\xF7": 0,
613
- "\x7E\x7F\x09\x03\xF7": 2,
614
- }
615
-
616
- # Some public-access tuples:
617
- MIDI_events = tuple('''note_off note_on key_after_touch
618
- control_change patch_change channel_after_touch
619
- pitch_wheel_change'''.split())
620
-
621
- Text_events = tuple('''text_event copyright_text_event
622
- track_name instrument_name lyric marker cue_point text_event_08
623
- text_event_09 text_event_0a text_event_0b text_event_0c
624
- text_event_0d text_event_0e text_event_0f'''.split())
625
-
626
- Nontext_meta_events = tuple('''end_track set_tempo
627
- smpte_offset time_signature key_signature sequencer_specific
628
- raw_meta_event sysex_f0 sysex_f7 song_position song_select
629
- tune_request'''.split())
630
- # unsupported: raw_data
631
-
632
- # Actually, 'tune_request' is is F-series event, not strictly a meta-event...
633
- Meta_events = Text_events + Nontext_meta_events
634
- All_events = MIDI_events + Meta_events
635
-
636
- # And three dictionaries:
637
- Number2patch = { # General MIDI patch numbers:
638
- 0:'Acoustic Grand',
639
- 1:'Bright Acoustic',
640
- 2:'Electric Grand',
641
- 3:'Honky-Tonk',
642
- 4:'Electric Piano 1',
643
- 5:'Electric Piano 2',
644
- 6:'Harpsichord',
645
- 7:'Clav',
646
- 8:'Celesta',
647
- 9:'Glockenspiel',
648
- 10:'Music Box',
649
- 11:'Vibraphone',
650
- 12:'Marimba',
651
- 13:'Xylophone',
652
- 14:'Tubular Bells',
653
- 15:'Dulcimer',
654
- 16:'Drawbar Organ',
655
- 17:'Percussive Organ',
656
- 18:'Rock Organ',
657
- 19:'Church Organ',
658
- 20:'Reed Organ',
659
- 21:'Accordion',
660
- 22:'Harmonica',
661
- 23:'Tango Accordion',
662
- 24:'Acoustic Guitar(nylon)',
663
- 25:'Acoustic Guitar(steel)',
664
- 26:'Electric Guitar(jazz)',
665
- 27:'Electric Guitar(clean)',
666
- 28:'Electric Guitar(muted)',
667
- 29:'Overdriven Guitar',
668
- 30:'Distortion Guitar',
669
- 31:'Guitar Harmonics',
670
- 32:'Acoustic Bass',
671
- 33:'Electric Bass(finger)',
672
- 34:'Electric Bass(pick)',
673
- 35:'Fretless Bass',
674
- 36:'Slap Bass 1',
675
- 37:'Slap Bass 2',
676
- 38:'Synth Bass 1',
677
- 39:'Synth Bass 2',
678
- 40:'Violin',
679
- 41:'Viola',
680
- 42:'Cello',
681
- 43:'Contrabass',
682
- 44:'Tremolo Strings',
683
- 45:'Pizzicato Strings',
684
- 46:'Orchestral Harp',
685
- 47:'Timpani',
686
- 48:'String Ensemble 1',
687
- 49:'String Ensemble 2',
688
- 50:'SynthStrings 1',
689
- 51:'SynthStrings 2',
690
- 52:'Choir Aahs',
691
- 53:'Voice Oohs',
692
- 54:'Synth Voice',
693
- 55:'Orchestra Hit',
694
- 56:'Trumpet',
695
- 57:'Trombone',
696
- 58:'Tuba',
697
- 59:'Muted Trumpet',
698
- 60:'French Horn',
699
- 61:'Brass Section',
700
- 62:'SynthBrass 1',
701
- 63:'SynthBrass 2',
702
- 64:'Soprano Sax',
703
- 65:'Alto Sax',
704
- 66:'Tenor Sax',
705
- 67:'Baritone Sax',
706
- 68:'Oboe',
707
- 69:'English Horn',
708
- 70:'Bassoon',
709
- 71:'Clarinet',
710
- 72:'Piccolo',
711
- 73:'Flute',
712
- 74:'Recorder',
713
- 75:'Pan Flute',
714
- 76:'Blown Bottle',
715
- 77:'Skakuhachi',
716
- 78:'Whistle',
717
- 79:'Ocarina',
718
- 80:'Lead 1 (square)',
719
- 81:'Lead 2 (sawtooth)',
720
- 82:'Lead 3 (calliope)',
721
- 83:'Lead 4 (chiff)',
722
- 84:'Lead 5 (charang)',
723
- 85:'Lead 6 (voice)',
724
- 86:'Lead 7 (fifths)',
725
- 87:'Lead 8 (bass+lead)',
726
- 88:'Pad 1 (new age)',
727
- 89:'Pad 2 (warm)',
728
- 90:'Pad 3 (polysynth)',
729
- 91:'Pad 4 (choir)',
730
- 92:'Pad 5 (bowed)',
731
- 93:'Pad 6 (metallic)',
732
- 94:'Pad 7 (halo)',
733
- 95:'Pad 8 (sweep)',
734
- 96:'FX 1 (rain)',
735
- 97:'FX 2 (soundtrack)',
736
- 98:'FX 3 (crystal)',
737
- 99:'FX 4 (atmosphere)',
738
- 100:'FX 5 (brightness)',
739
- 101:'FX 6 (goblins)',
740
- 102:'FX 7 (echoes)',
741
- 103:'FX 8 (sci-fi)',
742
- 104:'Sitar',
743
- 105:'Banjo',
744
- 106:'Shamisen',
745
- 107:'Koto',
746
- 108:'Kalimba',
747
- 109:'Bagpipe',
748
- 110:'Fiddle',
749
- 111:'Shanai',
750
- 112:'Tinkle Bell',
751
- 113:'Agogo',
752
- 114:'Steel Drums',
753
- 115:'Woodblock',
754
- 116:'Taiko Drum',
755
- 117:'Melodic Tom',
756
- 118:'Synth Drum',
757
- 119:'Reverse Cymbal',
758
- 120:'Guitar Fret Noise',
759
- 121:'Breath Noise',
760
- 122:'Seashore',
761
- 123:'Bird Tweet',
762
- 124:'Telephone Ring',
763
- 125:'Helicopter',
764
- 126:'Applause',
765
- 127:'Gunshot',
766
- }
767
- Notenum2percussion = { # General MIDI Percussion (on Channel 9):
768
- 35:'Acoustic Bass Drum',
769
- 36:'Bass Drum 1',
770
- 37:'Side Stick',
771
- 38:'Acoustic Snare',
772
- 39:'Hand Clap',
773
- 40:'Electric Snare',
774
- 41:'Low Floor Tom',
775
- 42:'Closed Hi-Hat',
776
- 43:'High Floor Tom',
777
- 44:'Pedal Hi-Hat',
778
- 45:'Low Tom',
779
- 46:'Open Hi-Hat',
780
- 47:'Low-Mid Tom',
781
- 48:'Hi-Mid Tom',
782
- 49:'Crash Cymbal 1',
783
- 50:'High Tom',
784
- 51:'Ride Cymbal 1',
785
- 52:'Chinese Cymbal',
786
- 53:'Ride Bell',
787
- 54:'Tambourine',
788
- 55:'Splash Cymbal',
789
- 56:'Cowbell',
790
- 57:'Crash Cymbal 2',
791
- 58:'Vibraslap',
792
- 59:'Ride Cymbal 2',
793
- 60:'Hi Bongo',
794
- 61:'Low Bongo',
795
- 62:'Mute Hi Conga',
796
- 63:'Open Hi Conga',
797
- 64:'Low Conga',
798
- 65:'High Timbale',
799
- 66:'Low Timbale',
800
- 67:'High Agogo',
801
- 68:'Low Agogo',
802
- 69:'Cabasa',
803
- 70:'Maracas',
804
- 71:'Short Whistle',
805
- 72:'Long Whistle',
806
- 73:'Short Guiro',
807
- 74:'Long Guiro',
808
- 75:'Claves',
809
- 76:'Hi Wood Block',
810
- 77:'Low Wood Block',
811
- 78:'Mute Cuica',
812
- 79:'Open Cuica',
813
- 80:'Mute Triangle',
814
- 81:'Open Triangle',
815
- }
816
-
817
- Event2channelindex = { 'note':3, 'note_off':2, 'note_on':2,
818
- 'key_after_touch':2, 'control_change':2, 'patch_change':2,
819
- 'channel_after_touch':2, 'pitch_wheel_change':2
820
- }
821
-
822
- ################################################################
823
- # The code below this line is full of frightening things, all to
824
- # do with the actual encoding and decoding of binary MIDI data.
825
-
826
- def _twobytes2int(byte_a):
827
- r'''decode a 16 bit quantity from two bytes,'''
828
- return (byte_a[1] | (byte_a[0] << 8))
829
-
830
- def _int2twobytes(int_16bit):
831
- r'''encode a 16 bit quantity into two bytes,'''
832
- return bytes([(int_16bit>>8) & 0xFF, int_16bit & 0xFF])
833
-
834
- def _read_14_bit(byte_a):
835
- r'''decode a 14 bit quantity from two bytes,'''
836
- return (byte_a[0] | (byte_a[1] << 7))
837
-
838
- def _write_14_bit(int_14bit):
839
- r'''encode a 14 bit quantity into two bytes,'''
840
- return bytes([int_14bit & 0x7F, (int_14bit>>7) & 0x7F])
841
-
842
- def _ber_compressed_int(integer):
843
- r'''BER compressed integer (not an ASN.1 BER, see perlpacktut for
844
- details). Its bytes represent an unsigned integer in base 128,
845
- most significant digit first, with as few digits as possible.
846
- Bit eight (the high bit) is set on each byte except the last.
847
- '''
848
- ber = bytearray(b'')
849
- seven_bits = 0x7F & integer
850
- ber.insert(0, seven_bits) # XXX surely should convert to a char ?
851
- integer >>= 7
852
- while integer > 0:
853
- seven_bits = 0x7F & integer
854
- ber.insert(0, 0x80|seven_bits) # XXX surely should convert to a char ?
855
- integer >>= 7
856
- return ber
857
-
858
- def _unshift_ber_int(ba):
859
- r'''Given a bytearray, returns a tuple of (the ber-integer at the
860
- start, and the remainder of the bytearray).
861
- '''
862
- if not len(ba): # 6.7
863
- _warn('_unshift_ber_int: no integer found')
864
- return ((0, b""))
865
- byte = ba[0]
866
- ba = ba[1:]
867
- integer = 0
868
- while True:
869
- integer += (byte & 0x7F)
870
- if not (byte & 0x80):
871
- return ((integer, ba))
872
- if not len(ba):
873
- _warn('_unshift_ber_int: no end-of-integer found')
874
- return ((0, ba))
875
- byte = ba[0]
876
- ba = ba[1:]
877
- integer <<= 7
878
-
879
-
880
- def _clean_up_warnings(): # 5.4
881
- # Call this before returning from any publicly callable function
882
- # whenever there's a possibility that a warning might have been printed
883
- # by the function, or by any private functions it might have called.
884
- if _no_warning:
885
- return
886
- global _previous_times
887
- global _previous_warning
888
- if _previous_times > 1:
889
- # E:1176, 0: invalid syntax (<string>, line 1176) (syntax-error) ???
890
- # print(' previous message repeated '+str(_previous_times)+' times', file=sys.stderr)
891
- # 6.7
892
- sys.stderr.write(' previous message repeated {0} times\n'.format(_previous_times))
893
- elif _previous_times > 0:
894
- sys.stderr.write(' previous message repeated\n')
895
- _previous_times = 0
896
- _previous_warning = ''
897
-
898
-
899
- def _warn(s=''):
900
- if _no_warning:
901
- return
902
- global _previous_times
903
- global _previous_warning
904
- if s == _previous_warning: # 5.4
905
- _previous_times = _previous_times + 1
906
- else:
907
- _clean_up_warnings()
908
- sys.stderr.write(str(s) + "\n")
909
- _previous_warning = s
910
-
911
-
912
- def _some_text_event(which_kind=0x01, text=b'some_text', text_encoding='ISO-8859-1'):
913
- if str(type(text)).find("'str'") >= 0: # 6.4 test for back-compatibility
914
- data = bytes(text, encoding=text_encoding)
915
- else:
916
- data = bytes(text)
917
- return b'\xFF' + bytes((which_kind,)) + _ber_compressed_int(len(data)) + data
918
-
919
-
920
- def _consistentise_ticks(scores): # 3.6
921
- # used by mix_scores, merge_scores, concatenate_scores
922
- if len(scores) == 1:
923
- return copy.deepcopy(scores)
924
- are_consistent = True
925
- ticks = scores[0][0]
926
- iscore = 1
927
- while iscore < len(scores):
928
- if scores[iscore][0] != ticks:
929
- are_consistent = False
930
- break
931
- iscore += 1
932
- if are_consistent:
933
- return copy.deepcopy(scores)
934
- new_scores = []
935
- iscore = 0
936
- while iscore < len(scores):
937
- score = scores[iscore]
938
- new_scores.append(opus2score(to_millisecs(score2opus(score))))
939
- iscore += 1
940
- return new_scores
941
-
942
-
943
- ###########################################################################
944
- def _decode(trackdata=b'', exclude=None, include=None,
945
- event_callback=None, exclusive_event_callback=None, no_eot_magic=False):
946
- r'''Decodes MIDI track data into an opus-style list of events.
947
- The options:
948
- 'exclude' is a list of event types which will be ignored SHOULD BE A SET
949
- 'include' (and no exclude), makes exclude a list
950
- of all possible events, /minus/ what include specifies
951
- 'event_callback' is a coderef
952
- 'exclusive_event_callback' is a coderef
953
- '''
954
- trackdata = bytearray(trackdata)
955
- if exclude == None:
956
- exclude = []
957
- if include == None:
958
- include = []
959
- if include and not exclude:
960
- exclude = All_events
961
- include = set(include)
962
- exclude = set(exclude)
963
-
964
- # Pointer = 0; not used here; we eat through the bytearray instead.
965
- event_code = -1; # used for running status
966
- event_count = 0;
967
- events = []
968
-
969
- while (len(trackdata)):
970
- # loop while there's anything to analyze ...
971
- eot = False # When True, the event registrar aborts this loop
972
- event_count += 1
973
-
974
- E = []
975
- # E for events - we'll feed it to the event registrar at the end.
976
-
977
- # Slice off the delta time code, and analyze it
978
- [time, trackdata] = _unshift_ber_int(trackdata)
979
-
980
- # Now let's see what we can make of the command
981
- first_byte = trackdata[0] & 0xFF
982
- trackdata = trackdata[1:]
983
- if (first_byte < 0xF0): # It's a MIDI event
984
- if (first_byte & 0x80):
985
- event_code = first_byte
986
- else:
987
- # It wants running status; use last event_code value
988
- trackdata.insert(0, first_byte)
989
- if (event_code == -1):
990
- _warn("Running status not set; Aborting track.")
991
- return []
992
-
993
- command = event_code & 0xF0
994
- channel = event_code & 0x0F
995
-
996
- if (command == 0xF6): # 0-byte argument
997
- pass
998
- elif (command == 0xC0 or command == 0xD0): # 1-byte argument
999
- parameter = trackdata[0] # could be B
1000
- trackdata = trackdata[1:]
1001
- else: # 2-byte argument could be BB or 14-bit
1002
- parameter = (trackdata[0], trackdata[1])
1003
- trackdata = trackdata[2:]
1004
-
1005
- #################################################################
1006
- # MIDI events
1007
-
1008
- if (command == 0x80):
1009
- if 'note_off' in exclude:
1010
- continue
1011
- E = ['note_off', time, channel, parameter[0], parameter[1]]
1012
- elif (command == 0x90):
1013
- if 'note_on' in exclude:
1014
- continue
1015
- E = ['note_on', time, channel, parameter[0], parameter[1]]
1016
- elif (command == 0xA0):
1017
- if 'key_after_touch' in exclude:
1018
- continue
1019
- E = ['key_after_touch', time, channel, parameter[0], parameter[1]]
1020
- elif (command == 0xB0):
1021
- if 'control_change' in exclude:
1022
- continue
1023
- E = ['control_change', time, channel, parameter[0], parameter[1]]
1024
- elif (command == 0xC0):
1025
- if 'patch_change' in exclude:
1026
- continue
1027
- E = ['patch_change', time, channel, parameter]
1028
- elif (command == 0xD0):
1029
- if 'channel_after_touch' in exclude:
1030
- continue
1031
- E = ['channel_after_touch', time, channel, parameter]
1032
- elif (command == 0xE0):
1033
- if 'pitch_wheel_change' in exclude:
1034
- continue
1035
- E = ['pitch_wheel_change', time, channel,
1036
- _read_14_bit(parameter) - 0x2000]
1037
- else:
1038
- _warn("Shouldn't get here; command=" + hex(command))
1039
-
1040
- elif (first_byte == 0xFF): # It's a Meta-Event! ##################
1041
- # [command, length, remainder] =
1042
- # unpack("xCwa*", substr(trackdata, $Pointer, 6));
1043
- # Pointer += 6 - len(remainder);
1044
- # # Move past JUST the length-encoded.
1045
- command = trackdata[0] & 0xFF
1046
- trackdata = trackdata[1:]
1047
- [length, trackdata] = _unshift_ber_int(trackdata)
1048
- if (command == 0x00):
1049
- if (length == 2):
1050
- E = ['set_sequence_number', time, _twobytes2int(trackdata)]
1051
- else:
1052
- _warn('set_sequence_number: length must be 2, not ' + str(length))
1053
- E = ['set_sequence_number', time, 0]
1054
-
1055
- elif command >= 0x01 and command <= 0x0f: # Text events
1056
- # 6.2 take it in bytes; let the user get the right encoding.
1057
- # text_str = trackdata[0:length].decode('ascii','ignore')
1058
- # text_str = trackdata[0:length].decode('ISO-8859-1')
1059
- # 6.4 take it in bytes; let the user get the right encoding.
1060
- text_data = bytes(trackdata[0:length]) # 6.4
1061
- # Defined text events
1062
- if (command == 0x01):
1063
- E = ['text_event', time, text_data]
1064
- elif (command == 0x02):
1065
- E = ['copyright_text_event', time, text_data]
1066
- elif (command == 0x03):
1067
- E = ['track_name', time, text_data]
1068
- elif (command == 0x04):
1069
- E = ['instrument_name', time, text_data]
1070
- elif (command == 0x05):
1071
- E = ['lyric', time, text_data]
1072
- elif (command == 0x06):
1073
- E = ['marker', time, text_data]
1074
- elif (command == 0x07):
1075
- E = ['cue_point', time, text_data]
1076
- # Reserved but apparently unassigned text events
1077
- elif (command == 0x08):
1078
- E = ['text_event_08', time, text_data]
1079
- elif (command == 0x09):
1080
- E = ['text_event_09', time, text_data]
1081
- elif (command == 0x0a):
1082
- E = ['text_event_0a', time, text_data]
1083
- elif (command == 0x0b):
1084
- E = ['text_event_0b', time, text_data]
1085
- elif (command == 0x0c):
1086
- E = ['text_event_0c', time, text_data]
1087
- elif (command == 0x0d):
1088
- E = ['text_event_0d', time, text_data]
1089
- elif (command == 0x0e):
1090
- E = ['text_event_0e', time, text_data]
1091
- elif (command == 0x0f):
1092
- E = ['text_event_0f', time, text_data]
1093
-
1094
- # Now the sticky events -------------------------------------
1095
- elif (command == 0x2F):
1096
- E = ['end_track', time]
1097
- # The code for handling this, oddly, comes LATER,
1098
- # in the event registrar.
1099
- elif (command == 0x51): # DTime, Microseconds/Crochet
1100
- if length != 3:
1101
- _warn('set_tempo event, but length=' + str(length))
1102
- E = ['set_tempo', time,
1103
- struct.unpack(">I", b'\x00' + trackdata[0:3])[0]]
1104
- elif (command == 0x54):
1105
- if length != 5: # DTime, HR, MN, SE, FR, FF
1106
- _warn('smpte_offset event, but length=' + str(length))
1107
- E = ['smpte_offset', time] + list(struct.unpack(">BBBBB", trackdata[0:5]))
1108
- elif (command == 0x58):
1109
- if length != 4: # DTime, NN, DD, CC, BB
1110
- _warn('time_signature event, but length=' + str(length))
1111
- E = ['time_signature', time] + list(trackdata[0:4])
1112
- elif (command == 0x59):
1113
- if length != 2: # DTime, SF(signed), MI
1114
- _warn('key_signature event, but length=' + str(length))
1115
- E = ['key_signature', time] + list(struct.unpack(">bB", trackdata[0:2]))
1116
- elif (command == 0x7F): # 6.4
1117
- E = ['sequencer_specific', time, bytes(trackdata[0:length])]
1118
- else:
1119
- E = ['raw_meta_event', time, command,
1120
- bytes(trackdata[0:length])] # 6.0
1121
- # "[uninterpretable meta-event command of length length]"
1122
- # DTime, Command, Binary Data
1123
- # It's uninterpretable; record it as raw_data.
1124
-
1125
- # Pointer += length; # Now move Pointer
1126
- trackdata = trackdata[length:]
1127
-
1128
- ######################################################################
1129
- elif (first_byte == 0xF0 or first_byte == 0xF7):
1130
- # Note that sysexes in MIDI /files/ are different than sysexes
1131
- # in MIDI transmissions!! The vast majority of system exclusive
1132
- # messages will just use the F0 format. For instance, the
1133
- # transmitted message F0 43 12 00 07 F7 would be stored in a
1134
- # MIDI file as F0 05 43 12 00 07 F7. As mentioned above, it is
1135
- # required to include the F7 at the end so that the reader of the
1136
- # MIDI file knows that it has read the entire message. (But the F7
1137
- # is omitted if this is a non-final block in a multiblock sysex;
1138
- # but the F7 (if there) is counted in the message's declared
1139
- # length, so we don't have to think about it anyway.)
1140
- # command = trackdata.pop(0)
1141
- [length, trackdata] = _unshift_ber_int(trackdata)
1142
- if first_byte == 0xF0:
1143
- # 20091008 added ISO-8859-1 to get an 8-bit str
1144
- # 6.4 return bytes instead
1145
- E = ['sysex_f0', time, bytes(trackdata[0:length])]
1146
- else:
1147
- E = ['sysex_f7', time, bytes(trackdata[0:length])]
1148
- trackdata = trackdata[length:]
1149
-
1150
- ######################################################################
1151
- # Now, the MIDI file spec says:
1152
- # <track data> = <MTrk event>+
1153
- # <MTrk event> = <delta-time> <event>
1154
- # <event> = <MIDI event> | <sysex event> | <meta-event>
1155
- # I know that, on the wire, <MIDI event> can include note_on,
1156
- # note_off, and all the other 8x to Ex events, AND Fx events
1157
- # other than F0, F7, and FF -- namely, <song position msg>,
1158
- # <song select msg>, and <tune request>.
1159
- #
1160
- # Whether these can occur in MIDI files is not clear specified
1161
- # from the MIDI file spec. So, I'm going to assume that
1162
- # they CAN, in practice, occur. I don't know whether it's
1163
- # proper for you to actually emit these into a MIDI file.
1164
-
1165
- elif (first_byte == 0xF2): # DTime, Beats
1166
- # <song position msg> ::= F2 <data pair>
1167
- E = ['song_position', time, _read_14_bit(trackdata[:2])]
1168
- trackdata = trackdata[2:]
1169
-
1170
- elif (first_byte == 0xF3): # <song select msg> ::= F3 <data singlet>
1171
- # E = ['song_select', time, struct.unpack('>B',trackdata.pop(0))[0]]
1172
- E = ['song_select', time, trackdata[0]]
1173
- trackdata = trackdata[1:]
1174
- # DTime, Thing (what?! song number? whatever ...)
1175
-
1176
- elif (first_byte == 0xF6): # DTime
1177
- E = ['tune_request', time]
1178
- # What would a tune request be doing in a MIDI /file/?
1179
-
1180
- #########################################################
1181
- # ADD MORE META-EVENTS HERE. TODO:
1182
- # f1 -- MTC Quarter Frame Message. One data byte follows
1183
- # the Status; it's the time code value, from 0 to 127.
1184
- # f8 -- MIDI clock. no data.
1185
- # fa -- MIDI start. no data.
1186
- # fb -- MIDI continue. no data.
1187
- # fc -- MIDI stop. no data.
1188
- # fe -- Active sense. no data.
1189
- # f4 f5 f9 fd -- unallocated
1190
-
1191
- r'''
1192
- elif (first_byte > 0xF0) { # Some unknown kinda F-series event ####
1193
- # Here we only produce a one-byte piece of raw data.
1194
- # But the encoder for 'raw_data' accepts any length of it.
1195
- E = [ 'raw_data',
1196
- time, substr(trackdata,Pointer,1) ]
1197
- # DTime and the Data (in this case, the one Event-byte)
1198
- ++Pointer; # itself
1199
-
1200
- '''
1201
- elif first_byte > 0xF0: # Some unknown F-series event
1202
- # Here we only produce a one-byte piece of raw data.
1203
- # E = ['raw_data', time, bytest(trackdata[0])] # 6.4
1204
- E = ['raw_data', time, trackdata[0]] # 6.4 6.7
1205
- trackdata = trackdata[1:]
1206
- else: # Fallthru.
1207
- _warn("Aborting track. Command-byte first_byte=" + hex(first_byte))
1208
- break
1209
- # End of the big if-group
1210
-
1211
- ######################################################################
1212
- # THE EVENT REGISTRAR...
1213
- if E and (E[0] == 'end_track'):
1214
- # This is the code for exceptional handling of the EOT event.
1215
- eot = True
1216
- if not no_eot_magic:
1217
- if E[1] > 0: # a null text-event to carry the delta-time
1218
- E = ['text_event', E[1], '']
1219
- else:
1220
- E = [] # EOT with a delta-time of 0; ignore it.
1221
-
1222
- if E and not (E[0] in exclude):
1223
- # if ( $exclusive_event_callback ):
1224
- # &{ $exclusive_event_callback }( @E );
1225
- # else:
1226
- # &{ $event_callback }( @E ) if $event_callback;
1227
- events.append(E)
1228
- if eot:
1229
- break
1230
-
1231
- # End of the big "Event" while-block
1232
-
1233
- return events
1234
-
1235
-
1236
- ###########################################################################
1237
- def _encode(events_lol, unknown_callback=None, never_add_eot=False,
1238
- no_eot_magic=False, no_running_status=False, text_encoding='ISO-8859-1'):
1239
- # encode an event structure, presumably for writing to a file
1240
- # Calling format:
1241
- # $data_r = MIDI::Event::encode( \@event_lol, { options } );
1242
- # Takes a REFERENCE to an event structure (a LoL)
1243
- # Returns an (unblessed) REFERENCE to track data.
1244
-
1245
- # If you want to use this to encode a /single/ event,
1246
- # you still have to do it as a reference to an event structure (a LoL)
1247
- # that just happens to have just one event. I.e.,
1248
- # encode( [ $event ] ) or encode( [ [ 'note_on', 100, 5, 42, 64] ] )
1249
- # If you're doing this, consider the never_add_eot track option, as in
1250
- # print MIDI ${ encode( [ $event], { 'never_add_eot' => 1} ) };
1251
-
1252
- data = [] # what I'll store the chunks of byte-data in
1253
-
1254
- # This is so my end_track magic won't corrupt the original
1255
- events = copy.deepcopy(events_lol)
1256
-
1257
- if not never_add_eot:
1258
- # One way or another, tack on an 'end_track'
1259
- if events:
1260
- last = events[-1]
1261
- if not (last[0] == 'end_track'): # no end_track already
1262
- if (last[0] == 'text_event' and len(last[2]) == 0):
1263
- # 0-length text event at track-end.
1264
- if no_eot_magic:
1265
- # Exceptional case: don't mess with track-final
1266
- # 0-length text_events; just peg on an end_track
1267
- events.append(['end_track', 0])
1268
- else:
1269
- # NORMAL CASE: replace with an end_track, leaving DTime
1270
- last[0] = 'end_track'
1271
- else:
1272
- # last event was neither 0-length text_event nor end_track
1273
- events.append(['end_track', 0])
1274
- else: # an eventless track!
1275
- events = [['end_track', 0],]
1276
-
1277
- # maybe_running_status = not no_running_status # unused? 4.7
1278
- last_status = -1
1279
-
1280
- for event_r in (events):
1281
- E = copy.deepcopy(event_r)
1282
- # otherwise the shifting'd corrupt the original
1283
- if not E:
1284
- continue
1285
-
1286
- event = E.pop(0)
1287
- if not len(event):
1288
- continue
1289
-
1290
- dtime = int(E.pop(0))
1291
- # print('event='+str(event)+' dtime='+str(dtime))
1292
-
1293
- event_data = ''
1294
-
1295
- if ( # MIDI events -- eligible for running status
1296
- event == 'note_on'
1297
- or event == 'note_off'
1298
- or event == 'control_change'
1299
- or event == 'key_after_touch'
1300
- or event == 'patch_change'
1301
- or event == 'channel_after_touch'
1302
- or event == 'pitch_wheel_change' ):
1303
-
1304
- # This block is where we spend most of the time. Gotta be tight.
1305
- if (event == 'note_off'):
1306
- status = 0x80 | (int(E[0]) & 0x0F)
1307
- parameters = struct.pack('>BB', int(E[1])&0x7F, int(E[2])&0x7F)
1308
- elif (event == 'note_on'):
1309
- status = 0x90 | (int(E[0]) & 0x0F)
1310
- parameters = struct.pack('>BB', int(E[1])&0x7F, int(E[2])&0x7F)
1311
- elif (event == 'key_after_touch'):
1312
- status = 0xA0 | (int(E[0]) & 0x0F)
1313
- parameters = struct.pack('>BB', int(E[1])&0x7F, int(E[2])&0x7F)
1314
- elif (event == 'control_change'):
1315
- status = 0xB0 | (int(E[0]) & 0x0F)
1316
- parameters = struct.pack('>BB', int(E[1])&0xFF, int(E[2])&0xFF)
1317
- elif (event == 'patch_change'):
1318
- status = 0xC0 | (int(E[0]) & 0x0F)
1319
- parameters = struct.pack('>B', int(E[1]) & 0xFF)
1320
- elif (event == 'channel_after_touch'):
1321
- status = 0xD0 | (int(E[0]) & 0x0F)
1322
- parameters = struct.pack('>B', int(E[1]) & 0xFF)
1323
- elif (event == 'pitch_wheel_change'):
1324
- status = 0xE0 | (int(E[0]) & 0x0F)
1325
- parameters = _write_14_bit(int(E[1]) + 0x2000)
1326
- else:
1327
- _warn("BADASS FREAKOUT ERROR 31415!")
1328
-
1329
- # And now the encoding
1330
- # w = BER compressed integer (not ASN.1 BER, see perlpacktut for
1331
- # details). Its bytes represent an unsigned integer in base 128,
1332
- # most significant digit first, with as few digits as possible.
1333
- # Bit eight (the high bit) is set on each byte except the last.
1334
-
1335
- data.append(_ber_compressed_int(dtime))
1336
- if (status != last_status) or no_running_status:
1337
- data.append(struct.pack('>B', status))
1338
- data.append(parameters)
1339
-
1340
- last_status = status
1341
- continue
1342
- else:
1343
- # Not a MIDI event.
1344
- # All the code in this block could be more efficient,
1345
- # but this is not where the code needs to be tight.
1346
- # print "zaz $event\n";
1347
- last_status = -1
1348
-
1349
- if event == 'raw_meta_event':
1350
- event_data = _some_text_event(int(E[0]), E[1], text_encoding)
1351
- elif (event == 'set_sequence_number'): # 3.9
1352
- event_data = b'\xFF\x00\x02'+_int2twobytes(E[0])
1353
-
1354
- # Text meta-events...
1355
- # a case for a dict, I think (pjb) ...
1356
- elif (event == 'text_event'):
1357
- event_data = _some_text_event(0x01, E[0], text_encoding)
1358
- elif (event == 'copyright_text_event'):
1359
- event_data = _some_text_event(0x02, E[0], text_encoding)
1360
- elif (event == 'track_name'):
1361
- event_data = _some_text_event(0x03, E[0], text_encoding)
1362
- elif (event == 'instrument_name'):
1363
- event_data = _some_text_event(0x04, E[0], text_encoding)
1364
- elif (event == 'lyric'):
1365
- event_data = _some_text_event(0x05, E[0], text_encoding)
1366
- elif (event == 'marker'):
1367
- event_data = _some_text_event(0x06, E[0], text_encoding)
1368
- elif (event == 'cue_point'):
1369
- event_data = _some_text_event(0x07, E[0], text_encoding)
1370
- elif (event == 'text_event_08'):
1371
- event_data = _some_text_event(0x08, E[0], text_encoding)
1372
- elif (event == 'text_event_09'):
1373
- event_data = _some_text_event(0x09, E[0], text_encoding)
1374
- elif (event == 'text_event_0a'):
1375
- event_data = _some_text_event(0x0A, E[0], text_encoding)
1376
- elif (event == 'text_event_0b'):
1377
- event_data = _some_text_event(0x0B, E[0], text_encoding)
1378
- elif (event == 'text_event_0c'):
1379
- event_data = _some_text_event(0x0C, E[0], text_encoding)
1380
- elif (event == 'text_event_0d'):
1381
- event_data = _some_text_event(0x0D, E[0], text_encoding)
1382
- elif (event == 'text_event_0e'):
1383
- event_data = _some_text_event(0x0E, E[0], text_encoding)
1384
- elif (event == 'text_event_0f'):
1385
- event_data = _some_text_event(0x0F, E[0], text_encoding)
1386
- # End of text meta-events
1387
-
1388
- elif (event == 'end_track'):
1389
- event_data = b"\xFF\x2F\x00"
1390
-
1391
- elif (event == 'set_tempo'):
1392
- #event_data = struct.pack(">BBwa*", 0xFF, 0x51, 3,
1393
- # substr( struct.pack('>I', E[0]), 1, 3))
1394
- event_data = b'\xFF\x51\x03'+struct.pack('>I',E[0])[1:]
1395
- elif (event == 'smpte_offset'):
1396
- # event_data = struct.pack(">BBwBBBBB", 0xFF, 0x54, 5, E[0:5] )
1397
- event_data = struct.pack(">BBBbBBBB", 0xFF,0x54,0x05,E[0],E[1],E[2],E[3],E[4])
1398
- elif (event == 'time_signature'):
1399
- # event_data = struct.pack(">BBwBBBB", 0xFF, 0x58, 4, E[0:4] )
1400
- event_data = struct.pack(">BBBbBBB", 0xFF, 0x58, 0x04, E[0],E[1],E[2],E[3])
1401
- elif (event == 'key_signature'):
1402
- event_data = struct.pack(">BBBbB", 0xFF, 0x59, 0x02, E[0],E[1])
1403
- elif (event == 'sequencer_specific'):
1404
- # event_data = struct.pack(">BBwa*", 0xFF,0x7F, len(E[0]), E[0])
1405
- event_data = _some_text_event(0x7F, E[0], text_encoding)
1406
- # End of Meta-events
1407
-
1408
- # Other Things...
1409
- elif (event == 'sysex_f0'):
1410
- #event_data = struct.pack(">Bwa*", 0xF0, len(E[0]), E[0])
1411
- #B=bitstring w=BER-compressed-integer a=null-padded-ascii-str
1412
- event_data = bytearray(b'\xF0')+_ber_compressed_int(len(E[0]))+bytearray(E[0])
1413
- elif (event == 'sysex_f7'):
1414
- #event_data = struct.pack(">Bwa*", 0xF7, len(E[0]), E[0])
1415
- event_data = bytearray(b'\xF7')+_ber_compressed_int(len(E[0]))+bytearray(E[0])
1416
-
1417
- elif (event == 'song_position'):
1418
- event_data = b"\xF2" + _write_14_bit( E[0] )
1419
- elif (event == 'song_select'):
1420
- event_data = struct.pack('>BB', 0xF3, E[0] )
1421
- elif (event == 'tune_request'):
1422
- event_data = b"\xF6"
1423
- elif (event == 'raw_data'):
1424
- _warn("_encode: raw_data event not supported")
1425
- # event_data = E[0]
1426
- continue
1427
- # End of Other Stuff
1428
-
1429
- else:
1430
- # The Big Fallthru
1431
- if unknown_callback:
1432
- # push(@data, &{ $unknown_callback }( @$event_r ))
1433
- pass
1434
- else:
1435
- _warn("Unknown event: "+str(event))
1436
- # To surpress complaint here, just set
1437
- # 'unknown_callback' => sub { return () }
1438
- continue
1439
-
1440
- #print "Event $event encoded part 2\n"
1441
- if str(type(event_data)).find("'str'") >= 0:
1442
- event_data = bytearray(event_data.encode('Latin1', 'ignore'))
1443
- if len(event_data): # how could $event_data be empty
1444
- # data.append(struct.pack('>wa*', dtime, event_data))
1445
- # print(' event_data='+str(event_data))
1446
- data.append(_ber_compressed_int(dtime)+event_data)
1447
-
1448
- return b''.join(data)
1449
-
1450
- ###################################################################################
1451
- ###################################################################################
1452
- ###################################################################################
1453
- #
1454
- # Tegridy MIDI X Module (TMIDI X / tee-midi eks)
1455
- #
1456
- # Based upon and includes the amazing MIDI.py module v.6.7. by Peter Billam
1457
- # pjb.com.au
1458
- #
1459
- # Project Los Angeles
1460
- # Tegridy Code 2025
1461
- #
1462
- # https://github.com/Tegridy-Code/Project-Los-Angeles
1463
- #
1464
- ###################################################################################
1465
- ###################################################################################
1466
- ###################################################################################
1467
-
1468
  import os
1469
-
1470
- import datetime
1471
-
1472
- from datetime import datetime
1473
-
1474
- import secrets
1475
-
1476
- import random
1477
-
1478
- import pickle
1479
-
1480
- import csv
1481
-
1482
- import tqdm
1483
-
1484
- import multiprocessing
1485
-
1486
- from itertools import zip_longest
1487
- from itertools import groupby
1488
-
1489
- from collections import Counter
1490
- from collections import defaultdict
1491
- from collections import OrderedDict
1492
-
1493
- from operator import itemgetter
1494
-
1495
- from abc import ABC, abstractmethod
1496
-
1497
- from difflib import SequenceMatcher as SM
1498
-
1499
- import statistics
1500
  import math
1501
-
1502
- import matplotlib.pyplot as plt
1503
-
1504
  import psutil
1505
-
1506
- import json
1507
-
1508
- from pathlib import Path
1509
-
1510
  import shutil
1511
-
1512
  import hashlib
 
 
 
 
1513
 
1514
  from array import array
1515
-
1516
  from pathlib import Path
1517
  from fnmatch import fnmatch
1518
-
1519
- ###################################################################################
1520
- #
1521
- # Original TMIDI Tegridy helper functions
1522
- #
1523
- ###################################################################################
1524
-
1525
- def Tegridy_TXT_to_INT_Converter(input_TXT_string, line_by_line_INT_string=True, max_INT = 0):
1526
-
1527
- '''Tegridy TXT to Intergers Converter
1528
-
1529
- Input: Input TXT string in the TMIDI-TXT format
1530
-
1531
- Type of output TXT INT string: line-by-line or one long string
1532
-
1533
- Maximum absolute integer to process. Maximum is inclusive
1534
- Default = process all integers. This helps to remove outliers/unwanted ints
1535
-
1536
- Output: List of pure intergers
1537
- String of intergers in the specified format: line-by-line or one long string
1538
- Number of processed integers
1539
- Number of skipped integers
1540
-
1541
- Project Los Angeles
1542
- Tegridy Code 2021'''
1543
-
1544
- print('Tegridy TXT to Intergers Converter')
1545
-
1546
- output_INT_list = []
1547
-
1548
- npi = 0
1549
- nsi = 0
1550
-
1551
- TXT_List = list(input_TXT_string)
1552
- for char in TXT_List:
1553
- if max_INT != 0:
1554
- if abs(ord(char)) <= max_INT:
1555
- output_INT_list.append(ord(char))
1556
- npi += 1
1557
- else:
1558
- nsi += 1
1559
- else:
1560
- output_INT_list.append(ord(char))
1561
- npi += 1
1562
-
1563
- if line_by_line_INT_string:
1564
- output_INT_string = '\n'.join([str(elem) for elem in output_INT_list])
1565
- else:
1566
- output_INT_string = ' '.join([str(elem) for elem in output_INT_list])
1567
-
1568
- print('Converted TXT to INTs:', npi, ' / ', nsi)
1569
-
1570
- return output_INT_list, output_INT_string, npi, nsi
1571
-
1572
- ###################################################################################
1573
-
1574
- def Tegridy_INT_to_TXT_Converter(input_INT_list):
1575
-
1576
- '''Tegridy Intergers to TXT Converter
1577
-
1578
- Input: List of intergers in TMIDI-TXT-INT format
1579
- Output: Decoded TXT string in TMIDI-TXT format
1580
- Project Los Angeles
1581
- Tegridy Code 2020'''
1582
-
1583
- output_TXT_string = ''
1584
-
1585
- for i in input_INT_list:
1586
- output_TXT_string += chr(int(i))
1587
-
1588
- return output_TXT_string
1589
-
1590
- ###################################################################################
1591
-
1592
- def Tegridy_INT_String_to_TXT_Converter(input_INT_String, line_by_line_input=True):
1593
-
1594
- '''Tegridy Intergers String to TXT Converter
1595
-
1596
- Input: List of intergers in TMIDI-TXT-INT-String format
1597
- Output: Decoded TXT string in TMIDI-TXT format
1598
- Project Los Angeles
1599
- Tegridy Code 2020'''
1600
-
1601
- print('Tegridy Intergers String to TXT Converter')
1602
-
1603
- if line_by_line_input:
1604
- input_string = input_INT_String.split('\n')
1605
- else:
1606
- input_string = input_INT_String.split(' ')
1607
-
1608
- output_TXT_string = ''
1609
-
1610
- for i in input_string:
1611
- try:
1612
- output_TXT_string += chr(abs(int(i)))
1613
- except:
1614
- print('Bad note:', i)
1615
- continue
1616
-
1617
- print('Done!')
1618
-
1619
- return output_TXT_string
1620
-
1621
- ###################################################################################
1622
-
1623
- def Tegridy_SONG_to_MIDI_Converter(SONG,
1624
- output_signature = 'Tegridy TMIDI Module',
1625
- track_name = 'Composition Track',
1626
- number_of_ticks_per_quarter = 425,
1627
- list_of_MIDI_patches = [0, 24, 32, 40, 42, 46, 56, 71, 73, 0, 0, 0, 0, 0, 0, 0],
1628
- output_file_name = 'TMIDI-Composition',
1629
- text_encoding='ISO-8859-1',
1630
- verbose=True):
1631
-
1632
- '''Tegridy SONG to MIDI Converter
1633
-
1634
- Input: Input SONG in TMIDI SONG/MIDI.py Score format
1635
- Output MIDI Track 0 name / MIDI Signature
1636
- Output MIDI Track 1 name / Composition track name
1637
- Number of ticks per quarter for the output MIDI
1638
- List of 16 MIDI patch numbers for output MIDI. Def. is MuseNet compatible patches.
1639
- Output file name w/o .mid extension.
1640
- Optional text encoding if you are working with text_events/lyrics. This is especially useful for Karaoke. Please note that anything but ISO-8859-1 is a non-standard way of encoding text_events according to MIDI specs.
1641
-
1642
- Output: MIDI File
1643
- Detailed MIDI stats
1644
-
1645
- Project Los Angeles
1646
- Tegridy Code 2020'''
1647
-
1648
- if verbose:
1649
- print('Converting to MIDI. Please stand-by...')
1650
-
1651
- output_header = [number_of_ticks_per_quarter,
1652
- [['track_name', 0, bytes(output_signature, text_encoding)]]]
1653
-
1654
- patch_list = [['patch_change', 0, 0, list_of_MIDI_patches[0]],
1655
- ['patch_change', 0, 1, list_of_MIDI_patches[1]],
1656
- ['patch_change', 0, 2, list_of_MIDI_patches[2]],
1657
- ['patch_change', 0, 3, list_of_MIDI_patches[3]],
1658
- ['patch_change', 0, 4, list_of_MIDI_patches[4]],
1659
- ['patch_change', 0, 5, list_of_MIDI_patches[5]],
1660
- ['patch_change', 0, 6, list_of_MIDI_patches[6]],
1661
- ['patch_change', 0, 7, list_of_MIDI_patches[7]],
1662
- ['patch_change', 0, 8, list_of_MIDI_patches[8]],
1663
- ['patch_change', 0, 9, list_of_MIDI_patches[9]],
1664
- ['patch_change', 0, 10, list_of_MIDI_patches[10]],
1665
- ['patch_change', 0, 11, list_of_MIDI_patches[11]],
1666
- ['patch_change', 0, 12, list_of_MIDI_patches[12]],
1667
- ['patch_change', 0, 13, list_of_MIDI_patches[13]],
1668
- ['patch_change', 0, 14, list_of_MIDI_patches[14]],
1669
- ['patch_change', 0, 15, list_of_MIDI_patches[15]],
1670
- ['track_name', 0, bytes(track_name, text_encoding)]]
1671
-
1672
- output = output_header + [patch_list + SONG]
1673
-
1674
- midi_data = score2midi(output, text_encoding)
1675
- detailed_MIDI_stats = score2stats(output)
1676
-
1677
- with open(output_file_name + '.mid', 'wb') as midi_file:
1678
- midi_file.write(midi_data)
1679
- midi_file.close()
1680
-
1681
- if verbose:
1682
- print('Done! Enjoy! :)')
1683
-
1684
- return detailed_MIDI_stats
1685
-
1686
- ###################################################################################
1687
-
1688
- def Tegridy_ms_SONG_to_MIDI_Converter(ms_SONG,
1689
- output_signature = 'Tegridy TMIDI Module',
1690
- track_name = 'Composition Track',
1691
- list_of_MIDI_patches = [0, 24, 32, 40, 42, 46, 56, 71, 73, 0, 0, 0, 0, 0, 0, 0],
1692
- output_file_name = 'TMIDI-Composition',
1693
- text_encoding='ISO-8859-1',
1694
- timings_multiplier=1,
1695
- verbose=True
1696
- ):
1697
-
1698
- '''Tegridy milisecond SONG to MIDI Converter
1699
-
1700
- Input: Input ms SONG in TMIDI ms SONG/MIDI.py ms Score format
1701
- Output MIDI Track 0 name / MIDI Signature
1702
- Output MIDI Track 1 name / Composition track name
1703
- List of 16 MIDI patch numbers for output MIDI. Def. is MuseNet compatible patches.
1704
- Output file name w/o .mid extension.
1705
- Optional text encoding if you are working with text_events/lyrics. This is especially useful for Karaoke. Please note that anything but ISO-8859-1 is a non-standard way of encoding text_events according to MIDI specs.
1706
- Optional timings multiplier
1707
- Optional verbose output
1708
-
1709
- Output: MIDI File
1710
- Detailed MIDI stats
1711
-
1712
- Project Los Angeles
1713
- Tegridy Code 2024'''
1714
-
1715
- if verbose:
1716
- print('Converting to MIDI. Please stand-by...')
1717
-
1718
- output_header = [1000,
1719
- [['set_tempo', 0, 1000000],
1720
- ['time_signature', 0, 4, 2, 24, 8],
1721
- ['track_name', 0, bytes(output_signature, text_encoding)]]]
1722
-
1723
- patch_list = [['patch_change', 0, 0, list_of_MIDI_patches[0]],
1724
- ['patch_change', 0, 1, list_of_MIDI_patches[1]],
1725
- ['patch_change', 0, 2, list_of_MIDI_patches[2]],
1726
- ['patch_change', 0, 3, list_of_MIDI_patches[3]],
1727
- ['patch_change', 0, 4, list_of_MIDI_patches[4]],
1728
- ['patch_change', 0, 5, list_of_MIDI_patches[5]],
1729
- ['patch_change', 0, 6, list_of_MIDI_patches[6]],
1730
- ['patch_change', 0, 7, list_of_MIDI_patches[7]],
1731
- ['patch_change', 0, 8, list_of_MIDI_patches[8]],
1732
- ['patch_change', 0, 9, list_of_MIDI_patches[9]],
1733
- ['patch_change', 0, 10, list_of_MIDI_patches[10]],
1734
- ['patch_change', 0, 11, list_of_MIDI_patches[11]],
1735
- ['patch_change', 0, 12, list_of_MIDI_patches[12]],
1736
- ['patch_change', 0, 13, list_of_MIDI_patches[13]],
1737
- ['patch_change', 0, 14, list_of_MIDI_patches[14]],
1738
- ['patch_change', 0, 15, list_of_MIDI_patches[15]],
1739
- ['track_name', 0, bytes(track_name, text_encoding)]]
1740
-
1741
- SONG = copy.deepcopy(ms_SONG)
1742
-
1743
- if timings_multiplier != 1:
1744
- for S in SONG:
1745
- S[1] = S[1] * timings_multiplier
1746
- if S[0] == 'note':
1747
- S[2] = S[2] * timings_multiplier
1748
-
1749
- output = output_header + [patch_list + SONG]
1750
-
1751
- midi_data = score2midi(output, text_encoding)
1752
- detailed_MIDI_stats = score2stats(output)
1753
-
1754
- with open(output_file_name + '.mid', 'wb') as midi_file:
1755
- midi_file.write(midi_data)
1756
- midi_file.close()
1757
-
1758
- if verbose:
1759
- print('Done! Enjoy! :)')
1760
-
1761
- return detailed_MIDI_stats
1762
-
1763
- ###################################################################################
1764
-
1765
- def hsv_to_rgb(h, s, v):
1766
- if s == 0.0:
1767
- return v, v, v
1768
- i = int(h*6.0)
1769
- f = (h*6.0) - i
1770
- p = v*(1.0 - s)
1771
- q = v*(1.0 - s*f)
1772
- t = v*(1.0 - s*(1.0-f))
1773
- i = i%6
1774
- return [(v, t, p), (q, v, p), (p, v, t), (p, q, v), (t, p, v), (v, p, q)][i]
1775
-
1776
- def generate_colors(n):
1777
- return [hsv_to_rgb(i/n, 1, 1) for i in range(n)]
1778
-
1779
- def add_arrays(a, b):
1780
- return [sum(pair) for pair in zip(a, b)]
1781
-
1782
- #-------------------------------------------------------------------------------
1783
-
1784
- def plot_ms_SONG(ms_song,
1785
- preview_length_in_notes=0,
1786
- block_lines_times_list = None,
1787
- plot_title='ms Song',
1788
- max_num_colors=129,
1789
- drums_color_num=128,
1790
- plot_size=(11,4),
1791
- note_height = 0.75,
1792
- show_grid_lines=False,
1793
- return_plt = False,
1794
- timings_multiplier=1,
1795
- save_plt='',
1796
- save_only_plt_image=True,
1797
- save_transparent=False
1798
- ):
1799
-
1800
- '''Tegridy ms SONG plotter/vizualizer'''
1801
-
1802
- notes = [s for s in ms_song if s[0] == 'note']
1803
-
1804
- if (len(max(notes, key=len)) != 7) and (len(min(notes, key=len)) != 7):
1805
- print('The song notes do not have patches information')
1806
- print('Ploease add patches to the notes in the song')
1807
-
1808
- else:
1809
-
1810
- start_times = [(s[1] * timings_multiplier) / 1000 for s in notes]
1811
- durations = [(s[2] * timings_multiplier) / 1000 for s in notes]
1812
- pitches = [s[4] for s in notes]
1813
- patches = [s[6] for s in notes]
1814
-
1815
- colors = generate_colors(max_num_colors)
1816
- colors[drums_color_num] = (1, 1, 1)
1817
-
1818
- pbl = (notes[preview_length_in_notes][1] * timings_multiplier) / 1000
1819
-
1820
- fig, ax = plt.subplots(figsize=plot_size)
1821
- #fig, ax = plt.subplots()
1822
-
1823
- # Create a rectangle for each note with color based on patch number
1824
- for start, duration, pitch, patch in zip(start_times, durations, pitches, patches):
1825
- rect = plt.Rectangle((start, pitch), duration, note_height, facecolor=colors[patch])
1826
- ax.add_patch(rect)
1827
-
1828
- # Set the limits of the plot
1829
- ax.set_xlim([min(start_times), max(add_arrays(start_times, durations))])
1830
- ax.set_ylim([min(pitches)-1, max(pitches)+1])
1831
-
1832
- # Set the background color to black
1833
- ax.set_facecolor('black')
1834
- fig.patch.set_facecolor('white')
1835
-
1836
- if preview_length_in_notes > 0:
1837
- ax.axvline(x=pbl, c='white')
1838
-
1839
- if block_lines_times_list:
1840
- for bl in block_lines_times_list:
1841
- ax.axvline(x=bl, c='white')
1842
-
1843
- if show_grid_lines:
1844
- ax.grid(color='white')
1845
-
1846
- plt.xlabel('Time (s)', c='black')
1847
- plt.ylabel('MIDI Pitch', c='black')
1848
-
1849
- plt.title(plot_title)
1850
-
1851
- if save_plt != '':
1852
- if save_only_plt_image:
1853
- plt.axis('off')
1854
- plt.title('')
1855
- plt.savefig(save_plt, transparent=save_transparent, bbox_inches='tight', pad_inches=0, facecolor='black')
1856
- plt.close()
1857
-
1858
- else:
1859
- plt.savefig(save_plt)
1860
- plt.close()
1861
-
1862
- if return_plt:
1863
- plt.close(fig)
1864
- return fig
1865
-
1866
- plt.show()
1867
- plt.close()
1868
-
1869
- ###################################################################################
1870
-
1871
- def Tegridy_SONG_to_Full_MIDI_Converter(SONG,
1872
- output_signature = 'Tegridy TMIDI Module',
1873
- track_name = 'Composition Track',
1874
- number_of_ticks_per_quarter = 1000,
1875
- output_file_name = 'TMIDI-Composition',
1876
- text_encoding='ISO-8859-1',
1877
- verbose=True):
1878
-
1879
- '''Tegridy SONG to Full MIDI Converter
1880
-
1881
- Input: Input SONG in Full TMIDI SONG/MIDI.py Score format
1882
- Output MIDI Track 0 name / MIDI Signature
1883
- Output MIDI Track 1 name / Composition track name
1884
- Number of ticks per quarter for the output MIDI
1885
- Output file name w/o .mid extension.
1886
- Optional text encoding if you are working with text_events/lyrics. This is especially useful for Karaoke. Please note that anything but ISO-8859-1 is a non-standard way of encoding text_events according to MIDI specs.
1887
-
1888
- Output: MIDI File
1889
- Detailed MIDI stats
1890
-
1891
- Project Los Angeles
1892
- Tegridy Code 2023'''
1893
-
1894
- if verbose:
1895
- print('Converting to MIDI. Please stand-by...')
1896
-
1897
- output_header = [number_of_ticks_per_quarter,
1898
- [['set_tempo', 0, 1000000],
1899
- ['track_name', 0, bytes(output_signature, text_encoding)]]]
1900
-
1901
- song_track = [['track_name', 0, bytes(track_name, text_encoding)]]
1902
-
1903
- output = output_header + [song_track + SONG]
1904
-
1905
- midi_data = score2midi(output, text_encoding)
1906
- detailed_MIDI_stats = score2stats(output)
1907
-
1908
- with open(output_file_name + '.mid', 'wb') as midi_file:
1909
- midi_file.write(midi_data)
1910
- midi_file.close()
1911
-
1912
- if verbose:
1913
- print('Done! Enjoy! :)')
1914
-
1915
- return detailed_MIDI_stats
1916
-
1917
- ###################################################################################
1918
-
1919
- def Tegridy_File_Time_Stamp(input_file_name='File_Created_on_', ext = ''):
1920
-
1921
- '''Tegridy File Time Stamp
1922
-
1923
- Input: Full path and file name without extention
1924
- File extension
1925
-
1926
- Output: File name string with time-stamp and extension (time-stamped file name)
1927
-
1928
- Project Los Angeles
1929
- Tegridy Code 2021'''
1930
-
1931
- print('Time-stamping output file...')
1932
-
1933
- now = ''
1934
- now_n = str(datetime.now())
1935
- now_n = now_n.replace(' ', '_')
1936
- now_n = now_n.replace(':', '_')
1937
- now = now_n.replace('.', '_')
1938
-
1939
- fname = input_file_name + str(now) + ext
1940
-
1941
- return(fname)
1942
-
1943
- ###################################################################################
1944
-
1945
- def Tegridy_Any_Pickle_File_Writer(Data, input_file_name='TMIDI_Pickle_File'):
1946
-
1947
- '''Tegridy Pickle File Writer
1948
-
1949
- Input: Data to write (I.e. a list)
1950
- Full path and file name without extention
1951
-
1952
- Output: Named Pickle file
1953
-
1954
- Project Los Angeles
1955
- Tegridy Code 2021'''
1956
-
1957
- print('Tegridy Pickle File Writer')
1958
-
1959
- full_path_to_output_dataset_to = input_file_name + '.pickle'
1960
-
1961
- if os.path.exists(full_path_to_output_dataset_to):
1962
- os.remove(full_path_to_output_dataset_to)
1963
- print('Removing old Dataset...')
1964
- else:
1965
- print("Creating new Dataset file...")
1966
-
1967
- with open(full_path_to_output_dataset_to, 'wb') as filehandle:
1968
- # store the data as binary data stream
1969
- pickle.dump(Data, filehandle, protocol=pickle.HIGHEST_PROTOCOL)
1970
-
1971
- print('Dataset was saved as:', full_path_to_output_dataset_to)
1972
- print('Task complete. Enjoy! :)')
1973
-
1974
- ###################################################################################
1975
-
1976
- def Tegridy_Any_Pickle_File_Reader(input_file_name='TMIDI_Pickle_File', ext='.pickle', verbose=True):
1977
-
1978
- '''Tegridy Pickle File Loader
1979
-
1980
- Input: Full path and file name with or without extention
1981
- File extension if different from default .pickle
1982
-
1983
- Output: Standard Python 3 unpickled data object
1984
-
1985
- Project Los Angeles
1986
- Tegridy Code 2021'''
1987
-
1988
- if verbose:
1989
- print('Tegridy Pickle File Loader')
1990
- print('Loading the pickle file. Please wait...')
1991
-
1992
- if os.path.basename(input_file_name).endswith(ext):
1993
- fname = input_file_name
1994
-
1995
- else:
1996
- fname = input_file_name + ext
1997
-
1998
- with open(fname, 'rb') as pickle_file:
1999
- content = pickle.load(pickle_file)
2000
-
2001
- if verbose:
2002
- print('Done!')
2003
-
2004
- return content
2005
 
2006
  ###################################################################################
2007
 
@@ -2091,7 +113,7 @@ def Optimus_MIDI_TXT_Processor(MIDI_file,
2091
  if debug: print('Processing File:', MIDI_file)
2092
 
2093
  try:
2094
- opus = midi2opus(midi_file.read())
2095
 
2096
  except:
2097
  print('Problematic MIDI. Skipping...')
@@ -2101,19 +123,19 @@ def Optimus_MIDI_TXT_Processor(MIDI_file,
2101
 
2102
  midi_file.close()
2103
 
2104
- score1 = to_millisecs(opus)
2105
- score2 = opus2score(score1)
2106
 
2107
- # score2 = opus2score(opus) # TODO Improve score timings when it will be possible.
2108
 
2109
  if MIDI_channel == 16: # Process all MIDI channels
2110
  score = score2
2111
 
2112
  if MIDI_channel >= 0 and MIDI_channel <= 15: # Process only a selected single MIDI channel
2113
- score = grep(score2, [MIDI_channel])
2114
 
2115
  if MIDI_channel == -1: # Process all channels except drums (except channel 9)
2116
- score = grep(score2, [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15])
2117
 
2118
  #print('Reading all MIDI events from the MIDI file...')
2119
  while itrack < len(score):
@@ -3642,13 +1664,6 @@ def int_to_pitches_chord(integer, chord_base_pitch=60):
3642
 
3643
  ###################################################################################
3644
 
3645
- def bad_chord(chord):
3646
- bad = any(b - a == 1 for a, b in zip(chord, chord[1:]))
3647
- if (0 in chord) and (11 in chord):
3648
- bad = True
3649
-
3650
- return bad
3651
-
3652
  def validate_pitches_chord(pitches_chord, return_sorted = True):
3653
 
3654
  pitches_chord = sorted(list(set([x for x in pitches_chord if 0 < x < 128])))
@@ -3952,7 +1967,6 @@ def fix_monophonic_score_durations(monophonic_score,
3952
 
3953
  ###################################################################################
3954
 
3955
- from itertools import product
3956
 
3957
  ALL_CHORDS = [[0], [7], [5], [9], [2], [4], [11], [10], [8], [6], [3], [1], [0, 9], [2, 5],
3958
  [4, 7], [7, 10], [2, 11], [0, 3], [6, 9], [1, 4], [8, 11], [5, 8], [1, 10],
@@ -4463,11 +2477,6 @@ def advanced_score_processor(raw_score,
4463
 
4464
  ###################################################################################
4465
 
4466
- import random
4467
- import copy
4468
-
4469
- ###################################################################################
4470
-
4471
  def replace_bad_tones_chord(bad_tones_chord):
4472
  bad_chord_p = [0] * 12
4473
  for b in bad_tones_chord:
@@ -5055,7 +3064,7 @@ def patch_list_from_enhanced_score_notes(enhanced_score_notes,
5055
  print('Composition patches')
5056
  print('=' * 70)
5057
  for c, p in enumerate(patches):
5058
- print('Cha', str(c).zfill(2), '---', str(p).zfill(3), Number2patch[p])
5059
  print('=' * 70)
5060
 
5061
  return patches
@@ -5175,14 +3184,14 @@ def patch_enhanced_score_notes(escore_notes,
5175
  print('Main composition patches')
5176
  print('=' * 70)
5177
  for c, p in enumerate(patches):
5178
- print('Cha', str(c).zfill(2), '---', str(p).zfill(3), Number2patch[p])
5179
  print('=' * 70)
5180
 
5181
  if overflow_patches:
5182
  print('Extra composition patches')
5183
  print('=' * 70)
5184
  for c, p in enumerate(overflow_patches):
5185
- print(str(p).zfill(3), Number2patch[p])
5186
  print('=' * 70)
5187
 
5188
  #===========================================================================
@@ -5321,10 +3330,6 @@ def check_and_fix_chords_in_chordified_score(chordified_score,
5321
 
5322
  ###################################################################################
5323
 
5324
- from itertools import combinations, groupby
5325
-
5326
- ###################################################################################
5327
-
5328
  def advanced_check_and_fix_chords_in_chordified_score(chordified_score,
5329
  channels_index=3,
5330
  pitches_index=4,
@@ -5963,10 +3968,6 @@ def enhanced_chord_to_tones_chord(enhanced_chord):
5963
 
5964
  ###################################################################################
5965
 
5966
- import hashlib
5967
-
5968
- ###################################################################################
5969
-
5970
  def md5_hash(file_path_or_data=None, original_md5_hash=None):
5971
 
5972
  if type(file_path_or_data) == str:
@@ -9573,11 +7574,6 @@ MIDI_TEXT_EVENTS = ['text_event',
9573
 
9574
  ###################################################################################
9575
 
9576
- import hashlib
9577
- import re
9578
-
9579
- ###################################################################################
9580
-
9581
  def get_md5_hash(data):
9582
  return hashlib.md5(data).hexdigest()
9583
 
@@ -9764,20 +7760,20 @@ def escore_notes_to_text_description(escore_notes,
9764
 
9765
  patches = ordered_set(all_patches)[:16]
9766
 
9767
- instruments = [alpha_str(Number2patch[p]) for p in patches if p < 128]
9768
 
9769
  if instruments:
9770
 
9771
  nd_patches_counts = Counter([p for p in all_patches if p < 128]).most_common()
9772
 
9773
- dominant_instrument = alpha_str(Number2patch[nd_patches_counts[0][0]])
9774
 
9775
  if 128 in patches:
9776
  drums_present = True
9777
 
9778
  drums_pitches = [e[4] for e in escore_notes if e[3] == 9]
9779
 
9780
- most_common_drums = [alpha_str(Notenum2percussion[p[0]]) for p in Counter(drums_pitches).most_common(3) if p[0] in Notenum2percussion]
9781
 
9782
  else:
9783
  drums_present = False
@@ -9859,10 +7855,10 @@ def escore_notes_to_text_description(escore_notes,
9859
  escore_avgs = escore_notes_pitches_range(escore_notes, range_patch = mel[0])
9860
 
9861
  if mel[0] in LEAD_INSTRUMENTS and escore_avgs[3] > 60:
9862
- lead_melodies.append([Number2patch[mel[0]], mel[1]])
9863
 
9864
  elif mel[0] in BASE_INSTRUMENTS and escore_avgs[3] <= 60:
9865
- base_melodies.append([Number2patch[mel[0]], mel[1]])
9866
 
9867
  if lead_melodies:
9868
  lead_melodies.sort(key=lambda x: x[1], reverse=True)
@@ -10425,8 +8421,6 @@ def escore_notes_monoponic_melodies(escore_notes,
10425
 
10426
  ###################################################################################
10427
 
10428
- from itertools import groupby
10429
- from operator import itemgetter
10430
 
10431
  def group_by_threshold(data, threshold, groupby_idx):
10432
 
@@ -12992,9 +10986,15 @@ def convert_escore_notes_pitches_chords_signature(signature, convert_to_full_cho
12992
 
12993
  ###################################################################################
12994
 
12995
- def convert_bytes_in_nested_list(lst, encoding='utf-8', errors='ignore'):
 
 
 
 
12996
 
12997
  new_list = []
 
 
12998
 
12999
  for item in lst:
13000
  if isinstance(item, list):
@@ -13002,11 +11002,16 @@ def convert_bytes_in_nested_list(lst, encoding='utf-8', errors='ignore'):
13002
 
13003
  elif isinstance(item, bytes):
13004
  new_list.append(item.decode(encoding, errors=errors))
 
13005
 
13006
  else:
13007
  new_list.append(item)
13008
 
13009
- return new_list
 
 
 
 
13010
 
13011
  ###################################################################################
13012
 
@@ -13355,35 +11360,6 @@ def chunks_shuffle(lst,
13355
  return flattened
13356
 
13357
  ###################################################################################
13358
-
13359
- def convert_bytes_in_nested_list(lst,
13360
- encoding='utf-8',
13361
- errors='ignore',
13362
- return_changed_events_count=False
13363
- ):
13364
-
13365
- new_list = []
13366
-
13367
- ce_count = 0
13368
-
13369
- for item in lst:
13370
- if isinstance(item, list):
13371
- new_list.append(convert_bytes_in_nested_list(item))
13372
-
13373
- elif isinstance(item, bytes):
13374
- new_list.append(item.decode(encoding, errors=errors))
13375
- ce_count += 1
13376
-
13377
- else:
13378
- new_list.append(item)
13379
-
13380
- if return_changed_events_count:
13381
- return new_list, ce_count
13382
-
13383
- else:
13384
- return new_list
13385
-
13386
- ###################################################################################
13387
 
13388
  def find_deepest_midi_dirs(roots,
13389
  marker_file="midi_score.mid",
 
1
  #! /usr/bin/python3
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import os
4
+ import re
5
+ import json
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  import math
7
+ import tqdm
8
+ import copy
 
9
  import psutil
 
 
 
 
 
10
  import shutil
11
+ import random
12
  import hashlib
13
+ import secrets
14
+ import statistics
15
+ import multiprocessing
16
+ from src import MIDI
17
 
18
  from array import array
 
19
  from pathlib import Path
20
  from fnmatch import fnmatch
21
+ from collections import Counter
22
+ from collections import defaultdict
23
+ from collections import OrderedDict
24
+ from difflib import SequenceMatcher as SM
25
+ from operator import itemgetter
26
+ from itertools import product, combinations, groupby
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  ###################################################################################
29
 
 
113
  if debug: print('Processing File:', MIDI_file)
114
 
115
  try:
116
+ opus = MIDI.midi2opus(midi_file.read())
117
 
118
  except:
119
  print('Problematic MIDI. Skipping...')
 
123
 
124
  midi_file.close()
125
 
126
+ score1 = MIDI.to_millisecs(opus)
127
+ score2 = MIDI.opus2score(score1)
128
 
129
+ # score2 = MIDI.opus2score(opus) # TODO Improve score timings when it will be possible.
130
 
131
  if MIDI_channel == 16: # Process all MIDI channels
132
  score = score2
133
 
134
  if MIDI_channel >= 0 and MIDI_channel <= 15: # Process only a selected single MIDI channel
135
+ score = MIDI.grep(score2, [MIDI_channel])
136
 
137
  if MIDI_channel == -1: # Process all channels except drums (except channel 9)
138
+ score = MIDI.grep(score2, [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15])
139
 
140
  #print('Reading all MIDI events from the MIDI file...')
141
  while itrack < len(score):
 
1664
 
1665
  ###################################################################################
1666
 
 
 
 
 
 
 
 
1667
  def validate_pitches_chord(pitches_chord, return_sorted = True):
1668
 
1669
  pitches_chord = sorted(list(set([x for x in pitches_chord if 0 < x < 128])))
 
1967
 
1968
  ###################################################################################
1969
 
 
1970
 
1971
  ALL_CHORDS = [[0], [7], [5], [9], [2], [4], [11], [10], [8], [6], [3], [1], [0, 9], [2, 5],
1972
  [4, 7], [7, 10], [2, 11], [0, 3], [6, 9], [1, 4], [8, 11], [5, 8], [1, 10],
 
2477
 
2478
  ###################################################################################
2479
 
 
 
 
 
 
2480
  def replace_bad_tones_chord(bad_tones_chord):
2481
  bad_chord_p = [0] * 12
2482
  for b in bad_tones_chord:
 
3064
  print('Composition patches')
3065
  print('=' * 70)
3066
  for c, p in enumerate(patches):
3067
+ print('Cha', str(c).zfill(2), '---', str(p).zfill(3), MIDI.Number2patch[p])
3068
  print('=' * 70)
3069
 
3070
  return patches
 
3184
  print('Main composition patches')
3185
  print('=' * 70)
3186
  for c, p in enumerate(patches):
3187
+ print('Cha', str(c).zfill(2), '---', str(p).zfill(3), MIDI.Number2patch[p])
3188
  print('=' * 70)
3189
 
3190
  if overflow_patches:
3191
  print('Extra composition patches')
3192
  print('=' * 70)
3193
  for c, p in enumerate(overflow_patches):
3194
+ print(str(p).zfill(3), MIDI.Number2patch[p])
3195
  print('=' * 70)
3196
 
3197
  #===========================================================================
 
3330
 
3331
  ###################################################################################
3332
 
 
 
 
 
3333
  def advanced_check_and_fix_chords_in_chordified_score(chordified_score,
3334
  channels_index=3,
3335
  pitches_index=4,
 
3968
 
3969
  ###################################################################################
3970
 
 
 
 
 
3971
  def md5_hash(file_path_or_data=None, original_md5_hash=None):
3972
 
3973
  if type(file_path_or_data) == str:
 
7574
 
7575
  ###################################################################################
7576
 
 
 
 
 
 
7577
  def get_md5_hash(data):
7578
  return hashlib.md5(data).hexdigest()
7579
 
 
7760
 
7761
  patches = ordered_set(all_patches)[:16]
7762
 
7763
+ instruments = [alpha_str(MIDI.Number2patch[p]) for p in patches if p < 128]
7764
 
7765
  if instruments:
7766
 
7767
  nd_patches_counts = Counter([p for p in all_patches if p < 128]).most_common()
7768
 
7769
+ dominant_instrument = alpha_str(MIDI.Number2patch[nd_patches_counts[0][0]])
7770
 
7771
  if 128 in patches:
7772
  drums_present = True
7773
 
7774
  drums_pitches = [e[4] for e in escore_notes if e[3] == 9]
7775
 
7776
+ most_common_drums = [alpha_str(MIDI.Notenum2percussion[p[0]]) for p in Counter(drums_pitches).most_common(3) if p[0] in MIDI.Notenum2percussion]
7777
 
7778
  else:
7779
  drums_present = False
 
7855
  escore_avgs = escore_notes_pitches_range(escore_notes, range_patch = mel[0])
7856
 
7857
  if mel[0] in LEAD_INSTRUMENTS and escore_avgs[3] > 60:
7858
+ lead_melodies.append([MIDI.Number2patch[mel[0]], mel[1]])
7859
 
7860
  elif mel[0] in BASE_INSTRUMENTS and escore_avgs[3] <= 60:
7861
+ base_melodies.append([MIDI.Number2patch[mel[0]], mel[1]])
7862
 
7863
  if lead_melodies:
7864
  lead_melodies.sort(key=lambda x: x[1], reverse=True)
 
8421
 
8422
  ###################################################################################
8423
 
 
 
8424
 
8425
  def group_by_threshold(data, threshold, groupby_idx):
8426
 
 
10986
 
10987
  ###################################################################################
10988
 
10989
+ def convert_bytes_in_nested_list(lst,
10990
+ encoding='utf-8',
10991
+ errors='ignore',
10992
+ return_changed_events_count=False
10993
+ ):
10994
 
10995
  new_list = []
10996
+
10997
+ ce_count = 0
10998
 
10999
  for item in lst:
11000
  if isinstance(item, list):
 
11002
 
11003
  elif isinstance(item, bytes):
11004
  new_list.append(item.decode(encoding, errors=errors))
11005
+ ce_count += 1
11006
 
11007
  else:
11008
  new_list.append(item)
11009
 
11010
+ if return_changed_events_count:
11011
+ return new_list, ce_count
11012
+
11013
+ else:
11014
+ return new_list
11015
 
11016
  ###################################################################################
11017
 
 
11360
  return flattened
11361
 
11362
  ###################################################################################
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11363
 
11364
  def find_deepest_midi_dirs(roots,
11365
  marker_file="midi_score.mid",
TPLOTS.py → src/TPLOTS.py RENAMED
@@ -1,1522 +1,1522 @@
1
- #! /usr/bin/python3
2
-
3
- r'''############################################################################
4
- ################################################################################
5
- #
6
- #
7
- # Tegridy Plots Python Module (TPLOTS)
8
- # Version 1.0
9
- #
10
- # Project Los Angeles
11
- #
12
- # Tegridy Code 2025
13
- #
14
- # https://github.com/asigalov61/tegridy-tools
15
- #
16
- #
17
- ################################################################################
18
- #
19
- # Copyright 2024 Project Los Angeles / Tegridy Code
20
- #
21
- # Licensed under the Apache License, Version 2.0 (the "License");
22
- # you may not use this file except in compliance with the License.
23
- # You may obtain a copy of the License at
24
- #
25
- # http://www.apache.org/licenses/LICENSE-2.0
26
- #
27
- # Unless required by applicable law or agreed to in writing, software
28
- # distributed under the License is distributed on an "AS IS" BASIS,
29
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
30
- # See the License for the specific language governing permissions and
31
- # limitations under the License.
32
- #
33
- ################################################################################
34
- ################################################################################
35
- #
36
- # Critical dependencies
37
- #
38
- # !pip install numpy==1.24.4
39
- # !pip install scipy
40
- # !pip install matplotlib
41
- # !pip install networkx
42
- # !pip3 install scikit-learn
43
- #
44
- ################################################################################
45
- #
46
- # Future critical dependencies
47
- #
48
- # !pip install umap-learn
49
- # !pip install alphashape
50
- #
51
- ################################################################################
52
- '''
53
-
54
- ################################################################################
55
- # Modules imports
56
- ################################################################################
57
-
58
- import os
59
- from collections import Counter
60
- from itertools import groupby
61
-
62
- import numpy as np
63
-
64
- import networkx as nx
65
-
66
- from sklearn.manifold import TSNE
67
- from sklearn import metrics
68
- from sklearn.preprocessing import MinMaxScaler
69
- from sklearn.decomposition import PCA
70
-
71
- from scipy.ndimage import zoom
72
- from scipy.spatial import distance_matrix
73
- from scipy.sparse.csgraph import minimum_spanning_tree
74
- from scipy.stats import zscore
75
-
76
- import matplotlib.pyplot as plt
77
- from PIL import Image
78
-
79
- ################################################################################
80
- # Constants
81
- ################################################################################
82
-
83
- ALL_CHORDS_FULL = [[0], [0, 3], [0, 3, 5], [0, 3, 5, 8], [0, 3, 5, 9], [0, 3, 5, 10], [0, 3, 6],
84
- [0, 3, 6, 9], [0, 3, 6, 10], [0, 3, 7], [0, 3, 7, 10], [0, 3, 8], [0, 3, 9],
85
- [0, 3, 10], [0, 4], [0, 4, 6], [0, 4, 6, 9], [0, 4, 6, 10], [0, 4, 7],
86
- [0, 4, 7, 10], [0, 4, 8], [0, 4, 9], [0, 4, 10], [0, 5], [0, 5, 8], [0, 5, 9],
87
- [0, 5, 10], [0, 6], [0, 6, 9], [0, 6, 10], [0, 7], [0, 7, 10], [0, 8], [0, 9],
88
- [0, 10], [1], [1, 4], [1, 4, 6], [1, 4, 6, 9], [1, 4, 6, 10], [1, 4, 6, 11],
89
- [1, 4, 7], [1, 4, 7, 10], [1, 4, 7, 11], [1, 4, 8], [1, 4, 8, 11], [1, 4, 9],
90
- [1, 4, 10], [1, 4, 11], [1, 5], [1, 5, 8], [1, 5, 8, 11], [1, 5, 9],
91
- [1, 5, 10], [1, 5, 11], [1, 6], [1, 6, 9], [1, 6, 10], [1, 6, 11], [1, 7],
92
- [1, 7, 10], [1, 7, 11], [1, 8], [1, 8, 11], [1, 9], [1, 10], [1, 11], [2],
93
- [2, 5], [2, 5, 8], [2, 5, 8, 11], [2, 5, 9], [2, 5, 10], [2, 5, 11], [2, 6],
94
- [2, 6, 9], [2, 6, 10], [2, 6, 11], [2, 7], [2, 7, 10], [2, 7, 11], [2, 8],
95
- [2, 8, 11], [2, 9], [2, 10], [2, 11], [3], [3, 5], [3, 5, 8], [3, 5, 8, 11],
96
- [3, 5, 9], [3, 5, 10], [3, 5, 11], [3, 6], [3, 6, 9], [3, 6, 10], [3, 6, 11],
97
- [3, 7], [3, 7, 10], [3, 7, 11], [3, 8], [3, 8, 11], [3, 9], [3, 10], [3, 11],
98
- [4], [4, 6], [4, 6, 9], [4, 6, 10], [4, 6, 11], [4, 7], [4, 7, 10], [4, 7, 11],
99
- [4, 8], [4, 8, 11], [4, 9], [4, 10], [4, 11], [5], [5, 8], [5, 8, 11], [5, 9],
100
- [5, 10], [5, 11], [6], [6, 9], [6, 10], [6, 11], [7], [7, 10], [7, 11], [8],
101
- [8, 11], [9], [10], [11]]
102
-
103
- ################################################################################
104
-
105
- CHORDS_TYPES = ['WHITE', 'BLACK', 'UNKNOWN', 'MIXED WHITE', 'MIXED BLACK', 'MIXED GRAY']
106
-
107
- ################################################################################
108
-
109
- WHITE_NOTES = [0, 2, 4, 5, 7, 9, 11]
110
-
111
- ################################################################################
112
-
113
- BLACK_NOTES = [1, 3, 6, 8, 10]
114
-
115
- ################################################################################
116
- # Helper functions
117
- ################################################################################
118
-
119
- def tones_chord_type(tones_chord,
120
- return_chord_type_index=True,
121
- ):
122
-
123
- """
124
- Returns tones chord type
125
- """
126
-
127
- WN = WHITE_NOTES
128
- BN = BLACK_NOTES
129
- MX = WHITE_NOTES + BLACK_NOTES
130
-
131
-
132
- CHORDS = ALL_CHORDS_FULL
133
-
134
- tones_chord = sorted(tones_chord)
135
-
136
- ctype = 'UNKNOWN'
137
-
138
- if tones_chord in CHORDS:
139
-
140
- if sorted(set(tones_chord) & set(WN)) == tones_chord:
141
- ctype = 'WHITE'
142
-
143
- elif sorted(set(tones_chord) & set(BN)) == tones_chord:
144
- ctype = 'BLACK'
145
-
146
- if len(tones_chord) > 1 and sorted(set(tones_chord) & set(MX)) == tones_chord:
147
-
148
- if len(sorted(set(tones_chord) & set(WN))) == len(sorted(set(tones_chord) & set(BN))):
149
- ctype = 'MIXED GRAY'
150
-
151
- elif len(sorted(set(tones_chord) & set(WN))) > len(sorted(set(tones_chord) & set(BN))):
152
- ctype = 'MIXED WHITE'
153
-
154
- elif len(sorted(set(tones_chord) & set(WN))) < len(sorted(set(tones_chord) & set(BN))):
155
- ctype = 'MIXED BLACK'
156
-
157
- if return_chord_type_index:
158
- return CHORDS_TYPES.index(ctype)
159
-
160
- else:
161
- return ctype
162
-
163
- ###################################################################################
164
-
165
- def tone_type(tone,
166
- return_tone_type_index=True
167
- ):
168
-
169
- """
170
- Returns tone type
171
- """
172
-
173
- tone = tone % 12
174
-
175
- if tone in BLACK_NOTES:
176
- if return_tone_type_index:
177
- return CHORDS_TYPES.index('BLACK')
178
- else:
179
- return "BLACK"
180
-
181
- else:
182
- if return_tone_type_index:
183
- return CHORDS_TYPES.index('WHITE')
184
- else:
185
- return "WHITE"
186
-
187
- ###################################################################################
188
-
189
- def find_closest_points(points, return_points=True):
190
-
191
- """
192
- Find closest 2D points
193
- """
194
-
195
- coords = np.array(points)
196
-
197
- num_points = coords.shape[0]
198
- closest_matches = np.zeros(num_points, dtype=int)
199
- distances = np.zeros((num_points, num_points))
200
-
201
- for i in range(num_points):
202
- for j in range(num_points):
203
- if i != j:
204
- distances[i, j] = np.linalg.norm(coords[i] - coords[j])
205
- else:
206
- distances[i, j] = np.inf
207
-
208
- closest_matches = np.argmin(distances, axis=1)
209
-
210
- if return_points:
211
- points_matches = coords[closest_matches].tolist()
212
- return points_matches
213
-
214
- else:
215
- return closest_matches.tolist()
216
-
217
- ################################################################################
218
-
219
- def reduce_dimensionality_tsne(list_of_valies,
220
- n_comp=2,
221
- n_iter=5000,
222
- verbose=True
223
- ):
224
-
225
- """
226
- Reduces the dimensionality of the values using t-SNE.
227
- """
228
-
229
- vals = np.array(list_of_valies)
230
-
231
- tsne = TSNE(n_components=n_comp,
232
- n_iter=n_iter,
233
- verbose=verbose)
234
-
235
- reduced_vals = tsne.fit_transform(vals)
236
-
237
- return reduced_vals.tolist()
238
-
239
- ################################################################################
240
-
241
- def compute_mst_edges(similarity_scores_list):
242
-
243
- """
244
- Computes the Minimum Spanning Tree (MST) edges based on the similarity scores.
245
- """
246
-
247
- num_tokens = len(similarity_scores_list[0])
248
-
249
- graph = nx.Graph()
250
-
251
- for i in range(num_tokens):
252
- for j in range(i + 1, num_tokens):
253
- weight = 1 - similarity_scores_list[i][j]
254
- graph.add_edge(i, j, weight=weight)
255
-
256
- mst = nx.minimum_spanning_tree(graph)
257
-
258
- mst_edges = list(mst.edges(data=False))
259
-
260
- return mst_edges
261
-
262
- ################################################################################
263
-
264
- def square_binary_matrix(binary_matrix,
265
- matrix_size=128,
266
- interpolation_order=5,
267
- return_square_matrix_points=False
268
- ):
269
-
270
- """
271
- Reduces an arbitrary binary matrix to a square binary matrix
272
- """
273
-
274
- zoom_factors = (matrix_size / len(binary_matrix), 1)
275
-
276
- resized_matrix = zoom(binary_matrix, zoom_factors, order=interpolation_order)
277
-
278
- resized_matrix = (resized_matrix > 0.5).astype(int)
279
-
280
- final_matrix = np.zeros((matrix_size, matrix_size), dtype=int)
281
- final_matrix[:, :resized_matrix.shape[1]] = resized_matrix
282
-
283
- points = np.column_stack(np.where(final_matrix == 1)).tolist()
284
-
285
- if return_square_matrix_points:
286
- return points
287
-
288
- else:
289
- return resized_matrix
290
-
291
- ################################################################################
292
-
293
- def square_matrix_points_colors(square_matrix_points):
294
-
295
- """
296
- Returns colors for square matrix points
297
- """
298
-
299
- cmap = generate_colors(12)
300
-
301
- chords = []
302
- chords_dict = set()
303
- counts = []
304
-
305
- for k, v in groupby(square_matrix_points, key=lambda x: x[0]):
306
- pgroup = [vv[1] for vv in v]
307
- chord = sorted(set(pgroup))
308
- tchord = sorted(set([p % 12 for p in chord]))
309
- chords_dict.add(tuple(tchord))
310
- chords.append(tuple(tchord))
311
- counts.append(len(pgroup))
312
-
313
- chords_dict = sorted(chords_dict)
314
-
315
- colors = []
316
-
317
- for i, c in enumerate(chords):
318
- colors.extend([cmap[round(sum(c) / len(c))]] * counts[i])
319
-
320
- return colors
321
-
322
- ################################################################################
323
-
324
- def hsv_to_rgb(h, s, v):
325
-
326
- if s == 0.0:
327
- return v, v, v
328
-
329
- i = int(h*6.0)
330
- f = (h*6.0) - i
331
- p = v*(1.0 - s)
332
- q = v*(1.0 - s*f)
333
- t = v*(1.0 - s*(1.0-f))
334
- i = i%6
335
-
336
- return [(v, t, p), (q, v, p), (p, v, t), (p, q, v), (t, p, v), (v, p, q)][i]
337
-
338
- ################################################################################
339
-
340
- def generate_colors(n):
341
- return [hsv_to_rgb(i/n, 1, 1) for i in range(n)]
342
-
343
- ################################################################################
344
-
345
- def add_arrays(a, b):
346
- return [sum(pair) for pair in zip(a, b)]
347
-
348
- ################################################################################
349
-
350
- def calculate_similarities(lists_of_values, metric='cosine'):
351
- return metrics.pairwise_distances(lists_of_values, metric=metric).tolist()
352
-
353
- ################################################################################
354
-
355
- def get_tokens_embeddings(x_transformer_model):
356
- return x_transformer_model.net.token_emb.emb.weight.detach().cpu().tolist()
357
-
358
- ################################################################################
359
-
360
- def minkowski_distance_matrix(X, p=3):
361
-
362
- X = np.array(X)
363
-
364
- n = X.shape[0]
365
- dist_matrix = np.zeros((n, n))
366
-
367
- for i in range(n):
368
- for j in range(n):
369
- dist_matrix[i, j] = np.sum(np.abs(X[i] - X[j])**p)**(1/p)
370
-
371
- return dist_matrix.tolist()
372
-
373
- ################################################################################
374
-
375
- def robust_normalize(values):
376
-
377
- values = np.array(values)
378
- q1 = np.percentile(values, 25)
379
- q3 = np.percentile(values, 75)
380
- iqr = q3 - q1
381
-
382
- filtered_values = values[(values >= q1 - 1.5 * iqr) & (values <= q3 + 1.5 * iqr)]
383
-
384
- min_val = np.min(filtered_values)
385
- max_val = np.max(filtered_values)
386
- normalized_values = (values - min_val) / (max_val - min_val)
387
-
388
- normalized_values = np.clip(normalized_values, 0, 1)
389
-
390
- return normalized_values.tolist()
391
-
392
- ################################################################################
393
-
394
- def min_max_normalize(values):
395
-
396
- scaler = MinMaxScaler()
397
-
398
- return scaler.fit_transform(values).tolist()
399
-
400
- ################################################################################
401
-
402
- def remove_points_outliers(points, z_score_threshold=3):
403
-
404
- points = np.array(points)
405
-
406
- z_scores = np.abs(zscore(points, axis=0))
407
-
408
- return points[(z_scores < z_score_threshold).all(axis=1)].tolist()
409
-
410
- ################################################################################
411
-
412
- def generate_labels(lists_of_values,
413
- return_indices_labels=False
414
- ):
415
-
416
- ordered_indices = list(range(len(lists_of_values)))
417
- ordered_indices_labels = [str(i) for i in ordered_indices]
418
- ordered_values_labels = [str(lists_of_values[i]) for i in ordered_indices]
419
-
420
- if return_indices_labels:
421
- return ordered_indices_labels
422
-
423
- else:
424
- return ordered_values_labels
425
-
426
- ################################################################################
427
-
428
- def reduce_dimensionality_pca(list_of_values, n_components=2):
429
-
430
- """
431
- Reduces the dimensionality of the values using PCA.
432
- """
433
-
434
- pca = PCA(n_components=n_components)
435
- pca_data = pca.fit_transform(list_of_values)
436
-
437
- return pca_data.tolist()
438
-
439
- def reduce_dimensionality_simple(list_of_values,
440
- return_means=True,
441
- return_std_devs=True,
442
- return_medians=False,
443
- return_vars=False
444
- ):
445
-
446
- '''
447
- Reduces dimensionality of the values in a simple way
448
- '''
449
-
450
- array = np.array(list_of_values)
451
- results = []
452
-
453
- if return_means:
454
- means = np.mean(array, axis=1)
455
- results.append(means)
456
-
457
- if return_std_devs:
458
- std_devs = np.std(array, axis=1)
459
- results.append(std_devs)
460
-
461
- if return_medians:
462
- medians = np.median(array, axis=1)
463
- results.append(medians)
464
-
465
- if return_vars:
466
- vars = np.var(array, axis=1)
467
- results.append(vars)
468
-
469
- merged_results = np.column_stack(results)
470
-
471
- return merged_results.tolist()
472
-
473
- ################################################################################
474
-
475
- def reduce_dimensionality_2d_distance(list_of_values, p=5):
476
-
477
- '''
478
- Reduces the dimensionality of the values using 2d distance
479
- '''
480
-
481
- values = np.array(list_of_values)
482
-
483
- dist_matrix = distance_matrix(values, values, p=p)
484
-
485
- mst = minimum_spanning_tree(dist_matrix).toarray()
486
-
487
- points = []
488
-
489
- for i in range(len(values)):
490
- for j in range(len(values)):
491
- if mst[i, j] > 0:
492
- points.append([i, j])
493
-
494
- return points
495
-
496
- ################################################################################
497
-
498
- def normalize_to_range(values, n):
499
-
500
- min_val = min(values)
501
- max_val = max(values)
502
-
503
- range_val = max_val - min_val
504
-
505
- normalized_values = [((value - min_val) / range_val * 2 * n) - n for value in values]
506
-
507
- return normalized_values
508
-
509
- ################################################################################
510
-
511
- def reduce_dimensionality_simple_pca(list_of_values, n_components=2):
512
-
513
- '''
514
- Reduces the dimensionality of the values using simple PCA
515
- '''
516
-
517
- reduced_values = []
518
-
519
- for l in list_of_values:
520
-
521
- norm_values = [round(v * len(l)) for v in normalize_to_range(l, (n_components+1) // 2)]
522
-
523
- pca_values = Counter(norm_values).most_common()
524
- pca_values = [vv[0] / len(l) for vv in pca_values]
525
- pca_values = pca_values[:n_components]
526
- pca_values = pca_values + [0] * (n_components - len(pca_values))
527
-
528
- reduced_values.append(pca_values)
529
-
530
- return reduced_values
531
-
532
- ################################################################################
533
-
534
- def filter_and_replace_values(list_of_values,
535
- threshold,
536
- replace_value,
537
- replace_above_threshold=False
538
- ):
539
-
540
- array = np.array(list_of_values)
541
-
542
- modified_array = np.copy(array)
543
-
544
- if replace_above_threshold:
545
- modified_array[modified_array > threshold] = replace_value
546
-
547
- else:
548
- modified_array[modified_array < threshold] = replace_value
549
-
550
- return modified_array.tolist()
551
-
552
- ################################################################################
553
-
554
- def find_shortest_constellation_path(points,
555
- start_point_idx,
556
- end_point_idx,
557
- p=5,
558
- return_path_length=False,
559
- return_path_points=False,
560
- ):
561
-
562
- """
563
- Finds the shortest path between two points of the points constellation
564
- """
565
-
566
- points = np.array(points)
567
-
568
- dist_matrix = distance_matrix(points, points, p=p)
569
-
570
- mst = minimum_spanning_tree(dist_matrix).toarray()
571
-
572
- G = nx.Graph()
573
-
574
- for i in range(len(points)):
575
- for j in range(len(points)):
576
- if mst[i, j] > 0:
577
- G.add_edge(i, j, weight=mst[i, j])
578
-
579
- path = nx.shortest_path(G,
580
- source=start_point_idx,
581
- target=end_point_idx,
582
- weight='weight'
583
- )
584
-
585
- path_length = nx.shortest_path_length(G,
586
- source=start_point_idx,
587
- target=end_point_idx,
588
- weight='weight')
589
-
590
- path_points = points[np.array(path)].tolist()
591
-
592
-
593
- if return_path_points:
594
- return path_points
595
-
596
- if return_path_length:
597
- return path_length
598
-
599
- return path
600
-
601
- ################################################################################
602
- # Core functions
603
- ################################################################################
604
-
605
- def plot_ms_SONG(ms_song,
606
- preview_length_in_notes=0,
607
- block_lines_times_list = None,
608
- plot_title='ms Song',
609
- max_num_colors=129,
610
- drums_color_num=128,
611
- plot_size=(11,4),
612
- note_height = 0.75,
613
- show_grid_lines=False,
614
- return_plt = False,
615
- timings_multiplier=1,
616
- save_plt='',
617
- save_only_plt_image=True,
618
- save_transparent=False
619
- ):
620
-
621
- '''ms SONG plot'''
622
-
623
- notes = [s for s in ms_song if s[0] == 'note']
624
-
625
- if (len(max(notes, key=len)) != 7) and (len(min(notes, key=len)) != 7):
626
- print('The song notes do not have patches information')
627
- print('Ploease add patches to the notes in the song')
628
-
629
- else:
630
-
631
- start_times = [(s[1] * timings_multiplier) / 1000 for s in notes]
632
- durations = [(s[2] * timings_multiplier) / 1000 for s in notes]
633
- pitches = [s[4] for s in notes]
634
- patches = [s[6] for s in notes]
635
-
636
- colors = generate_colors(max_num_colors)
637
- colors[drums_color_num] = (1, 1, 1)
638
-
639
- pbl = (notes[preview_length_in_notes][1] * timings_multiplier) / 1000
640
-
641
- fig, ax = plt.subplots(figsize=plot_size)
642
-
643
- for start, duration, pitch, patch in zip(start_times, durations, pitches, patches):
644
- rect = plt.Rectangle((start, pitch), duration, note_height, facecolor=colors[patch])
645
- ax.add_patch(rect)
646
-
647
- ax.set_xlim([min(start_times), max(add_arrays(start_times, durations))])
648
- ax.set_ylim([min(pitches)-1, max(pitches)+1])
649
-
650
- ax.set_facecolor('black')
651
- fig.patch.set_facecolor('white')
652
-
653
- if preview_length_in_notes > 0:
654
- ax.axvline(x=pbl, c='white')
655
-
656
- if block_lines_times_list:
657
- for bl in block_lines_times_list:
658
- ax.axvline(x=bl, c='white')
659
-
660
- if show_grid_lines:
661
- ax.grid(color='white')
662
-
663
- plt.xlabel('Time (s)', c='black')
664
- plt.ylabel('MIDI Pitch', c='black')
665
-
666
- plt.title(plot_title)
667
-
668
- if save_plt != '':
669
- if save_only_plt_image:
670
- plt.axis('off')
671
- plt.title('')
672
- plt.savefig(save_plt,
673
- transparent=save_transparent,
674
- bbox_inches='tight',
675
- pad_inches=0,
676
- facecolor='black'
677
- )
678
- plt.close()
679
-
680
- else:
681
- plt.savefig(save_plt)
682
- plt.close()
683
-
684
- if return_plt:
685
- return fig
686
-
687
- plt.show()
688
- plt.close()
689
-
690
- ################################################################################
691
-
692
- def plot_square_matrix_points(list_of_points,
693
- list_of_points_colors,
694
- plot_size=(7, 7),
695
- point_size = 10,
696
- show_grid_lines=False,
697
- plot_title = 'Square Matrix Points Plot',
698
- return_plt=False,
699
- save_plt='',
700
- save_only_plt_image=True,
701
- save_transparent=False
702
- ):
703
-
704
- '''Square matrix points plot'''
705
-
706
- fig, ax = plt.subplots(figsize=plot_size)
707
-
708
- ax.set_facecolor('black')
709
-
710
- if show_grid_lines:
711
- ax.grid(color='white')
712
-
713
- plt.xlabel('Time Step', c='black')
714
- plt.ylabel('MIDI Pitch', c='black')
715
-
716
- plt.title(plot_title)
717
-
718
- plt.scatter([p[0] for p in list_of_points],
719
- [p[1] for p in list_of_points],
720
- c=list_of_points_colors,
721
- s=point_size
722
- )
723
-
724
- if save_plt != '':
725
- if save_only_plt_image:
726
- plt.axis('off')
727
- plt.title('')
728
- plt.savefig(save_plt,
729
- transparent=save_transparent,
730
- bbox_inches='tight',
731
- pad_inches=0,
732
- facecolor='black'
733
- )
734
- plt.close()
735
-
736
- else:
737
- plt.savefig(save_plt)
738
- plt.close()
739
-
740
- if return_plt:
741
- return fig
742
-
743
- plt.show()
744
- plt.close()
745
-
746
- ################################################################################
747
-
748
- def plot_cosine_similarities(lists_of_values,
749
- plot_size=(7, 7),
750
- save_plot=''
751
- ):
752
-
753
- """
754
- Cosine similarities plot
755
- """
756
-
757
- cos_sim = metrics.pairwise_distances(lists_of_values, metric='cosine')
758
-
759
- plt.figure(figsize=plot_size)
760
-
761
- plt.imshow(cos_sim, cmap="inferno", interpolation="nearest")
762
-
763
- im_ratio = cos_sim.shape[0] / cos_sim.shape[1]
764
-
765
- plt.colorbar(fraction=0.046 * im_ratio, pad=0.04)
766
-
767
- plt.xlabel("Index")
768
- plt.ylabel("Index")
769
-
770
- plt.tight_layout()
771
-
772
- if save_plot != '':
773
- plt.savefig(save_plot, bbox_inches="tight")
774
- plt.close()
775
-
776
- plt.show()
777
- plt.close()
778
-
779
- ################################################################################
780
-
781
- def plot_points_with_mst_lines(points,
782
- points_labels,
783
- points_mst_edges,
784
- plot_size=(20, 20),
785
- labels_size=24,
786
- save_plot=''
787
- ):
788
-
789
- """
790
- Plots 2D points with labels and MST lines.
791
- """
792
-
793
- plt.figure(figsize=plot_size)
794
-
795
- for i, label in enumerate(points_labels):
796
- plt.scatter(points[i][0], points[i][1])
797
- plt.annotate(label, (points[i][0], points[i][1]), fontsize=labels_size)
798
-
799
- for edge in points_mst_edges:
800
- i, j = edge
801
- plt.plot([points[i][0], points[j][0]], [points[i][1], points[j][1]], 'k-', alpha=0.5)
802
-
803
- plt.title('Points Map with MST Lines', fontsize=labels_size)
804
- plt.xlabel('X-axis', fontsize=labels_size)
805
- plt.ylabel('Y-axis', fontsize=labels_size)
806
-
807
- if save_plot != '':
808
- plt.savefig(save_plot, bbox_inches="tight")
809
- plt.close()
810
-
811
- plt.show()
812
-
813
- plt.close()
814
-
815
- ################################################################################
816
-
817
- def plot_points_constellation(points,
818
- points_labels,
819
- p=5,
820
- plot_size=(15, 15),
821
- labels_size=12,
822
- show_grid=False,
823
- save_plot=''
824
- ):
825
-
826
- """
827
- Plots 2D points constellation
828
- """
829
-
830
- points = np.array(points)
831
-
832
- dist_matrix = distance_matrix(points, points, p=p)
833
-
834
- mst = minimum_spanning_tree(dist_matrix).toarray()
835
-
836
- plt.figure(figsize=plot_size)
837
-
838
- plt.scatter(points[:, 0], points[:, 1], color='blue')
839
-
840
- for i, label in enumerate(points_labels):
841
- plt.annotate(label, (points[i, 0], points[i, 1]),
842
- textcoords="offset points",
843
- xytext=(0, 10),
844
- ha='center',
845
- fontsize=labels_size
846
- )
847
-
848
- for i in range(len(points)):
849
- for j in range(len(points)):
850
- if mst[i, j] > 0:
851
- plt.plot([points[i, 0], points[j, 0]], [points[i, 1], points[j, 1]], 'k--')
852
-
853
- plt.xlabel('X-axis', fontsize=labels_size)
854
- plt.ylabel('Y-axis', fontsize=labels_size)
855
- plt.title('2D Coordinates with Minimum Spanning Tree', fontsize=labels_size)
856
-
857
- plt.grid(show_grid)
858
-
859
- if save_plot != '':
860
- plt.savefig(save_plot, bbox_inches="tight")
861
- plt.close()
862
-
863
- plt.show()
864
-
865
- plt.close()
866
-
867
- ################################################################################
868
-
869
- def binary_matrix_to_images(matrix,
870
- step,
871
- overlap,
872
- output_folder='./Dataset/',
873
- output_img_prefix='image',
874
- output_img_ext='.png',
875
- save_to_array=False,
876
- verbose=True
877
- ):
878
-
879
- if not save_to_array:
880
-
881
- if verbose:
882
- print('=' * 70)
883
- print('Checking output folder dir...')
884
-
885
- os.makedirs(os.path.dirname(output_folder), exist_ok=True)
886
-
887
- if verbose:
888
- print('Done!')
889
-
890
- if verbose:
891
- print('=' * 70)
892
- print('Writing images...')
893
-
894
- matrix = np.array(matrix, dtype=np.uint8)
895
-
896
- image_array = []
897
-
898
- for i in range(0, max(1, matrix.shape[0]), overlap):
899
-
900
- submatrix = matrix[i:i+step, :]
901
-
902
- if submatrix.shape[0] < 128:
903
- zeros_array = np.zeros((128-submatrix.shape[0], 128))
904
- submatrix = np.vstack((submatrix, zeros_array))
905
-
906
- img = Image.fromarray(submatrix * 255).convert('1')
907
-
908
- if save_to_array:
909
- image_array.append(np.array(img))
910
-
911
- else:
912
- img.save(output_folder + output_img_prefix + '_' + str(matrix.shape[1]) + '_' + str(i).zfill(7) + output_img_ext)
913
-
914
- if verbose:
915
- print('Done!')
916
- print('=' * 70)
917
- print('Saved', (matrix.shape[0] // min(step, overlap))+1, 'imges!')
918
- print('=' * 70)
919
-
920
- if save_to_array:
921
- return np.array(image_array).tolist()
922
-
923
- ################################################################################
924
-
925
- def images_to_binary_matrix(list_of_images):
926
-
927
- image_array = np.array(list_of_images)
928
-
929
- original_matrix = []
930
-
931
- for img in image_array:
932
-
933
- submatrix = np.array(img)
934
- original_matrix.extend(submatrix.tolist())
935
-
936
- return original_matrix
937
-
938
- ################################################################################
939
-
940
- def square_image_matrix(image_matrix,
941
- matrix_size=128,
942
- num_pca_components=5,
943
- filter_out_zero_rows=False,
944
- return_square_matrix_points=False
945
- ):
946
-
947
- """
948
- Reduces an arbitrary image matrix to a square image matrix
949
- """
950
-
951
- matrix = np.array(image_matrix)
952
-
953
- if filter_out_zero_rows:
954
- matrix = matrix[~np.all(matrix == 0, axis=1)]
955
-
956
- target_rows = matrix_size
957
-
958
- rows_per_group = matrix.shape[0] // target_rows
959
-
960
- compressed_matrix = np.zeros((target_rows, matrix.shape[1]), dtype=np.int32)
961
-
962
- for i in range(target_rows):
963
- start_row = i * rows_per_group
964
- end_row = (i + 1) * rows_per_group
965
- group = matrix[start_row:end_row, :]
966
-
967
- pca = PCA(n_components=num_pca_components)
968
- pca.fit(group)
969
-
970
- principal_component = np.mean(pca.components_, axis=0)
971
- contributions = np.dot(group, principal_component)
972
- selected_row_index = np.argmax(contributions)
973
-
974
- compressed_matrix[i, :] = group[selected_row_index, :]
975
-
976
- if return_square_matrix_points:
977
- filtered_matrix = compressed_matrix[~np.all(compressed_matrix == 0, axis=1)]
978
-
979
- row_indexes, col_indexes = np.where(filtered_matrix != 0)
980
- points = np.column_stack((row_indexes, filtered_matrix[row_indexes, col_indexes])).tolist()
981
-
982
- return points
983
-
984
- else:
985
- return compressed_matrix.tolist()
986
-
987
- ################################################################################
988
-
989
- def image_matrix_to_images(image_matrix,
990
- step,
991
- overlap,
992
- num_img_channels=3,
993
- output_folder='./Dataset/',
994
- output_img_prefix='image',
995
- output_img_ext='.png',
996
- save_to_array=False,
997
- verbose=True
998
- ):
999
-
1000
- if num_img_channels > 1:
1001
- n_mat_channels = 3
1002
-
1003
- else:
1004
- n_mat_channels = 1
1005
-
1006
- if not save_to_array:
1007
-
1008
- if verbose:
1009
- print('=' * 70)
1010
- print('Checking output folder dir...')
1011
-
1012
- os.makedirs(os.path.dirname(output_folder), exist_ok=True)
1013
-
1014
- if verbose:
1015
- print('Done!')
1016
-
1017
- if verbose:
1018
- print('=' * 70)
1019
- print('Writing images...')
1020
-
1021
- matrix = np.array(image_matrix)
1022
-
1023
- image_array = []
1024
-
1025
- for i in range(0, max(1, matrix.shape[0]), overlap):
1026
-
1027
- submatrix = matrix[i:i+step, :]
1028
-
1029
- if submatrix.shape[0] < 128:
1030
- zeros_array = np.zeros((128-submatrix.shape[0], 128))
1031
- submatrix = np.vstack((submatrix, zeros_array))
1032
-
1033
- if n_mat_channels == 3:
1034
-
1035
- r = (submatrix // (256*256)) % 256
1036
- g = (submatrix // 256) % 256
1037
- b = submatrix % 256
1038
-
1039
- rgb_image = np.stack((r, g, b), axis=-1).astype(np.uint8)
1040
- img = Image.fromarray(rgb_image, 'RGB')
1041
-
1042
- else:
1043
- grayscale_image = submatrix.astype(np.uint8)
1044
- img = Image.fromarray(grayscale_image, 'L')
1045
-
1046
- if save_to_array:
1047
- image_array.append(np.array(img))
1048
-
1049
- else:
1050
- img.save(output_folder + output_img_prefix + '_' + str(matrix.shape[1]) + '_' + str(i).zfill(7) + output_img_ext)
1051
-
1052
- if verbose:
1053
- print('Done!')
1054
- print('=' * 70)
1055
- print('Saved', (matrix.shape[0] // min(step, overlap))+1, 'imges!')
1056
- print('=' * 70)
1057
-
1058
- if save_to_array:
1059
- return np.array(image_array).tolist()
1060
-
1061
- ################################################################################
1062
-
1063
- def images_to_image_matrix(list_of_images,
1064
- num_img_channels=3
1065
- ):
1066
-
1067
- if num_img_channels > 1:
1068
- n_mat_channels = 3
1069
-
1070
- else:
1071
- n_mat_channels = 1
1072
-
1073
- image_array = np.array(list_of_images)
1074
-
1075
- original_matrix = []
1076
-
1077
- for img in image_array:
1078
-
1079
- if num_img_channels == 3:
1080
-
1081
- rgb_array = np.array(img)
1082
-
1083
- matrix = (rgb_array[..., 0].astype(np.int64) * 256*256 +
1084
- rgb_array[..., 1].astype(np.int64) * 256 +
1085
- rgb_array[..., 2].astype(np.int64))
1086
-
1087
- else:
1088
- matrix = np.array(img)
1089
-
1090
- original_matrix.extend(matrix)
1091
-
1092
- return original_matrix
1093
-
1094
- ################################################################################
1095
-
1096
- def square_matrix_to_RGB_matrix(square_matrix):
1097
-
1098
- smatrix = np.array(square_matrix)
1099
- sq_matrix = smatrix[:smatrix.shape[1]]
1100
-
1101
- r = (sq_matrix // (256 ** 2)) % 256
1102
- g = (sq_matrix // 256) % 256
1103
- b = sq_matrix % 256
1104
-
1105
- rgb_array = np.stack((r, g, b), axis=-1)
1106
-
1107
- return rgb_array.tolist()
1108
-
1109
- ################################################################################
1110
-
1111
- def upsample_square_matrix(square_matrix, upsampling_factor=4):
1112
-
1113
- smatrix = np.array(square_matrix)
1114
- sq_matrix = smatrix[:smatrix.shape[1]]
1115
-
1116
- scaling_array = np.ones((upsampling_factor, upsampling_factor))
1117
- scaled_array = np.kron(sq_matrix, scaling_array)
1118
- scaled_array = scaled_array.astype('int')
1119
-
1120
- return scaled_array.tolist()
1121
-
1122
- ################################################################################
1123
-
1124
- def downsample_square_matrix(square_matrix, downsampling_factor=4):
1125
-
1126
- smatrix = np.array(square_matrix)
1127
- sq_matrix = smatrix[:smatrix.shape[1]]
1128
-
1129
- dmatrix = sq_matrix[::downsampling_factor, ::downsampling_factor]
1130
- dmatrix = dmatrix.astype('int')
1131
-
1132
- return dmatrix.tolist()
1133
-
1134
- ################################################################################
1135
-
1136
- def plot_parsons_code(parsons_code,
1137
- start_pitch=60,
1138
- return_plot_dict=False,
1139
- return_plot_string=False,
1140
- plot_size=(10, 10),
1141
- labels_size=16,
1142
- save_plot=''
1143
- ):
1144
-
1145
- '''
1146
- Plot parsons code string
1147
- '''
1148
-
1149
- if parsons_code[0] != "*":
1150
- return None
1151
-
1152
- contour_dict = {}
1153
- pitch = 0
1154
- index = 0
1155
-
1156
- maxp = 0
1157
- minp = 0
1158
-
1159
- contour_dict[(pitch, index)] = "*"
1160
-
1161
- for point in parsons_code:
1162
- if point == "R":
1163
- index += 1
1164
- contour_dict[(pitch, index)] = "-"
1165
-
1166
- index += 1
1167
- contour_dict[(pitch, index)] = "*"
1168
-
1169
- elif point == "U":
1170
- index += 1
1171
- pitch -= 1
1172
- contour_dict[(pitch, index)] = "/"
1173
-
1174
- index += 1
1175
- pitch -= 1
1176
- contour_dict[(pitch, index)] = "*"
1177
-
1178
- if pitch < maxp:
1179
- maxp = pitch
1180
-
1181
- elif point == "D":
1182
- index += 1
1183
- pitch += 1
1184
- contour_dict[(pitch, index)] = "\\"
1185
-
1186
- index += 1
1187
- pitch += 1
1188
- contour_dict[(pitch, index)] = "*"
1189
-
1190
- if pitch > minp:
1191
- minp = pitch
1192
-
1193
- if return_plot_dict:
1194
- return contour_dict
1195
-
1196
- if return_plot_string:
1197
-
1198
- plot_string = ''
1199
-
1200
- for pitch in range(maxp, minp+1):
1201
- line = [" " for _ in range(index + 1)]
1202
- for pos in range(index + 1):
1203
- if (pitch, pos) in contour_dict:
1204
- line[pos] = contour_dict[(pitch, pos)]
1205
-
1206
- plot_string = "".join(line)
1207
-
1208
- return plot_string
1209
-
1210
- labels = []
1211
- pitches = []
1212
- positions = []
1213
- cur_pitch = start_pitch
1214
- pitch_idx = 0
1215
-
1216
- for k, v in contour_dict.items():
1217
-
1218
- if v != '*':
1219
-
1220
- pitches.append(cur_pitch)
1221
- positions.append(pitch_idx)
1222
-
1223
- if v == '/':
1224
- cur_pitch += 1
1225
- labels.append('U')
1226
-
1227
- elif v == '\\':
1228
- cur_pitch -= 1
1229
- labels.append('D')
1230
-
1231
- elif v == '-':
1232
- labels.append('R')
1233
-
1234
- pitch_idx += 1
1235
-
1236
- plt.figure(figsize=plot_size)
1237
-
1238
-
1239
- plt.plot(pitches)
1240
-
1241
- for i, point in enumerate(zip(positions, pitches)):
1242
- plt.annotate(labels[i], point, fontsize=labels_size)
1243
-
1244
-
1245
- plt.title('Parsons Code with Labels', fontsize=labels_size)
1246
- plt.xlabel('Position', fontsize=labels_size)
1247
- plt.ylabel('Pitch', fontsize=labels_size)
1248
-
1249
- if save_plot != '':
1250
- plt.savefig(save_plot, bbox_inches="tight")
1251
- plt.close()
1252
-
1253
- plt.show()
1254
-
1255
- plt.close()
1256
-
1257
- ################################################################################
1258
-
1259
- def plot_tokens_embeddings_constellation(tokens_embeddings,
1260
- start_token,
1261
- end_token,
1262
- plot_size=(10, 10),
1263
- labels_size=12,
1264
- show_grid=False,
1265
- save_plot=''):
1266
-
1267
- """
1268
- Plots token embeddings constellation using MST and graph layout
1269
- without dimensionality reduction.
1270
- """
1271
-
1272
- distance_matrix = metrics.pairwise_distances(tokens_embeddings[start_token:end_token], metric='cosine')
1273
-
1274
- token_labels = [str(i) for i in range(start_token, end_token)]
1275
-
1276
- mst = minimum_spanning_tree(distance_matrix).toarray()
1277
-
1278
- n = distance_matrix.shape[0]
1279
- G = nx.Graph()
1280
-
1281
- for i in range(n):
1282
- for j in range(n):
1283
- if mst[i, j] > 0:
1284
- weight = 1 / (distance_matrix[i, j] + 1e-8)
1285
- G.add_edge(i, j, weight=weight)
1286
-
1287
- pos = nx.kamada_kawai_layout(G, weight='weight')
1288
-
1289
- points = np.array([pos[i] for i in range(n)])
1290
-
1291
- plt.figure(figsize=plot_size)
1292
- plt.scatter(points[:, 0], points[:, 1], color='blue')
1293
-
1294
- for i, label in enumerate(token_labels):
1295
- plt.annotate(label, (points[i, 0], points[i, 1]),
1296
- textcoords="offset points",
1297
- xytext=(0, 10),
1298
- ha='center',
1299
- fontsize=labels_size)
1300
-
1301
- for i in range(n):
1302
- for j in range(n):
1303
- if mst[i, j] > 0:
1304
- plt.plot([points[i, 0], points[j, 0]],
1305
- [points[i, 1], points[j, 1]],
1306
- 'k--', alpha=0.5)
1307
-
1308
- plt.title('Token Embeddings Constellation with MST', fontsize=labels_size)
1309
- plt.grid(show_grid)
1310
-
1311
- if save_plot:
1312
- plt.savefig(save_plot, bbox_inches="tight")
1313
- plt.close()
1314
-
1315
- else:
1316
- plt.show()
1317
-
1318
- plt.close()
1319
-
1320
- ################################################################################
1321
-
1322
- def find_token_path(tokens_embeddings,
1323
- start_token,
1324
- end_token,
1325
- verbose=False
1326
- ):
1327
-
1328
- """
1329
- Finds the path of tokens between start_token and end_token using
1330
- the Minimum Spanning Tree (MST) derived from the distance matrix.
1331
- """
1332
-
1333
- distance_matrix = metrics.pairwise_distances(tokens_embeddings, metric='cosine')
1334
-
1335
- token_labels = [str(i) for i in range(len(distance_matrix))]
1336
-
1337
- if verbose:
1338
- print('Total number of tokens:', len(distance_matrix))
1339
-
1340
- mst = minimum_spanning_tree(distance_matrix).toarray()
1341
-
1342
- n = distance_matrix.shape[0]
1343
- G = nx.Graph()
1344
-
1345
- for i in range(n):
1346
- for j in range(n):
1347
- if mst[i, j] > 0:
1348
- weight = 1 / (distance_matrix[i, j] + 1e-8)
1349
- G.add_edge(i, j, weight=weight)
1350
-
1351
- try:
1352
- start_idx = token_labels.index(str(start_token))
1353
- end_idx = token_labels.index(str(end_token))
1354
-
1355
- except ValueError:
1356
- raise ValueError("Start or end token not found in the provided token labels.")
1357
-
1358
- path_indices = nx.shortest_path(G, source=start_idx, target=end_idx)
1359
-
1360
- token_path = [int(token_labels[idx]) for idx in path_indices]
1361
-
1362
- return token_path
1363
-
1364
- ################################################################################
1365
- # [WIP] Future dev functions
1366
- ################################################################################
1367
-
1368
- '''
1369
- import umap
1370
-
1371
- def reduce_dimensionality_umap(list_of_values,
1372
- n_comp=2,
1373
- n_neighbors=15,
1374
- ):
1375
-
1376
- """
1377
- Reduces the dimensionality of the values using UMAP.
1378
- """
1379
-
1380
- vals = np.array(list_of_values)
1381
-
1382
- umap_reducer = umap.UMAP(n_components=n_comp,
1383
- n_neighbors=n_neighbors,
1384
- n_epochs=5000,
1385
- verbose=True
1386
- )
1387
-
1388
- reduced_vals = umap_reducer.fit_transform(vals)
1389
-
1390
- return reduced_vals.tolist()
1391
- '''
1392
-
1393
- ################################################################################
1394
-
1395
- '''
1396
- import alphashape
1397
- from shapely.geometry import Point
1398
- from matplotlib.tri import Triangulation, LinearTriInterpolator
1399
- from scipy.stats import zscore
1400
-
1401
- #===============================================================================
1402
-
1403
- coordinates = points
1404
-
1405
- dist_matrix = minkowski_distance_matrix(coordinates, p=3) # You can change the value of p as needed
1406
-
1407
- # Centering matrix
1408
- n = dist_matrix.shape[0]
1409
- H = np.eye(n) - np.ones((n, n)) / n
1410
-
1411
- # Apply double centering
1412
- B = -0.5 * H @ dist_matrix**2 @ H
1413
-
1414
- # Eigen decomposition
1415
- eigvals, eigvecs = np.linalg.eigh(B)
1416
-
1417
- # Sort eigenvalues and eigenvectors
1418
- idx = np.argsort(eigvals)[::-1]
1419
- eigvals = eigvals[idx]
1420
- eigvecs = eigvecs[:, idx]
1421
-
1422
- # Select the top 2 eigenvectors
1423
- X_transformed = eigvecs[:, :2] * np.sqrt(eigvals[:2])
1424
-
1425
- #===============================================================================
1426
-
1427
- src_points = X_transformed
1428
- src_values = np.array([[p[1]] for p in points]) #np.random.rand(X_transformed.shape[0])
1429
-
1430
- #===============================================================================
1431
-
1432
- # Normalize the points to the range [0, 1]
1433
- scaler = MinMaxScaler()
1434
- points_normalized = scaler.fit_transform(src_points)
1435
-
1436
- values_normalized = custom_normalize(src_values)
1437
-
1438
- # Remove outliers based on z-score
1439
- z_scores = np.abs(zscore(points_normalized, axis=0))
1440
- filtered_points = points_normalized[(z_scores < 3).all(axis=1)]
1441
- filtered_values = values_normalized[(z_scores < 3).all(axis=1)]
1442
-
1443
- # Compute the concave hull (alpha shape)
1444
- alpha = 8 # Adjust alpha as needed
1445
- hull = alphashape.alphashape(filtered_points, alpha)
1446
-
1447
- # Create a triangulation
1448
- tri = Triangulation(filtered_points[:, 0], filtered_points[:, 1])
1449
-
1450
- # Interpolate the values on the triangulation
1451
- interpolator = LinearTriInterpolator(tri, filtered_values[:, 0])
1452
- xi, yi = np.meshgrid(np.linspace(0, 1, 100), np.linspace(0, 1, 100))
1453
- zi = interpolator(xi, yi)
1454
-
1455
- # Mask out points outside the concave hull
1456
- mask = np.array([hull.contains(Point(x, y)) for x, y in zip(xi.flatten(), yi.flatten())])
1457
- zi = np.ma.array(zi, mask=~mask.reshape(zi.shape))
1458
-
1459
- # Plot the filled contour based on the interpolated values
1460
- plt.contourf(xi, yi, zi, levels=50, cmap='viridis')
1461
-
1462
- # Plot the original points
1463
- #plt.scatter(filtered_points[:, 0], filtered_points[:, 1], c=filtered_values, edgecolors='k')
1464
-
1465
- plt.title('Filled Contour Plot with Original Values')
1466
- plt.xlabel('X-axis')
1467
- plt.ylabel('Y-axis')
1468
- plt.colorbar(label='Value')
1469
- plt.show()
1470
- '''
1471
-
1472
- ################################################################################
1473
-
1474
- def plot_tree_horizontal(data):
1475
-
1476
- """
1477
- Given data as a list of levels (each level is a tuple or list of
1478
- displacements for each branch), this function computes the cumulative
1479
- value per branch (starting from 0) and plots each branch
1480
- with the tree level mapped to the x-axis and the cumulative value mapped
1481
- to the y-axis. This gives a left-to-right tree with branches spanning up
1482
- (positive) and down (negative).
1483
-
1484
- Parameters:
1485
- data (list of tuple/list): Each element represents a tree level.
1486
- It is assumed every level has the same length.
1487
- """
1488
-
1489
- # Convert data to a NumPy array with shape (n_levels, n_branches)
1490
- data = np.array(data)
1491
- n_levels, n_branches = data.shape
1492
-
1493
- # Compute cumulative sums along each branch.
1494
- # Each branch starts at 0 at level 0.
1495
- cum = np.zeros((n_levels + 1, n_branches))
1496
- for i in range(n_levels):
1497
- cum[i + 1, :] = cum[i, :] + data[i, :]
1498
-
1499
- plt.figure(figsize=(12, 8))
1500
-
1501
- # Plot each branch as a line. For branch j:
1502
- # - x coordinates are the tree levels (0 to n_levels)
1503
- # - y coordinates are the corresponding cumulative values.
1504
- for j in range(n_branches):
1505
- x = np.arange(n_levels + 1)
1506
- y = cum[:, j]
1507
- plt.plot(x, y, marker='o', label=f'Branch {j}')
1508
-
1509
- plt.title("Horizontal Tree Visualization: Branches Spanning Up and Down", fontsize=14)
1510
- plt.xlabel("Tree Level (Left = Root)")
1511
- plt.ylabel("Cumulative Value (Up = Positive, Down = Negative)")
1512
-
1513
- # Add a horizontal line at y=0 to emphasize the center.
1514
- plt.axhline(0, color="gray", linestyle="--")
1515
-
1516
- #plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
1517
- plt.tight_layout()
1518
- plt.show()
1519
-
1520
- ################################################################################
1521
- # This is the end of TPLOTS Python modules
1522
  ################################################################################
 
1
+ #! /usr/bin/python3
2
+
3
+ r'''############################################################################
4
+ ################################################################################
5
+ #
6
+ #
7
+ # Tegridy Plots Python Module (TPLOTS)
8
+ # Version 1.0
9
+ #
10
+ # Project Los Angeles
11
+ #
12
+ # Tegridy Code 2025
13
+ #
14
+ # https://github.com/asigalov61/tegridy-tools
15
+ #
16
+ #
17
+ ################################################################################
18
+ #
19
+ # Copyright 2024 Project Los Angeles / Tegridy Code
20
+ #
21
+ # Licensed under the Apache License, Version 2.0 (the "License");
22
+ # you may not use this file except in compliance with the License.
23
+ # You may obtain a copy of the License at
24
+ #
25
+ # http://www.apache.org/licenses/LICENSE-2.0
26
+ #
27
+ # Unless required by applicable law or agreed to in writing, software
28
+ # distributed under the License is distributed on an "AS IS" BASIS,
29
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
30
+ # See the License for the specific language governing permissions and
31
+ # limitations under the License.
32
+ #
33
+ ################################################################################
34
+ ################################################################################
35
+ #
36
+ # Critical dependencies
37
+ #
38
+ # !pip install numpy==1.24.4
39
+ # !pip install scipy
40
+ # !pip install matplotlib
41
+ # !pip install networkx
42
+ # !pip3 install scikit-learn
43
+ #
44
+ ################################################################################
45
+ #
46
+ # Future critical dependencies
47
+ #
48
+ # !pip install umap-learn
49
+ # !pip install alphashape
50
+ #
51
+ ################################################################################
52
+ '''
53
+
54
+ ################################################################################
55
+ # Modules imports
56
+ ################################################################################
57
+
58
+ import os
59
+ from collections import Counter
60
+ from itertools import groupby
61
+
62
+ import numpy as np
63
+
64
+ import networkx as nx
65
+
66
+ from sklearn.manifold import TSNE
67
+ from sklearn import metrics
68
+ from sklearn.preprocessing import MinMaxScaler
69
+ from sklearn.decomposition import PCA
70
+
71
+ from scipy.ndimage import zoom
72
+ from scipy.spatial import distance_matrix
73
+ from scipy.sparse.csgraph import minimum_spanning_tree
74
+ from scipy.stats import zscore
75
+
76
+ import matplotlib.pyplot as plt
77
+ from PIL import Image
78
+
79
+ ################################################################################
80
+ # Constants
81
+ ################################################################################
82
+
83
+ ALL_CHORDS_FULL = [[0], [0, 3], [0, 3, 5], [0, 3, 5, 8], [0, 3, 5, 9], [0, 3, 5, 10], [0, 3, 6],
84
+ [0, 3, 6, 9], [0, 3, 6, 10], [0, 3, 7], [0, 3, 7, 10], [0, 3, 8], [0, 3, 9],
85
+ [0, 3, 10], [0, 4], [0, 4, 6], [0, 4, 6, 9], [0, 4, 6, 10], [0, 4, 7],
86
+ [0, 4, 7, 10], [0, 4, 8], [0, 4, 9], [0, 4, 10], [0, 5], [0, 5, 8], [0, 5, 9],
87
+ [0, 5, 10], [0, 6], [0, 6, 9], [0, 6, 10], [0, 7], [0, 7, 10], [0, 8], [0, 9],
88
+ [0, 10], [1], [1, 4], [1, 4, 6], [1, 4, 6, 9], [1, 4, 6, 10], [1, 4, 6, 11],
89
+ [1, 4, 7], [1, 4, 7, 10], [1, 4, 7, 11], [1, 4, 8], [1, 4, 8, 11], [1, 4, 9],
90
+ [1, 4, 10], [1, 4, 11], [1, 5], [1, 5, 8], [1, 5, 8, 11], [1, 5, 9],
91
+ [1, 5, 10], [1, 5, 11], [1, 6], [1, 6, 9], [1, 6, 10], [1, 6, 11], [1, 7],
92
+ [1, 7, 10], [1, 7, 11], [1, 8], [1, 8, 11], [1, 9], [1, 10], [1, 11], [2],
93
+ [2, 5], [2, 5, 8], [2, 5, 8, 11], [2, 5, 9], [2, 5, 10], [2, 5, 11], [2, 6],
94
+ [2, 6, 9], [2, 6, 10], [2, 6, 11], [2, 7], [2, 7, 10], [2, 7, 11], [2, 8],
95
+ [2, 8, 11], [2, 9], [2, 10], [2, 11], [3], [3, 5], [3, 5, 8], [3, 5, 8, 11],
96
+ [3, 5, 9], [3, 5, 10], [3, 5, 11], [3, 6], [3, 6, 9], [3, 6, 10], [3, 6, 11],
97
+ [3, 7], [3, 7, 10], [3, 7, 11], [3, 8], [3, 8, 11], [3, 9], [3, 10], [3, 11],
98
+ [4], [4, 6], [4, 6, 9], [4, 6, 10], [4, 6, 11], [4, 7], [4, 7, 10], [4, 7, 11],
99
+ [4, 8], [4, 8, 11], [4, 9], [4, 10], [4, 11], [5], [5, 8], [5, 8, 11], [5, 9],
100
+ [5, 10], [5, 11], [6], [6, 9], [6, 10], [6, 11], [7], [7, 10], [7, 11], [8],
101
+ [8, 11], [9], [10], [11]]
102
+
103
+ ################################################################################
104
+
105
+ CHORDS_TYPES = ['WHITE', 'BLACK', 'UNKNOWN', 'MIXED WHITE', 'MIXED BLACK', 'MIXED GRAY']
106
+
107
+ ################################################################################
108
+
109
+ WHITE_NOTES = [0, 2, 4, 5, 7, 9, 11]
110
+
111
+ ################################################################################
112
+
113
+ BLACK_NOTES = [1, 3, 6, 8, 10]
114
+
115
+ ################################################################################
116
+ # Helper functions
117
+ ################################################################################
118
+
119
+ def tones_chord_type(tones_chord,
120
+ return_chord_type_index=True,
121
+ ):
122
+
123
+ """
124
+ Returns tones chord type
125
+ """
126
+
127
+ WN = WHITE_NOTES
128
+ BN = BLACK_NOTES
129
+ MX = WHITE_NOTES + BLACK_NOTES
130
+
131
+
132
+ CHORDS = ALL_CHORDS_FULL
133
+
134
+ tones_chord = sorted(tones_chord)
135
+
136
+ ctype = 'UNKNOWN'
137
+
138
+ if tones_chord in CHORDS:
139
+
140
+ if sorted(set(tones_chord) & set(WN)) == tones_chord:
141
+ ctype = 'WHITE'
142
+
143
+ elif sorted(set(tones_chord) & set(BN)) == tones_chord:
144
+ ctype = 'BLACK'
145
+
146
+ if len(tones_chord) > 1 and sorted(set(tones_chord) & set(MX)) == tones_chord:
147
+
148
+ if len(sorted(set(tones_chord) & set(WN))) == len(sorted(set(tones_chord) & set(BN))):
149
+ ctype = 'MIXED GRAY'
150
+
151
+ elif len(sorted(set(tones_chord) & set(WN))) > len(sorted(set(tones_chord) & set(BN))):
152
+ ctype = 'MIXED WHITE'
153
+
154
+ elif len(sorted(set(tones_chord) & set(WN))) < len(sorted(set(tones_chord) & set(BN))):
155
+ ctype = 'MIXED BLACK'
156
+
157
+ if return_chord_type_index:
158
+ return CHORDS_TYPES.index(ctype)
159
+
160
+ else:
161
+ return ctype
162
+
163
+ ###################################################################################
164
+
165
+ def tone_type(tone,
166
+ return_tone_type_index=True
167
+ ):
168
+
169
+ """
170
+ Returns tone type
171
+ """
172
+
173
+ tone = tone % 12
174
+
175
+ if tone in BLACK_NOTES:
176
+ if return_tone_type_index:
177
+ return CHORDS_TYPES.index('BLACK')
178
+ else:
179
+ return "BLACK"
180
+
181
+ else:
182
+ if return_tone_type_index:
183
+ return CHORDS_TYPES.index('WHITE')
184
+ else:
185
+ return "WHITE"
186
+
187
+ ###################################################################################
188
+
189
+ def find_closest_points(points, return_points=True):
190
+
191
+ """
192
+ Find closest 2D points
193
+ """
194
+
195
+ coords = np.array(points)
196
+
197
+ num_points = coords.shape[0]
198
+ closest_matches = np.zeros(num_points, dtype=int)
199
+ distances = np.zeros((num_points, num_points))
200
+
201
+ for i in range(num_points):
202
+ for j in range(num_points):
203
+ if i != j:
204
+ distances[i, j] = np.linalg.norm(coords[i] - coords[j])
205
+ else:
206
+ distances[i, j] = np.inf
207
+
208
+ closest_matches = np.argmin(distances, axis=1)
209
+
210
+ if return_points:
211
+ points_matches = coords[closest_matches].tolist()
212
+ return points_matches
213
+
214
+ else:
215
+ return closest_matches.tolist()
216
+
217
+ ################################################################################
218
+
219
+ def reduce_dimensionality_tsne(list_of_valies,
220
+ n_comp=2,
221
+ n_iter=5000,
222
+ verbose=True
223
+ ):
224
+
225
+ """
226
+ Reduces the dimensionality of the values using t-SNE.
227
+ """
228
+
229
+ vals = np.array(list_of_valies)
230
+
231
+ tsne = TSNE(n_components=n_comp,
232
+ n_iter=n_iter,
233
+ verbose=verbose)
234
+
235
+ reduced_vals = tsne.fit_transform(vals)
236
+
237
+ return reduced_vals.tolist()
238
+
239
+ ################################################################################
240
+
241
+ def compute_mst_edges(similarity_scores_list):
242
+
243
+ """
244
+ Computes the Minimum Spanning Tree (MST) edges based on the similarity scores.
245
+ """
246
+
247
+ num_tokens = len(similarity_scores_list[0])
248
+
249
+ graph = nx.Graph()
250
+
251
+ for i in range(num_tokens):
252
+ for j in range(i + 1, num_tokens):
253
+ weight = 1 - similarity_scores_list[i][j]
254
+ graph.add_edge(i, j, weight=weight)
255
+
256
+ mst = nx.minimum_spanning_tree(graph)
257
+
258
+ mst_edges = list(mst.edges(data=False))
259
+
260
+ return mst_edges
261
+
262
+ ################################################################################
263
+
264
+ def square_binary_matrix(binary_matrix,
265
+ matrix_size=128,
266
+ interpolation_order=5,
267
+ return_square_matrix_points=False
268
+ ):
269
+
270
+ """
271
+ Reduces an arbitrary binary matrix to a square binary matrix
272
+ """
273
+
274
+ zoom_factors = (matrix_size / len(binary_matrix), 1)
275
+
276
+ resized_matrix = zoom(binary_matrix, zoom_factors, order=interpolation_order)
277
+
278
+ resized_matrix = (resized_matrix > 0.5).astype(int)
279
+
280
+ final_matrix = np.zeros((matrix_size, matrix_size), dtype=int)
281
+ final_matrix[:, :resized_matrix.shape[1]] = resized_matrix
282
+
283
+ points = np.column_stack(np.where(final_matrix == 1)).tolist()
284
+
285
+ if return_square_matrix_points:
286
+ return points
287
+
288
+ else:
289
+ return resized_matrix
290
+
291
+ ################################################################################
292
+
293
+ def square_matrix_points_colors(square_matrix_points):
294
+
295
+ """
296
+ Returns colors for square matrix points
297
+ """
298
+
299
+ cmap = generate_colors(12)
300
+
301
+ chords = []
302
+ chords_dict = set()
303
+ counts = []
304
+
305
+ for k, v in groupby(square_matrix_points, key=lambda x: x[0]):
306
+ pgroup = [vv[1] for vv in v]
307
+ chord = sorted(set(pgroup))
308
+ tchord = sorted(set([p % 12 for p in chord]))
309
+ chords_dict.add(tuple(tchord))
310
+ chords.append(tuple(tchord))
311
+ counts.append(len(pgroup))
312
+
313
+ chords_dict = sorted(chords_dict)
314
+
315
+ colors = []
316
+
317
+ for i, c in enumerate(chords):
318
+ colors.extend([cmap[round(sum(c) / len(c))]] * counts[i])
319
+
320
+ return colors
321
+
322
+ ################################################################################
323
+
324
+ def hsv_to_rgb(h, s, v):
325
+
326
+ if s == 0.0:
327
+ return v, v, v
328
+
329
+ i = int(h*6.0)
330
+ f = (h*6.0) - i
331
+ p = v*(1.0 - s)
332
+ q = v*(1.0 - s*f)
333
+ t = v*(1.0 - s*(1.0-f))
334
+ i = i%6
335
+
336
+ return [(v, t, p), (q, v, p), (p, v, t), (p, q, v), (t, p, v), (v, p, q)][i]
337
+
338
+ ################################################################################
339
+
340
+ def generate_colors(n):
341
+ return [hsv_to_rgb(i/n, 1, 1) for i in range(n)]
342
+
343
+ ################################################################################
344
+
345
+ def add_arrays(a, b):
346
+ return [sum(pair) for pair in zip(a, b)]
347
+
348
+ ################################################################################
349
+
350
+ def calculate_similarities(lists_of_values, metric='cosine'):
351
+ return metrics.pairwise_distances(lists_of_values, metric=metric).tolist()
352
+
353
+ ################################################################################
354
+
355
+ def get_tokens_embeddings(x_transformer_model):
356
+ return x_transformer_model.net.token_emb.emb.weight.detach().cpu().tolist()
357
+
358
+ ################################################################################
359
+
360
+ def minkowski_distance_matrix(X, p=3):
361
+
362
+ X = np.array(X)
363
+
364
+ n = X.shape[0]
365
+ dist_matrix = np.zeros((n, n))
366
+
367
+ for i in range(n):
368
+ for j in range(n):
369
+ dist_matrix[i, j] = np.sum(np.abs(X[i] - X[j])**p)**(1/p)
370
+
371
+ return dist_matrix.tolist()
372
+
373
+ ################################################################################
374
+
375
+ def robust_normalize(values):
376
+
377
+ values = np.array(values)
378
+ q1 = np.percentile(values, 25)
379
+ q3 = np.percentile(values, 75)
380
+ iqr = q3 - q1
381
+
382
+ filtered_values = values[(values >= q1 - 1.5 * iqr) & (values <= q3 + 1.5 * iqr)]
383
+
384
+ min_val = np.min(filtered_values)
385
+ max_val = np.max(filtered_values)
386
+ normalized_values = (values - min_val) / (max_val - min_val)
387
+
388
+ normalized_values = np.clip(normalized_values, 0, 1)
389
+
390
+ return normalized_values.tolist()
391
+
392
+ ################################################################################
393
+
394
+ def min_max_normalize(values):
395
+
396
+ scaler = MinMaxScaler()
397
+
398
+ return scaler.fit_transform(values).tolist()
399
+
400
+ ################################################################################
401
+
402
+ def remove_points_outliers(points, z_score_threshold=3):
403
+
404
+ points = np.array(points)
405
+
406
+ z_scores = np.abs(zscore(points, axis=0))
407
+
408
+ return points[(z_scores < z_score_threshold).all(axis=1)].tolist()
409
+
410
+ ################################################################################
411
+
412
+ def generate_labels(lists_of_values,
413
+ return_indices_labels=False
414
+ ):
415
+
416
+ ordered_indices = list(range(len(lists_of_values)))
417
+ ordered_indices_labels = [str(i) for i in ordered_indices]
418
+ ordered_values_labels = [str(lists_of_values[i]) for i in ordered_indices]
419
+
420
+ if return_indices_labels:
421
+ return ordered_indices_labels
422
+
423
+ else:
424
+ return ordered_values_labels
425
+
426
+ ################################################################################
427
+
428
+ def reduce_dimensionality_pca(list_of_values, n_components=2):
429
+
430
+ """
431
+ Reduces the dimensionality of the values using PCA.
432
+ """
433
+
434
+ pca = PCA(n_components=n_components)
435
+ pca_data = pca.fit_transform(list_of_values)
436
+
437
+ return pca_data.tolist()
438
+
439
+ def reduce_dimensionality_simple(list_of_values,
440
+ return_means=True,
441
+ return_std_devs=True,
442
+ return_medians=False,
443
+ return_vars=False
444
+ ):
445
+
446
+ '''
447
+ Reduces dimensionality of the values in a simple way
448
+ '''
449
+
450
+ array = np.array(list_of_values)
451
+ results = []
452
+
453
+ if return_means:
454
+ means = np.mean(array, axis=1)
455
+ results.append(means)
456
+
457
+ if return_std_devs:
458
+ std_devs = np.std(array, axis=1)
459
+ results.append(std_devs)
460
+
461
+ if return_medians:
462
+ medians = np.median(array, axis=1)
463
+ results.append(medians)
464
+
465
+ if return_vars:
466
+ vars = np.var(array, axis=1)
467
+ results.append(vars)
468
+
469
+ merged_results = np.column_stack(results)
470
+
471
+ return merged_results.tolist()
472
+
473
+ ################################################################################
474
+
475
+ def reduce_dimensionality_2d_distance(list_of_values, p=5):
476
+
477
+ '''
478
+ Reduces the dimensionality of the values using 2d distance
479
+ '''
480
+
481
+ values = np.array(list_of_values)
482
+
483
+ dist_matrix = distance_matrix(values, values, p=p)
484
+
485
+ mst = minimum_spanning_tree(dist_matrix).toarray()
486
+
487
+ points = []
488
+
489
+ for i in range(len(values)):
490
+ for j in range(len(values)):
491
+ if mst[i, j] > 0:
492
+ points.append([i, j])
493
+
494
+ return points
495
+
496
+ ################################################################################
497
+
498
+ def normalize_to_range(values, n):
499
+
500
+ min_val = min(values)
501
+ max_val = max(values)
502
+
503
+ range_val = max_val - min_val
504
+
505
+ normalized_values = [((value - min_val) / range_val * 2 * n) - n for value in values]
506
+
507
+ return normalized_values
508
+
509
+ ################################################################################
510
+
511
+ def reduce_dimensionality_simple_pca(list_of_values, n_components=2):
512
+
513
+ '''
514
+ Reduces the dimensionality of the values using simple PCA
515
+ '''
516
+
517
+ reduced_values = []
518
+
519
+ for l in list_of_values:
520
+
521
+ norm_values = [round(v * len(l)) for v in normalize_to_range(l, (n_components+1) // 2)]
522
+
523
+ pca_values = Counter(norm_values).most_common()
524
+ pca_values = [vv[0] / len(l) for vv in pca_values]
525
+ pca_values = pca_values[:n_components]
526
+ pca_values = pca_values + [0] * (n_components - len(pca_values))
527
+
528
+ reduced_values.append(pca_values)
529
+
530
+ return reduced_values
531
+
532
+ ################################################################################
533
+
534
+ def filter_and_replace_values(list_of_values,
535
+ threshold,
536
+ replace_value,
537
+ replace_above_threshold=False
538
+ ):
539
+
540
+ array = np.array(list_of_values)
541
+
542
+ modified_array = np.copy(array)
543
+
544
+ if replace_above_threshold:
545
+ modified_array[modified_array > threshold] = replace_value
546
+
547
+ else:
548
+ modified_array[modified_array < threshold] = replace_value
549
+
550
+ return modified_array.tolist()
551
+
552
+ ################################################################################
553
+
554
+ def find_shortest_constellation_path(points,
555
+ start_point_idx,
556
+ end_point_idx,
557
+ p=5,
558
+ return_path_length=False,
559
+ return_path_points=False,
560
+ ):
561
+
562
+ """
563
+ Finds the shortest path between two points of the points constellation
564
+ """
565
+
566
+ points = np.array(points)
567
+
568
+ dist_matrix = distance_matrix(points, points, p=p)
569
+
570
+ mst = minimum_spanning_tree(dist_matrix).toarray()
571
+
572
+ G = nx.Graph()
573
+
574
+ for i in range(len(points)):
575
+ for j in range(len(points)):
576
+ if mst[i, j] > 0:
577
+ G.add_edge(i, j, weight=mst[i, j])
578
+
579
+ path = nx.shortest_path(G,
580
+ source=start_point_idx,
581
+ target=end_point_idx,
582
+ weight='weight'
583
+ )
584
+
585
+ path_length = nx.shortest_path_length(G,
586
+ source=start_point_idx,
587
+ target=end_point_idx,
588
+ weight='weight')
589
+
590
+ path_points = points[np.array(path)].tolist()
591
+
592
+
593
+ if return_path_points:
594
+ return path_points
595
+
596
+ if return_path_length:
597
+ return path_length
598
+
599
+ return path
600
+
601
+ ################################################################################
602
+ # Core functions
603
+ ################################################################################
604
+
605
+ def plot_ms_SONG(ms_song,
606
+ preview_length_in_notes=0,
607
+ block_lines_times_list = None,
608
+ plot_title='ms Song',
609
+ max_num_colors=129,
610
+ drums_color_num=128,
611
+ plot_size=(11,4),
612
+ note_height = 0.75,
613
+ show_grid_lines=False,
614
+ return_plt = False,
615
+ timings_multiplier=1,
616
+ save_plt='',
617
+ save_only_plt_image=True,
618
+ save_transparent=False
619
+ ):
620
+
621
+ '''ms SONG plot'''
622
+
623
+ notes = [s for s in ms_song if s[0] == 'note']
624
+
625
+ if (len(max(notes, key=len)) != 7) and (len(min(notes, key=len)) != 7):
626
+ print('The song notes do not have patches information')
627
+ print('Ploease add patches to the notes in the song')
628
+
629
+ else:
630
+
631
+ start_times = [(s[1] * timings_multiplier) / 1000 for s in notes]
632
+ durations = [(s[2] * timings_multiplier) / 1000 for s in notes]
633
+ pitches = [s[4] for s in notes]
634
+ patches = [s[6] for s in notes]
635
+
636
+ colors = generate_colors(max_num_colors)
637
+ colors[drums_color_num] = (1, 1, 1)
638
+
639
+ pbl = (notes[preview_length_in_notes][1] * timings_multiplier) / 1000
640
+
641
+ fig, ax = plt.subplots(figsize=plot_size)
642
+
643
+ for start, duration, pitch, patch in zip(start_times, durations, pitches, patches):
644
+ rect = plt.Rectangle((start, pitch), duration, note_height, facecolor=colors[patch])
645
+ ax.add_patch(rect)
646
+
647
+ ax.set_xlim([min(start_times), max(add_arrays(start_times, durations))])
648
+ ax.set_ylim([min(pitches)-1, max(pitches)+1])
649
+
650
+ ax.set_facecolor('black')
651
+ fig.patch.set_facecolor('white')
652
+
653
+ if preview_length_in_notes > 0:
654
+ ax.axvline(x=pbl, c='white')
655
+
656
+ if block_lines_times_list:
657
+ for bl in block_lines_times_list:
658
+ ax.axvline(x=bl, c='white')
659
+
660
+ if show_grid_lines:
661
+ ax.grid(color='white')
662
+
663
+ plt.xlabel('Time (s)', c='black')
664
+ plt.ylabel('MIDI Pitch', c='black')
665
+
666
+ plt.title(plot_title)
667
+
668
+ if save_plt != '':
669
+ if save_only_plt_image:
670
+ plt.axis('off')
671
+ plt.title('')
672
+ plt.savefig(save_plt,
673
+ transparent=save_transparent,
674
+ bbox_inches='tight',
675
+ pad_inches=0,
676
+ facecolor='black'
677
+ )
678
+ plt.close()
679
+
680
+ else:
681
+ plt.savefig(save_plt)
682
+ plt.close()
683
+
684
+ if return_plt:
685
+ return fig
686
+
687
+ plt.show()
688
+ plt.close()
689
+
690
+ ################################################################################
691
+
692
+ def plot_square_matrix_points(list_of_points,
693
+ list_of_points_colors,
694
+ plot_size=(7, 7),
695
+ point_size = 10,
696
+ show_grid_lines=False,
697
+ plot_title = 'Square Matrix Points Plot',
698
+ return_plt=False,
699
+ save_plt='',
700
+ save_only_plt_image=True,
701
+ save_transparent=False
702
+ ):
703
+
704
+ '''Square matrix points plot'''
705
+
706
+ fig, ax = plt.subplots(figsize=plot_size)
707
+
708
+ ax.set_facecolor('black')
709
+
710
+ if show_grid_lines:
711
+ ax.grid(color='white')
712
+
713
+ plt.xlabel('Time Step', c='black')
714
+ plt.ylabel('MIDI Pitch', c='black')
715
+
716
+ plt.title(plot_title)
717
+
718
+ plt.scatter([p[0] for p in list_of_points],
719
+ [p[1] for p in list_of_points],
720
+ c=list_of_points_colors,
721
+ s=point_size
722
+ )
723
+
724
+ if save_plt != '':
725
+ if save_only_plt_image:
726
+ plt.axis('off')
727
+ plt.title('')
728
+ plt.savefig(save_plt,
729
+ transparent=save_transparent,
730
+ bbox_inches='tight',
731
+ pad_inches=0,
732
+ facecolor='black'
733
+ )
734
+ plt.close()
735
+
736
+ else:
737
+ plt.savefig(save_plt)
738
+ plt.close()
739
+
740
+ if return_plt:
741
+ return fig
742
+
743
+ plt.show()
744
+ plt.close()
745
+
746
+ ################################################################################
747
+
748
+ def plot_cosine_similarities(lists_of_values,
749
+ plot_size=(7, 7),
750
+ save_plot=''
751
+ ):
752
+
753
+ """
754
+ Cosine similarities plot
755
+ """
756
+
757
+ cos_sim = metrics.pairwise_distances(lists_of_values, metric='cosine')
758
+
759
+ plt.figure(figsize=plot_size)
760
+
761
+ plt.imshow(cos_sim, cmap="inferno", interpolation="nearest")
762
+
763
+ im_ratio = cos_sim.shape[0] / cos_sim.shape[1]
764
+
765
+ plt.colorbar(fraction=0.046 * im_ratio, pad=0.04)
766
+
767
+ plt.xlabel("Index")
768
+ plt.ylabel("Index")
769
+
770
+ plt.tight_layout()
771
+
772
+ if save_plot != '':
773
+ plt.savefig(save_plot, bbox_inches="tight")
774
+ plt.close()
775
+
776
+ plt.show()
777
+ plt.close()
778
+
779
+ ################################################################################
780
+
781
+ def plot_points_with_mst_lines(points,
782
+ points_labels,
783
+ points_mst_edges,
784
+ plot_size=(20, 20),
785
+ labels_size=24,
786
+ save_plot=''
787
+ ):
788
+
789
+ """
790
+ Plots 2D points with labels and MST lines.
791
+ """
792
+
793
+ plt.figure(figsize=plot_size)
794
+
795
+ for i, label in enumerate(points_labels):
796
+ plt.scatter(points[i][0], points[i][1])
797
+ plt.annotate(label, (points[i][0], points[i][1]), fontsize=labels_size)
798
+
799
+ for edge in points_mst_edges:
800
+ i, j = edge
801
+ plt.plot([points[i][0], points[j][0]], [points[i][1], points[j][1]], 'k-', alpha=0.5)
802
+
803
+ plt.title('Points Map with MST Lines', fontsize=labels_size)
804
+ plt.xlabel('X-axis', fontsize=labels_size)
805
+ plt.ylabel('Y-axis', fontsize=labels_size)
806
+
807
+ if save_plot != '':
808
+ plt.savefig(save_plot, bbox_inches="tight")
809
+ plt.close()
810
+
811
+ plt.show()
812
+
813
+ plt.close()
814
+
815
+ ################################################################################
816
+
817
+ def plot_points_constellation(points,
818
+ points_labels,
819
+ p=5,
820
+ plot_size=(15, 15),
821
+ labels_size=12,
822
+ show_grid=False,
823
+ save_plot=''
824
+ ):
825
+
826
+ """
827
+ Plots 2D points constellation
828
+ """
829
+
830
+ points = np.array(points)
831
+
832
+ dist_matrix = distance_matrix(points, points, p=p)
833
+
834
+ mst = minimum_spanning_tree(dist_matrix).toarray()
835
+
836
+ plt.figure(figsize=plot_size)
837
+
838
+ plt.scatter(points[:, 0], points[:, 1], color='blue')
839
+
840
+ for i, label in enumerate(points_labels):
841
+ plt.annotate(label, (points[i, 0], points[i, 1]),
842
+ textcoords="offset points",
843
+ xytext=(0, 10),
844
+ ha='center',
845
+ fontsize=labels_size
846
+ )
847
+
848
+ for i in range(len(points)):
849
+ for j in range(len(points)):
850
+ if mst[i, j] > 0:
851
+ plt.plot([points[i, 0], points[j, 0]], [points[i, 1], points[j, 1]], 'k--')
852
+
853
+ plt.xlabel('X-axis', fontsize=labels_size)
854
+ plt.ylabel('Y-axis', fontsize=labels_size)
855
+ plt.title('2D Coordinates with Minimum Spanning Tree', fontsize=labels_size)
856
+
857
+ plt.grid(show_grid)
858
+
859
+ if save_plot != '':
860
+ plt.savefig(save_plot, bbox_inches="tight")
861
+ plt.close()
862
+
863
+ plt.show()
864
+
865
+ plt.close()
866
+
867
+ ################################################################################
868
+
869
+ def binary_matrix_to_images(matrix,
870
+ step,
871
+ overlap,
872
+ output_folder='./Dataset/',
873
+ output_img_prefix='image',
874
+ output_img_ext='.png',
875
+ save_to_array=False,
876
+ verbose=True
877
+ ):
878
+
879
+ if not save_to_array:
880
+
881
+ if verbose:
882
+ print('=' * 70)
883
+ print('Checking output folder dir...')
884
+
885
+ os.makedirs(os.path.dirname(output_folder), exist_ok=True)
886
+
887
+ if verbose:
888
+ print('Done!')
889
+
890
+ if verbose:
891
+ print('=' * 70)
892
+ print('Writing images...')
893
+
894
+ matrix = np.array(matrix, dtype=np.uint8)
895
+
896
+ image_array = []
897
+
898
+ for i in range(0, max(1, matrix.shape[0]), overlap):
899
+
900
+ submatrix = matrix[i:i+step, :]
901
+
902
+ if submatrix.shape[0] < 128:
903
+ zeros_array = np.zeros((128-submatrix.shape[0], 128))
904
+ submatrix = np.vstack((submatrix, zeros_array))
905
+
906
+ img = Image.fromarray(submatrix * 255).convert('1')
907
+
908
+ if save_to_array:
909
+ image_array.append(np.array(img))
910
+
911
+ else:
912
+ img.save(output_folder + output_img_prefix + '_' + str(matrix.shape[1]) + '_' + str(i).zfill(7) + output_img_ext)
913
+
914
+ if verbose:
915
+ print('Done!')
916
+ print('=' * 70)
917
+ print('Saved', (matrix.shape[0] // min(step, overlap))+1, 'imges!')
918
+ print('=' * 70)
919
+
920
+ if save_to_array:
921
+ return np.array(image_array).tolist()
922
+
923
+ ################################################################################
924
+
925
+ def images_to_binary_matrix(list_of_images):
926
+
927
+ image_array = np.array(list_of_images)
928
+
929
+ original_matrix = []
930
+
931
+ for img in image_array:
932
+
933
+ submatrix = np.array(img)
934
+ original_matrix.extend(submatrix.tolist())
935
+
936
+ return original_matrix
937
+
938
+ ################################################################################
939
+
940
+ def square_image_matrix(image_matrix,
941
+ matrix_size=128,
942
+ num_pca_components=5,
943
+ filter_out_zero_rows=False,
944
+ return_square_matrix_points=False
945
+ ):
946
+
947
+ """
948
+ Reduces an arbitrary image matrix to a square image matrix
949
+ """
950
+
951
+ matrix = np.array(image_matrix)
952
+
953
+ if filter_out_zero_rows:
954
+ matrix = matrix[~np.all(matrix == 0, axis=1)]
955
+
956
+ target_rows = matrix_size
957
+
958
+ rows_per_group = matrix.shape[0] // target_rows
959
+
960
+ compressed_matrix = np.zeros((target_rows, matrix.shape[1]), dtype=np.int32)
961
+
962
+ for i in range(target_rows):
963
+ start_row = i * rows_per_group
964
+ end_row = (i + 1) * rows_per_group
965
+ group = matrix[start_row:end_row, :]
966
+
967
+ pca = PCA(n_components=num_pca_components)
968
+ pca.fit(group)
969
+
970
+ principal_component = np.mean(pca.components_, axis=0)
971
+ contributions = np.dot(group, principal_component)
972
+ selected_row_index = np.argmax(contributions)
973
+
974
+ compressed_matrix[i, :] = group[selected_row_index, :]
975
+
976
+ if return_square_matrix_points:
977
+ filtered_matrix = compressed_matrix[~np.all(compressed_matrix == 0, axis=1)]
978
+
979
+ row_indexes, col_indexes = np.where(filtered_matrix != 0)
980
+ points = np.column_stack((row_indexes, filtered_matrix[row_indexes, col_indexes])).tolist()
981
+
982
+ return points
983
+
984
+ else:
985
+ return compressed_matrix.tolist()
986
+
987
+ ################################################################################
988
+
989
+ def image_matrix_to_images(image_matrix,
990
+ step,
991
+ overlap,
992
+ num_img_channels=3,
993
+ output_folder='./Dataset/',
994
+ output_img_prefix='image',
995
+ output_img_ext='.png',
996
+ save_to_array=False,
997
+ verbose=True
998
+ ):
999
+
1000
+ if num_img_channels > 1:
1001
+ n_mat_channels = 3
1002
+
1003
+ else:
1004
+ n_mat_channels = 1
1005
+
1006
+ if not save_to_array:
1007
+
1008
+ if verbose:
1009
+ print('=' * 70)
1010
+ print('Checking output folder dir...')
1011
+
1012
+ os.makedirs(os.path.dirname(output_folder), exist_ok=True)
1013
+
1014
+ if verbose:
1015
+ print('Done!')
1016
+
1017
+ if verbose:
1018
+ print('=' * 70)
1019
+ print('Writing images...')
1020
+
1021
+ matrix = np.array(image_matrix)
1022
+
1023
+ image_array = []
1024
+
1025
+ for i in range(0, max(1, matrix.shape[0]), overlap):
1026
+
1027
+ submatrix = matrix[i:i+step, :]
1028
+
1029
+ if submatrix.shape[0] < 128:
1030
+ zeros_array = np.zeros((128-submatrix.shape[0], 128))
1031
+ submatrix = np.vstack((submatrix, zeros_array))
1032
+
1033
+ if n_mat_channels == 3:
1034
+
1035
+ r = (submatrix // (256*256)) % 256
1036
+ g = (submatrix // 256) % 256
1037
+ b = submatrix % 256
1038
+
1039
+ rgb_image = np.stack((r, g, b), axis=-1).astype(np.uint8)
1040
+ img = Image.fromarray(rgb_image, 'RGB')
1041
+
1042
+ else:
1043
+ grayscale_image = submatrix.astype(np.uint8)
1044
+ img = Image.fromarray(grayscale_image, 'L')
1045
+
1046
+ if save_to_array:
1047
+ image_array.append(np.array(img))
1048
+
1049
+ else:
1050
+ img.save(output_folder + output_img_prefix + '_' + str(matrix.shape[1]) + '_' + str(i).zfill(7) + output_img_ext)
1051
+
1052
+ if verbose:
1053
+ print('Done!')
1054
+ print('=' * 70)
1055
+ print('Saved', (matrix.shape[0] // min(step, overlap))+1, 'imges!')
1056
+ print('=' * 70)
1057
+
1058
+ if save_to_array:
1059
+ return np.array(image_array).tolist()
1060
+
1061
+ ################################################################################
1062
+
1063
+ def images_to_image_matrix(list_of_images,
1064
+ num_img_channels=3
1065
+ ):
1066
+
1067
+ if num_img_channels > 1:
1068
+ n_mat_channels = 3
1069
+
1070
+ else:
1071
+ n_mat_channels = 1
1072
+
1073
+ image_array = np.array(list_of_images)
1074
+
1075
+ original_matrix = []
1076
+
1077
+ for img in image_array:
1078
+
1079
+ if num_img_channels == 3:
1080
+
1081
+ rgb_array = np.array(img)
1082
+
1083
+ matrix = (rgb_array[..., 0].astype(np.int64) * 256*256 +
1084
+ rgb_array[..., 1].astype(np.int64) * 256 +
1085
+ rgb_array[..., 2].astype(np.int64))
1086
+
1087
+ else:
1088
+ matrix = np.array(img)
1089
+
1090
+ original_matrix.extend(matrix)
1091
+
1092
+ return original_matrix
1093
+
1094
+ ################################################################################
1095
+
1096
+ def square_matrix_to_RGB_matrix(square_matrix):
1097
+
1098
+ smatrix = np.array(square_matrix)
1099
+ sq_matrix = smatrix[:smatrix.shape[1]]
1100
+
1101
+ r = (sq_matrix // (256 ** 2)) % 256
1102
+ g = (sq_matrix // 256) % 256
1103
+ b = sq_matrix % 256
1104
+
1105
+ rgb_array = np.stack((r, g, b), axis=-1)
1106
+
1107
+ return rgb_array.tolist()
1108
+
1109
+ ################################################################################
1110
+
1111
+ def upsample_square_matrix(square_matrix, upsampling_factor=4):
1112
+
1113
+ smatrix = np.array(square_matrix)
1114
+ sq_matrix = smatrix[:smatrix.shape[1]]
1115
+
1116
+ scaling_array = np.ones((upsampling_factor, upsampling_factor))
1117
+ scaled_array = np.kron(sq_matrix, scaling_array)
1118
+ scaled_array = scaled_array.astype('int')
1119
+
1120
+ return scaled_array.tolist()
1121
+
1122
+ ################################################################################
1123
+
1124
+ def downsample_square_matrix(square_matrix, downsampling_factor=4):
1125
+
1126
+ smatrix = np.array(square_matrix)
1127
+ sq_matrix = smatrix[:smatrix.shape[1]]
1128
+
1129
+ dmatrix = sq_matrix[::downsampling_factor, ::downsampling_factor]
1130
+ dmatrix = dmatrix.astype('int')
1131
+
1132
+ return dmatrix.tolist()
1133
+
1134
+ ################################################################################
1135
+
1136
+ def plot_parsons_code(parsons_code,
1137
+ start_pitch=60,
1138
+ return_plot_dict=False,
1139
+ return_plot_string=False,
1140
+ plot_size=(10, 10),
1141
+ labels_size=16,
1142
+ save_plot=''
1143
+ ):
1144
+
1145
+ '''
1146
+ Plot parsons code string
1147
+ '''
1148
+
1149
+ if parsons_code[0] != "*":
1150
+ return None
1151
+
1152
+ contour_dict = {}
1153
+ pitch = 0
1154
+ index = 0
1155
+
1156
+ maxp = 0
1157
+ minp = 0
1158
+
1159
+ contour_dict[(pitch, index)] = "*"
1160
+
1161
+ for point in parsons_code:
1162
+ if point == "R":
1163
+ index += 1
1164
+ contour_dict[(pitch, index)] = "-"
1165
+
1166
+ index += 1
1167
+ contour_dict[(pitch, index)] = "*"
1168
+
1169
+ elif point == "U":
1170
+ index += 1
1171
+ pitch -= 1
1172
+ contour_dict[(pitch, index)] = "/"
1173
+
1174
+ index += 1
1175
+ pitch -= 1
1176
+ contour_dict[(pitch, index)] = "*"
1177
+
1178
+ if pitch < maxp:
1179
+ maxp = pitch
1180
+
1181
+ elif point == "D":
1182
+ index += 1
1183
+ pitch += 1
1184
+ contour_dict[(pitch, index)] = "\\"
1185
+
1186
+ index += 1
1187
+ pitch += 1
1188
+ contour_dict[(pitch, index)] = "*"
1189
+
1190
+ if pitch > minp:
1191
+ minp = pitch
1192
+
1193
+ if return_plot_dict:
1194
+ return contour_dict
1195
+
1196
+ if return_plot_string:
1197
+
1198
+ plot_string = ''
1199
+
1200
+ for pitch in range(maxp, minp+1):
1201
+ line = [" " for _ in range(index + 1)]
1202
+ for pos in range(index + 1):
1203
+ if (pitch, pos) in contour_dict:
1204
+ line[pos] = contour_dict[(pitch, pos)]
1205
+
1206
+ plot_string = "".join(line)
1207
+
1208
+ return plot_string
1209
+
1210
+ labels = []
1211
+ pitches = []
1212
+ positions = []
1213
+ cur_pitch = start_pitch
1214
+ pitch_idx = 0
1215
+
1216
+ for k, v in contour_dict.items():
1217
+
1218
+ if v != '*':
1219
+
1220
+ pitches.append(cur_pitch)
1221
+ positions.append(pitch_idx)
1222
+
1223
+ if v == '/':
1224
+ cur_pitch += 1
1225
+ labels.append('U')
1226
+
1227
+ elif v == '\\':
1228
+ cur_pitch -= 1
1229
+ labels.append('D')
1230
+
1231
+ elif v == '-':
1232
+ labels.append('R')
1233
+
1234
+ pitch_idx += 1
1235
+
1236
+ plt.figure(figsize=plot_size)
1237
+
1238
+
1239
+ plt.plot(pitches)
1240
+
1241
+ for i, point in enumerate(zip(positions, pitches)):
1242
+ plt.annotate(labels[i], point, fontsize=labels_size)
1243
+
1244
+
1245
+ plt.title('Parsons Code with Labels', fontsize=labels_size)
1246
+ plt.xlabel('Position', fontsize=labels_size)
1247
+ plt.ylabel('Pitch', fontsize=labels_size)
1248
+
1249
+ if save_plot != '':
1250
+ plt.savefig(save_plot, bbox_inches="tight")
1251
+ plt.close()
1252
+
1253
+ plt.show()
1254
+
1255
+ plt.close()
1256
+
1257
+ ################################################################################
1258
+
1259
+ def plot_tokens_embeddings_constellation(tokens_embeddings,
1260
+ start_token,
1261
+ end_token,
1262
+ plot_size=(10, 10),
1263
+ labels_size=12,
1264
+ show_grid=False,
1265
+ save_plot=''):
1266
+
1267
+ """
1268
+ Plots token embeddings constellation using MST and graph layout
1269
+ without dimensionality reduction.
1270
+ """
1271
+
1272
+ distance_matrix = metrics.pairwise_distances(tokens_embeddings[start_token:end_token], metric='cosine')
1273
+
1274
+ token_labels = [str(i) for i in range(start_token, end_token)]
1275
+
1276
+ mst = minimum_spanning_tree(distance_matrix).toarray()
1277
+
1278
+ n = distance_matrix.shape[0]
1279
+ G = nx.Graph()
1280
+
1281
+ for i in range(n):
1282
+ for j in range(n):
1283
+ if mst[i, j] > 0:
1284
+ weight = 1 / (distance_matrix[i, j] + 1e-8)
1285
+ G.add_edge(i, j, weight=weight)
1286
+
1287
+ pos = nx.kamada_kawai_layout(G, weight='weight')
1288
+
1289
+ points = np.array([pos[i] for i in range(n)])
1290
+
1291
+ plt.figure(figsize=plot_size)
1292
+ plt.scatter(points[:, 0], points[:, 1], color='blue')
1293
+
1294
+ for i, label in enumerate(token_labels):
1295
+ plt.annotate(label, (points[i, 0], points[i, 1]),
1296
+ textcoords="offset points",
1297
+ xytext=(0, 10),
1298
+ ha='center',
1299
+ fontsize=labels_size)
1300
+
1301
+ for i in range(n):
1302
+ for j in range(n):
1303
+ if mst[i, j] > 0:
1304
+ plt.plot([points[i, 0], points[j, 0]],
1305
+ [points[i, 1], points[j, 1]],
1306
+ 'k--', alpha=0.5)
1307
+
1308
+ plt.title('Token Embeddings Constellation with MST', fontsize=labels_size)
1309
+ plt.grid(show_grid)
1310
+
1311
+ if save_plot:
1312
+ plt.savefig(save_plot, bbox_inches="tight")
1313
+ plt.close()
1314
+
1315
+ else:
1316
+ plt.show()
1317
+
1318
+ plt.close()
1319
+
1320
+ ################################################################################
1321
+
1322
+ def find_token_path(tokens_embeddings,
1323
+ start_token,
1324
+ end_token,
1325
+ verbose=False
1326
+ ):
1327
+
1328
+ """
1329
+ Finds the path of tokens between start_token and end_token using
1330
+ the Minimum Spanning Tree (MST) derived from the distance matrix.
1331
+ """
1332
+
1333
+ distance_matrix = metrics.pairwise_distances(tokens_embeddings, metric='cosine')
1334
+
1335
+ token_labels = [str(i) for i in range(len(distance_matrix))]
1336
+
1337
+ if verbose:
1338
+ print('Total number of tokens:', len(distance_matrix))
1339
+
1340
+ mst = minimum_spanning_tree(distance_matrix).toarray()
1341
+
1342
+ n = distance_matrix.shape[0]
1343
+ G = nx.Graph()
1344
+
1345
+ for i in range(n):
1346
+ for j in range(n):
1347
+ if mst[i, j] > 0:
1348
+ weight = 1 / (distance_matrix[i, j] + 1e-8)
1349
+ G.add_edge(i, j, weight=weight)
1350
+
1351
+ try:
1352
+ start_idx = token_labels.index(str(start_token))
1353
+ end_idx = token_labels.index(str(end_token))
1354
+
1355
+ except ValueError:
1356
+ raise ValueError("Start or end token not found in the provided token labels.")
1357
+
1358
+ path_indices = nx.shortest_path(G, source=start_idx, target=end_idx)
1359
+
1360
+ token_path = [int(token_labels[idx]) for idx in path_indices]
1361
+
1362
+ return token_path
1363
+
1364
+ ################################################################################
1365
+ # [WIP] Future dev functions
1366
+ ################################################################################
1367
+
1368
+ '''
1369
+ import umap
1370
+
1371
+ def reduce_dimensionality_umap(list_of_values,
1372
+ n_comp=2,
1373
+ n_neighbors=15,
1374
+ ):
1375
+
1376
+ """
1377
+ Reduces the dimensionality of the values using UMAP.
1378
+ """
1379
+
1380
+ vals = np.array(list_of_values)
1381
+
1382
+ umap_reducer = umap.UMAP(n_components=n_comp,
1383
+ n_neighbors=n_neighbors,
1384
+ n_epochs=5000,
1385
+ verbose=True
1386
+ )
1387
+
1388
+ reduced_vals = umap_reducer.fit_transform(vals)
1389
+
1390
+ return reduced_vals.tolist()
1391
+ '''
1392
+
1393
+ ################################################################################
1394
+
1395
+ '''
1396
+ import alphashape
1397
+ from shapely.geometry import Point
1398
+ from matplotlib.tri import Triangulation, LinearTriInterpolator
1399
+ from scipy.stats import zscore
1400
+
1401
+ #===============================================================================
1402
+
1403
+ coordinates = points
1404
+
1405
+ dist_matrix = minkowski_distance_matrix(coordinates, p=3) # You can change the value of p as needed
1406
+
1407
+ # Centering matrix
1408
+ n = dist_matrix.shape[0]
1409
+ H = np.eye(n) - np.ones((n, n)) / n
1410
+
1411
+ # Apply double centering
1412
+ B = -0.5 * H @ dist_matrix**2 @ H
1413
+
1414
+ # Eigen decomposition
1415
+ eigvals, eigvecs = np.linalg.eigh(B)
1416
+
1417
+ # Sort eigenvalues and eigenvectors
1418
+ idx = np.argsort(eigvals)[::-1]
1419
+ eigvals = eigvals[idx]
1420
+ eigvecs = eigvecs[:, idx]
1421
+
1422
+ # Select the top 2 eigenvectors
1423
+ X_transformed = eigvecs[:, :2] * np.sqrt(eigvals[:2])
1424
+
1425
+ #===============================================================================
1426
+
1427
+ src_points = X_transformed
1428
+ src_values = np.array([[p[1]] for p in points]) #np.random.rand(X_transformed.shape[0])
1429
+
1430
+ #===============================================================================
1431
+
1432
+ # Normalize the points to the range [0, 1]
1433
+ scaler = MinMaxScaler()
1434
+ points_normalized = scaler.fit_transform(src_points)
1435
+
1436
+ values_normalized = custom_normalize(src_values)
1437
+
1438
+ # Remove outliers based on z-score
1439
+ z_scores = np.abs(zscore(points_normalized, axis=0))
1440
+ filtered_points = points_normalized[(z_scores < 3).all(axis=1)]
1441
+ filtered_values = values_normalized[(z_scores < 3).all(axis=1)]
1442
+
1443
+ # Compute the concave hull (alpha shape)
1444
+ alpha = 8 # Adjust alpha as needed
1445
+ hull = alphashape.alphashape(filtered_points, alpha)
1446
+
1447
+ # Create a triangulation
1448
+ tri = Triangulation(filtered_points[:, 0], filtered_points[:, 1])
1449
+
1450
+ # Interpolate the values on the triangulation
1451
+ interpolator = LinearTriInterpolator(tri, filtered_values[:, 0])
1452
+ xi, yi = np.meshgrid(np.linspace(0, 1, 100), np.linspace(0, 1, 100))
1453
+ zi = interpolator(xi, yi)
1454
+
1455
+ # Mask out points outside the concave hull
1456
+ mask = np.array([hull.contains(Point(x, y)) for x, y in zip(xi.flatten(), yi.flatten())])
1457
+ zi = np.ma.array(zi, mask=~mask.reshape(zi.shape))
1458
+
1459
+ # Plot the filled contour based on the interpolated values
1460
+ plt.contourf(xi, yi, zi, levels=50, cmap='viridis')
1461
+
1462
+ # Plot the original points
1463
+ #plt.scatter(filtered_points[:, 0], filtered_points[:, 1], c=filtered_values, edgecolors='k')
1464
+
1465
+ plt.title('Filled Contour Plot with Original Values')
1466
+ plt.xlabel('X-axis')
1467
+ plt.ylabel('Y-axis')
1468
+ plt.colorbar(label='Value')
1469
+ plt.show()
1470
+ '''
1471
+
1472
+ ################################################################################
1473
+
1474
+ def plot_tree_horizontal(data):
1475
+
1476
+ """
1477
+ Given data as a list of levels (each level is a tuple or list of
1478
+ displacements for each branch), this function computes the cumulative
1479
+ value per branch (starting from 0) and plots each branch
1480
+ with the tree level mapped to the x-axis and the cumulative value mapped
1481
+ to the y-axis. This gives a left-to-right tree with branches spanning up
1482
+ (positive) and down (negative).
1483
+
1484
+ Parameters:
1485
+ data (list of tuple/list): Each element represents a tree level.
1486
+ It is assumed every level has the same length.
1487
+ """
1488
+
1489
+ # Convert data to a NumPy array with shape (n_levels, n_branches)
1490
+ data = np.array(data)
1491
+ n_levels, n_branches = data.shape
1492
+
1493
+ # Compute cumulative sums along each branch.
1494
+ # Each branch starts at 0 at level 0.
1495
+ cum = np.zeros((n_levels + 1, n_branches))
1496
+ for i in range(n_levels):
1497
+ cum[i + 1, :] = cum[i, :] + data[i, :]
1498
+
1499
+ plt.figure(figsize=(12, 8))
1500
+
1501
+ # Plot each branch as a line. For branch j:
1502
+ # - x coordinates are the tree levels (0 to n_levels)
1503
+ # - y coordinates are the corresponding cumulative values.
1504
+ for j in range(n_branches):
1505
+ x = np.arange(n_levels + 1)
1506
+ y = cum[:, j]
1507
+ plt.plot(x, y, marker='o', label=f'Branch {j}')
1508
+
1509
+ plt.title("Horizontal Tree Visualization: Branches Spanning Up and Down", fontsize=14)
1510
+ plt.xlabel("Tree Level (Left = Root)")
1511
+ plt.ylabel("Cumulative Value (Up = Positive, Down = Negative)")
1512
+
1513
+ # Add a horizontal line at y=0 to emphasize the center.
1514
+ plt.axhline(0, color="gray", linestyle="--")
1515
+
1516
+ #plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
1517
+ plt.tight_layout()
1518
+ plt.show()
1519
+
1520
+ ################################################################################
1521
+ # This is the end of TPLOTS Python modules
1522
  ################################################################################
src/midi_to_colab_audio.py ADDED
@@ -0,0 +1,475 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ r'''#===================================================================================================================
2
+ #
3
+ # MIDI to Colab AUdio Python Module
4
+ #
5
+ # Converts any MIDI file to raw audio which is compatible
6
+ # with Google Colab or HUgging Face Gradio
7
+ #
8
+ # Version 2.0
9
+ #
10
+ # Includes full source code of MIDI and pyfluidsynth
11
+ #
12
+ # Original source code for all modules was retrieved on 07/31/2025
13
+ #
14
+ # Project Los Angeles
15
+ # Tegridy Code 2025
16
+ #
17
+ #===================================================================================================================
18
+ #
19
+ # Critical dependencies
20
+ #
21
+ # pip install numpy
22
+ # sudo apt install fluidsynth
23
+ #
24
+ #===================================================================================================================
25
+ #
26
+ # Example usage:
27
+ #
28
+ # from midi_to_colab_audio import midi_to_colab_audio
29
+ # from IPython.display import display, Audio
30
+ #
31
+ # raw_audio = midi_to_colab_audio('/content/input.mid')
32
+ #
33
+ # display(Audio(raw_audio, rate=16000, normalize=False))
34
+ #
35
+ #===================================================================================================================
36
+ '''
37
+
38
+ import fluidsynth
39
+ from src import MIDI
40
+
41
+ #===============================================================================
42
+
43
+ import numpy as np
44
+ import wave
45
+
46
+ #===============================================================================
47
+
48
+ def normalize_audio(audio: np.ndarray,
49
+ method: str = 'peak',
50
+ target_level_db: float = -1.0,
51
+ per_channel: bool = False,
52
+ eps: float = 1e-9
53
+ ) -> np.ndarray:
54
+
55
+ """
56
+ Normalize audio to a target dBFS level.
57
+
58
+ Parameters
59
+ ----------
60
+ audio : np.ndarray
61
+ Float-valued array in range [-1, 1] with shape (channels, samples)
62
+ or (samples,) for mono.
63
+ method : {'peak', 'rms'}
64
+ - 'peak': scale so that max(|audio|) = target_level_lin
65
+ - 'rms' : scale so that RMS(audio) = target_level_lin
66
+ target_level_db : float
67
+ Desired output level, in dBFS (0 dBFS = max digital full scale).
68
+ e.g. -1.0 dBFS means ~0.8913 linear gain.
69
+ per_channel : bool
70
+ If True, normalize each channel independently. Otherwise, use a
71
+ global measure across all channels.
72
+ eps : float
73
+ Small constant to avoid division by zero.
74
+
75
+ Returns
76
+ -------
77
+ normalized : np.ndarray
78
+ Audio array of same shape, scaled so that levels meet the target.
79
+ """
80
+
81
+ # Convert target dB to linear gain
82
+ target_lin = 10 ** (target_level_db / 20.0)
83
+
84
+ # Ensure audio is float
85
+ audio = audio.astype(np.float32)
86
+
87
+ # if mono, make it (1, N)
88
+ if audio.ndim == 1:
89
+ audio = audio[np.newaxis, :]
90
+
91
+ # Choose measurement axis
92
+ axis = 1 if per_channel else None
93
+
94
+ if method == 'peak':
95
+ # Compute peak per channel or global
96
+ peak = np.max(np.abs(audio), axis=axis, keepdims=True)
97
+ peak = np.maximum(peak, eps)
98
+ scales = target_lin / peak
99
+
100
+ elif method == 'rms':
101
+ # Compute RMS per channel or global
102
+ rms = np.sqrt(np.mean(audio ** 2, axis=axis, keepdims=True))
103
+ rms = np.maximum(rms, eps)
104
+ scales = target_lin / rms
105
+
106
+ else:
107
+ raise ValueError(f"Unsupported method '{method}'; choose 'peak' or 'rms'.")
108
+
109
+ # Broadcast scales back to audio shape
110
+ normalized = audio * scales
111
+
112
+ # Clip just in case of rounding
113
+ return np.clip(normalized, -1.0, 1.0)
114
+
115
+ #===============================================================================
116
+
117
+ def midi_opus_to_colab_audio(midi_opus,
118
+ soundfont_path='/usr/share/sounds/sf2/FluidR3_GM.sf2',
119
+ sample_rate=16000, # 44100
120
+ volume_level_db=-1,
121
+ trim_silence=True,
122
+ silence_threshold=0.1,
123
+ output_for_gradio=False,
124
+ write_audio_to_WAV=''
125
+ ):
126
+
127
+ if midi_opus[1]:
128
+
129
+ ticks_per_beat, *tracks = midi_opus
130
+ if not tracks:
131
+ return None
132
+
133
+ # Flatten & convert delta-times to absolute-time
134
+ events = []
135
+ for track in tracks:
136
+ abs_t = 0
137
+ for name, dt, *data in track:
138
+ abs_t += dt
139
+ events.append([name, abs_t, *data])
140
+ events.sort(key=lambda e: e[1])
141
+
142
+ # Setup FluidSynth
143
+ fl = fluidsynth.Synth(samplerate=float(sample_rate))
144
+ sfid = fl.sfload(soundfont_path)
145
+ for chan in range(16):
146
+ # channel 9 = percussion GM bank 128
147
+ fl.program_select(chan, sfid, 128 if chan == 9 else 0, 0)
148
+
149
+ # Playback vars
150
+ tempo = int((60 / 120) * 1e6) # default 120bpm
151
+ last_t = 0
152
+ ss = np.empty((0, 2), dtype=np.int16)
153
+
154
+ for name, cur_t, *data in events:
155
+ # compute how many samples have passed since the last event
156
+ delta_ticks = cur_t - last_t
157
+ last_t = cur_t
158
+ dt_seconds = (delta_ticks / ticks_per_beat) * (tempo / 1e6)
159
+ sample_len = int(dt_seconds * sample_rate)
160
+ if sample_len > 0:
161
+ buf = fl.get_samples(sample_len).reshape(-1, 2)
162
+ ss = np.concatenate([ss, buf], axis=0)
163
+
164
+ # Dispatch every known event
165
+ if name == "note_on" and data[2] > 0:
166
+ chan, note, vel = data
167
+ fl.noteon(chan, note, vel)
168
+
169
+ elif name == "note_off" or (name == "note_on" and data[2] == 0):
170
+ chan, note = data[:2]
171
+ fl.noteoff(chan, note)
172
+
173
+ elif name == "patch_change":
174
+ chan, patch = data[:2]
175
+ bank = 128 if chan == 9 else 0
176
+ fl.program_select(chan, sfid, bank, patch)
177
+
178
+ elif name == "control_change":
179
+ chan, ctrl, val = data[:3]
180
+ fl.cc(chan, ctrl, val)
181
+
182
+ elif name == "key_after_touch":
183
+ chan, note, vel = data
184
+ fl.key_pressure(chan, note, vel)
185
+
186
+ elif name == "channel_after_touch":
187
+ chan, vel = data
188
+ fl.channel_pressure(chan, vel)
189
+
190
+ elif name == "pitch_wheel_change":
191
+ chan, wheel = data
192
+ fl.pitch_bend(chan, wheel)
193
+
194
+ elif name == "song_position":
195
+ # song_pos = data[0]; # often not needed for playback
196
+ pass
197
+
198
+ elif name == "song_select":
199
+ # song_number = data[0]
200
+ pass
201
+
202
+ elif name == "tune_request":
203
+ # typically resets tuning; FS handles internally
204
+ pass
205
+
206
+ elif name in ("sysex_f0", "sysex_f7"):
207
+ raw_bytes = data[0]
208
+ fl.sysex(raw_bytes)
209
+
210
+ # Meta events & others—no direct audio effect, so we skip or log
211
+ elif name in (
212
+ "set_tempo", # handled below
213
+ "end_track",
214
+ "text_event", "text_event_08", "text_event_09", "text_event_0a",
215
+ "text_event_0b", "text_event_0c", "text_event_0d", "text_event_0e", "text_event_0f",
216
+ "copyright_text_event", "track_name", "instrument_name",
217
+ "lyric", "marker", "cue_point",
218
+ "smpte_offset", "time_signature", "key_signature",
219
+ "sequencer_specific", "raw_meta_event"
220
+ ):
221
+ if name == "set_tempo":
222
+ tempo = data[0]
223
+ # else: skip all other meta & text; you could hook in logging here
224
+ continue
225
+
226
+ else:
227
+ # unknown event type
228
+ continue
229
+
230
+ # Cleanup synth
231
+ fl.delete()
232
+
233
+ if ss.size:
234
+ maxv = np.abs(ss).max()
235
+ if maxv:
236
+ ss = (ss / maxv) * np.iinfo(np.int16).max
237
+ ss = ss.astype(np.int16)
238
+
239
+ # Optional trimming of trailing silence
240
+ if trim_silence and ss.size:
241
+ thresh = np.std(np.abs(ss)) * silence_threshold
242
+ idx = np.where(np.abs(ss) > thresh)[0]
243
+ if idx.size:
244
+ ss = ss[: idx[-1] + 1]
245
+
246
+ # For Gradio you might want raw int16 PCM
247
+ if output_for_gradio:
248
+ return ss
249
+
250
+ # Swap to (channels, samples) and normalize for playback
251
+ ss = ss.T
252
+ raw_audio = normalize_audio(ss, target_level_db=volume_level_db)
253
+
254
+ # Optionally write WAV to disk
255
+ if write_audio_to_WAV:
256
+ wav_name = midi_file.rsplit('.', 1)[0] + '.wav'
257
+ pcm = np.int16(raw_audio.T / np.max(np.abs(raw_audio)) * 32767)
258
+ with wave.open(wav_name, 'wb') as wf:
259
+ wf.setframerate(sample_rate)
260
+ wf.setsampwidth(2)
261
+ wf.setnchannels(pcm.shape[1])
262
+ wf.writeframes(pcm.tobytes())
263
+
264
+ return raw_audio
265
+
266
+ else:
267
+ return None
268
+
269
+ #===============================================================================
270
+
271
+ def midi_to_colab_audio(midi_file,
272
+ soundfont_path='/usr/share/sounds/sf2/FluidR3_GM.sf2',
273
+ sample_rate=16000,
274
+ volume_level_db=-1,
275
+ trim_silence=True,
276
+ silence_threshold=0.1,
277
+ output_for_gradio=False,
278
+ write_audio_to_WAV=False
279
+ ):
280
+ """
281
+ Returns raw audio to pass to IPython.disaply.Audio func
282
+
283
+ Example usage:
284
+
285
+ from IPython.display import Audio
286
+
287
+ display(Audio(raw_audio, rate=16000, normalize=False))
288
+ """
289
+
290
+ # Check if midi_input is a path (string) or file content (bytes)
291
+ if isinstance(midi_file, str):
292
+ # It's a file path, open and read it.
293
+ try:
294
+ with open(midi_file, 'rb') as f:
295
+ midi_bytes = f.read()
296
+ except FileNotFoundError:
297
+ print(f"Error: Could not find or open the file at {midi_file}")
298
+ return None # Or handle the error appropriately
299
+ elif isinstance(midi_file, bytes):
300
+ # It's already the file content.
301
+ midi_bytes = midi_file
302
+ else:
303
+ raise TypeError("midi_input must be a file path (str) or file content (bytes)")
304
+
305
+ # Read and decode MIDI → opus event list from bytes
306
+ ticks_per_beat, *tracks = MIDI.midi2opus(midi_bytes)
307
+ if not tracks:
308
+ return None
309
+
310
+ # Flatten & convert delta-times to absolute-time
311
+ events = []
312
+ for track in tracks:
313
+ abs_t = 0
314
+ for name, dt, *data in track:
315
+ abs_t += dt
316
+ events.append([name, abs_t, *data])
317
+ events.sort(key=lambda e: e[1])
318
+
319
+ # Setup FluidSynth
320
+ fl = fluidsynth.Synth(samplerate=float(sample_rate))
321
+ sfid = fl.sfload(soundfont_path)
322
+ for chan in range(16):
323
+ # channel 9 = percussion GM bank 128
324
+ fl.program_select(chan, sfid, 128 if chan == 9 else 0, 0)
325
+
326
+ # Playback vars
327
+ tempo = int((60 / 120) * 1e6) # default 120bpm
328
+ last_t = 0
329
+
330
+ # Initialize a Python list to store audio chunks
331
+ audio_chunks = []
332
+
333
+ for name, cur_t, *data in events:
334
+ # compute how many samples have passed since the last event
335
+ delta_ticks = cur_t - last_t
336
+ last_t = cur_t
337
+ dt_seconds = (delta_ticks / ticks_per_beat) * (tempo / 1e6)
338
+ sample_len = int(dt_seconds * sample_rate)
339
+
340
+ if sample_len > 0:
341
+ buf = fl.get_samples(sample_len).reshape(-1, 2)
342
+ # Append the audio chunk to the list
343
+ audio_chunks.append(buf)
344
+
345
+ # Dispatch every known event
346
+ if name == "note_on" and data[2] > 0:
347
+ chan, note, vel = data
348
+ fl.noteon(chan, note, vel)
349
+
350
+ elif name == "note_off" or (name == "note_on" and data[2] == 0):
351
+ chan, note = data[:2]
352
+ fl.noteoff(chan, note)
353
+
354
+ elif name == "patch_change":
355
+ chan, patch = data[:2]
356
+ bank = 128 if chan == 9 else 0
357
+ fl.program_select(chan, sfid, bank, patch)
358
+
359
+ elif name == "control_change":
360
+ chan, ctrl, val = data[:3]
361
+ fl.cc(chan, ctrl, val)
362
+
363
+ elif name == "key_after_touch":
364
+ chan, note, vel = data
365
+ fl.key_pressure(chan, note, vel)
366
+
367
+ elif name == "channel_after_touch":
368
+ chan, vel = data
369
+ fl.channel_pressure(chan, vel)
370
+
371
+ elif name == "pitch_wheel_change":
372
+ chan, wheel = data
373
+ fl.pitch_bend(chan, wheel)
374
+
375
+ elif name == "song_position":
376
+ # song_pos = data[0]; # often not needed for playback
377
+ pass
378
+
379
+ elif name == "song_select":
380
+ # song_number = data[0]
381
+ pass
382
+
383
+ elif name == "tune_request":
384
+ # typically resets tuning; FS handles internally
385
+ pass
386
+
387
+ elif name in ("sysex_f0", "sysex_f7"):
388
+ raw_bytes = data[0]
389
+ fl.sysex(raw_bytes)
390
+
391
+ # Meta events & others—no direct audio effect, so we skip or log
392
+ elif name in (
393
+ "set_tempo", # handled below
394
+ "end_track",
395
+ "text_event", "text_event_08", "text_event_09", "text_event_0a",
396
+ "text_event_0b", "text_event_0c", "text_event_0d", "text_event_0e", "text_event_0f",
397
+ "copyright_text_event", "track_name", "instrument_name",
398
+ "lyric", "marker", "cue_point",
399
+ "smpte_offset", "time_signature", "key_signature",
400
+ "sequencer_specific", "raw_meta_event"
401
+ ):
402
+ if name == "set_tempo":
403
+ tempo = data[0]
404
+ # else: skip all other meta & text; you could hook in logging here
405
+ continue
406
+
407
+ else:
408
+ # unknown event type
409
+ continue
410
+
411
+ # This captures the sound of the last notes, allowing them to decay naturally.
412
+ # We render an extra 2 seconds of audio. A shorter time like 1 second might be sufficient.
413
+ tail_len_seconds = 2
414
+ tail_buf = fl.get_samples(int(sample_rate * tail_len_seconds)).reshape(-1, 2)
415
+ audio_chunks.append(tail_buf)
416
+
417
+ # Cleanup synth
418
+ fl.delete()
419
+
420
+ # After the loop finishes, concatenate all audio chunks in a single operation
421
+ if not audio_chunks:
422
+ return None # No audio was generated
423
+ ss = np.concatenate(audio_chunks, axis=0)
424
+
425
+
426
+ # Optimized silence trimming logic
427
+ if trim_silence and ss.size:
428
+ # Using a fixed amplitude threshold based on the data type's max value.
429
+ # This is more robust than using standard deviation for trimming the tail.
430
+ dtype_max = np.iinfo(ss.dtype).max
431
+ fixed_threshold = int(dtype_max * 0.005) # 0.5% of max amplitude
432
+
433
+ # Find the first and last samples exceeding the threshold.
434
+ indices = np.where(np.abs(ss) > fixed_threshold)[0]
435
+ if indices.size > 0:
436
+ # We trim from the start as well in case of leading silence
437
+ first_idx = indices[0]
438
+ last_idx = indices[-1]
439
+ ss = ss[first_idx : last_idx + 1]
440
+ else:
441
+ # If it's all silence, return an empty array.
442
+ ss = np.empty((0, 2), dtype=ss.dtype)
443
+
444
+ if ss.size:
445
+ maxv = np.abs(ss).max()
446
+ if maxv:
447
+ ss = (ss / maxv) * np.iinfo(np.int16).max
448
+ ss = ss.astype(np.int16)
449
+
450
+ # For Gradio you might want raw int16 PCM
451
+ if output_for_gradio:
452
+ return ss
453
+
454
+ # Swap to (channels, samples) and normalize for playback
455
+ ss = ss.T
456
+ raw_audio = normalize_audio(ss, target_level_db=volume_level_db)
457
+
458
+ # Optionally write WAV to disk
459
+ if write_audio_to_WAV and isinstance(midi_file, str):
460
+ wav_name = midi_file.rsplit('.', 1)[0] + '.wav'
461
+ # Note: raw_audio is float, needs conversion back to int16 for WAV format.
462
+ if np.max(np.abs(raw_audio)) > 0:
463
+ pcm = np.int16(raw_audio.T / np.max(np.abs(raw_audio)) * 32767)
464
+ else:
465
+ pcm = np.int16(raw_audio.T * 32767)
466
+
467
+ with wave.open(wav_name, 'wb') as wf:
468
+ wf.setframerate(sample_rate)
469
+ wf.setsampwidth(2)
470
+ wf.setnchannels(pcm.shape[1])
471
+ wf.writeframes(pcm.tobytes())
472
+
473
+ return raw_audio
474
+
475
+ #===================================================================================================================
src/piano_transcription/utils.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/piano_transcription/utils.py
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Final
6
+
7
+ # Import for the new downloader
8
+ from huggingface_hub import hf_hub_download
9
+
10
+ # Imports for the patch
11
+ import numpy as np
12
+ import librosa
13
+ import audioread
14
+ from piano_transcription_inference import utilities
15
+
16
+ # --- Constants ---
17
+ # By convention, uppercase variables are treated as constants and should not be modified.
18
+ # Using typing.Final to indicate to static type checkers that these should not be reassigned.
19
+ MODEL_NAME: Final[str] = "CRNN_note_F1=0.9677_pedal_F1=0.9186.pth"
20
+ REPO_ID: Final[str] = "Genius-Society/piano_trans"
21
+
22
+
23
+ # --- Model Download Function ---
24
+
25
+ def download_model_from_hf_if_needed():
26
+ """
27
+ Checks for the model and downloads it from the Hugging Face Hub if not present.
28
+ The hf_hub_download function handles caching and existence checks automatically.
29
+ """
30
+ # Assuming this utils.py is in 'src/piano_transcription/', models are in 'src/models/'
31
+ utils_dir = Path(__file__).parent
32
+ base_dir = utils_dir.parent # This should be the 'src' directory
33
+ model_dir = base_dir / "models"
34
+ model_path = model_dir / MODEL_NAME
35
+
36
+ print(f"Checking for model '{MODEL_NAME}' from Hugging Face Hub repo '{REPO_ID}'...")
37
+
38
+ try:
39
+ # hf_hub_download will download the file to a cache and return the path.
40
+ # To place it directly in our desired folder, we use `local_dir`.
41
+ # `local_dir_use_symlinks=False` ensures the actual file is copied to model_dir.
42
+ hf_hub_download(
43
+ repo_id=REPO_ID,
44
+ filename=MODEL_NAME,
45
+ local_dir=model_dir,
46
+ # local_dir_use_symlinks=False, # Recommended for moving projects around
47
+ # resume_download=True,
48
+ )
49
+ print(f"Model is available at '{model_path}'")
50
+
51
+ except AttributeError as e:
52
+ print(f"Error downloading from Hugging Face Hub. Please check your network connection and the repo/filename.")
53
+ print(f"Details: {e}")
54
+ # You might want to exit or raise the exception if the model is critical
55
+ # raise e
56
+ except Exception as e:
57
+ print(f"An unexpected error occurred: {e}")
58
+ # raise e
59
+
60
+
61
+ # --- Monkey Patching Function ---
62
+
63
+ def _fixed_load_audio(path, sr=22050, mono=True, offset=0.0, duration=None,
64
+ dtype=np.float32, res_type='kaiser_best',
65
+ backends=[audioread.ffdec.FFmpegAudioFile]):
66
+ """
67
+ A patched version of load_audio that uses updated function paths
68
+ for newer librosa versions. This function is intended to replace the
69
+ original one in the `piano_transcription_inference` library.
70
+ """
71
+ # (The code for this function remains unchanged)
72
+ y = []
73
+ with audioread.audio_open(os.path.realpath(path), backends=backends) as input_file:
74
+ sr_native = input_file.samplerate
75
+ n_channels = input_file.channels
76
+ s_start = int(np.round(sr_native * offset)) * n_channels
77
+ if duration is None:
78
+ s_end = np.inf
79
+ else:
80
+ s_end = s_start + (int(np.round(sr_native * duration)) * n_channels)
81
+ n = 0
82
+ for frame in input_file:
83
+ frame = librosa.util.buf_to_float(frame, dtype=dtype)
84
+ n_prev = n
85
+ n = n + len(frame)
86
+ if n < s_start:
87
+ continue
88
+ if s_end < n_prev:
89
+ break
90
+ if s_end < n:
91
+ frame = frame[:s_end - n_prev]
92
+ if n_prev <= s_start <= n:
93
+ frame = frame[(s_start - n_prev):]
94
+ y.append(frame)
95
+ if y:
96
+ y = np.concatenate(y)
97
+ if n_channels > 1:
98
+ y = y.reshape((-1, n_channels)).T
99
+ if mono:
100
+ y = librosa.to_mono(y)
101
+ if sr is not None:
102
+ y = librosa.resample(y, orig_sr=sr_native, target_sr=sr, res_type=res_type)
103
+ else:
104
+ sr = sr_native
105
+ y = np.ascontiguousarray(y, dtype=dtype)
106
+ return (y, sr)
107
+
108
+
109
+ def apply_monkey_patch():
110
+ """
111
+ Applies the patch to the `piano_transcription_inference` library by
112
+ replacing its `load_audio` function with our fixed version.
113
+ """
114
+ print("Applying librosa compatibility patch...")
115
+ utilities.load_audio = _fixed_load_audio
116
+
117
+
118
+ # --- Main Initializer ---
119
+
120
+ def initialize_app():
121
+ """
122
+ Main initialization function. Call this at the start of your app.
123
+ It downloads the model from Hugging Face and applies the necessary patches.
124
+ """
125
+ print("--- Initializing Application ---")
126
+ download_model_from_hf_if_needed()
127
+ apply_monkey_patch()
128
+ print("--- Initialization Complete ---")
webui.bat ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+
3
+ :: The original source of the webui.bat file is stable-diffusion-webui
4
+ :: Modified and enhanced by Gemini with features for venv management and requirements handling.
5
+
6
+ :: --------- Configuration ---------
7
+ set COMMANDLINE_ARGS=
8
+ :: Define the name of the Launch application
9
+ set APPLICATION_NAME=app.py
10
+ :: Define the name of the virtual environment directory
11
+ set VENV_NAME=venv
12
+ :: Set to 1 to always attempt to update packages from requirements.txt on every launch
13
+ set ALWAYS_UPDATE_REQS=0
14
+ :: ---------------------------------
15
+
16
+
17
+ :: Set PYTHON executable if not already defined
18
+ if not defined PYTHON (set PYTHON=python)
19
+ :: Set VENV_DIR using VENV_NAME if not already defined
20
+ if not defined VENV_DIR (set "VENV_DIR=%~dp0%VENV_NAME%")
21
+
22
+ mkdir tmp 2>NUL
23
+
24
+ :: Check if Python is callable
25
+ %PYTHON% -c "" >tmp/stdout.txt 2>tmp/stderr.txt
26
+ if %ERRORLEVEL% == 0 goto :check_pip
27
+ echo Couldn't launch python
28
+ goto :show_stdout_stderr
29
+
30
+ :check_pip
31
+ :: Check if pip is available
32
+ %PYTHON% -mpip --help >tmp/stdout.txt 2>tmp/stderr.txt
33
+ if %ERRORLEVEL% == 0 goto :start_venv
34
+ :: If pip is not available and PIP_INSTALLER_LOCATION is set, try to install pip
35
+ if "%PIP_INSTALLER_LOCATION%" == "" goto :show_stdout_stderr
36
+ %PYTHON% "%PIP_INSTALLER_LOCATION%" >tmp/stdout.txt 2>tmp/stderr.txt
37
+ if %ERRORLEVEL% == 0 goto :start_venv
38
+ echo Couldn't install pip
39
+ goto :show_stdout_stderr
40
+
41
+ :start_venv
42
+ :: Skip venv creation/activation if VENV_DIR is explicitly set to "-"
43
+ if ["%VENV_DIR%"] == ["-"] goto :skip_venv_entirely
44
+ :: Skip venv creation/activation if SKIP_VENV is set to "1"
45
+ if ["%SKIP_VENV%"] == ["1"] goto :skip_venv_entirely
46
+
47
+ :: Check if the venv already exists by looking for Python.exe in its Scripts directory
48
+ dir "%VENV_DIR%\Scripts\Python.exe" >tmp/stdout.txt 2>tmp/stderr.txt
49
+ if %ERRORLEVEL% == 0 goto :activate_venv_and_maybe_update
50
+
51
+ :: Venv does not exist, create it
52
+ echo Virtual environment not found in "%VENV_DIR%". Creating a new one.
53
+ for /f "delims=" %%i in ('CALL %PYTHON% -c "import sys; print(sys.executable)"') do set PYTHON_FULLNAME="%%i"
54
+ echo Creating venv in directory %VENV_DIR% using python %PYTHON_FULLNAME%
55
+ %PYTHON_FULLNAME% -m venv "%VENV_DIR%" >tmp/stdout.txt 2>tmp/stderr.txt
56
+ if %ERRORLEVEL% NEQ 0 (
57
+ echo Unable to create venv in directory "%VENV_DIR%"
58
+ goto :show_stdout_stderr
59
+ )
60
+ echo Venv created.
61
+
62
+ :: Install requirements for the first time if venv was just created
63
+ :: This section handles the initial installation of packages from requirements.txt
64
+ :: immediately after a new virtual environment is created.
65
+ echo Checking for requirements.txt for initial setup in %~dp0
66
+ if exist "%~dp0requirements.txt" (
67
+ echo Found requirements.txt, attempting to install for initial setup...
68
+ call "%VENV_DIR%\Scripts\activate.bat"
69
+ echo Installing packages from requirements.txt ^(initial setup^)...
70
+ "%VENV_DIR%\Scripts\python.exe" -m pip install -r "%~dp0requirements.txt"
71
+ if %ERRORLEVEL% NEQ 0 (
72
+ echo Failed to install requirements during initial setup. Please check the output above.
73
+ pause
74
+ goto :show_stdout_stderr_custom_pip_initial
75
+ )
76
+ echo Initial requirements installed successfully.
77
+ call "%VENV_DIR%\Scripts\deactivate.bat"
78
+ ) else (
79
+ echo No requirements.txt found for initial setup, skipping package installation.
80
+ )
81
+ goto :activate_venv_and_maybe_update
82
+
83
+
84
+ :activate_venv_and_maybe_update
85
+ :: This label is reached if the venv exists or was just created.
86
+ :: Set PYTHON to point to the venv's Python interpreter.
87
+ set PYTHON="%VENV_DIR%\Scripts\Python.exe"
88
+ echo Activating venv: %PYTHON%
89
+
90
+ :: Always update requirements if ALWAYS_UPDATE_REQS is 1
91
+ :: This section allows for updating packages from requirements.txt on every launch
92
+ :: if the ALWAYS_UPDATE_REQS variable is set to 1.
93
+ if defined ALWAYS_UPDATE_REQS (
94
+ if "%ALWAYS_UPDATE_REQS%"=="1" (
95
+ echo ALWAYS_UPDATE_REQS is enabled.
96
+ if exist "%~dp0requirements.txt" (
97
+ echo Attempting to update packages from requirements.txt...
98
+ REM No need to call activate.bat here again, PYTHON is already set to the venv's python
99
+ %PYTHON% -m pip install -r "%~dp0requirements.txt"
100
+ if %ERRORLEVEL% NEQ 0 (
101
+ echo Failed to update requirements. Please check the output above.
102
+ pause
103
+ goto :endofscript
104
+ )
105
+ echo Requirements updated successfully.
106
+ ) else (
107
+ echo ALWAYS_UPDATE_REQS is enabled, but no requirements.txt found. Skipping update.
108
+ )
109
+ ) else (
110
+ echo ALWAYS_UPDATE_REQS is not enabled or not set to 1. Skipping routine update.
111
+ )
112
+ )
113
+
114
+ goto :launch
115
+
116
+ :skip_venv_entirely
117
+ :: This label is reached if venv usage is explicitly skipped.
118
+ echo Skipping venv.
119
+ goto :launch
120
+
121
+ :launch
122
+ :: Launch the main application
123
+ echo Launching Web UI with arguments: %COMMANDLINE_ARGS% %*
124
+ %PYTHON% %APPLICATION_NAME% %COMMANDLINE_ARGS% %*
125
+ echo Launch finished.
126
+ pause
127
+ exit /b
128
+
129
+ :show_stdout_stderr_custom_pip_initial
130
+ :: Custom error handler for failures during the initial pip install process.
131
+ echo.
132
+ echo exit code ^(pip initial install^): %errorlevel%
133
+ echo Errors during initial pip install. See output above.
134
+ echo.
135
+ echo Launch unsuccessful. Exiting.
136
+ pause
137
+ exit /b
138
+
139
+
140
+ :show_stdout_stderr
141
+ :: General error handler: displays stdout and stderr from the tmp directory.
142
+ echo.
143
+ echo exit code: %errorlevel%
144
+
145
+ for /f %%i in ("tmp\stdout.txt") do set size=%%~zi
146
+ if %size% equ 0 goto :show_stderr
147
+ echo.
148
+ echo stdout:
149
+ type tmp\stdout.txt
150
+
151
+ :show_stderr
152
+ for /f %%i in ("tmp\stderr.txt") do set size=%%~zi
153
+ if %size% equ 0 goto :endofscript
154
+ echo.
155
+ echo stderr:
156
+ type tmp\stderr.txt
157
+
158
+ :endofscript
159
+ echo.
160
+ echo Launch unsuccessful. Exiting.
161
+ pause
162
+ exit /b