midiharmony 26.1.27__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- midiharmony/MIDI.py +1732 -0
- midiharmony/README.md +29 -0
- midiharmony/TMIDIX.py +15044 -0
- midiharmony/__init__.py +2 -0
- midiharmony/artwork/Project-Los-Angeles.png +0 -0
- midiharmony/artwork/README.md +10 -0
- midiharmony/artwork/Tegridy-Code-2026.png +0 -0
- midiharmony/artwork/__init__.py +0 -0
- midiharmony/artwork/midiharmony.png +0 -0
- midiharmony/data/README.md +17 -0
- midiharmony/data/__init__.py +0 -0
- midiharmony/data/all_harmonic_chords_quads.npz +0 -0
- midiharmony/helpers.py +274 -0
- midiharmony/midi_to_colab_audio.py +3637 -0
- midiharmony/midiharmony.py +519 -0
- midiharmony-26.1.27.dist-info/METADATA +72 -0
- midiharmony-26.1.27.dist-info/RECORD +20 -0
- midiharmony-26.1.27.dist-info/WHEEL +5 -0
- midiharmony-26.1.27.dist-info/licenses/LICENSE +201 -0
- midiharmony-26.1.27.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
#! /usr/bin/python3
|
|
2
|
+
|
|
3
|
+
r'''###############################################################################
|
|
4
|
+
###################################################################################
|
|
5
|
+
#
|
|
6
|
+
#
|
|
7
|
+
# midiharmony Python module
|
|
8
|
+
# Version 1.0
|
|
9
|
+
#
|
|
10
|
+
# Project Los Angeles
|
|
11
|
+
#
|
|
12
|
+
# Tegridy Code 2026
|
|
13
|
+
#
|
|
14
|
+
# https://github.com/Tegridy-Code/Project-Los-Angeles
|
|
15
|
+
#
|
|
16
|
+
#
|
|
17
|
+
###################################################################################
|
|
18
|
+
###################################################################################
|
|
19
|
+
#
|
|
20
|
+
# Copyright 2026 Project Los Angeles / Tegridy Code
|
|
21
|
+
#
|
|
22
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
23
|
+
# you may not use this file except in compliance with the License.
|
|
24
|
+
# You may obtain a copy of the License at
|
|
25
|
+
#
|
|
26
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
27
|
+
#
|
|
28
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
29
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
30
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
31
|
+
# See the License for the specific language governing permissions and
|
|
32
|
+
# limitations under the License.
|
|
33
|
+
#
|
|
34
|
+
###################################################################################
|
|
35
|
+
###################################################################################
|
|
36
|
+
#
|
|
37
|
+
# Critical dependencies
|
|
38
|
+
#
|
|
39
|
+
# !pip install cupy-cuda12x
|
|
40
|
+
# !pip install matplotlib
|
|
41
|
+
# !pip install numpy==1.26.4
|
|
42
|
+
# !pip install tqdm
|
|
43
|
+
#
|
|
44
|
+
###################################################################################
|
|
45
|
+
'''
|
|
46
|
+
|
|
47
|
+
###################################################################################
|
|
48
|
+
###################################################################################
|
|
49
|
+
|
|
50
|
+
print('=' * 70)
|
|
51
|
+
print('Loading midiharmony Python module...')
|
|
52
|
+
print('Please wait...')
|
|
53
|
+
print('=' * 70)
|
|
54
|
+
|
|
55
|
+
__version__ = '1.0.0'
|
|
56
|
+
|
|
57
|
+
print('midiharmony module version', __version__)
|
|
58
|
+
print('=' * 70)
|
|
59
|
+
|
|
60
|
+
###################################################################################
|
|
61
|
+
###################################################################################
|
|
62
|
+
|
|
63
|
+
import os, copy, time
|
|
64
|
+
|
|
65
|
+
from typing import List, Optional, Union, Tuple, Dict, Any
|
|
66
|
+
|
|
67
|
+
import tqdm
|
|
68
|
+
|
|
69
|
+
###################################################################################
|
|
70
|
+
|
|
71
|
+
from . import TMIDIX
|
|
72
|
+
|
|
73
|
+
from .helpers import get_package_data
|
|
74
|
+
|
|
75
|
+
###################################################################################
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
import cupy as cp
|
|
79
|
+
|
|
80
|
+
print('CuPy is loaded!')
|
|
81
|
+
|
|
82
|
+
CUPY_AVAILABLE = True
|
|
83
|
+
|
|
84
|
+
except ImportError:
|
|
85
|
+
|
|
86
|
+
print('Could not load CuPy!')
|
|
87
|
+
|
|
88
|
+
CUPY_AVAILABLE = False
|
|
89
|
+
|
|
90
|
+
import numpy as np
|
|
91
|
+
|
|
92
|
+
###################################################################################
|
|
93
|
+
###################################################################################
|
|
94
|
+
|
|
95
|
+
def find_quads_fast_cupy(src_array, trg_array, seed: int = 0) -> int:
|
|
96
|
+
|
|
97
|
+
"""
|
|
98
|
+
Count how many rows in src_array also appear in trg_array using CuPy (GPU).
|
|
99
|
+
Uses a non-linear 64-bit FNV-1a hash over raw bytes to avoid collisions.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
src = cp.ascontiguousarray(cp.asarray(src_array))
|
|
103
|
+
trg = cp.ascontiguousarray(cp.asarray(trg_array))
|
|
104
|
+
|
|
105
|
+
if src.dtype != trg.dtype or src.ndim != 2 or trg.ndim != 2 or src.shape[1] != trg.shape[1]:
|
|
106
|
+
raise ValueError("src and trg must be 2D arrays with same dtype and same number of columns")
|
|
107
|
+
|
|
108
|
+
# bytes per row
|
|
109
|
+
bpr = src.dtype.itemsize * src.shape[1]
|
|
110
|
+
|
|
111
|
+
# view rows as bytes
|
|
112
|
+
src_bytes = src.view(cp.uint8).reshape(src.shape[0], bpr)
|
|
113
|
+
trg_bytes = trg.view(cp.uint8).reshape(trg.shape[0], bpr)
|
|
114
|
+
|
|
115
|
+
# FNV-1a constants
|
|
116
|
+
FNV_OFFSET = cp.uint64(0xcbf29ce484222325 ^ seed)
|
|
117
|
+
FNV_PRIME = cp.uint64(0x100000001b3)
|
|
118
|
+
|
|
119
|
+
# hash rows
|
|
120
|
+
def fnv1a_hash(byte_matrix):
|
|
121
|
+
h = cp.full((byte_matrix.shape[0],), FNV_OFFSET, dtype=cp.uint64)
|
|
122
|
+
for i in range(bpr):
|
|
123
|
+
h ^= byte_matrix[:, i].astype(cp.uint64)
|
|
124
|
+
h *= FNV_PRIME
|
|
125
|
+
return h
|
|
126
|
+
|
|
127
|
+
src_fp = fnv1a_hash(src_bytes)
|
|
128
|
+
trg_fp = fnv1a_hash(trg_bytes)
|
|
129
|
+
|
|
130
|
+
# count matches
|
|
131
|
+
return int(cp.isin(src_fp, trg_fp).sum())
|
|
132
|
+
|
|
133
|
+
###################################################################################
|
|
134
|
+
|
|
135
|
+
def find_quads_fast_numpy(src_array: np.ndarray, trg_array: np.ndarray) -> int:
|
|
136
|
+
|
|
137
|
+
"""
|
|
138
|
+
Count how many rows in src_array also appear in trg_array using NumPy (CPU).
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
# ensure contiguous memory and same dtype
|
|
142
|
+
src = np.ascontiguousarray(src_array)
|
|
143
|
+
trg = np.ascontiguousarray(trg_array)
|
|
144
|
+
if src.dtype != trg.dtype or src.shape[1] != trg.shape[1]:
|
|
145
|
+
raise ValueError("src and trg must have same dtype and same number of columns")
|
|
146
|
+
|
|
147
|
+
# view each row as a single fixed-size byte string
|
|
148
|
+
row_dtype = np.dtype((np.void, src.dtype.itemsize * src.shape[1]))
|
|
149
|
+
src_keys = src.view(row_dtype).ravel()
|
|
150
|
+
trg_keys = trg.view(row_dtype).ravel()
|
|
151
|
+
|
|
152
|
+
# count how many src rows appear in trg
|
|
153
|
+
return int(np.isin(src_keys, trg_keys).sum())
|
|
154
|
+
|
|
155
|
+
###################################################################################
|
|
156
|
+
|
|
157
|
+
_trg_cache_np = None
|
|
158
|
+
_trg_cache_cp = None
|
|
159
|
+
|
|
160
|
+
def get_trg_array(use_gpu=False, verbose=True):
|
|
161
|
+
|
|
162
|
+
global _trg_cache_np, _trg_cache_cp
|
|
163
|
+
|
|
164
|
+
if not use_gpu:
|
|
165
|
+
if _trg_cache_np is None:
|
|
166
|
+
if verbose:
|
|
167
|
+
print("Loading trg array (NumPy)...")
|
|
168
|
+
|
|
169
|
+
_trg_cache_np = np.load(get_package_data()[0]['path'])['harmonic_chords_quads']
|
|
170
|
+
|
|
171
|
+
return _trg_cache_np
|
|
172
|
+
|
|
173
|
+
# GPU path
|
|
174
|
+
try:
|
|
175
|
+
import cupy as cp
|
|
176
|
+
|
|
177
|
+
except Exception:
|
|
178
|
+
return get_trg_array(use_gpu=False)
|
|
179
|
+
|
|
180
|
+
if _trg_cache_cp is None:
|
|
181
|
+
if verbose:
|
|
182
|
+
print("Loading trg array (CuPy)...")
|
|
183
|
+
|
|
184
|
+
_trg_cache_cp = cp.asarray(get_trg_array(use_gpu=False,
|
|
185
|
+
verbose=False
|
|
186
|
+
),
|
|
187
|
+
dtype=cp.int16
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return _trg_cache_cp
|
|
191
|
+
|
|
192
|
+
###################################################################################
|
|
193
|
+
|
|
194
|
+
def process_midi(midi_file_path,
|
|
195
|
+
verbose=True
|
|
196
|
+
):
|
|
197
|
+
|
|
198
|
+
midi_name = os.path.splitext(os.path.basename(midi_file_path))[0]
|
|
199
|
+
|
|
200
|
+
if verbose:
|
|
201
|
+
|
|
202
|
+
start_time = time.time()
|
|
203
|
+
|
|
204
|
+
print('=' * 70)
|
|
205
|
+
print('Processing', midi_name)
|
|
206
|
+
|
|
207
|
+
return_dict = {}
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
|
|
211
|
+
raw_score = TMIDIX.midi2single_track_ms_score(midi_file_path,
|
|
212
|
+
do_not_check_MIDI_signature=True,
|
|
213
|
+
verbose=verbose
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
escore_notes = TMIDIX.advanced_score_processor(raw_score, return_enhanced_score_notes=True)
|
|
217
|
+
|
|
218
|
+
# ==================================================================================
|
|
219
|
+
|
|
220
|
+
if escore_notes and escore_notes[0]:
|
|
221
|
+
|
|
222
|
+
escore_notes = TMIDIX.strip_drums_from_escore_notes(escore_notes[0])
|
|
223
|
+
|
|
224
|
+
if escore_notes:
|
|
225
|
+
|
|
226
|
+
escore_notes = TMIDIX.augment_enhanced_score_notes(escore_notes)
|
|
227
|
+
|
|
228
|
+
cscore = TMIDIX.chordify_score([1000, escore_notes])
|
|
229
|
+
|
|
230
|
+
# ==================================================================================
|
|
231
|
+
|
|
232
|
+
chords = []
|
|
233
|
+
|
|
234
|
+
bchords = 0
|
|
235
|
+
|
|
236
|
+
for c in cscore:
|
|
237
|
+
tones_chord = sorted(set([e[4] % 12 for e in c]))
|
|
238
|
+
|
|
239
|
+
if tones_chord not in TMIDIX.ALL_CHORDS_SORTED:
|
|
240
|
+
tones_chord = TMIDIX.check_and_fix_tones_chord(tones_chord,
|
|
241
|
+
use_full_chords=False
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
bchords += 1
|
|
245
|
+
|
|
246
|
+
chord_tok = TMIDIX.ALL_CHORDS_SORTED.index(tones_chord)
|
|
247
|
+
|
|
248
|
+
chords.append(chord_tok)
|
|
249
|
+
|
|
250
|
+
# ==================================================================================
|
|
251
|
+
|
|
252
|
+
gchords = TMIDIX.grouped_set(chords)
|
|
253
|
+
|
|
254
|
+
# ==================================================================================
|
|
255
|
+
|
|
256
|
+
quads = set()
|
|
257
|
+
|
|
258
|
+
qcount = 0
|
|
259
|
+
|
|
260
|
+
for i in range(len(gchords)):
|
|
261
|
+
quad = gchords[i:i+4]
|
|
262
|
+
|
|
263
|
+
if len(quad) == 4:
|
|
264
|
+
quads.add(tuple(quad))
|
|
265
|
+
qcount += 1
|
|
266
|
+
|
|
267
|
+
else:
|
|
268
|
+
last_quad = gchords[-4:]
|
|
269
|
+
|
|
270
|
+
if len(last_quad) == 4:
|
|
271
|
+
quads.add(tuple(last_quad))
|
|
272
|
+
qcount += 1
|
|
273
|
+
|
|
274
|
+
quads = list(quads)
|
|
275
|
+
|
|
276
|
+
# ==================================================================================
|
|
277
|
+
|
|
278
|
+
return_dict['midi_path'] = midi_file_path
|
|
279
|
+
return_dict['midi_name'] = midi_name
|
|
280
|
+
|
|
281
|
+
return_dict['total_chords_count'] = len(cscore)
|
|
282
|
+
return_dict['bad_chords_count'] = bchords
|
|
283
|
+
return_dict['grouped_chords_count'] = len(gchords)
|
|
284
|
+
|
|
285
|
+
return_dict['total_quads_count'] = qcount
|
|
286
|
+
return_dict['unique_quads_count'] = len(quads)
|
|
287
|
+
return_dict['quads'] = quads
|
|
288
|
+
|
|
289
|
+
# ==================================================================================
|
|
290
|
+
|
|
291
|
+
if verbose:
|
|
292
|
+
print('Done!')
|
|
293
|
+
print('=' * 70)
|
|
294
|
+
|
|
295
|
+
end_time = time.time()
|
|
296
|
+
|
|
297
|
+
execution_time = end_time - start_time
|
|
298
|
+
|
|
299
|
+
print(f"Execution time: {execution_time:.4f} seconds")
|
|
300
|
+
print('=' * 70)
|
|
301
|
+
|
|
302
|
+
return return_dict
|
|
303
|
+
|
|
304
|
+
else:
|
|
305
|
+
if verbose:
|
|
306
|
+
print('MIDI is a drum track without chords!')
|
|
307
|
+
print('=' * 70)
|
|
308
|
+
|
|
309
|
+
end_time = time.time()
|
|
310
|
+
|
|
311
|
+
execution_time = end_time - start_time
|
|
312
|
+
|
|
313
|
+
print(f"Execution time: {execution_time:.4f} seconds")
|
|
314
|
+
print('=' * 70)
|
|
315
|
+
|
|
316
|
+
return return_dict
|
|
317
|
+
|
|
318
|
+
else:
|
|
319
|
+
if verbose:
|
|
320
|
+
print('MIDI does not appear to have any events!')
|
|
321
|
+
print('=' * 70)
|
|
322
|
+
|
|
323
|
+
end_time = time.time()
|
|
324
|
+
|
|
325
|
+
execution_time = end_time - start_time
|
|
326
|
+
|
|
327
|
+
print(f"Execution time: {execution_time:.4f} seconds")
|
|
328
|
+
print('=' * 70)
|
|
329
|
+
|
|
330
|
+
return return_dict
|
|
331
|
+
|
|
332
|
+
except Exception as ex:
|
|
333
|
+
|
|
334
|
+
if verbose:
|
|
335
|
+
print('WARNING !!!')
|
|
336
|
+
print('=' * 70)
|
|
337
|
+
print(f)
|
|
338
|
+
print('=' * 70)
|
|
339
|
+
print('Error detected:', ex)
|
|
340
|
+
print('=' * 70)
|
|
341
|
+
|
|
342
|
+
end_time = time.time()
|
|
343
|
+
|
|
344
|
+
execution_time = end_time - start_time
|
|
345
|
+
|
|
346
|
+
print(f"Execution time: {execution_time:.4f} seconds")
|
|
347
|
+
print('=' * 70)
|
|
348
|
+
|
|
349
|
+
return return_dict
|
|
350
|
+
|
|
351
|
+
###################################################################################
|
|
352
|
+
|
|
353
|
+
def analyze_processed_midi(processed_midi_dict,
|
|
354
|
+
keep_quads=False,
|
|
355
|
+
verbose=False
|
|
356
|
+
):
|
|
357
|
+
|
|
358
|
+
if verbose:
|
|
359
|
+
|
|
360
|
+
start_time = time.time()
|
|
361
|
+
|
|
362
|
+
print('=' * 70)
|
|
363
|
+
print('Analyzing harmony...')
|
|
364
|
+
print('=' * 70)
|
|
365
|
+
|
|
366
|
+
if verbose:
|
|
367
|
+
print('Prepping src_array...')
|
|
368
|
+
|
|
369
|
+
if CUPY_AVAILABLE:
|
|
370
|
+
src_array = cp.asarray(processed_midi_dict['quads'], dtype=cp.int16)
|
|
371
|
+
|
|
372
|
+
else:
|
|
373
|
+
src_array = np.asarray(processed_midi_dict['quads'], dtype=np.int16)
|
|
374
|
+
|
|
375
|
+
trg_array = get_trg_array(CUPY_AVAILABLE, verbose=verbose)
|
|
376
|
+
|
|
377
|
+
if verbose:
|
|
378
|
+
print('Processing...')
|
|
379
|
+
|
|
380
|
+
res = 0
|
|
381
|
+
src_array_len = max(1, src_array.shape[0])
|
|
382
|
+
|
|
383
|
+
if CUPY_AVAILABLE:
|
|
384
|
+
if verbose:
|
|
385
|
+
print('Using CuPy / GPU...')
|
|
386
|
+
|
|
387
|
+
res = find_quads_fast_cupy(src_array, trg_array)
|
|
388
|
+
|
|
389
|
+
else:
|
|
390
|
+
|
|
391
|
+
if verbose:
|
|
392
|
+
print('Using NumPy / CPU...')
|
|
393
|
+
|
|
394
|
+
res = find_quads_fast_numpy(src_array, trg_array)
|
|
395
|
+
|
|
396
|
+
if keep_quads:
|
|
397
|
+
harmony_dict = copy.deepcopy(processed_midi_dict)
|
|
398
|
+
|
|
399
|
+
else:
|
|
400
|
+
harmony_dict = {k: v for k, v in processed_midi_dict.items() if k != 'quads'}
|
|
401
|
+
|
|
402
|
+
harmony_dict['harmonic_quads_count'] = res
|
|
403
|
+
harmony_dict['harmony_ratio'] = res / src_array_len
|
|
404
|
+
|
|
405
|
+
if verbose:
|
|
406
|
+
|
|
407
|
+
print('Done!')
|
|
408
|
+
print('=' * 70)
|
|
409
|
+
|
|
410
|
+
end_time = time.time()
|
|
411
|
+
|
|
412
|
+
execution_time = end_time - start_time
|
|
413
|
+
|
|
414
|
+
print(f"Execution time: {execution_time:.4f} seconds")
|
|
415
|
+
print('=' * 70)
|
|
416
|
+
|
|
417
|
+
return harmony_dict
|
|
418
|
+
|
|
419
|
+
###################################################################################
|
|
420
|
+
|
|
421
|
+
def analyze_midi(midi_file_path,
|
|
422
|
+
verbose=True
|
|
423
|
+
):
|
|
424
|
+
|
|
425
|
+
processed_midi_dict = {}
|
|
426
|
+
harmony_dict = {}
|
|
427
|
+
|
|
428
|
+
if verbose:
|
|
429
|
+
start_time = time.time()
|
|
430
|
+
|
|
431
|
+
processed_midi_dict = process_midi(midi_file_path,
|
|
432
|
+
verbose=verbose
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
if processed_midi_dict:
|
|
436
|
+
harmony_dict = analyze_processed_midi(processed_midi_dict,
|
|
437
|
+
verbose=verbose
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
if verbose:
|
|
441
|
+
|
|
442
|
+
end_time = time.time()
|
|
443
|
+
|
|
444
|
+
execution_time = end_time - start_time
|
|
445
|
+
|
|
446
|
+
print(f"Total execution time: {execution_time:.4f} seconds")
|
|
447
|
+
print('=' * 70)
|
|
448
|
+
|
|
449
|
+
return harmony_dict
|
|
450
|
+
|
|
451
|
+
###################################################################################
|
|
452
|
+
|
|
453
|
+
def analyze_midi_folder(midi_folders_paths_list,
|
|
454
|
+
midi_files_extensions=['.mid', '.midi', '.kar'],
|
|
455
|
+
show_progress_bar=True,
|
|
456
|
+
verbose=False
|
|
457
|
+
):
|
|
458
|
+
|
|
459
|
+
if verbose:
|
|
460
|
+
start_time = time.time()
|
|
461
|
+
|
|
462
|
+
print('=' * 70)
|
|
463
|
+
print('Processing MIDI folders:', midi_folders_paths_list)
|
|
464
|
+
|
|
465
|
+
midi_files_list = TMIDIX.create_files_list(midi_folders_paths_list,
|
|
466
|
+
files_exts=midi_files_extensions,
|
|
467
|
+
verbose=verbose
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
all_harmony_dicts_list = []
|
|
471
|
+
|
|
472
|
+
for midi_file in tqdm.tqdm(midi_files_list,
|
|
473
|
+
desc='Processing MIDIs',
|
|
474
|
+
unit='file',
|
|
475
|
+
disable=not show_progress_bar
|
|
476
|
+
):
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
processed_midi_dict = {}
|
|
480
|
+
harmony_dict = {}
|
|
481
|
+
|
|
482
|
+
processed_midi_dict = process_midi(midi_file,
|
|
483
|
+
verbose=verbose
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
if processed_midi_dict:
|
|
487
|
+
harmony_dict = analyze_processed_midi(processed_midi_dict,
|
|
488
|
+
verbose=verbose
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
all_harmony_dicts_list.append(harmony_dict)
|
|
492
|
+
|
|
493
|
+
if verbose:
|
|
494
|
+
|
|
495
|
+
print('Done!')
|
|
496
|
+
print('=' * 70)
|
|
497
|
+
print('Total number of input MIDIs:', len(midi_files_list))
|
|
498
|
+
print('Total number of output MIDIs:', len(all_harmony_dicts_list))
|
|
499
|
+
print('Good / bad ratio:', len(all_harmony_dicts_list) / max(1, len(midi_files_list)))
|
|
500
|
+
print('=' * 70)
|
|
501
|
+
|
|
502
|
+
end_time = time.time()
|
|
503
|
+
|
|
504
|
+
execution_time = end_time - start_time
|
|
505
|
+
|
|
506
|
+
print(f"Total execution time: {execution_time:.4f} seconds")
|
|
507
|
+
print('=' * 70)
|
|
508
|
+
|
|
509
|
+
return all_harmony_dicts_list
|
|
510
|
+
|
|
511
|
+
###################################################################################
|
|
512
|
+
|
|
513
|
+
print('Module is loaded!')
|
|
514
|
+
print('Enjoy! :)')
|
|
515
|
+
print('=' * 70)
|
|
516
|
+
|
|
517
|
+
###################################################################################
|
|
518
|
+
# This is the end of the midiharmony Python module
|
|
519
|
+
###################################################################################
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: midiharmony
|
|
3
|
+
Version: 26.1.27
|
|
4
|
+
Summary: Fast, data‑driven harmony analysis for any MIDI file
|
|
5
|
+
Author-email: Alex Lev <alexlev61@proton.me>
|
|
6
|
+
Maintainer-email: Alex Lev <alexlev61@proton.me>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Project-URL: Homepage, https://github.com/asigalov61/midiharmony
|
|
9
|
+
Project-URL: SoundCloud, https://soundcloud.com/aleksandr-sigalov-61/
|
|
10
|
+
Project-URL: Documentation, https://github.com/asigalov61/midiharmony
|
|
11
|
+
Project-URL: Issues, https://github.com/asigalov61/midiharmony/issues
|
|
12
|
+
Project-URL: Discussions, https://github.com/asigalov61/midiharmony/discussions
|
|
13
|
+
Project-URL: Dataset, https://huggingface.co/datasets/projectlosangeles/Discover-MIDI-Dataset
|
|
14
|
+
Keywords: MIDI,MIDI harmony,harmony,symbolic music,music,music harmony,harmony analysis,harmonic analysis,music information retrieval,MIR,music discovery,music search,music quality,music metadata,music recommendation,research,musicology,music analysis
|
|
15
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Intended Audience :: Science/Research
|
|
18
|
+
Classifier: Intended Audience :: Education
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
27
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
28
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: MIDI
|
|
29
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
30
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
31
|
+
Classifier: Environment :: Console
|
|
32
|
+
Classifier: Natural Language :: English
|
|
33
|
+
Classifier: Typing :: Typed
|
|
34
|
+
Requires-Python: >=3.8
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
License-File: LICENSE
|
|
37
|
+
Requires-Dist: tqdm
|
|
38
|
+
Requires-Dist: matplotlib
|
|
39
|
+
Requires-Dist: midirenderer
|
|
40
|
+
Requires-Dist: cupy-cuda12x
|
|
41
|
+
Requires-Dist: numpy==1.26.4
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
# midiharmony
|
|
45
|
+
## Fast, data‑driven harmony analysis for any MIDI file
|
|
46
|
+
|
|
47
|
+
<img width="1024" height="1024" alt="midiharmony" src="https://github.com/user-attachments/assets/586cf022-ce06-4cea-bfdc-ffcf9b0365d1" />
|
|
48
|
+
|
|
49
|
+
***
|
|
50
|
+
|
|
51
|
+
## Abstract
|
|
52
|
+
|
|
53
|
+
*midiharmony provides fast, stand‑alone harmony analysis for MIDI files by comparing their chord and note relationships against a high‑quality database of extracted chord quads. This approach enables reliable detection of strong harmonic structure, musically coherent progressions, and potential inconsistencies, making it a practical tool for music‑AI pipelines, composition analysis, and large‑scale MIDI processing.*
|
|
54
|
+
|
|
55
|
+
***
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
!pip install -U midiharmony
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
***
|
|
64
|
+
|
|
65
|
+
## Basic use example
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import midiharmony
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Project Los Angeles
|
|
72
|
+
### Tegridy Code 2026
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
midiharmony/MIDI.py,sha256=kANk5j1-EMnW0cWlrKQca5A-KM7oy0BtE5eYUKPuG-k,69614
|
|
2
|
+
midiharmony/README.md,sha256=-DMhg4O9Dt9l5qj67R7RIO274sC_raxh1b-CVq2ySpE,823
|
|
3
|
+
midiharmony/TMIDIX.py,sha256=KhSxEMs8tB0PXOHcBRbYqa63kgDBxAsvyFa9oAO2bu0,548390
|
|
4
|
+
midiharmony/__init__.py,sha256=7gSewe5Li5yGW1RHy3H5fupLaL2NmXNgkpFpzSqMx2E,50
|
|
5
|
+
midiharmony/helpers.py,sha256=xc6RWrNz_T2HXWJlzRDDgvKb-zexNJKpmRrAIZ9Obtc,8886
|
|
6
|
+
midiharmony/midi_to_colab_audio.py,sha256=dP-YMJVrCN_orOs3iYAmlRMwlHOzme3irahnLubJohw,145302
|
|
7
|
+
midiharmony/midiharmony.py,sha256=FDqsIvAVdG4iLW-GtXe45aPzyRC3_W6BLx_m-aCxBz8,16657
|
|
8
|
+
midiharmony/artwork/Project-Los-Angeles.png,sha256=6SBoK-YgXOo7mluLjS_MH0KZljkhd5-9N0s3sMbax4E,989875
|
|
9
|
+
midiharmony/artwork/README.md,sha256=_7QXLfyPNFWzs96MnoPecTJNWigEfSr3uWRnG6uZjk4,159
|
|
10
|
+
midiharmony/artwork/Tegridy-Code-2026.png,sha256=gzDtGxeTNFvQPKzP6isxXtYIdMN7RbkTlcE6xywc_S8,594966
|
|
11
|
+
midiharmony/artwork/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
midiharmony/artwork/midiharmony.png,sha256=7TwKQScUh8D8-ZKEnlOO8gFSSnmXICYC3XvHN0Ih-mY,978143
|
|
13
|
+
midiharmony/data/README.md,sha256=vpxZ5ZERJeO9HkmEI91DqtvTKnFhxfwnrVI18prYEtg,694
|
|
14
|
+
midiharmony/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
midiharmony/data/all_harmonic_chords_quads.npz,sha256=-oxuq8KbyAS1CfEo-Ud1Dr6WQgAJfEMMPthaPlGRODk,37146464
|
|
16
|
+
midiharmony-26.1.27.dist-info/licenses/LICENSE,sha256=Uar9eD8GksWLFWX8YWqJDo8uIstPH1at58jnS_FEDZw,11364
|
|
17
|
+
midiharmony-26.1.27.dist-info/METADATA,sha256=JMWeYHqfikTr5xVX-cVRrDwD43gII9P-Py-jjwamSsA,3024
|
|
18
|
+
midiharmony-26.1.27.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
19
|
+
midiharmony-26.1.27.dist-info/top_level.txt,sha256=5SwvhlIS9lrTsXPh_XuuIkfxNmPMbPqOk0CVQz9eMog,12
|
|
20
|
+
midiharmony-26.1.27.dist-info/RECORD,,
|