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
|