CreativePython 0.3.6__tar.gz → 1.0.0__tar.gz
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.
- {creativepython-0.3.6/src/CreativePython.egg-info → creativepython-1.0.0}/PKG-INFO +2 -4
- {creativepython-0.3.6 → creativepython-1.0.0}/pyproject.toml +4 -6
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/notationRenderer.py +70 -20
- {creativepython-0.3.6 → creativepython-1.0.0/src/CreativePython.egg-info}/PKG-INFO +2 -4
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython.egg-info/requires.txt +1 -4
- {creativepython-0.3.6 → creativepython-1.0.0}/src/gui.py +82 -53
- {creativepython-0.3.6 → creativepython-1.0.0}/src/image.py +0 -1
- {creativepython-0.3.6 → creativepython-1.0.0}/src/midi.py +6 -1
- {creativepython-0.3.6 → creativepython-1.0.0}/src/music.py +47 -24
- {creativepython-0.3.6 → creativepython-1.0.0}/src/timer.py +10 -12
- {creativepython-0.3.6 → creativepython-1.0.0}/src/zipf.py +176 -22
- {creativepython-0.3.6 → creativepython-1.0.0}/LICENSE +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/LICENSE-PSF +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/MANIFEST.in +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/README.md +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/setup.cfg +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/RealtimeAudioPlayer.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/__init__.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/Java-Comparison-Tests/advMetricRunner.pythonSurvey.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/Java-Comparison-Tests/compareMetrics_Java-Vs-Python.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/RunMetrics.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/Surveyor.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/__init__.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/Confidence.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/Contig.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/ExtendedNote.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/Histogram.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/Judgement.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/Measurement.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/PianoRoll.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/PianoRollOld.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/__init__.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/test_ExtendedNote.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/test_Histogram.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/test_Measurement.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/test_PianoRoll_assertions.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/test_PianoRoll_integration.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/test_PianoRoll_quantization.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/test_PianoRoll_unit.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/Metric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/ZipfMetrics.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/__init__.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/ChordDensityMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/ChordDistanceMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/ChordMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/ChordNormalizedMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/ChromaticToneMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/ContourBasslineDurationMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/ContourBasslineDurationQuantizedMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/ContourBasslinePitchMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/ContourMelodyDurationMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/ContourMelodyDurationQuantizedMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/ContourMelodyPitchMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/DurationBigramMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/DurationDistanceMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/DurationMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/DurationQuantizedBigramMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/DurationQuantizedDistanceMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/DurationQuantizedMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/HarmonicBigramMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/HarmonicConsonanceMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/HarmonicIntervalMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/MelodicBigramMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/MelodicConsonanceMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/MelodicIntervalMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/PitchDistanceMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/PitchDurationMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/PitchDurationQuantizedMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/PitchMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/RestMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/__init__.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/test_DurationMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_BasicIntervalsAndBigrams.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_ChordsAndConsonance.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_ContoursAndChromatic.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/test_Metrics_QuantizedDurationsAndDistances.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/test_PitchMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/test_RestMetric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/test_Metric.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/utilities/CSVWriter.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/utilities/PowerLawRandom.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/utilities/__init__.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython.egg-info/SOURCES.txt +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython.egg-info/dependency_links.txt +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython.egg-info/top_level.txt +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/bin/libportaudio.2.dylib +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/iannix.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/markov.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/src/osc.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/tests/testAnimate.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/tests/testPeer.py +0 -0
- {creativepython-0.3.6 → creativepython-1.0.0}/tests/test_keyEvent.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: CreativePython
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: A Python-based software environment for developing algorithmic art projects.
|
|
5
5
|
Author-email: "Dr. Bill Manaris" <manaris@cofc.edu>, Taj Ballinger <ballingertj@g.cofc.edu>, Trevor Ritchie <ritchiets@g.cofc.edu>
|
|
6
6
|
License: MIT License
|
|
@@ -65,9 +65,7 @@ Requires-Dist: pooch>=1.8
|
|
|
65
65
|
Requires-Dist: pypianoroll>=1.0
|
|
66
66
|
Requires-Dist: verovio>=5.6.0
|
|
67
67
|
Requires-Dist: pymusicxml>=0.5.6
|
|
68
|
-
|
|
69
|
-
Requires-Dist: build; extra == "dev"
|
|
70
|
-
Requires-Dist: twine; extra == "dev"
|
|
68
|
+
Requires-Dist: pyinstaller>=6.16.0
|
|
71
69
|
Dynamic: license-file
|
|
72
70
|
|
|
73
71
|
# CreativePython
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "CreativePython"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "1.0.0"
|
|
8
8
|
description = "A Python-based software environment for developing algorithmic art projects."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -33,7 +33,8 @@ dependencies = [
|
|
|
33
33
|
"pooch>=1.8",
|
|
34
34
|
"pypianoroll>=1.0",
|
|
35
35
|
"verovio>=5.6.0",
|
|
36
|
-
"pymusicxml>=0.5.6"
|
|
36
|
+
"pymusicxml>=0.5.6",
|
|
37
|
+
"pyinstaller>=6.16.0"
|
|
37
38
|
]
|
|
38
39
|
|
|
39
40
|
[tool.setuptools]
|
|
@@ -47,7 +48,4 @@ where = ["src"]
|
|
|
47
48
|
"CreativePython" = ["bin/libportaudio.2.dylib"]
|
|
48
49
|
|
|
49
50
|
[project.urls]
|
|
50
|
-
Homepage = "https://jythonmusic.me"
|
|
51
|
-
|
|
52
|
-
[project.optional-dependencies]
|
|
53
|
-
dev = ["build", "twine"]
|
|
51
|
+
Homepage = "https://jythonmusic.me"
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
import xml.etree.ElementTree as ET
|
|
20
20
|
from xml.dom import minidom
|
|
21
21
|
import os
|
|
22
|
-
import subprocess
|
|
23
22
|
import sys
|
|
24
23
|
|
|
25
24
|
# NOTE: Local, function-level imports are used in this module. This is a
|
|
@@ -73,7 +72,7 @@ _DRUM_MAP = {
|
|
|
73
72
|
|
|
74
73
|
### API (called from music.py) ###############################################
|
|
75
74
|
|
|
76
|
-
def _showNotation(score, title="Sheet Music"):
|
|
75
|
+
def _showNotation(score, title="Sheet Music", writeToFile=False):
|
|
77
76
|
"""
|
|
78
77
|
Renders a Score object into visual sheet music and displays it.
|
|
79
78
|
|
|
@@ -103,10 +102,10 @@ def _showNotation(score, title="Sheet Music"):
|
|
|
103
102
|
# fallback for interactive sessions
|
|
104
103
|
script_dir = os.getcwd()
|
|
105
104
|
|
|
106
|
-
output_dir = os.path.join(script_dir, "notation")
|
|
105
|
+
# output_dir = os.path.join(script_dir, "notation")
|
|
107
106
|
|
|
108
|
-
# create the notation directory if it doesn't exist
|
|
109
|
-
os.makedirs(output_dir, exist_ok=True)
|
|
107
|
+
# # create the notation directory if it doesn't exist
|
|
108
|
+
# os.makedirs(output_dir, exist_ok=True)
|
|
110
109
|
|
|
111
110
|
# Generate filename from title
|
|
112
111
|
if title and title.strip() and title.strip() != "Untitled Score":
|
|
@@ -115,7 +114,7 @@ def _showNotation(score, title="Sheet Music"):
|
|
|
115
114
|
else:
|
|
116
115
|
svg_filename = "sheet_music.svg"
|
|
117
116
|
|
|
118
|
-
output_path = os.path.join(
|
|
117
|
+
output_path = os.path.join(script_dir, svg_filename)
|
|
119
118
|
|
|
120
119
|
# generate MusicXML
|
|
121
120
|
musicxml = _scoreToMusicXML(score)
|
|
@@ -125,7 +124,7 @@ def _showNotation(score, title="Sheet Music"):
|
|
|
125
124
|
|
|
126
125
|
if svg is not None:
|
|
127
126
|
# success - save file and open in viewer
|
|
128
|
-
_openInViewer(svg, output_path)
|
|
127
|
+
_openInViewer(svg, output_path, writeToFile)
|
|
129
128
|
print(f"Sheet music saved to: {output_path}")
|
|
130
129
|
return True
|
|
131
130
|
else:
|
|
@@ -768,7 +767,10 @@ def _renderToSVG(musicxml):
|
|
|
768
767
|
return None # verovio not available
|
|
769
768
|
|
|
770
769
|
# create toolkit
|
|
771
|
-
tk = verovio.toolkit()
|
|
770
|
+
tk = verovio.toolkit(False)
|
|
771
|
+
|
|
772
|
+
# ensure verovio fonts are available
|
|
773
|
+
tk.setResourcePath(os.path.join(os.path.dirname(verovio.__file__), "data"))
|
|
772
774
|
|
|
773
775
|
# configure options for the svg output, controlling layout and appearance
|
|
774
776
|
tk.setOptions({
|
|
@@ -787,7 +789,7 @@ def _renderToSVG(musicxml):
|
|
|
787
789
|
return svg
|
|
788
790
|
|
|
789
791
|
|
|
790
|
-
def _openInViewer(svg, path):
|
|
792
|
+
def _openInViewer(svg, path, writeToFile):
|
|
791
793
|
"""
|
|
792
794
|
Saves an SVG string to a file and opens it with the default system viewer.
|
|
793
795
|
|
|
@@ -799,17 +801,65 @@ def _openInViewer(svg, path):
|
|
|
799
801
|
svg (str): The SVG data to save and display.
|
|
800
802
|
path (str): The absolute file path to save the SVG to.
|
|
801
803
|
"""
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
804
|
+
import pathlib, webbrowser, tempfile, urllib.parse
|
|
805
|
+
|
|
806
|
+
if writeToFile:
|
|
807
|
+
# save to specified path
|
|
808
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
809
|
+
f.write(svg)
|
|
810
|
+
svgPath = pathlib.Path(path).expanduser().resolve() # absolute path to written SVG
|
|
811
|
+
|
|
812
|
+
else:
|
|
813
|
+
# save to temporary location
|
|
814
|
+
with tempfile.NamedTemporaryFile("w", suffix=".svg", delete=False) as f:
|
|
815
|
+
f.write(svg)
|
|
816
|
+
svgPath = pathlib.Path(f.name).resolve() # absolute path to temporary SVG
|
|
817
|
+
|
|
818
|
+
if svgPath.is_file():
|
|
819
|
+
svgURL = svgPath.as_uri() # convert filepath to URL (with correct space handling)
|
|
820
|
+
|
|
821
|
+
# generate minimal HTML wrapper
|
|
822
|
+
# SVG is a generic document type, so often get opened in text editors
|
|
823
|
+
# (TextMate, VSCode, etc.). We want to open the SVG in the user's
|
|
824
|
+
# default web browser, so we wrap it in HTML before opening.
|
|
825
|
+
html = f"""<!doctype html>
|
|
826
|
+
<html>
|
|
827
|
+
<head>
|
|
828
|
+
<meta charset="utf-8">
|
|
829
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
830
|
+
<title>{svgPath.name}</title>
|
|
831
|
+
<style>
|
|
832
|
+
html, body {{ height: 100%; margin: 0; }}
|
|
833
|
+
body {{ display: grid; place-items: center; background: #fff; }}
|
|
834
|
+
img {{ max-width: 100vw; max-height: 100vh; }}
|
|
835
|
+
</style>
|
|
836
|
+
</head>
|
|
837
|
+
<body>
|
|
838
|
+
<img src="{svgURL}" alt="{svgPath.name}">
|
|
839
|
+
</body>
|
|
840
|
+
</html>
|
|
841
|
+
"""
|
|
842
|
+
|
|
843
|
+
# write wrapper to a temporary file
|
|
844
|
+
with tempfile.NamedTemporaryFile("w", suffix=".html", delete=False, encoding="utf-8") as f:
|
|
845
|
+
f.write(html)
|
|
846
|
+
htmlPath = pathlib.Path(f.name).resolve() # absolute path to HTML
|
|
847
|
+
htmlURL = htmlPath.as_uri() # convert to URL
|
|
848
|
+
|
|
849
|
+
ok = webbrowser.open(htmlURL, new=2) # open in new tab, if possible
|
|
850
|
+
if not ok:
|
|
851
|
+
webbrowser.open(htmlURL) # fallback to new browser window
|
|
852
|
+
|
|
853
|
+
else:
|
|
854
|
+
print(f"View.notate(): Couldn't open generated SVG '{path}' (You can open this notation in most web browsers).")
|
|
855
|
+
|
|
856
|
+
# # use the appropriate system command to open the file in the default viewer
|
|
857
|
+
# if sys.platform == 'win32':
|
|
858
|
+
# os.startfile(path)
|
|
859
|
+
# elif sys.platform == 'darwin': # macOS
|
|
860
|
+
# subprocess.run(['open', path])
|
|
861
|
+
# else: # linux
|
|
862
|
+
# subprocess.run(['xdg-open', path])
|
|
813
863
|
|
|
814
864
|
|
|
815
865
|
def _saveMusicXML(musicxml, filename):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: CreativePython
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: A Python-based software environment for developing algorithmic art projects.
|
|
5
5
|
Author-email: "Dr. Bill Manaris" <manaris@cofc.edu>, Taj Ballinger <ballingertj@g.cofc.edu>, Trevor Ritchie <ritchiets@g.cofc.edu>
|
|
6
6
|
License: MIT License
|
|
@@ -65,9 +65,7 @@ Requires-Dist: pooch>=1.8
|
|
|
65
65
|
Requires-Dist: pypianoroll>=1.0
|
|
66
66
|
Requires-Dist: verovio>=5.6.0
|
|
67
67
|
Requires-Dist: pymusicxml>=0.5.6
|
|
68
|
-
|
|
69
|
-
Requires-Dist: build; extra == "dev"
|
|
70
|
-
Requires-Dist: twine; extra == "dev"
|
|
68
|
+
Requires-Dist: pyinstaller>=6.16.0
|
|
71
69
|
Dynamic: license-file
|
|
72
70
|
|
|
73
71
|
# CreativePython
|
|
@@ -5,11 +5,7 @@
|
|
|
5
5
|
import PySide6.QtWidgets as _QtWidgets
|
|
6
6
|
import PySide6.QtGui as _QtGui
|
|
7
7
|
import PySide6.QtCore as _QtCore
|
|
8
|
-
import PySide6.QtSvg as _QtSvg
|
|
9
8
|
import PySide6.QtOpenGLWidgets as _QtOpenGL
|
|
10
|
-
import PySide6.QtWebEngineCore as _QtWeb
|
|
11
|
-
import PySide6.QtWebEngineWidgets as _QtWebW
|
|
12
|
-
import PySide6.QtSvg as _QtSvg
|
|
13
9
|
import numpy as np
|
|
14
10
|
#######################################################################################
|
|
15
11
|
|
|
@@ -1468,6 +1464,8 @@ class Drawable:
|
|
|
1468
1464
|
self._originalWidth = 0 # initial dimensions, used for rebuilding shapes
|
|
1469
1465
|
self._originalHeight = 0 # ...
|
|
1470
1466
|
self._rotation = 0 # ...
|
|
1467
|
+
self._anchorX = None # ... rotation origin, defaults to center if not set
|
|
1468
|
+
self._anchorY = None # ...
|
|
1471
1469
|
self._toolTipText = None # text to Display on mouse over (None == disabled)
|
|
1472
1470
|
|
|
1473
1471
|
def __str__(self):
|
|
@@ -1598,19 +1596,68 @@ class Drawable:
|
|
|
1598
1596
|
"""
|
|
1599
1597
|
return int(self._rotation)
|
|
1600
1598
|
|
|
1601
|
-
def setRotation(self, rotation):
|
|
1599
|
+
def setRotation(self, rotation, anchorX=None, anchorY=None):
|
|
1602
1600
|
"""
|
|
1603
1601
|
Sets the item's rotation (in degrees).
|
|
1604
1602
|
Rotation starts at 3 o'clock, increasing counter-clockwise.
|
|
1605
|
-
Items rotate around their
|
|
1603
|
+
Items rotate around their center by default,
|
|
1604
|
+
or around the origin, if specified.
|
|
1606
1605
|
"""
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
self.
|
|
1613
|
-
|
|
1606
|
+
if anchorX is not None:
|
|
1607
|
+
# anchorX is specified, so remember it for later
|
|
1608
|
+
self._anchorX = anchorX
|
|
1609
|
+
else:
|
|
1610
|
+
# anchorX isn't specified, so use existing anchor
|
|
1611
|
+
if self._anchorX is not None:
|
|
1612
|
+
# anchorX has been set before, so use it
|
|
1613
|
+
anchorX = self._anchorX
|
|
1614
|
+
else:
|
|
1615
|
+
# anchorX hasn't been set yet, so default to centerX
|
|
1616
|
+
anchorX = self.getWidth() / 2
|
|
1617
|
+
|
|
1618
|
+
if anchorY is not None:
|
|
1619
|
+
# anchorY is specified, so remember it for later
|
|
1620
|
+
self._anchorY = anchorY
|
|
1621
|
+
else:
|
|
1622
|
+
# anchorY isn't specified, so use existing anchor
|
|
1623
|
+
if self._anchorY is not None:
|
|
1624
|
+
# anchorY has been set before, so use it
|
|
1625
|
+
anchorY = self._anchorY
|
|
1626
|
+
else:
|
|
1627
|
+
# anchorY hasn't been set yet, so default to centerY
|
|
1628
|
+
anchorY = self.getHeight() / 2
|
|
1629
|
+
|
|
1630
|
+
self._rotation = rotation # remember rotation
|
|
1631
|
+
qtRotation = -rotation % 360 # reverse increasing direction (CCW -> clockwise)
|
|
1632
|
+
self._qObject.setTransformOriginPoint(anchorX, anchorY)
|
|
1633
|
+
self._qObject.prepareGeometryChange() # invalidate Qt hitbox
|
|
1634
|
+
self._qObject.setRotation(qtRotation) # update rotation
|
|
1635
|
+
|
|
1636
|
+
def getAnchor(self):
|
|
1637
|
+
"""
|
|
1638
|
+
Returns the item's (x, y) anchor position (in pixels).
|
|
1639
|
+
Rotations are centered around the anchor, relative to the item's position.
|
|
1640
|
+
"""
|
|
1641
|
+
anchorX = self._anchorX
|
|
1642
|
+
anchorY = self._anchorY
|
|
1643
|
+
|
|
1644
|
+
# default to center coordinates
|
|
1645
|
+
if anchorX is None:
|
|
1646
|
+
anchorX = self.getWidth() / 2
|
|
1647
|
+
if anchorY is None:
|
|
1648
|
+
anchorY = self.getHeight() / 2
|
|
1649
|
+
|
|
1650
|
+
return (int(anchorX), int(anchorY))
|
|
1651
|
+
|
|
1652
|
+
def setAnchor(self, anchorX, anchorY):
|
|
1653
|
+
"""
|
|
1654
|
+
Sets the item's (x, y) anchor position (in pixels).
|
|
1655
|
+
Rotations are centered around the anchor, relative to the item's position.
|
|
1656
|
+
Setting either anchor to None defaults to the center.
|
|
1657
|
+
"""
|
|
1658
|
+
self._anchorX = anchorX
|
|
1659
|
+
self._anchorY = anchorY
|
|
1660
|
+
# we don't need to alter the Qt object until the rotation changes
|
|
1614
1661
|
|
|
1615
1662
|
##### CONVENIENCE METHODS
|
|
1616
1663
|
# These methods are aliases for the methods above,
|
|
@@ -1674,11 +1721,11 @@ class Drawable:
|
|
|
1674
1721
|
x, y = self.getPosition()
|
|
1675
1722
|
self.setPosition(x + dx, y + dy)
|
|
1676
1723
|
|
|
1677
|
-
def rotate(self, angle):
|
|
1724
|
+
def rotate(self, angle, anchorX=None, anchorY=None):
|
|
1678
1725
|
"""
|
|
1679
1726
|
Rotates the item by the given angle (in degrees).
|
|
1680
1727
|
"""
|
|
1681
|
-
self.setRotation(self.getRotation() + angle)
|
|
1728
|
+
self.setRotation(self.getRotation() + angle, anchorX, anchorY)
|
|
1682
1729
|
|
|
1683
1730
|
##### LOCATION TESTS
|
|
1684
1731
|
# These methods help with hit testing and location detection.
|
|
@@ -2209,12 +2256,18 @@ class Control(Drawable, Interactable):
|
|
|
2209
2256
|
self._width = width
|
|
2210
2257
|
self._height = height
|
|
2211
2258
|
|
|
2212
|
-
def setRotation(self, rotation):
|
|
2259
|
+
def setRotation(self, rotation, anchorX=None, anchorY=None):
|
|
2213
2260
|
"""
|
|
2214
2261
|
Controls cannot be rotated.
|
|
2215
2262
|
"""
|
|
2216
2263
|
print(f"{type(self).__name__}.setRotation(): Controls cannot be rotated.")
|
|
2217
2264
|
|
|
2265
|
+
def setAnchor(self, anchorX, anchorY):
|
|
2266
|
+
"""
|
|
2267
|
+
Controls cannot be rotated, so anchors cannot be set.
|
|
2268
|
+
"""
|
|
2269
|
+
print(f"{type(self).__name__}.setAnchor(): Controls cannot be rotated, so anchors cannot be set.")
|
|
2270
|
+
|
|
2218
2271
|
|
|
2219
2272
|
#######################################################################################
|
|
2220
2273
|
# Graphics Objects (Geometric shapes, text, and images)
|
|
@@ -2893,41 +2946,11 @@ class Icon(Graphics):
|
|
|
2893
2946
|
Read more: https://doc.qt.io/qt-6/qpixmap.html#reading-and-writing-image-files
|
|
2894
2947
|
"""
|
|
2895
2948
|
Graphics.__init__(self)
|
|
2949
|
+
from pathlib import Path
|
|
2950
|
+
path = Path(filename)
|
|
2896
2951
|
|
|
2897
2952
|
# initialize internal shape
|
|
2898
2953
|
try:
|
|
2899
|
-
# if filename.lower().endswith(".svg"): # rasterize SVG file
|
|
2900
|
-
# # render with QWebEngine... (fails to launch web event loop)
|
|
2901
|
-
# view = _QtWebW.QWebEngineView()
|
|
2902
|
-
# with open(filename, "r", encoding="utf-8") as f:
|
|
2903
|
-
# svg = f.read()
|
|
2904
|
-
|
|
2905
|
-
# html = f"""
|
|
2906
|
-
# <!DOCTYPE html>
|
|
2907
|
-
# <html>
|
|
2908
|
-
# <body style="margin:0; padding:0; overflow:hidden;">
|
|
2909
|
-
# {svg}
|
|
2910
|
-
# </body>
|
|
2911
|
-
# </html>
|
|
2912
|
-
# """
|
|
2913
|
-
# view.setHtml(html, _QtCore.QUrl.fromLocalFile(filename))
|
|
2914
|
-
# view.show()
|
|
2915
|
-
# size = _QtCore.QSize(600, 400)
|
|
2916
|
-
# pixmap = _QtGui.QPixmap(size)
|
|
2917
|
-
# pixmap.fill(_QtGui.QColorConstants.Transparent)
|
|
2918
|
-
|
|
2919
|
-
# # render with SVG Renderer... (only supports Tiny 1.2)
|
|
2920
|
-
# renderer = _QtSvg.QSvgRenderer(filename)
|
|
2921
|
-
# size = renderer.defaultSize()
|
|
2922
|
-
# pixmap = _QtGui.QPixmap(size)
|
|
2923
|
-
# pixmap.fill(_QtGui.QColorConstants.Transparent)
|
|
2924
|
-
# painter = _QtGui.QPainter(pixmap)
|
|
2925
|
-
# renderer.render(painter)
|
|
2926
|
-
# painter.end()
|
|
2927
|
-
|
|
2928
|
-
# else:
|
|
2929
|
-
# pixmap = _QtGui.QPixmap(filename) # create pixmap from file
|
|
2930
|
-
|
|
2931
2954
|
pixmap = _QtGui.QPixmap(filename) # create pixmap from file
|
|
2932
2955
|
|
|
2933
2956
|
if width is None and height is None: # no scaling needed
|
|
@@ -2940,7 +2963,7 @@ class Icon(Graphics):
|
|
|
2940
2963
|
|
|
2941
2964
|
scaledPixmap = pixmap.scaled(width, height) # scale new pixmap
|
|
2942
2965
|
|
|
2943
|
-
except:
|
|
2966
|
+
except: # ... create blank pixmap if file fails to load
|
|
2944
2967
|
if width is None:
|
|
2945
2968
|
width = 600
|
|
2946
2969
|
if height is None:
|
|
@@ -3184,7 +3207,7 @@ class Label(Graphics):
|
|
|
3184
3207
|
"""
|
|
3185
3208
|
return self.getColor()
|
|
3186
3209
|
|
|
3187
|
-
def setForegroundColor(self, color):
|
|
3210
|
+
def setForegroundColor(self, color=None):
|
|
3188
3211
|
"""
|
|
3189
3212
|
Sets the label's font color.
|
|
3190
3213
|
If no color is provided, a color selection box will appear.
|
|
@@ -3198,13 +3221,19 @@ class Label(Graphics):
|
|
|
3198
3221
|
r, g, b, a = self._backgroundColor
|
|
3199
3222
|
return Color(r, g, b, a)
|
|
3200
3223
|
|
|
3201
|
-
def setBackgroundColor(self, color):
|
|
3224
|
+
def setBackgroundColor(self, color=None):
|
|
3202
3225
|
"""
|
|
3203
3226
|
Sets the label's background color.
|
|
3204
3227
|
If no color is provided, a color selection box will appear.
|
|
3205
3228
|
"""
|
|
3206
|
-
if color is None:
|
|
3207
|
-
|
|
3229
|
+
if color is None:
|
|
3230
|
+
# open color selector if no value provided
|
|
3231
|
+
r, g, b = _selectColor()
|
|
3232
|
+
color = Color(r, g, b)
|
|
3233
|
+
|
|
3234
|
+
elif not isinstance(color, Color):
|
|
3235
|
+
# throw error if wrong data type entered
|
|
3236
|
+
raise TypeError(f'{type(self).__name__}.setBackgroundColor(): color should be a Color object (it was {type(color).__name__})')
|
|
3208
3237
|
|
|
3209
3238
|
r, g, b, a = color.getRGBA()
|
|
3210
3239
|
self._backgroundColor = [r, g, b, a]
|
|
@@ -56,7 +56,6 @@ class Image():
|
|
|
56
56
|
def _fromPNGBytes(data):
|
|
57
57
|
"""
|
|
58
58
|
Returns a new Image object built from raw PNG data.
|
|
59
|
-
Used by music.View to render images from Verovio.
|
|
60
59
|
"""
|
|
61
60
|
icon = Icon._fromPNGBytes(data) # generate icon from data
|
|
62
61
|
width, height = icon.getSize() # get icon dimensions
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
|
|
22
22
|
import mido # provides MIDI input/output and message handling
|
|
23
23
|
from timer import Timer2 # for scheduling MIDI events
|
|
24
|
-
from gui import Display, DropDownList, Color # gui elements for MIDI device selection and display
|
|
24
|
+
# from gui import Display, DropDownList, Color # gui elements for MIDI device selection and display
|
|
25
|
+
# gui elements are lazy-loaded only when device selection GUI is needed
|
|
25
26
|
import PySide6.QtCore as _QtCore # Qt core utilities for event handling and integration
|
|
26
27
|
from music import freqToNote # converts frequency values to MIDI note numbers
|
|
27
28
|
import atexit # ensures cleanup of MIDI resources on program exit
|
|
@@ -167,6 +168,8 @@ class MidiIn(_QtCore.QObject):
|
|
|
167
168
|
items.sort()
|
|
168
169
|
|
|
169
170
|
if len(items) > 0: # if availabale inputs exist
|
|
171
|
+
from gui import Display, DropDownList, Color
|
|
172
|
+
|
|
170
173
|
# create selection display
|
|
171
174
|
self.display = Display("Select MIDI Input", 400, 125) # display info to user
|
|
172
175
|
self.display.drawLabel('Select a MIDI input device from the list', 45, 30)
|
|
@@ -916,6 +919,8 @@ class MidiOut:
|
|
|
916
919
|
items.sort()
|
|
917
920
|
|
|
918
921
|
if len(items) > 0: # if available outputs exist
|
|
922
|
+
from gui import Display, DropDownList, Color
|
|
923
|
+
|
|
919
924
|
# create selection display
|
|
920
925
|
self.display = Display("Select MIDI Output", 400, 125) # display info to user
|
|
921
926
|
self.display.drawLabel('Select a MIDI output device from the list', 45, 30)
|
|
@@ -3318,25 +3318,34 @@ class Mod():
|
|
|
3318
3318
|
raise TypeError(f"Unrecognized material type {type(material)} - expected Phrase, Part, or Score.")
|
|
3319
3319
|
|
|
3320
3320
|
@staticmethod
|
|
3321
|
-
def invert(phrase, pitchAxis):
|
|
3321
|
+
def invert(phrase, pitchAxis, scale=CHROMATIC_SCALE, key=0):
|
|
3322
3322
|
"""
|
|
3323
3323
|
Invert phrase using pitch as the mirror (pivot) axis.
|
|
3324
3324
|
phrase: Phrase to invert
|
|
3325
3325
|
pitchAxis: Pitch axis to pivot around (0-127)
|
|
3326
|
+
scale:
|
|
3326
3327
|
"""
|
|
3327
3328
|
# do some basic error checking
|
|
3328
3329
|
if type(pitchAxis) is not int:
|
|
3329
3330
|
raise TypeError(f"Unrecognized pitchAxis type {type(pitchAxis)} - expected int.")
|
|
3330
3331
|
if type(phrase) is not Phrase:
|
|
3331
3332
|
raise TypeError(f"Unrecognized material type {type(phrase)} - expected Phrase.")
|
|
3333
|
+
|
|
3332
3334
|
# traverse list of notes, and adjust pitches accordingly
|
|
3335
|
+
smallestDuration = None
|
|
3336
|
+
|
|
3333
3337
|
for note in phrase.getNoteList():
|
|
3334
3338
|
|
|
3335
3339
|
if not note.isRest(): # modify regular notes only (i.e., do not modify rests)
|
|
3336
3340
|
invertedPitch = pitchAxis + (pitchAxis - note.getPitch()) # find mirror pitch around axis (by adding difference)
|
|
3337
3341
|
note.setPitch(invertedPitch) # and update it
|
|
3338
3342
|
|
|
3339
|
-
|
|
3343
|
+
if (smallestDuration is None) or (note.getDuration() < smallestDuration):
|
|
3344
|
+
# track the smallest non-REST duration in the phrase
|
|
3345
|
+
smallestDuration = note.getDuration()
|
|
3346
|
+
|
|
3347
|
+
# now, all notes have been updated. Next, quantize to filter into the given scale
|
|
3348
|
+
Mod.quantize(phrase, smallestDuration, scale, key)
|
|
3340
3349
|
|
|
3341
3350
|
@staticmethod
|
|
3342
3351
|
def merge(material1, material2):
|
|
@@ -3475,19 +3484,32 @@ class Mod():
|
|
|
3475
3484
|
to fit multiples of quantum (e.g., 1.0), using the specified scale (e.g., MAJOR_SCALE),
|
|
3476
3485
|
and the specified tonic (0 means C, 1 means C sharp, 2 means D, and so on).
|
|
3477
3486
|
"""
|
|
3478
|
-
|
|
3479
3487
|
# define helper functions
|
|
3480
3488
|
def quantizeNote(note):
|
|
3481
3489
|
"""Helper function to quantize a note."""
|
|
3482
|
-
if note.getPitch() != REST:
|
|
3483
|
-
interval = note.getPitch() % 12
|
|
3484
|
-
while interval not in scale: # if pitch is not in the scale...
|
|
3485
|
-
interval -= 1 # lower pitch by one semitone
|
|
3486
|
-
|
|
3490
|
+
if note.getPitch() != REST: # ignore rests
|
|
3487
3491
|
# calculate new duration as a multiple of quantum
|
|
3488
3492
|
newDuration = round(note.getDuration() / quantum) * quantum
|
|
3489
3493
|
note.setDuration(newDuration)
|
|
3490
3494
|
|
|
3495
|
+
# coerce new pitch to given scale
|
|
3496
|
+
noteInterval = note.getPitch() % 12 # interval from base key
|
|
3497
|
+
noteKey = note.getPitch() - noteInterval # base key
|
|
3498
|
+
i = len(scale) - 1
|
|
3499
|
+
|
|
3500
|
+
while i >= 0: # check each interval in scale, back to front
|
|
3501
|
+
scaleInterval = scale[i]
|
|
3502
|
+
if noteInterval >= scaleInterval:
|
|
3503
|
+
# set noteInterval to first scaleInterval it exceeds
|
|
3504
|
+
noteInterval = scaleInterval
|
|
3505
|
+
i = -1 # break
|
|
3506
|
+
else:
|
|
3507
|
+
i = i - 1
|
|
3508
|
+
|
|
3509
|
+
# adjust pitch accordingly
|
|
3510
|
+
newPitch = noteKey + noteInterval
|
|
3511
|
+
note.setPitch(newPitch)
|
|
3512
|
+
|
|
3491
3513
|
def quantizePhrase(phrase):
|
|
3492
3514
|
"""Helper function to quantize a phrase."""
|
|
3493
3515
|
for note in phrase.getNoteList():
|
|
@@ -3503,25 +3525,25 @@ class Mod():
|
|
|
3503
3525
|
for part in score.getPartList():
|
|
3504
3526
|
quantizePart(part)
|
|
3505
3527
|
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3528
|
+
# check type of steps
|
|
3529
|
+
if type(quantum) not in (int, float):
|
|
3530
|
+
raise TypeError( "Unrecognized quantum type " + str(type(quantum)) + " - expected a number." )
|
|
3509
3531
|
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3532
|
+
# check type of material and execute the appropriate code
|
|
3533
|
+
if isinstance(material, Score):
|
|
3534
|
+
quantizeScore(material)
|
|
3513
3535
|
|
|
3514
|
-
|
|
3515
|
-
|
|
3536
|
+
elif isinstance(material, Part):
|
|
3537
|
+
quantizePart(material)
|
|
3516
3538
|
|
|
3517
|
-
|
|
3518
|
-
|
|
3539
|
+
elif isinstance(material, Phrase):
|
|
3540
|
+
quantizePhrase(material)
|
|
3519
3541
|
|
|
3520
|
-
|
|
3521
|
-
|
|
3542
|
+
elif isinstance(material, Note):
|
|
3543
|
+
quantizeNote(material)
|
|
3522
3544
|
|
|
3523
|
-
|
|
3524
|
-
|
|
3545
|
+
else: # error check
|
|
3546
|
+
raise TypeError( "Unrecognized material type " + str(type(material)) + " - expected Note, Phrase, Part, or Score." )
|
|
3525
3547
|
|
|
3526
3548
|
@staticmethod
|
|
3527
3549
|
def randomize(material, pitchAmount, durationAmount=0, volumeAmount=0):
|
|
@@ -6404,9 +6426,10 @@ class View:
|
|
|
6404
6426
|
print(f'View.sketch(): Unrecognized type {type(material)}, expected Score, Part, or Phrase.')
|
|
6405
6427
|
|
|
6406
6428
|
@staticmethod
|
|
6407
|
-
def notate(material):
|
|
6429
|
+
def notate(material, writeToFile=False):
|
|
6408
6430
|
"""
|
|
6409
6431
|
Visualize music as staff notation, where material may be a Score, Part, or Phrase.
|
|
6432
|
+
Setting writeToFile to True saves the visualization as an SVG file.
|
|
6410
6433
|
"""
|
|
6411
6434
|
# import internal notation renderer
|
|
6412
6435
|
from CreativePython.notationRenderer import _showNotation
|
|
@@ -6424,7 +6447,7 @@ class View:
|
|
|
6424
6447
|
title = material.getTitle() if material.getTitle() else "Sheet Music"
|
|
6425
6448
|
|
|
6426
6449
|
# delegate to internal renderer
|
|
6427
|
-
_showNotation(material, title)
|
|
6450
|
+
_showNotation(material, title, writeToFile)
|
|
6428
6451
|
|
|
6429
6452
|
else:
|
|
6430
6453
|
print("View.notate: material must be a Score, Part, Phrase, or Note.")
|
|
@@ -18,15 +18,7 @@
|
|
|
18
18
|
#
|
|
19
19
|
###############################################################################
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
from PySide6.QtWidgets import QApplication
|
|
23
|
-
from PySide6.QtCore import QTimer
|
|
24
|
-
from PySide6 import QtCore as _QtCore
|
|
25
|
-
|
|
26
|
-
# Timer2 (a custom implementation, used internally)
|
|
27
|
-
import threading
|
|
28
|
-
import time
|
|
29
|
-
import atexit
|
|
21
|
+
import threading, time, atexit
|
|
30
22
|
|
|
31
23
|
##############################################################################
|
|
32
24
|
# ensure a QApplication exists (needed for QTimer in Timer class)
|
|
@@ -38,6 +30,8 @@ def _ensureApp():
|
|
|
38
30
|
# this function is called whenever we create a new display,
|
|
39
31
|
# or queue a function that modifies the display (or the display's items)
|
|
40
32
|
global _QTAPP_
|
|
33
|
+
from PySide6.QtWidgets import QApplication
|
|
34
|
+
|
|
41
35
|
if _QTAPP_ is None:
|
|
42
36
|
# try to find an existing QApplication instance
|
|
43
37
|
_QTAPP_ = QApplication.instance()
|
|
@@ -52,9 +46,6 @@ def _ensureApp():
|
|
|
52
46
|
}
|
|
53
47
|
""")
|
|
54
48
|
|
|
55
|
-
_ensureApp()
|
|
56
|
-
|
|
57
|
-
|
|
58
49
|
###############################################################################
|
|
59
50
|
# Timer
|
|
60
51
|
#
|
|
@@ -90,6 +81,13 @@ class Timer():
|
|
|
90
81
|
def __init__ (self, timeInterval, function, parameters=[], repeat=True):
|
|
91
82
|
"""Specify time interval (in milliseconds), which function to call when the time interval has passed
|
|
92
83
|
and the parameters to pass this function, and whether to repeat (True) or do it only once."""
|
|
84
|
+
# lazy import Qt modules only when Timer is instantiated
|
|
85
|
+
from PySide6.QtCore import QTimer
|
|
86
|
+
from PySide6 import QtCore as _QtCore
|
|
87
|
+
|
|
88
|
+
# ensure QApplication exists before creating QTimer
|
|
89
|
+
_ensureApp()
|
|
90
|
+
|
|
93
91
|
# create an internal QTimer
|
|
94
92
|
self._timer = QTimer() # default QTimer() is a CoarseTimer
|
|
95
93
|
self._timer.setTimerType(_QtCore.Qt.TimerType.PreciseTimer) # use PreciseTimer for smoother animations
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
# version 1.0 (May 10, 2003)
|
|
59
59
|
#
|
|
60
60
|
|
|
61
|
-
from CreativePython.nevmuse import *
|
|
62
|
-
from math import log, sqrt
|
|
61
|
+
from CreativePython.nevmuse import * # Zipf Metrics
|
|
62
|
+
from math import log, sqrt # logarithmic calculations
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
def byRank(counts):
|
|
@@ -251,16 +251,93 @@ def measureMidi(files, metrics, quantum=0.25):
|
|
|
251
251
|
return measureScore(scores, metrics, quantum)
|
|
252
252
|
|
|
253
253
|
|
|
254
|
+
def measureDataByRank(datasets, metricName="ByRank"):
|
|
255
|
+
"""
|
|
256
|
+
Calculates Zipf rank-frequency metrics for raw data.
|
|
257
|
+
|
|
258
|
+
datasets: list of lists - each inner list is a list of numeric frequency values
|
|
259
|
+
Example: [[10, 5, 3], [20, 15, 10], ...]
|
|
260
|
+
metricName: string - name for the metric in CSV headers (default: "ByRank")
|
|
261
|
+
|
|
262
|
+
Returns a list of lists - each inner list contains [SimpleMeasurement]
|
|
263
|
+
compatible with writeCSV()
|
|
264
|
+
"""
|
|
265
|
+
# validate datasets is a list
|
|
266
|
+
if not isinstance(datasets, list):
|
|
267
|
+
raise TypeError("datasets must be a list")
|
|
268
|
+
|
|
269
|
+
if len(datasets) == 0:
|
|
270
|
+
raise TypeError("datasets must be a non-empty list")
|
|
271
|
+
|
|
272
|
+
allMeasurements = []
|
|
273
|
+
|
|
274
|
+
for counts in datasets:
|
|
275
|
+
# call byRank to get slope, r2, yint
|
|
276
|
+
slope, r2, yint = byRank(counts)
|
|
277
|
+
|
|
278
|
+
# create SimpleMeasurement object
|
|
279
|
+
measurement = _SimpleMeasurement(metricName, slope, r2, yint)
|
|
280
|
+
|
|
281
|
+
# append [measurement] to results
|
|
282
|
+
allMeasurements.append([measurement])
|
|
283
|
+
|
|
284
|
+
return allMeasurements
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def measureDataBySize(datasets, metricName="BySize"):
|
|
288
|
+
"""
|
|
289
|
+
Calculates Zipf size-frequency metrics for raw data.
|
|
290
|
+
|
|
291
|
+
datasets: list of [sizes, counts] pairs
|
|
292
|
+
Each element is [sizes, counts] where:
|
|
293
|
+
sizes: list of numeric values (x-axis)
|
|
294
|
+
counts: list of numeric values (y-axis)
|
|
295
|
+
Example: [[[1, 2, 3], [10, 8, 5]], [[5, 10, 15], [20, 15, 10]], ...]
|
|
296
|
+
metricName: string - name for the metric in CSV headers (default: "BySize")
|
|
297
|
+
|
|
298
|
+
Returns a list of lists - each inner list contains [SimpleMeasurement]
|
|
299
|
+
compatible with writeCSV()
|
|
300
|
+
"""
|
|
301
|
+
# validate datasets is a list
|
|
302
|
+
if not isinstance(datasets, list):
|
|
303
|
+
raise TypeError("datasets must be a list")
|
|
304
|
+
|
|
305
|
+
if len(datasets) == 0:
|
|
306
|
+
raise TypeError("datasets must be a non-empty list")
|
|
307
|
+
|
|
308
|
+
allMeasurements = []
|
|
309
|
+
|
|
310
|
+
for dataset in datasets:
|
|
311
|
+
# unpack each dataset
|
|
312
|
+
try:
|
|
313
|
+
sizes, counts = dataset
|
|
314
|
+
except (ValueError, TypeError):
|
|
315
|
+
raise TypeError("each dataset must be a list [sizes, counts]")
|
|
316
|
+
|
|
317
|
+
# call bySize to get slope, r2, yint
|
|
318
|
+
slope, r2, yint = bySize(sizes, counts)
|
|
319
|
+
|
|
320
|
+
# create SimpleMeasurement object
|
|
321
|
+
measurement = _SimpleMeasurement(metricName, slope, r2, yint)
|
|
322
|
+
|
|
323
|
+
# append [measurement] to results
|
|
324
|
+
allMeasurements.append([measurement])
|
|
325
|
+
|
|
326
|
+
return allMeasurements
|
|
327
|
+
|
|
328
|
+
|
|
254
329
|
def writeCSV(measurements, filename):
|
|
255
330
|
"""
|
|
256
331
|
Writes Zipf metric results to a CSV file.
|
|
257
|
-
Measurements should come from measureScore or
|
|
332
|
+
Measurements should come from measureScore, measureMidi, measureDataByRank, or measureDataBySize.
|
|
258
333
|
|
|
259
|
-
measurements: list of lists - results from
|
|
260
|
-
|
|
334
|
+
measurements: list of lists - results from measure functions
|
|
335
|
+
Format 1 (with labels): [Measurement1, Measurement2, ..., label]
|
|
336
|
+
Format 2 (no labels): [Measurement1, Measurement2, ...]
|
|
261
337
|
filename: string - output CSV file path
|
|
262
338
|
|
|
263
|
-
Creates a CSV with columns: metric values..., Index
|
|
339
|
+
Creates a CSV with columns: metric values..., Index
|
|
340
|
+
Or with labels: metric values..., SourceName, Index
|
|
264
341
|
"""
|
|
265
342
|
import os
|
|
266
343
|
import unicodedata
|
|
@@ -273,13 +350,23 @@ def writeCSV(measurements, filename):
|
|
|
273
350
|
if not isinstance(measurements[0], (list, tuple)):
|
|
274
351
|
raise TypeError("measurements must be a list of lists (from measureMidi/measureScore)")
|
|
275
352
|
|
|
276
|
-
#
|
|
353
|
+
# check if the last element is a label (string) or a Measurement object
|
|
354
|
+
# to determine if we have source names
|
|
355
|
+
lastElement = measurements[0][-1]
|
|
356
|
+
hasSourceNames = isinstance(lastElement, str)
|
|
357
|
+
|
|
358
|
+
# extract measurements and optional names
|
|
277
359
|
measurementsList = []
|
|
278
360
|
names = []
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
361
|
+
if hasSourceNames:
|
|
362
|
+
for measurementRow in measurements:
|
|
363
|
+
# last element is the name/label, everything else is Measurement objects
|
|
364
|
+
measurementsList.append(measurementRow[:-1])
|
|
365
|
+
names.append(measurementRow[-1])
|
|
366
|
+
else:
|
|
367
|
+
# all elements are Measurement objects
|
|
368
|
+
for measurementRow in measurements:
|
|
369
|
+
measurementsList.append(measurementRow)
|
|
283
370
|
|
|
284
371
|
# helper function to convert to ASCII (from advMetricRunner.jythonSurvey.py)
|
|
285
372
|
def convertToASCII(string):
|
|
@@ -306,9 +393,10 @@ def writeCSV(measurements, filename):
|
|
|
306
393
|
for key in keys:
|
|
307
394
|
csvfile.write(key + ",")
|
|
308
395
|
|
|
309
|
-
# add final headers (
|
|
310
|
-
|
|
311
|
-
|
|
396
|
+
# add final headers (SourceName and/or Index columns)
|
|
397
|
+
if hasSourceNames:
|
|
398
|
+
csvfile.write("SourceName,")
|
|
399
|
+
csvfile.write("Index") # no trailing comma, no newline yet
|
|
312
400
|
|
|
313
401
|
# write data rows
|
|
314
402
|
for i, measurements in enumerate(measurementsList):
|
|
@@ -320,16 +408,43 @@ def writeCSV(measurements, filename):
|
|
|
320
408
|
for value in values:
|
|
321
409
|
csvfile.write(convertToASCII(value) + ",")
|
|
322
410
|
|
|
323
|
-
# write
|
|
411
|
+
# write source name if present
|
|
412
|
+
if hasSourceNames:
|
|
413
|
+
sourceName = names[i]
|
|
414
|
+
if os.path.sep in sourceName:
|
|
415
|
+
sourceName = os.path.basename(sourceName)
|
|
416
|
+
sourceName = convertToASCII(sourceName)
|
|
417
|
+
csvfile.write(sourceName + ",")
|
|
418
|
+
|
|
419
|
+
# write index (1-based, last column, no trailing comma or newline)
|
|
324
420
|
index = i + 1
|
|
325
|
-
csvfile.write(convertToASCII(index)
|
|
421
|
+
csvfile.write(convertToASCII(index))
|
|
422
|
+
|
|
326
423
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
424
|
+
class _SimpleMeasurement:
|
|
425
|
+
"""
|
|
426
|
+
Simple measurement object for raw Zipf analysis.
|
|
427
|
+
Compatible with writeCSV() but produces cleaner output than standard Measurement class.
|
|
428
|
+
Only includes Slope, R2, and Y-Intercept columns (no LocalVariability fields).
|
|
429
|
+
"""
|
|
430
|
+
|
|
431
|
+
def __init__(self, metricName, slope, r2, yint):
|
|
432
|
+
self.metricName = metricName
|
|
433
|
+
self.slope = slope
|
|
434
|
+
self.r2 = r2
|
|
435
|
+
self.yint = yint
|
|
436
|
+
|
|
437
|
+
def keysToArray(self):
|
|
438
|
+
"""Returns column headers for CSV output"""
|
|
439
|
+
return [
|
|
440
|
+
f"{self.metricName}_Slope",
|
|
441
|
+
f"{self.metricName}_R2",
|
|
442
|
+
f"{self.metricName}_YInt"
|
|
443
|
+
]
|
|
444
|
+
|
|
445
|
+
def valuesToArray(self):
|
|
446
|
+
"""Returns values for CSV output"""
|
|
447
|
+
return [self.slope, self.r2, self.yint]
|
|
333
448
|
|
|
334
449
|
|
|
335
450
|
if __name__ == '__main__':
|
|
@@ -364,3 +479,42 @@ if __name__ == '__main__':
|
|
|
364
479
|
sizes = histogram.keys()
|
|
365
480
|
slope, r2, yint = bySize(sizes, counts)
|
|
366
481
|
print(f"The bySize slope is {slope} and the R^2 is {r2}")
|
|
482
|
+
|
|
483
|
+
# Test measureDataByRank
|
|
484
|
+
print("\n=== Testing measureDataByRank ===")
|
|
485
|
+
rankDatasets = [
|
|
486
|
+
[10, 5, 5, 3, 3, 3],
|
|
487
|
+
[20, 15, 10, 5],
|
|
488
|
+
[100, 50, 33, 25, 20]
|
|
489
|
+
]
|
|
490
|
+
rankResults = measureDataByRank(rankDatasets)
|
|
491
|
+
print(f"Results: {len(rankResults)} datasets analyzed")
|
|
492
|
+
for i, row in enumerate(rankResults, 1):
|
|
493
|
+
measurement = row[0]
|
|
494
|
+
print(f" Dataset {i}: slope={measurement.slope:.3f}, r2={measurement.r2:.3f}, yint={measurement.yint:.3f}")
|
|
495
|
+
|
|
496
|
+
# Test measureDataBySize
|
|
497
|
+
print("\n=== Testing measureDataBySize ===")
|
|
498
|
+
sizeDatasets = [
|
|
499
|
+
[[1, 2, 3, 4], [10, 8, 5, 2]],
|
|
500
|
+
[[5, 10, 15], [20, 15, 10]]
|
|
501
|
+
]
|
|
502
|
+
sizeResults = measureDataBySize(sizeDatasets)
|
|
503
|
+
print(f"Results: {len(sizeResults)} datasets analyzed")
|
|
504
|
+
for i, row in enumerate(sizeResults, 1):
|
|
505
|
+
measurement = row[0]
|
|
506
|
+
print(f" Dataset {i}: slope={measurement.slope:.3f}, r2={measurement.r2:.3f}, yint={measurement.yint:.3f}")
|
|
507
|
+
|
|
508
|
+
# Test CSV compatibility - separate files for different metric types
|
|
509
|
+
print("\n=== Testing CSV output ===")
|
|
510
|
+
writeCSV(rankResults, "test_zipf_rank.csv")
|
|
511
|
+
print("Rank CSV file created successfully")
|
|
512
|
+
|
|
513
|
+
writeCSV(sizeResults, "test_zipf_size.csv")
|
|
514
|
+
print("Size CSV file created successfully")
|
|
515
|
+
|
|
516
|
+
# Test custom metric names
|
|
517
|
+
print("\n=== Testing custom metric names ===")
|
|
518
|
+
customResults = measureDataByRank(rankDatasets, metricName="CustomFreq")
|
|
519
|
+
writeCSV(customResults, "test_zipf_custom.csv")
|
|
520
|
+
print("Custom metric CSV file created successfully")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/ExtendedNote.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/Measurement.py
RENAMED
|
File without changes
|
|
File without changes
|
{creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/PianoRollOld.py
RENAMED
|
File without changes
|
|
File without changes
|
{creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/test_ExtendedNote.py
RENAMED
|
File without changes
|
{creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/test_Histogram.py
RENAMED
|
File without changes
|
{creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/test_Measurement.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/data/test_PianoRoll_unit.py
RENAMED
|
File without changes
|
|
File without changes
|
{creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/ZipfMetrics.py
RENAMED
|
File without changes
|
{creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/simple/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/metrics/test_Metric.py
RENAMED
|
File without changes
|
{creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/utilities/CSVWriter.py
RENAMED
|
File without changes
|
{creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/utilities/PowerLawRandom.py
RENAMED
|
File without changes
|
{creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython/nevmuse/utilities/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{creativepython-0.3.6 → creativepython-1.0.0}/src/CreativePython.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|