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 +12 -0
- KBH-Real-Choir-V2.5.sf2 +0 -3
- Live HQ Natural SoundFont GM.sf2 +0 -3
- Nice-Strings-PlusOrchestra-v1.6.sf2 +0 -3
- Orpheus_18.06.2020.sf2 +0 -3
- ProtoSquare.sf2 +0 -0
- README.md +3 -3
- SGM-v2.01-YamahaGrand-Guit-Bass-v2.7.sf2 +0 -3
- SuperGameBoy.sf2 +0 -0
- TCUPY.py +0 -1093
- app.py +682 -296
- midi_to_colab_audio.py +0 -0
- packages.txt +3 -1
- requirements.txt +21 -3
- MIDI.py → src/MIDI.py +828 -427
- TMIDIX.py → src/TMIDIX.py +42 -2066
- TPLOTS.py → src/TPLOTS.py +1521 -1521
- src/midi_to_colab_audio.py +475 -0
- src/piano_transcription/utils.py +128 -0
- webui.bat +162 -0
@@ -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_/
|
@@ -1,3 +0,0 @@
|
|
1 |
-
version https://git-lfs.github.com/spec/v1
|
2 |
-
oid sha256:04848e9c6cb7e27f2131156dd2ecedc283c1805bd326b5ea64ed2b6da23e106a
|
3 |
-
size 17360134
|
|
|
|
|
|
|
|
@@ -1,3 +0,0 @@
|
|
1 |
-
version https://git-lfs.github.com/spec/v1
|
2 |
-
oid sha256:5ed1b6a205686e43ead7386560c6610406b3cf4a0dfda230b89b8403dcf5efb7
|
3 |
-
size 836038682
|
|
|
|
|
|
|
|
@@ -1,3 +0,0 @@
|
|
1 |
-
version https://git-lfs.github.com/spec/v1
|
2 |
-
oid sha256:215e41f740172ddf72bf729c7df2f38356866f84a99fac39f84f861a02eddddd
|
3 |
-
size 442272364
|
|
|
|
|
|
|
|
@@ -1,3 +0,0 @@
|
|
1 |
-
version https://git-lfs.github.com/spec/v1
|
2 |
-
oid sha256:759fb756dcc8560c46f7e911ce981c69690e1d4aa3a634c537dbf658ccd11615
|
3 |
-
size 1288303498
|
|
|
|
|
|
|
|
Binary file (364 kB)
|
|
@@ -1,5 +1,5 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
emoji: 🎹
|
4 |
colorFrom: purple
|
5 |
colorTo: green
|
@@ -13,8 +13,8 @@ tags:
|
|
13 |
- renderer
|
14 |
- MIDI rendering
|
15 |
- MIDI renderer
|
16 |
-
short_description:
|
17 |
-
sdk_version: 5.
|
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 |
---
|
@@ -1,3 +0,0 @@
|
|
1 |
-
version https://git-lfs.github.com/spec/v1
|
2 |
-
oid sha256:cd41a4639c9e7a96413b4b22540d48e6741e24bcdabcb2eff22cd65929df3cfa
|
3 |
-
size 553961496
|
|
|
|
|
|
|
|
Binary file (112 kB)
|
|
@@ -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 |
-
###################################################################################
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
-
#
|
|
|
20 |
#
|
21 |
-
#
|
|
|
22 |
#
|
23 |
-
#
|
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
|
44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
45 |
|
46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
47 |
|
48 |
-
|
|
|
|
|
49 |
|
50 |
-
def Render_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 |
-
|
|
|
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('
|
81 |
-
print('Input MIDI
|
82 |
-
print('Input MIDI md5 hash', input_midi_md5hash)
|
83 |
print('-' * 70)
|
84 |
-
print('Render type:
|
85 |
-
print('
|
86 |
-
print('Audio 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('
|
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=
|
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 |
-
|
258 |
-
|
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 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
280 |
else:
|
281 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
282 |
|
283 |
-
|
284 |
-
|
285 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
286 |
else:
|
287 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
288 |
|
289 |
-
|
290 |
-
|
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 |
-
|
299 |
-
|
300 |
-
|
|
|
|
|
301 |
|
302 |
-
print('
|
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 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
331 |
|
332 |
-
|
|
|
|
|
|
|
333 |
|
334 |
if __name__ == "__main__":
|
335 |
-
|
336 |
-
|
|
|
337 |
|
338 |
-
|
339 |
-
|
340 |
-
|
|
|
|
|
341 |
|
342 |
-
|
343 |
-
|
344 |
-
|
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.
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
|
383 |
-
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
413 |
-
|
414 |
-
|
415 |
-
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
|
429 |
-
|
430 |
-
|
431 |
-
|
432 |
-
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
|
439 |
-
|
440 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
441 |
|
442 |
-
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
-
|
447 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
The diff for this file is too large to render.
See raw diff
|
|
@@ -1 +1,3 @@
|
|
1 |
-
fluidsynth
|
|
|
|
|
|
1 |
+
fluidsynth
|
2 |
+
portaudio19-dev
|
3 |
+
libportaudio2
|
@@ -1,6 +1,24 @@
|
|
1 |
-
|
|
|
|
|
2 |
numpy
|
3 |
-
|
|
|
|
|
|
|
|
|
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
|
@@ -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 |
-
|
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 |
-
|
|
|
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 =
|
443 |
i_tempo_ticks = 0
|
444 |
ticks_so_far = 0
|
445 |
ms_so_far = 0.0
|
446 |
previous_ms_so_far = 0.0
|
447 |
-
|
|
|
|
|
|
|
|
|
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 |
-
|
464 |
-
|
465 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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):
|
1157 |
_warn('_unshift_ber_int: no integer found')
|
1158 |
return ((0, b""))
|
1159 |
-
byte = ba
|
|
|
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
|
|
|
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 |
-
|
1198 |
-
|
1199 |
-
|
|
|
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 |
-
|
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 |
-
|
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;
|
1251 |
event_count = 0;
|
1252 |
events = []
|
1253 |
|
1254 |
-
while(len(trackdata)):
|
1255 |
# loop while there's anything to analyze ...
|
1256 |
-
eot = False
|
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,
|
1264 |
|
1265 |
# Now let's see what we can make of the command
|
1266 |
-
first_byte = trackdata
|
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): #
|
1282 |
pass
|
1283 |
-
elif (command == 0xC0 or command == 0xD0): #
|
1284 |
-
parameter = trackdata
|
1285 |
-
|
1286 |
-
|
|
|
|
|
1287 |
|
1288 |
#################################################################
|
1289 |
# MIDI events
|
1290 |
|
1291 |
-
if (command
|
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 |
-
|
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
|
|
|
1329 |
[length, trackdata] = _unshift_ber_int(trackdata)
|
1330 |
-
if (command
|
1331 |
-
|
1332 |
-
|
1333 |
-
|
1334 |
-
|
1335 |
-
|
1336 |
-
|
1337 |
-
elif command >= 0x01 and command <= 0x0f:
|
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])
|
1343 |
# Defined text events
|
1344 |
if (command == 0x01):
|
1345 |
-
|
1346 |
elif (command == 0x02):
|
1347 |
-
|
1348 |
elif (command == 0x03):
|
1349 |
-
|
1350 |
elif (command == 0x04):
|
1351 |
-
|
1352 |
elif (command == 0x05):
|
1353 |
-
|
1354 |
elif (command == 0x06):
|
1355 |
-
|
1356 |
elif (command == 0x07):
|
1357 |
-
|
1358 |
# Reserved but apparently unassigned text events
|
1359 |
elif (command == 0x08):
|
1360 |
-
|
1361 |
elif (command == 0x09):
|
1362 |
-
|
1363 |
elif (command == 0x0a):
|
1364 |
-
|
1365 |
elif (command == 0x0b):
|
1366 |
-
|
1367 |
elif (command == 0x0c):
|
1368 |
-
|
1369 |
elif (command == 0x0d):
|
1370 |
-
|
1371 |
elif (command == 0x0e):
|
1372 |
-
|
1373 |
elif (command == 0x0f):
|
1374 |
-
|
1375 |
|
1376 |
# Now the sticky events -------------------------------------
|
1377 |
elif (command == 0x2F):
|
1378 |
-
|
1379 |
-
|
1380 |
-
|
1381 |
-
elif (command == 0x51):
|
1382 |
-
|
1383 |
-
|
1384 |
-
|
1385 |
-
|
1386 |
elif (command == 0x54):
|
1387 |
-
|
1388 |
-
|
1389 |
-
|
1390 |
elif (command == 0x58):
|
1391 |
-
|
1392 |
-
|
1393 |
-
|
1394 |
elif (command == 0x59):
|
1395 |
-
|
1396 |
-
|
1397 |
-
|
1398 |
-
elif (command == 0x7F):
|
1399 |
-
|
1400 |
else:
|
1401 |
-
|
1402 |
-
|
1403 |
-
|
1404 |
-
|
1405 |
-
|
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):
|
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):
|
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):
|
1459 |
E = ['tune_request', time]
|
1460 |
# What would a tune request be doing in a MIDI /file/?
|
1461 |
|
1462 |
-
|
1463 |
-
|
1464 |
-
|
1465 |
-
|
1466 |
-
|
1467 |
-
|
1468 |
-
|
1469 |
-
|
1470 |
-
|
1471 |
-
|
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]]
|
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
|
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 = []
|
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 |
-
|
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
|
@@ -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
|
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
|
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 |
-
|
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,
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
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",
|
@@ -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 |
################################################################################
|
@@ -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 |
+
#===================================================================================================================
|
@@ -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 ---")
|
@@ -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
|