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.
@@ -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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+