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,2 @@
1
+ from .midiharmony import *
2
+ from .helpers import *
@@ -0,0 +1,10 @@
1
+ # MIDI Harmony Concept Artwork
2
+
3
+ ***
4
+
5
+ ## Images were created with Stable Diffusion 3.5 Large image AI model
6
+
7
+ ***
8
+
9
+ ### Project Los Angeles
10
+ ### Tegridy Code 2026
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
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
+ ###################################################################################