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
midiharmony/__init__.py
ADDED
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# MIDI Harmony Data
|
|
2
|
+
|
|
3
|
+
***
|
|
4
|
+
|
|
5
|
+
### midiharmony chords quads data was extracted from the following quality datasets
|
|
6
|
+
|
|
7
|
+
* [asap=dataset](https://github.com/fosfrancesco/asap-dataset)
|
|
8
|
+
* [ACPAS-dataset](https://github.com/cheriell/ACPAS-dataset)
|
|
9
|
+
* [POP909](https://github.com/music-x-lab/POP909-Dataset)
|
|
10
|
+
* [POP1k7](https://zenodo.org/records/13167761)
|
|
11
|
+
* [Beautiful Music Seeds](https://github.com/asigalov61/Tegridy-MIDI-Dataset/blob/master/Beautiful-Music-Seeds-CC-BY-NC-SA.zip)
|
|
12
|
+
* [The Ultimate 200 Anime Songs Piano Medley](https://github.com/asigalov61/Tegridy-MIDI-Dataset/blob/master/Misc/The-Ultimate-200-Anime-Songs-Piano-Medley-CC-BY-NC-SA.zip)
|
|
13
|
+
|
|
14
|
+
***
|
|
15
|
+
|
|
16
|
+
### Project Los Angeles
|
|
17
|
+
### Tegridy Code 2026
|
|
File without changes
|
|
Binary file
|
midiharmony/helpers.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#! /usr/bin/python3
|
|
2
|
+
|
|
3
|
+
r'''###############################################################################
|
|
4
|
+
###################################################################################
|
|
5
|
+
#
|
|
6
|
+
# Helpers Python Module
|
|
7
|
+
# Version 1.0
|
|
8
|
+
#
|
|
9
|
+
# Project Los Angeles
|
|
10
|
+
#
|
|
11
|
+
# Tegridy Code 2026
|
|
12
|
+
#
|
|
13
|
+
# https://github.com/Tegridy-Code/Project-Los-Angeles
|
|
14
|
+
#
|
|
15
|
+
###################################################################################
|
|
16
|
+
###################################################################################
|
|
17
|
+
#
|
|
18
|
+
# Copyright 2026 Project Los Angeles / Tegridy Code
|
|
19
|
+
#
|
|
20
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
21
|
+
# you may not use this file except in compliance with the License.
|
|
22
|
+
# You may obtain a copy of the License at
|
|
23
|
+
#
|
|
24
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
25
|
+
#
|
|
26
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
27
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
28
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
29
|
+
# See the License for the specific language governing permissions and
|
|
30
|
+
# limitations under the License.
|
|
31
|
+
#
|
|
32
|
+
###################################################################################
|
|
33
|
+
'''
|
|
34
|
+
|
|
35
|
+
print('=' * 70)
|
|
36
|
+
print('Loading midiharmony helpers module...')
|
|
37
|
+
print('Please wait...')
|
|
38
|
+
print('=' * 70)
|
|
39
|
+
|
|
40
|
+
__version__ = '1.0.0'
|
|
41
|
+
|
|
42
|
+
print('midiharmony helpers module version', __version__)
|
|
43
|
+
print('=' * 70)
|
|
44
|
+
|
|
45
|
+
###################################################################################
|
|
46
|
+
|
|
47
|
+
import os
|
|
48
|
+
import shutil
|
|
49
|
+
import subprocess
|
|
50
|
+
import time
|
|
51
|
+
|
|
52
|
+
import hashlib
|
|
53
|
+
|
|
54
|
+
import importlib.resources as pkg_resources
|
|
55
|
+
|
|
56
|
+
from . import data
|
|
57
|
+
|
|
58
|
+
from .TMIDIX import midi2score, score2midi
|
|
59
|
+
|
|
60
|
+
from typing import List, Dict
|
|
61
|
+
|
|
62
|
+
###################################################################################
|
|
63
|
+
|
|
64
|
+
def get_package_data() -> List[Dict]:
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
Get package data included with midisim package
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
List of dicts: {'data': data_file_name,
|
|
72
|
+
'path': data_full_path
|
|
73
|
+
}
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
data_dict = []
|
|
77
|
+
|
|
78
|
+
for resource in pkg_resources.contents(data):
|
|
79
|
+
if resource.endswith('.npz'):
|
|
80
|
+
with pkg_resources.path(data, resource) as p:
|
|
81
|
+
mdic = {'data': resource,
|
|
82
|
+
'path': str(p)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
data_dict.append(mdic)
|
|
86
|
+
|
|
87
|
+
return sorted(data_dict, key=lambda x: x['data'])
|
|
88
|
+
|
|
89
|
+
###################################################################################
|
|
90
|
+
|
|
91
|
+
def get_normalized_midi_md5_hash(midi_file: str) -> Dict:
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
Helper function which computes normalized MD5 hash for any MIDI file
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
Dictionary with MIDI file name, original MD5 hash and normalized MD5 hash
|
|
99
|
+
{'midi_name', 'original_md5', 'normalized_md5'}
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
bfn = os.path.basename(midi_file)
|
|
103
|
+
fn = os.path.splitext(bfn)[0]
|
|
104
|
+
|
|
105
|
+
midi_data = open(midi_file, 'rb').read()
|
|
106
|
+
|
|
107
|
+
old_md5 = hashlib.md5(midi_data).hexdigest()
|
|
108
|
+
|
|
109
|
+
score = midi2score(midi_data, do_not_check_MIDI_signature=True)
|
|
110
|
+
|
|
111
|
+
norm_midi = score2midi(score)
|
|
112
|
+
|
|
113
|
+
new_md5 = hashlib.md5(norm_midi).hexdigest()
|
|
114
|
+
|
|
115
|
+
output_dic = {'midi_name': fn,
|
|
116
|
+
'original_md5': old_md5,
|
|
117
|
+
'normalized_md5': new_md5
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return output_dic
|
|
121
|
+
|
|
122
|
+
###################################################################################
|
|
123
|
+
|
|
124
|
+
def normalize_midi_file(midi_file: str,
|
|
125
|
+
output_dir: str = '',
|
|
126
|
+
output_file_name: str = ''
|
|
127
|
+
) -> str:
|
|
128
|
+
|
|
129
|
+
"""
|
|
130
|
+
Helper function which normalizes any MIDI file and writes it to disk
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
Path string to a written normalized MIDI file
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
if not output_file_name:
|
|
138
|
+
output_file_name = os.path.basename(midi_file)
|
|
139
|
+
|
|
140
|
+
if not output_dir:
|
|
141
|
+
output_dir = os.getcwd()
|
|
142
|
+
|
|
143
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
144
|
+
|
|
145
|
+
midi_path = os.path.join(output_dir, output_file_name)
|
|
146
|
+
|
|
147
|
+
if os.path.exists(midi_path):
|
|
148
|
+
fn = os.path.splitext(output_file_name)[0]
|
|
149
|
+
output_file_name = f'{fn}_normalized.mid'
|
|
150
|
+
midi_path = os.path.join(output_dir, output_file_name)
|
|
151
|
+
|
|
152
|
+
midi_data = open(midi_file, 'rb').read()
|
|
153
|
+
|
|
154
|
+
score = midi2score(midi_data, do_not_check_MIDI_signature=True)
|
|
155
|
+
|
|
156
|
+
norm_midi = score2midi(score)
|
|
157
|
+
|
|
158
|
+
with open(midi_path, 'wb') as fi:
|
|
159
|
+
fi.write(norm_midi)
|
|
160
|
+
|
|
161
|
+
return midi_path
|
|
162
|
+
|
|
163
|
+
###################################################################################
|
|
164
|
+
|
|
165
|
+
def is_installed(pkg: str) -> bool:
|
|
166
|
+
"""Return True if package is already installed (dpkg-query)."""
|
|
167
|
+
try:
|
|
168
|
+
subprocess.run(
|
|
169
|
+
["dpkg-query", "-W", "-f=${Status}", pkg],
|
|
170
|
+
check=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True
|
|
171
|
+
)
|
|
172
|
+
# dpkg-query returns "install ok installed" on success
|
|
173
|
+
out = subprocess.run(["dpkg-query", "-W", "-f=${Status}", pkg],
|
|
174
|
+
stdout=subprocess.PIPE, text=True).stdout.strip()
|
|
175
|
+
return "installed" in out
|
|
176
|
+
except subprocess.CalledProcessError:
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
###################################################################################
|
|
180
|
+
|
|
181
|
+
def _run_apt_get(args, timeout):
|
|
182
|
+
base = ["apt-get", "-y", "-o", "Dpkg::Options::=--force-confdef", "-o", "Dpkg::Options::=--force-confold"]
|
|
183
|
+
cmd = base + args
|
|
184
|
+
return subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=timeout)
|
|
185
|
+
|
|
186
|
+
###################################################################################
|
|
187
|
+
|
|
188
|
+
def install_apt_package(pkg: str = 'fluidsynth',
|
|
189
|
+
update: bool = True,
|
|
190
|
+
timeout: int = 600,
|
|
191
|
+
require_root: bool = True,
|
|
192
|
+
use_python_apt: bool = False
|
|
193
|
+
) -> Dict:
|
|
194
|
+
|
|
195
|
+
"""
|
|
196
|
+
Install an apt package idempotently.
|
|
197
|
+
- pkg: package name (default: 'fluidsynth')
|
|
198
|
+
- update: run apt-get update first
|
|
199
|
+
- timeout: seconds for apt operations
|
|
200
|
+
- require_root: if True, will prefix with sudo when not root (may prompt)
|
|
201
|
+
- use_python_apt: try python-apt API first if True
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
Status dict {'status', 'package'}
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
if is_installed(pkg):
|
|
209
|
+
return {"status": "already_installed", "package": pkg}
|
|
210
|
+
|
|
211
|
+
# Optionally try python-apt (requires python-apt installed and running as root)
|
|
212
|
+
if use_python_apt:
|
|
213
|
+
try:
|
|
214
|
+
import apt
|
|
215
|
+
cache = apt.Cache()
|
|
216
|
+
cache.update()
|
|
217
|
+
cache.open(None)
|
|
218
|
+
if pkg in cache:
|
|
219
|
+
pkg_obj = cache[pkg]
|
|
220
|
+
if not pkg_obj.is_installed:
|
|
221
|
+
pkg_obj.mark_install()
|
|
222
|
+
cache.commit()
|
|
223
|
+
return {"status": "installed_via_python_apt", "package": pkg}
|
|
224
|
+
except Exception:
|
|
225
|
+
# fall through to subprocess fallback
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
# Build command environment
|
|
229
|
+
prefix = []
|
|
230
|
+
if require_root and os.geteuid() != 0:
|
|
231
|
+
if shutil.which("sudo"):
|
|
232
|
+
prefix = ["sudo"]
|
|
233
|
+
else:
|
|
234
|
+
raise PermissionError("Root privileges required and sudo not available.")
|
|
235
|
+
|
|
236
|
+
# Optionally update
|
|
237
|
+
if update:
|
|
238
|
+
tries = 5
|
|
239
|
+
for attempt in range(tries):
|
|
240
|
+
try:
|
|
241
|
+
subprocess.run(prefix + ["apt-get", "update"], check=True, timeout=timeout, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
242
|
+
break
|
|
243
|
+
except subprocess.CalledProcessError as e:
|
|
244
|
+
if attempt + 1 == tries:
|
|
245
|
+
raise
|
|
246
|
+
time.sleep(2 ** attempt)
|
|
247
|
+
|
|
248
|
+
# Install with retry for transient locks
|
|
249
|
+
tries = 6
|
|
250
|
+
for attempt in range(tries):
|
|
251
|
+
try:
|
|
252
|
+
subprocess.run(prefix + ["apt-get", "-y", "install", pkg], check=True, timeout=timeout, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
253
|
+
if is_installed(pkg):
|
|
254
|
+
return {"status": "installed", "package": pkg}
|
|
255
|
+
else:
|
|
256
|
+
raise RuntimeError("apt-get reported success but package not found installed.")
|
|
257
|
+
except subprocess.CalledProcessError as e:
|
|
258
|
+
# common cause: dpkg lock; backoff and retry
|
|
259
|
+
if "Could not get lock" in e.stderr or "dpkg was interrupted" in e.stderr:
|
|
260
|
+
time.sleep(2 ** attempt)
|
|
261
|
+
continue
|
|
262
|
+
raise
|
|
263
|
+
|
|
264
|
+
raise RuntimeError("Failed to install package after retries.")
|
|
265
|
+
|
|
266
|
+
###################################################################################
|
|
267
|
+
|
|
268
|
+
print('Module is loaded!')
|
|
269
|
+
print('Enjoy! :)')
|
|
270
|
+
print('=' * 70)
|
|
271
|
+
|
|
272
|
+
###################################################################################
|
|
273
|
+
# This is the end of the Helpers Python Module
|
|
274
|
+
###################################################################################
|