Spaces:
Runtime error
Runtime error
Next
commited on
Commit
•
e2da60d
1
Parent(s):
875831c
Upload 17 files
Browse files- .gitattributes +36 -35
- .gitignore +153 -0
- Dockerfile +43 -0
- MIDI.py +1735 -0
- README.md +13 -13
- app.py +298 -0
- example/Bach--Fugue-in-D-Minor.mid +3 -0
- example/Beethoven--Symphony-No5-in-C-Minor-Fate-Opus-67.mid +3 -0
- example/Chopin--Nocturne No. 9 in B Major, Opus 32 No.1, Andante Sostenuto.mid +3 -0
- example/Mozart--Requiem, No.1..mid +3 -0
- example/castle_in_the_sky.mid +3 -0
- example/eva-残酷な天使のテーゼ.mid +3 -0
- javascript/app.js +449 -0
- midi_synthesizer.py +53 -0
- midi_tokenizer.py +254 -0
- packages.txt +1 -0
- requirements.txt +5 -0
.gitattributes
CHANGED
@@ -1,35 +1,36 @@
|
|
1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
+
*.mid filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Byte-compiled / optimized / DLL files
|
2 |
+
__pycache__/
|
3 |
+
*.py[cod]
|
4 |
+
*$py.class
|
5 |
+
|
6 |
+
# C extensions
|
7 |
+
*.so
|
8 |
+
|
9 |
+
# Distribution / packaging
|
10 |
+
.Python
|
11 |
+
build/
|
12 |
+
develop-eggs/
|
13 |
+
dist/
|
14 |
+
downloads/
|
15 |
+
eggs/
|
16 |
+
.eggs/
|
17 |
+
lib/
|
18 |
+
lib64/
|
19 |
+
parts/
|
20 |
+
sdist/
|
21 |
+
var/
|
22 |
+
wheels/
|
23 |
+
share/python-wheels/
|
24 |
+
*.egg-info/
|
25 |
+
.installed.cfg
|
26 |
+
*.egg
|
27 |
+
MANIFEST
|
28 |
+
|
29 |
+
# PyInstaller
|
30 |
+
# Usually these files are written by a python script from a template
|
31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
32 |
+
*.manifest
|
33 |
+
*.spec
|
34 |
+
|
35 |
+
# Installer logs
|
36 |
+
pip-log.txt
|
37 |
+
pip-delete-this-directory.txt
|
38 |
+
|
39 |
+
# Unit test / coverage reports
|
40 |
+
htmlcov/
|
41 |
+
.tox/
|
42 |
+
.nox/
|
43 |
+
.coverage
|
44 |
+
.coverage.*
|
45 |
+
.cache
|
46 |
+
nosetests.xml
|
47 |
+
coverage.xml
|
48 |
+
*.cover
|
49 |
+
*.py,cover
|
50 |
+
.hypothesis/
|
51 |
+
.pytest_cache/
|
52 |
+
cover/
|
53 |
+
|
54 |
+
# Translations
|
55 |
+
*.mo
|
56 |
+
*.pot
|
57 |
+
|
58 |
+
# Django stuff:
|
59 |
+
*.log
|
60 |
+
local_settings.py
|
61 |
+
db.sqlite3
|
62 |
+
db.sqlite3-journal
|
63 |
+
|
64 |
+
# Flask stuff:
|
65 |
+
instance/
|
66 |
+
.webassets-cache
|
67 |
+
|
68 |
+
# Scrapy stuff:
|
69 |
+
.scrapy
|
70 |
+
|
71 |
+
# Sphinx documentation
|
72 |
+
docs/_build/
|
73 |
+
|
74 |
+
# PyBuilder
|
75 |
+
.pybuilder/
|
76 |
+
target/
|
77 |
+
|
78 |
+
# Jupyter Notebook
|
79 |
+
.ipynb_checkpoints
|
80 |
+
|
81 |
+
# IPython
|
82 |
+
profile_default/
|
83 |
+
ipython_config.py
|
84 |
+
|
85 |
+
# pyenv
|
86 |
+
# For a library or package, you might want to ignore these files since the code is
|
87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
88 |
+
# .python-version
|
89 |
+
|
90 |
+
# pipenv
|
91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
94 |
+
# install all needed dependencies.
|
95 |
+
#Pipfile.lock
|
96 |
+
|
97 |
+
# poetry
|
98 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
100 |
+
# commonly ignored for libraries.
|
101 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
102 |
+
#poetry.lock
|
103 |
+
|
104 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
105 |
+
__pypackages__/
|
106 |
+
|
107 |
+
# Celery stuff
|
108 |
+
celerybeat-schedule
|
109 |
+
celerybeat.pid
|
110 |
+
|
111 |
+
# SageMath parsed files
|
112 |
+
*.sage.py
|
113 |
+
|
114 |
+
# Environments
|
115 |
+
.env
|
116 |
+
.venv
|
117 |
+
env/
|
118 |
+
venv/
|
119 |
+
ENV/
|
120 |
+
env.bak/
|
121 |
+
venv.bak/
|
122 |
+
|
123 |
+
# Spyder project settings
|
124 |
+
.spyderproject
|
125 |
+
.spyproject
|
126 |
+
|
127 |
+
# Rope project settings
|
128 |
+
.ropeproject
|
129 |
+
|
130 |
+
# mkdocs documentation
|
131 |
+
/site
|
132 |
+
|
133 |
+
# mypy
|
134 |
+
.mypy_cache/
|
135 |
+
.dmypy.json
|
136 |
+
dmypy.json
|
137 |
+
|
138 |
+
# Pyre type checker
|
139 |
+
.pyre/
|
140 |
+
|
141 |
+
# pytype static type analyzer
|
142 |
+
.pytype/
|
143 |
+
|
144 |
+
# Cython debug symbols
|
145 |
+
cython_debug/
|
146 |
+
|
147 |
+
# PyCharm
|
148 |
+
# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
|
149 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
150 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
151 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
152 |
+
.idea/
|
153 |
+
output.mid
|
Dockerfile
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM nvidia/cuda:11.6.1-cudnn8-devel-ubuntu20.04
|
2 |
+
|
3 |
+
ARG DEBIAN_FRONTEND=noninteractive
|
4 |
+
|
5 |
+
ENV PYTHONUNBUFFERED=1
|
6 |
+
|
7 |
+
RUN apt-get update && apt-get install --no-install-recommends -y \
|
8 |
+
build-essential \
|
9 |
+
python3.9 \
|
10 |
+
python3-pip \
|
11 |
+
git \
|
12 |
+
ffmpeg \
|
13 |
+
fluidsynth \
|
14 |
+
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
15 |
+
|
16 |
+
WORKDIR /code
|
17 |
+
|
18 |
+
COPY ./requirements.txt /code/requirements.txt
|
19 |
+
|
20 |
+
# Set up a new user named "user" with user ID 1000
|
21 |
+
RUN useradd -m -u 1000 user
|
22 |
+
# Switch to the "user" user
|
23 |
+
USER user
|
24 |
+
# Set home to the user's home directory
|
25 |
+
ENV HOME=/home/user \
|
26 |
+
PATH=/home/user/.local/bin:$PATH \
|
27 |
+
PYTHONPATH=$HOME/app \
|
28 |
+
PYTHONUNBUFFERED=1 \
|
29 |
+
GRADIO_ALLOW_FLAGGING=never \
|
30 |
+
GRADIO_NUM_PORTS=1 \
|
31 |
+
GRADIO_SERVER_NAME=0.0.0.0 \
|
32 |
+
GRADIO_THEME=huggingface \
|
33 |
+
SYSTEM=spaces
|
34 |
+
|
35 |
+
RUN pip3 install --no-cache-dir --upgrade -r /code/requirements.txt
|
36 |
+
|
37 |
+
# Set the working directory to the user's home directory
|
38 |
+
WORKDIR $HOME/app
|
39 |
+
|
40 |
+
# Copy the current directory contents into the container at $HOME/app setting the owner to the user
|
41 |
+
COPY --chown=user . $HOME/app
|
42 |
+
|
43 |
+
CMD ["python3", "app.py"]
|
MIDI.py
ADDED
@@ -0,0 +1,1735 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#! /usr/bin/python3
|
2 |
+
# unsupported 20091104 ...
|
3 |
+
# ['set_sequence_number', dtime, sequence]
|
4 |
+
# ['raw_data', dtime, raw]
|
5 |
+
|
6 |
+
# 20150914 jimbo1qaz MIDI.py str/bytes bug report
|
7 |
+
# I found a MIDI file which had Shift-JIS titles. When midi.py decodes it as
|
8 |
+
# latin-1, it produces a string which cannot even be accessed without raising
|
9 |
+
# a UnicodeDecodeError. Maybe, when converting raw byte strings from MIDI,
|
10 |
+
# you should keep them as bytes, not improperly decode them. However, this
|
11 |
+
# would change the API. (ie: text = a "string" ? of 0 or more bytes). It
|
12 |
+
# could break compatiblity, but there's not much else you can do to fix the bug
|
13 |
+
# https://en.wikipedia.org/wiki/Shift_JIS
|
14 |
+
|
15 |
+
r'''
|
16 |
+
This module offers functions: concatenate_scores(), grep(),
|
17 |
+
merge_scores(), mix_scores(), midi2opus(), midi2score(), opus2midi(),
|
18 |
+
opus2score(), play_score(), score2midi(), score2opus(), score2stats(),
|
19 |
+
score_type(), segment(), timeshift() and to_millisecs(),
|
20 |
+
where "midi" means the MIDI-file bytes (as can be put in a .mid file,
|
21 |
+
or piped into aplaymidi), and "opus" and "score" are list-structures
|
22 |
+
as inspired by Sean Burke's MIDI-Perl CPAN module.
|
23 |
+
|
24 |
+
Warning: Version 6.4 is not necessarily backward-compatible with
|
25 |
+
previous versions, in that text-data is now bytes, not strings.
|
26 |
+
This reflects the fact that many MIDI files have text data in
|
27 |
+
encodings other that ISO-8859-1, for example in Shift-JIS.
|
28 |
+
|
29 |
+
Download MIDI.py from http://www.pjb.com.au/midi/free/MIDI.py
|
30 |
+
and put it in your PYTHONPATH. MIDI.py depends on Python3.
|
31 |
+
|
32 |
+
There is also a call-compatible translation into Lua of this
|
33 |
+
module: see http://www.pjb.com.au/comp/lua/MIDI.html
|
34 |
+
|
35 |
+
The "opus" is a direct translation of the midi-file-events, where
|
36 |
+
the times are delta-times, in ticks, since the previous event.
|
37 |
+
|
38 |
+
The "score" is more human-centric; it uses absolute times, and
|
39 |
+
combines the separate note_on and note_off events into one "note"
|
40 |
+
event, with a duration:
|
41 |
+
['note', start_time, duration, channel, note, velocity] # in a "score"
|
42 |
+
|
43 |
+
EVENTS (in an "opus" structure)
|
44 |
+
['note_off', dtime, channel, note, velocity] # in an "opus"
|
45 |
+
['note_on', dtime, channel, note, velocity] # in an "opus"
|
46 |
+
['key_after_touch', dtime, channel, note, velocity]
|
47 |
+
['control_change', dtime, channel, controller(0-127), value(0-127)]
|
48 |
+
['patch_change', dtime, channel, patch]
|
49 |
+
['channel_after_touch', dtime, channel, velocity]
|
50 |
+
['pitch_wheel_change', dtime, channel, pitch_wheel]
|
51 |
+
['text_event', dtime, text]
|
52 |
+
['copyright_text_event', dtime, text]
|
53 |
+
['track_name', dtime, text]
|
54 |
+
['instrument_name', dtime, text]
|
55 |
+
['lyric', dtime, text]
|
56 |
+
['marker', dtime, text]
|
57 |
+
['cue_point', dtime, text]
|
58 |
+
['text_event_08', dtime, text]
|
59 |
+
['text_event_09', dtime, text]
|
60 |
+
['text_event_0a', dtime, text]
|
61 |
+
['text_event_0b', dtime, text]
|
62 |
+
['text_event_0c', dtime, text]
|
63 |
+
['text_event_0d', dtime, text]
|
64 |
+
['text_event_0e', dtime, text]
|
65 |
+
['text_event_0f', dtime, text]
|
66 |
+
['end_track', dtime]
|
67 |
+
['set_tempo', dtime, tempo]
|
68 |
+
['smpte_offset', dtime, hr, mn, se, fr, ff]
|
69 |
+
['time_signature', dtime, nn, dd, cc, bb]
|
70 |
+
['key_signature', dtime, sf, mi]
|
71 |
+
['sequencer_specific', dtime, raw]
|
72 |
+
['raw_meta_event', dtime, command(0-255), raw]
|
73 |
+
['sysex_f0', dtime, raw]
|
74 |
+
['sysex_f7', dtime, raw]
|
75 |
+
['song_position', dtime, song_pos]
|
76 |
+
['song_select', dtime, song_number]
|
77 |
+
['tune_request', dtime]
|
78 |
+
|
79 |
+
DATA TYPES
|
80 |
+
channel = a value 0 to 15
|
81 |
+
controller = 0 to 127 (see http://www.pjb.com.au/muscript/gm.html#cc )
|
82 |
+
dtime = time measured in "ticks", 0 to 268435455
|
83 |
+
velocity = a value 0 (soft) to 127 (loud)
|
84 |
+
note = a value 0 to 127 (middle-C is 60)
|
85 |
+
patch = 0 to 127 (see http://www.pjb.com.au/muscript/gm.html )
|
86 |
+
pitch_wheel = a value -8192 to 8191 (0x1FFF)
|
87 |
+
raw = bytes, of length 0 or more (for sysex events see below)
|
88 |
+
sequence_number = a value 0 to 65,535 (0xFFFF)
|
89 |
+
song_pos = a value 0 to 16,383 (0x3FFF)
|
90 |
+
song_number = a value 0 to 127
|
91 |
+
tempo = microseconds per crochet (quarter-note), 0 to 16777215
|
92 |
+
text = bytes, of length 0 or more
|
93 |
+
ticks = the number of ticks per crochet (quarter-note)
|
94 |
+
|
95 |
+
In sysex_f0 events, the raw data must not start with a \xF0 byte,
|
96 |
+
since this gets added automatically;
|
97 |
+
but it must end with an explicit \xF7 byte!
|
98 |
+
In the very unlikely case that you ever need to split sysex data
|
99 |
+
into one sysex_f0 followed by one or more sysex_f7s, then only the
|
100 |
+
last of those sysex_f7 events must end with the explicit \xF7 byte
|
101 |
+
(again, the raw data of individual sysex_f7 events must not start
|
102 |
+
with any \xF7 byte, since this gets added automatically).
|
103 |
+
|
104 |
+
Since version 6.4, text data is in bytes, not in a ISO-8859-1 string.
|
105 |
+
|
106 |
+
|
107 |
+
GOING THROUGH A SCORE WITHIN A PYTHON PROGRAM
|
108 |
+
channels = {2,3,5,8,13}
|
109 |
+
itrack = 1 # skip 1st element which is ticks
|
110 |
+
while itrack < len(score):
|
111 |
+
for event in score[itrack]:
|
112 |
+
if event[0] == 'note': # for example,
|
113 |
+
pass # do something to all notes
|
114 |
+
# or, to work on events in only particular channels...
|
115 |
+
channel_index = MIDI.Event2channelindex.get(event[0], False)
|
116 |
+
if channel_index and (event[channel_index] in channels):
|
117 |
+
pass # do something to channels 2,3,5,8 and 13
|
118 |
+
itrack += 1
|
119 |
+
|
120 |
+
'''
|
121 |
+
|
122 |
+
import sys, struct, copy
|
123 |
+
# sys.stdout = os.fdopen(sys.stdout.fileno(), 'wb')
|
124 |
+
Version = '6.7'
|
125 |
+
VersionDate = '20201120'
|
126 |
+
# 20201120 6.7 call to bytest() removed, and protect _unshift_ber_int
|
127 |
+
# 20160702 6.6 to_millisecs() now handles set_tempo across multiple Tracks
|
128 |
+
# 20150921 6.5 segment restores controllers as well as patch and tempo
|
129 |
+
# 20150914 6.4 text data is bytes or bytearray, not ISO-8859-1 strings
|
130 |
+
# 20150628 6.3 absent any set_tempo, default is 120bpm (see MIDI file spec 1.1)
|
131 |
+
# 20150101 6.2 all text events can be 8-bit; let user get the right encoding
|
132 |
+
# 20141231 6.1 fix _some_text_event; sequencer_specific data can be 8-bit
|
133 |
+
# 20141230 6.0 synth_specific data can be 8-bit
|
134 |
+
# 20120504 5.9 add the contents of mid_opus_tracks()
|
135 |
+
# 20120208 5.8 fix num_notes_by_channel() ; should be a dict
|
136 |
+
# 20120129 5.7 _encode handles empty tracks; score2stats num_notes_by_channel
|
137 |
+
# 20111111 5.6 fix patch 45 and 46 in Number2patch, should be Harp
|
138 |
+
# 20110129 5.5 add mix_opus_tracks() and event2alsaseq()
|
139 |
+
# 20110126 5.4 "previous message repeated N times" to save space on stderr
|
140 |
+
# 20110125 5.2 opus2score terminates unended notes at the end of the track
|
141 |
+
# 20110124 5.1 the warnings in midi2opus display track_num
|
142 |
+
# 21110122 5.0 if garbage, midi2opus returns the opus so far
|
143 |
+
# 21110119 4.9 non-ascii chars stripped out of the text_events
|
144 |
+
# 21110110 4.8 note_on with velocity=0 treated as a note-off
|
145 |
+
# 21110108 4.6 unknown F-series event correctly eats just one byte
|
146 |
+
# 21011010 4.2 segment() uses start_time, end_time named params
|
147 |
+
# 21011005 4.1 timeshift() must not pad the set_tempo command
|
148 |
+
# 21011003 4.0 pitch2note_event must be chapitch2note_event
|
149 |
+
# 21010918 3.9 set_sequence_number supported, FWIW
|
150 |
+
# 20100913 3.7 many small bugfixes; passes all tests
|
151 |
+
# 20100910 3.6 concatenate_scores enforce ticks=1000, just like merge_scores
|
152 |
+
# 20100908 3.5 minor bugs fixed in score2stats
|
153 |
+
# 20091104 3.4 tune_request now supported
|
154 |
+
# 20091104 3.3 fixed bug in decoding song_position and song_select
|
155 |
+
# 20091104 3.2 unsupported: set_sequence_number tune_request raw_data
|
156 |
+
# 20091101 3.1 document how to traverse a score within Python
|
157 |
+
# 20091021 3.0 fixed bug in score2stats detecting GM-mode = 0
|
158 |
+
# 20091020 2.9 score2stats reports GM-mode and bank msb,lsb events
|
159 |
+
# 20091019 2.8 in merge_scores, channel 9 must remain channel 9 (in GM)
|
160 |
+
# 20091018 2.7 handles empty tracks gracefully
|
161 |
+
# 20091015 2.6 grep() selects channels
|
162 |
+
# 20091010 2.5 merge_scores reassigns channels to avoid conflicts
|
163 |
+
# 20091010 2.4 fixed bug in to_millisecs which now only does opusses
|
164 |
+
# 20091010 2.3 score2stats returns channels & patch_changes, by_track & total
|
165 |
+
# 20091010 2.2 score2stats() returns also pitches and percussion dicts
|
166 |
+
# 20091010 2.1 bugs: >= not > in segment, to notice patch_change at time 0
|
167 |
+
# 20091010 2.0 bugs: spurious pop(0) ( in _decode sysex
|
168 |
+
# 20091008 1.9 bugs: ISO decoding in sysex; str( not int( in note-off warning
|
169 |
+
# 20091008 1.8 add concatenate_scores()
|
170 |
+
# 20091006 1.7 score2stats() measures nticks and ticks_per_quarter
|
171 |
+
# 20091004 1.6 first mix_scores() and merge_scores()
|
172 |
+
# 20090424 1.5 timeshift() bugfix: earliest only sees events after from_time
|
173 |
+
# 20090330 1.4 timeshift() has also a from_time argument
|
174 |
+
# 20090322 1.3 timeshift() has also a start_time argument
|
175 |
+
# 20090319 1.2 add segment() and timeshift()
|
176 |
+
# 20090301 1.1 add to_millisecs()
|
177 |
+
|
178 |
+
_previous_warning = '' # 5.4
|
179 |
+
_previous_times = 0 # 5.4
|
180 |
+
_no_warning = True
|
181 |
+
#------------------------------- Encoding stuff --------------------------
|
182 |
+
|
183 |
+
def opus2midi(opus=[]):
|
184 |
+
r'''The argument is a list: the first item in the list is the "ticks"
|
185 |
+
parameter, the others are the tracks. Each track is a list
|
186 |
+
of midi-events, and each event is itself a list; see above.
|
187 |
+
opus2midi() returns a bytestring of the MIDI, which can then be
|
188 |
+
written either to a file opened in binary mode (mode='wb'),
|
189 |
+
or to stdout by means of: sys.stdout.buffer.write()
|
190 |
+
|
191 |
+
my_opus = [
|
192 |
+
96,
|
193 |
+
[ # track 0:
|
194 |
+
['patch_change', 0, 1, 8], # and these are the events...
|
195 |
+
['note_on', 5, 1, 25, 96],
|
196 |
+
['note_off', 96, 1, 25, 0],
|
197 |
+
['note_on', 0, 1, 29, 96],
|
198 |
+
['note_off', 96, 1, 29, 0],
|
199 |
+
], # end of track 0
|
200 |
+
]
|
201 |
+
my_midi = opus2midi(my_opus)
|
202 |
+
sys.stdout.buffer.write(my_midi)
|
203 |
+
'''
|
204 |
+
if len(opus) < 2:
|
205 |
+
opus=[1000, [],]
|
206 |
+
tracks = copy.deepcopy(opus)
|
207 |
+
ticks = int(tracks.pop(0))
|
208 |
+
ntracks = len(tracks)
|
209 |
+
if ntracks == 1:
|
210 |
+
format = 0
|
211 |
+
else:
|
212 |
+
format = 1
|
213 |
+
|
214 |
+
my_midi = b"MThd\x00\x00\x00\x06"+struct.pack('>HHH',format,ntracks,ticks)
|
215 |
+
for track in tracks:
|
216 |
+
events = _encode(track)
|
217 |
+
my_midi += b'MTrk' + struct.pack('>I',len(events)) + events
|
218 |
+
_clean_up_warnings()
|
219 |
+
return my_midi
|
220 |
+
|
221 |
+
|
222 |
+
def score2opus(score=None):
|
223 |
+
r'''
|
224 |
+
The argument is a list: the first item in the list is the "ticks"
|
225 |
+
parameter, the others are the tracks. Each track is a list
|
226 |
+
of score-events, and each event is itself a list. A score-event
|
227 |
+
is similar to an opus-event (see above), except that in a score:
|
228 |
+
1) the times are expressed as an absolute number of ticks
|
229 |
+
from the track's start time
|
230 |
+
2) the pairs of 'note_on' and 'note_off' events in an "opus"
|
231 |
+
are abstracted into a single 'note' event in a "score":
|
232 |
+
['note', start_time, duration, channel, pitch, velocity]
|
233 |
+
score2opus() returns a list specifying the equivalent "opus".
|
234 |
+
|
235 |
+
my_score = [
|
236 |
+
96,
|
237 |
+
[ # track 0:
|
238 |
+
['patch_change', 0, 1, 8],
|
239 |
+
['note', 5, 96, 1, 25, 96],
|
240 |
+
['note', 101, 96, 1, 29, 96]
|
241 |
+
], # end of track 0
|
242 |
+
]
|
243 |
+
my_opus = score2opus(my_score)
|
244 |
+
'''
|
245 |
+
if len(score) < 2:
|
246 |
+
score=[1000, [],]
|
247 |
+
tracks = copy.deepcopy(score)
|
248 |
+
ticks = int(tracks.pop(0))
|
249 |
+
opus_tracks = []
|
250 |
+
for scoretrack in tracks:
|
251 |
+
time2events = dict([])
|
252 |
+
for scoreevent in scoretrack:
|
253 |
+
if scoreevent[0] == 'note':
|
254 |
+
note_on_event = ['note_on',scoreevent[1],
|
255 |
+
scoreevent[3],scoreevent[4],scoreevent[5]]
|
256 |
+
note_off_event = ['note_off',scoreevent[1]+scoreevent[2],
|
257 |
+
scoreevent[3],scoreevent[4],scoreevent[5]]
|
258 |
+
if time2events.get(note_on_event[1]):
|
259 |
+
time2events[note_on_event[1]].append(note_on_event)
|
260 |
+
else:
|
261 |
+
time2events[note_on_event[1]] = [note_on_event,]
|
262 |
+
if time2events.get(note_off_event[1]):
|
263 |
+
time2events[note_off_event[1]].append(note_off_event)
|
264 |
+
else:
|
265 |
+
time2events[note_off_event[1]] = [note_off_event,]
|
266 |
+
continue
|
267 |
+
if time2events.get(scoreevent[1]):
|
268 |
+
time2events[scoreevent[1]].append(scoreevent)
|
269 |
+
else:
|
270 |
+
time2events[scoreevent[1]] = [scoreevent,]
|
271 |
+
|
272 |
+
sorted_times = [] # list of keys
|
273 |
+
for k in time2events.keys():
|
274 |
+
sorted_times.append(k)
|
275 |
+
sorted_times.sort()
|
276 |
+
|
277 |
+
sorted_events = [] # once-flattened list of values sorted by key
|
278 |
+
for time in sorted_times:
|
279 |
+
sorted_events.extend(time2events[time])
|
280 |
+
|
281 |
+
abs_time = 0
|
282 |
+
for event in sorted_events: # convert abs times => delta times
|
283 |
+
delta_time = event[1] - abs_time
|
284 |
+
abs_time = event[1]
|
285 |
+
event[1] = delta_time
|
286 |
+
opus_tracks.append(sorted_events)
|
287 |
+
opus_tracks.insert(0,ticks)
|
288 |
+
_clean_up_warnings()
|
289 |
+
return opus_tracks
|
290 |
+
|
291 |
+
def score2midi(score=None):
|
292 |
+
r'''
|
293 |
+
Translates a "score" into MIDI, using score2opus() then opus2midi()
|
294 |
+
'''
|
295 |
+
return opus2midi(score2opus(score))
|
296 |
+
|
297 |
+
#--------------------------- Decoding stuff ------------------------
|
298 |
+
|
299 |
+
def midi2opus(midi=b''):
|
300 |
+
r'''Translates MIDI into a "opus". For a description of the
|
301 |
+
"opus" format, see opus2midi()
|
302 |
+
'''
|
303 |
+
my_midi=bytearray(midi)
|
304 |
+
if len(my_midi) < 4:
|
305 |
+
_clean_up_warnings()
|
306 |
+
return [1000,[],]
|
307 |
+
id = bytes(my_midi[0:4])
|
308 |
+
if id != b'MThd':
|
309 |
+
_warn("midi2opus: midi starts with "+str(id)+" instead of 'MThd'")
|
310 |
+
_clean_up_warnings()
|
311 |
+
return [1000,[],]
|
312 |
+
[length, format, tracks_expected, ticks] = struct.unpack(
|
313 |
+
'>IHHH', bytes(my_midi[4:14]))
|
314 |
+
if length != 6:
|
315 |
+
_warn("midi2opus: midi header length was "+str(length)+" instead of 6")
|
316 |
+
_clean_up_warnings()
|
317 |
+
return [1000,[],]
|
318 |
+
my_opus = [ticks,]
|
319 |
+
my_midi = my_midi[14:]
|
320 |
+
track_num = 1 # 5.1
|
321 |
+
while len(my_midi) >= 8:
|
322 |
+
track_type = bytes(my_midi[0:4])
|
323 |
+
if track_type != b'MTrk':
|
324 |
+
_warn('midi2opus: Warning: track #'+str(track_num)+' type is '+str(track_type)+" instead of b'MTrk'")
|
325 |
+
[track_length] = struct.unpack('>I', my_midi[4:8])
|
326 |
+
my_midi = my_midi[8:]
|
327 |
+
if track_length > len(my_midi):
|
328 |
+
_warn('midi2opus: track #'+str(track_num)+' length '+str(track_length)+' is too large')
|
329 |
+
_clean_up_warnings()
|
330 |
+
return my_opus # 5.0
|
331 |
+
my_midi_track = my_midi[0:track_length]
|
332 |
+
my_track = _decode(my_midi_track)
|
333 |
+
my_opus.append(my_track)
|
334 |
+
my_midi = my_midi[track_length:]
|
335 |
+
track_num += 1 # 5.1
|
336 |
+
_clean_up_warnings()
|
337 |
+
return my_opus
|
338 |
+
|
339 |
+
def opus2score(opus=[]):
|
340 |
+
r'''For a description of the "opus" and "score" formats,
|
341 |
+
see opus2midi() and score2opus().
|
342 |
+
'''
|
343 |
+
if len(opus) < 2:
|
344 |
+
_clean_up_warnings()
|
345 |
+
return [1000,[],]
|
346 |
+
tracks = copy.deepcopy(opus) # couple of slices probably quicker...
|
347 |
+
ticks = int(tracks.pop(0))
|
348 |
+
score = [ticks,]
|
349 |
+
for opus_track in tracks:
|
350 |
+
ticks_so_far = 0
|
351 |
+
score_track = []
|
352 |
+
chapitch2note_on_events = dict([]) # 4.0
|
353 |
+
for opus_event in opus_track:
|
354 |
+
ticks_so_far += opus_event[1]
|
355 |
+
if opus_event[0] == 'note_off' or (opus_event[0] == 'note_on' and opus_event[4] == 0): # 4.8
|
356 |
+
cha = opus_event[2]
|
357 |
+
pitch = opus_event[3]
|
358 |
+
key = cha*128 + pitch
|
359 |
+
if chapitch2note_on_events.get(key):
|
360 |
+
new_event = chapitch2note_on_events[key].pop(0)
|
361 |
+
new_event[2] = ticks_so_far - new_event[1]
|
362 |
+
score_track.append(new_event)
|
363 |
+
elif pitch > 127:
|
364 |
+
pass #_warn('opus2score: note_off with no note_on, bad pitch='+str(pitch))
|
365 |
+
else:
|
366 |
+
pass #_warn('opus2score: note_off with no note_on cha='+str(cha)+' pitch='+str(pitch))
|
367 |
+
elif opus_event[0] == 'note_on':
|
368 |
+
cha = opus_event[2]
|
369 |
+
pitch = opus_event[3]
|
370 |
+
key = cha*128 + pitch
|
371 |
+
new_event = ['note',ticks_so_far,0,cha,pitch, opus_event[4]]
|
372 |
+
if chapitch2note_on_events.get(key):
|
373 |
+
chapitch2note_on_events[key].append(new_event)
|
374 |
+
else:
|
375 |
+
chapitch2note_on_events[key] = [new_event,]
|
376 |
+
else:
|
377 |
+
opus_event[1] = ticks_so_far
|
378 |
+
score_track.append(opus_event)
|
379 |
+
# check for unterminated notes (Oisín) -- 5.2
|
380 |
+
for chapitch in chapitch2note_on_events:
|
381 |
+
note_on_events = chapitch2note_on_events[chapitch]
|
382 |
+
for new_e in note_on_events:
|
383 |
+
new_e[2] = ticks_so_far - new_e[1]
|
384 |
+
score_track.append(new_e)
|
385 |
+
pass #_warn("opus2score: note_on with no note_off cha="+str(new_e[3])+' pitch='+str(new_e[4])+'; adding note_off at end')
|
386 |
+
score.append(score_track)
|
387 |
+
_clean_up_warnings()
|
388 |
+
return score
|
389 |
+
|
390 |
+
def midi2score(midi=b''):
|
391 |
+
r'''
|
392 |
+
Translates MIDI into a "score", using midi2opus() then opus2score()
|
393 |
+
'''
|
394 |
+
return opus2score(midi2opus(midi))
|
395 |
+
|
396 |
+
def midi2ms_score(midi=b''):
|
397 |
+
r'''
|
398 |
+
Translates MIDI into a "score" with one beat per second and one
|
399 |
+
tick per millisecond, using midi2opus() then to_millisecs()
|
400 |
+
then opus2score()
|
401 |
+
'''
|
402 |
+
return opus2score(to_millisecs(midi2opus(midi)))
|
403 |
+
|
404 |
+
#------------------------ Other Transformations ---------------------
|
405 |
+
|
406 |
+
def to_millisecs(old_opus=None):
|
407 |
+
r'''Recallibrates all the times in an "opus" to use one beat
|
408 |
+
per second and one tick per millisecond. This makes it
|
409 |
+
hard to retrieve any information about beats or barlines,
|
410 |
+
but it does make it easy to mix different scores together.
|
411 |
+
'''
|
412 |
+
if old_opus == None:
|
413 |
+
return [1000,[],]
|
414 |
+
try:
|
415 |
+
old_tpq = int(old_opus[0])
|
416 |
+
except IndexError: # 5.0
|
417 |
+
_warn('to_millisecs: the opus '+str(type(old_opus))+' has no elements')
|
418 |
+
return [1000,[],]
|
419 |
+
new_opus = [1000,]
|
420 |
+
# 6.7 first go through building a table of set_tempos by absolute-tick
|
421 |
+
ticks2tempo = {}
|
422 |
+
itrack = 1
|
423 |
+
while itrack < len(old_opus):
|
424 |
+
ticks_so_far = 0
|
425 |
+
for old_event in old_opus[itrack]:
|
426 |
+
if old_event[0] == 'note':
|
427 |
+
raise TypeError('to_millisecs needs an opus, not a score')
|
428 |
+
ticks_so_far += old_event[1]
|
429 |
+
if old_event[0] == 'set_tempo':
|
430 |
+
ticks2tempo[ticks_so_far] = old_event[2]
|
431 |
+
itrack += 1
|
432 |
+
# then get the sorted-array of their keys
|
433 |
+
tempo_ticks = [] # list of keys
|
434 |
+
for k in ticks2tempo.keys():
|
435 |
+
tempo_ticks.append(k)
|
436 |
+
tempo_ticks.sort()
|
437 |
+
# then go through converting to millisec, testing if the next
|
438 |
+
# set_tempo lies before the next track-event, and using it if so.
|
439 |
+
itrack = 1
|
440 |
+
while itrack < len(old_opus):
|
441 |
+
ms_per_old_tick = 500.0 / old_tpq # float: will round later 6.3
|
442 |
+
i_tempo_ticks = 0
|
443 |
+
ticks_so_far = 0
|
444 |
+
ms_so_far = 0.0
|
445 |
+
previous_ms_so_far = 0.0
|
446 |
+
new_track = [['set_tempo',0,1000000],] # new "crochet" is 1 sec
|
447 |
+
for old_event in old_opus[itrack]:
|
448 |
+
# detect if ticks2tempo has something before this event
|
449 |
+
# 20160702 if ticks2tempo is at the same time, leave it
|
450 |
+
event_delta_ticks = old_event[1]
|
451 |
+
if (i_tempo_ticks < len(tempo_ticks) and
|
452 |
+
tempo_ticks[i_tempo_ticks] < (ticks_so_far + old_event[1])):
|
453 |
+
delta_ticks = tempo_ticks[i_tempo_ticks] - ticks_so_far
|
454 |
+
ms_so_far += (ms_per_old_tick * delta_ticks)
|
455 |
+
ticks_so_far = tempo_ticks[i_tempo_ticks]
|
456 |
+
ms_per_old_tick = ticks2tempo[ticks_so_far] / (1000.0*old_tpq)
|
457 |
+
i_tempo_ticks += 1
|
458 |
+
event_delta_ticks -= delta_ticks
|
459 |
+
new_event = copy.deepcopy(old_event) # now handle the new event
|
460 |
+
ms_so_far += (ms_per_old_tick * old_event[1])
|
461 |
+
new_event[1] = round(ms_so_far - previous_ms_so_far)
|
462 |
+
if old_event[0] != 'set_tempo':
|
463 |
+
previous_ms_so_far = ms_so_far
|
464 |
+
new_track.append(new_event)
|
465 |
+
ticks_so_far += event_delta_ticks
|
466 |
+
new_opus.append(new_track)
|
467 |
+
itrack += 1
|
468 |
+
_clean_up_warnings()
|
469 |
+
return new_opus
|
470 |
+
|
471 |
+
def event2alsaseq(event=None): # 5.5
|
472 |
+
r'''Converts an event into the format needed by the alsaseq module,
|
473 |
+
http://pp.com.mx/python/alsaseq
|
474 |
+
The type of track (opus or score) is autodetected.
|
475 |
+
'''
|
476 |
+
pass
|
477 |
+
|
478 |
+
def grep(score=None, channels=None):
|
479 |
+
r'''Returns a "score" containing only the channels specified
|
480 |
+
'''
|
481 |
+
if score == None:
|
482 |
+
return [1000,[],]
|
483 |
+
ticks = score[0]
|
484 |
+
new_score = [ticks,]
|
485 |
+
if channels == None:
|
486 |
+
return new_score
|
487 |
+
channels = set(channels)
|
488 |
+
global Event2channelindex
|
489 |
+
itrack = 1
|
490 |
+
while itrack < len(score):
|
491 |
+
new_score.append([])
|
492 |
+
for event in score[itrack]:
|
493 |
+
channel_index = Event2channelindex.get(event[0], False)
|
494 |
+
if channel_index:
|
495 |
+
if event[channel_index] in channels:
|
496 |
+
new_score[itrack].append(event)
|
497 |
+
else:
|
498 |
+
new_score[itrack].append(event)
|
499 |
+
itrack += 1
|
500 |
+
return new_score
|
501 |
+
|
502 |
+
def play_score(score=None):
|
503 |
+
r'''Converts the "score" to midi, and feeds it into 'aplaymidi -'
|
504 |
+
'''
|
505 |
+
if score == None:
|
506 |
+
return
|
507 |
+
import subprocess
|
508 |
+
pipe = subprocess.Popen(['aplaymidi','-'], stdin=subprocess.PIPE)
|
509 |
+
if score_type(score) == 'opus':
|
510 |
+
pipe.stdin.write(opus2midi(score))
|
511 |
+
else:
|
512 |
+
pipe.stdin.write(score2midi(score))
|
513 |
+
pipe.stdin.close()
|
514 |
+
|
515 |
+
def timeshift(score=None, shift=None, start_time=None, from_time=0, tracks={0,1,2,3,4,5,6,7,8,10,12,13,14,15}):
|
516 |
+
r'''Returns a "score" shifted in time by "shift" ticks, or shifted
|
517 |
+
so that the first event starts at "start_time" ticks.
|
518 |
+
|
519 |
+
If "from_time" is specified, only those events in the score
|
520 |
+
that begin after it are shifted. If "start_time" is less than
|
521 |
+
"from_time" (or "shift" is negative), then the intermediate
|
522 |
+
notes are deleted, though patch-change events are preserved.
|
523 |
+
|
524 |
+
If "tracks" are specified, then only those tracks get shifted.
|
525 |
+
"tracks" can be a list, tuple or set; it gets converted to set
|
526 |
+
internally.
|
527 |
+
|
528 |
+
It is deprecated to specify both "shift" and "start_time".
|
529 |
+
If this does happen, timeshift() will print a warning to
|
530 |
+
stderr and ignore the "shift" argument.
|
531 |
+
|
532 |
+
If "shift" is negative and sufficiently large that it would
|
533 |
+
leave some event with a negative tick-value, then the score
|
534 |
+
is shifted so that the first event occurs at time 0. This
|
535 |
+
also occurs if "start_time" is negative, and is also the
|
536 |
+
default if neither "shift" nor "start_time" are specified.
|
537 |
+
'''
|
538 |
+
#_warn('tracks='+str(tracks))
|
539 |
+
if score == None or len(score) < 2:
|
540 |
+
return [1000, [],]
|
541 |
+
new_score = [score[0],]
|
542 |
+
my_type = score_type(score)
|
543 |
+
if my_type == '':
|
544 |
+
return new_score
|
545 |
+
if my_type == 'opus':
|
546 |
+
_warn("timeshift: opus format is not supported\n")
|
547 |
+
# _clean_up_scores() 6.2; doesn't exist! what was it supposed to do?
|
548 |
+
return new_score
|
549 |
+
if not (shift == None) and not (start_time == None):
|
550 |
+
_warn("timeshift: shift and start_time specified: ignoring shift\n")
|
551 |
+
shift = None
|
552 |
+
if shift == None:
|
553 |
+
if (start_time == None) or (start_time < 0):
|
554 |
+
start_time = 0
|
555 |
+
# shift = start_time - from_time
|
556 |
+
|
557 |
+
i = 1 # ignore first element (ticks)
|
558 |
+
tracks = set(tracks) # defend against tuples and lists
|
559 |
+
earliest = 1000000000
|
560 |
+
if not (start_time == None) or shift < 0: # first find the earliest event
|
561 |
+
while i < len(score):
|
562 |
+
if len(tracks) and not ((i-1) in tracks):
|
563 |
+
i += 1
|
564 |
+
continue
|
565 |
+
for event in score[i]:
|
566 |
+
if event[1] < from_time:
|
567 |
+
continue # just inspect the to_be_shifted events
|
568 |
+
if event[1] < earliest:
|
569 |
+
earliest = event[1]
|
570 |
+
i += 1
|
571 |
+
if earliest > 999999999:
|
572 |
+
earliest = 0
|
573 |
+
if shift == None:
|
574 |
+
shift = start_time - earliest
|
575 |
+
elif (earliest + shift) < 0:
|
576 |
+
start_time = 0
|
577 |
+
shift = 0 - earliest
|
578 |
+
|
579 |
+
i = 1 # ignore first element (ticks)
|
580 |
+
while i < len(score):
|
581 |
+
if len(tracks) == 0 or not ((i-1) in tracks): # 3.8
|
582 |
+
new_score.append(score[i])
|
583 |
+
i += 1
|
584 |
+
continue
|
585 |
+
new_track = []
|
586 |
+
for event in score[i]:
|
587 |
+
new_event = list(event)
|
588 |
+
#if new_event[1] == 0 and shift > 0 and new_event[0] != 'note':
|
589 |
+
# pass
|
590 |
+
#elif new_event[1] >= from_time:
|
591 |
+
if new_event[1] >= from_time:
|
592 |
+
# 4.1 must not rightshift set_tempo
|
593 |
+
if new_event[0] != 'set_tempo' or shift<0:
|
594 |
+
new_event[1] += shift
|
595 |
+
elif (shift < 0) and (new_event[1] >= (from_time+shift)):
|
596 |
+
continue
|
597 |
+
new_track.append(new_event)
|
598 |
+
if len(new_track) > 0:
|
599 |
+
new_score.append(new_track)
|
600 |
+
i += 1
|
601 |
+
_clean_up_warnings()
|
602 |
+
return new_score
|
603 |
+
|
604 |
+
def segment(score=None, start_time=None, end_time=None, start=0, end=100000000,
|
605 |
+
tracks={0,1,2,3,4,5,6,7,8,10,11,12,13,14,15}):
|
606 |
+
r'''Returns a "score" which is a segment of the one supplied
|
607 |
+
as the argument, beginning at "start_time" ticks and ending
|
608 |
+
at "end_time" ticks (or at the end if "end_time" is not supplied).
|
609 |
+
If the set "tracks" is specified, only those tracks will
|
610 |
+
be returned.
|
611 |
+
'''
|
612 |
+
if score == None or len(score) < 2:
|
613 |
+
return [1000, [],]
|
614 |
+
if start_time == None: # as of 4.2 start_time is recommended
|
615 |
+
start_time = start # start is legacy usage
|
616 |
+
if end_time == None: # likewise
|
617 |
+
end_time = end
|
618 |
+
new_score = [score[0],]
|
619 |
+
my_type = score_type(score)
|
620 |
+
if my_type == '':
|
621 |
+
return new_score
|
622 |
+
if my_type == 'opus':
|
623 |
+
# more difficult (disconnecting note_on's from their note_off's)...
|
624 |
+
_warn("segment: opus format is not supported\n")
|
625 |
+
_clean_up_warnings()
|
626 |
+
return new_score
|
627 |
+
i = 1 # ignore first element (ticks); we count in ticks anyway
|
628 |
+
tracks = set(tracks) # defend against tuples and lists
|
629 |
+
while i < len(score):
|
630 |
+
if len(tracks) and not ((i-1) in tracks):
|
631 |
+
i += 1
|
632 |
+
continue
|
633 |
+
new_track = []
|
634 |
+
channel2cc_num = {} # most recent controller change before start
|
635 |
+
channel2cc_val = {}
|
636 |
+
channel2cc_time = {}
|
637 |
+
channel2patch_num = {} # keep most recent patch change before start
|
638 |
+
channel2patch_time = {}
|
639 |
+
set_tempo_num = 500000 # most recent tempo change before start 6.3
|
640 |
+
set_tempo_time = 0
|
641 |
+
earliest_note_time = end_time
|
642 |
+
for event in score[i]:
|
643 |
+
if event[0] == 'control_change': # 6.5
|
644 |
+
cc_time = channel2cc_time.get(event[2]) or 0
|
645 |
+
if (event[1] <= start_time) and (event[1] >= cc_time):
|
646 |
+
channel2cc_num[event[2]] = event[3]
|
647 |
+
channel2cc_val[event[2]] = event[4]
|
648 |
+
channel2cc_time[event[2]] = event[1]
|
649 |
+
elif event[0] == 'patch_change':
|
650 |
+
patch_time = channel2patch_time.get(event[2]) or 0
|
651 |
+
if (event[1]<=start_time) and (event[1] >= patch_time): # 2.0
|
652 |
+
channel2patch_num[event[2]] = event[3]
|
653 |
+
channel2patch_time[event[2]] = event[1]
|
654 |
+
elif event[0] == 'set_tempo':
|
655 |
+
if (event[1]<=start_time) and (event[1]>=set_tempo_time): #6.4
|
656 |
+
set_tempo_num = event[2]
|
657 |
+
set_tempo_time = event[1]
|
658 |
+
if (event[1] >= start_time) and (event[1] <= end_time):
|
659 |
+
new_track.append(event)
|
660 |
+
if (event[0] == 'note') and (event[1] < earliest_note_time):
|
661 |
+
earliest_note_time = event[1]
|
662 |
+
if len(new_track) > 0:
|
663 |
+
new_track.append(['set_tempo', start_time, set_tempo_num])
|
664 |
+
for c in channel2patch_num:
|
665 |
+
new_track.append(['patch_change',start_time,c,channel2patch_num[c]],)
|
666 |
+
for c in channel2cc_num: # 6.5
|
667 |
+
new_track.append(['control_change',start_time,c,channel2cc_num[c],channel2cc_val[c]])
|
668 |
+
new_score.append(new_track)
|
669 |
+
i += 1
|
670 |
+
_clean_up_warnings()
|
671 |
+
return new_score
|
672 |
+
|
673 |
+
def score_type(opus_or_score=None):
|
674 |
+
r'''Returns a string, either 'opus' or 'score' or ''
|
675 |
+
'''
|
676 |
+
if opus_or_score == None or str(type(opus_or_score)).find('list')<0 or len(opus_or_score) < 2:
|
677 |
+
return ''
|
678 |
+
i = 1 # ignore first element
|
679 |
+
while i < len(opus_or_score):
|
680 |
+
for event in opus_or_score[i]:
|
681 |
+
if event[0] == 'note':
|
682 |
+
return 'score'
|
683 |
+
elif event[0] == 'note_on':
|
684 |
+
return 'opus'
|
685 |
+
i += 1
|
686 |
+
return ''
|
687 |
+
|
688 |
+
def concatenate_scores(scores):
|
689 |
+
r'''Concatenates a list of scores into one score.
|
690 |
+
If the scores differ in their "ticks" parameter,
|
691 |
+
they will all get converted to millisecond-tick format.
|
692 |
+
'''
|
693 |
+
# the deepcopys are needed if the input_score's are refs to the same obj
|
694 |
+
# e.g. if invoked by midisox's repeat()
|
695 |
+
input_scores = _consistentise_ticks(scores) # 3.7
|
696 |
+
output_score = copy.deepcopy(input_scores[0])
|
697 |
+
for input_score in input_scores[1:]:
|
698 |
+
output_stats = score2stats(output_score)
|
699 |
+
delta_ticks = output_stats['nticks']
|
700 |
+
itrack = 1
|
701 |
+
while itrack < len(input_score):
|
702 |
+
if itrack >= len(output_score): # new output track if doesn't exist
|
703 |
+
output_score.append([])
|
704 |
+
for event in input_score[itrack]:
|
705 |
+
output_score[itrack].append(copy.deepcopy(event))
|
706 |
+
output_score[itrack][-1][1] += delta_ticks
|
707 |
+
itrack += 1
|
708 |
+
return output_score
|
709 |
+
|
710 |
+
def merge_scores(scores):
|
711 |
+
r'''Merges a list of scores into one score. A merged score comprises
|
712 |
+
all of the tracks from all of the input scores; un-merging is possible
|
713 |
+
by selecting just some of the tracks. If the scores differ in their
|
714 |
+
"ticks" parameter, they will all get converted to millisecond-tick
|
715 |
+
format. merge_scores attempts to resolve channel-conflicts,
|
716 |
+
but there are of course only 15 available channels...
|
717 |
+
'''
|
718 |
+
input_scores = _consistentise_ticks(scores) # 3.6
|
719 |
+
output_score = [1000]
|
720 |
+
channels_so_far = set()
|
721 |
+
all_channels = {0,1,2,3,4,5,6,7,8,10,11,12,13,14,15}
|
722 |
+
global Event2channelindex
|
723 |
+
for input_score in input_scores:
|
724 |
+
new_channels = set(score2stats(input_score).get('channels_total', []))
|
725 |
+
new_channels.discard(9) # 2.8 cha9 must remain cha9 (in GM)
|
726 |
+
for channel in channels_so_far & new_channels:
|
727 |
+
# consistently choose lowest avaiable, to ease testing
|
728 |
+
free_channels = list(all_channels - (channels_so_far|new_channels))
|
729 |
+
if len(free_channels) > 0:
|
730 |
+
free_channels.sort()
|
731 |
+
free_channel = free_channels[0]
|
732 |
+
else:
|
733 |
+
free_channel = None
|
734 |
+
break
|
735 |
+
itrack = 1
|
736 |
+
while itrack < len(input_score):
|
737 |
+
for input_event in input_score[itrack]:
|
738 |
+
channel_index=Event2channelindex.get(input_event[0],False)
|
739 |
+
if channel_index and input_event[channel_index]==channel:
|
740 |
+
input_event[channel_index] = free_channel
|
741 |
+
itrack += 1
|
742 |
+
channels_so_far.add(free_channel)
|
743 |
+
|
744 |
+
channels_so_far |= new_channels
|
745 |
+
output_score.extend(input_score[1:])
|
746 |
+
return output_score
|
747 |
+
|
748 |
+
def _ticks(event):
|
749 |
+
return event[1]
|
750 |
+
def mix_opus_tracks(input_tracks): # 5.5
|
751 |
+
r'''Mixes an array of tracks into one track. A mixed track
|
752 |
+
cannot be un-mixed. It is assumed that the tracks share the same
|
753 |
+
ticks parameter and the same tempo.
|
754 |
+
Mixing score-tracks is trivial (just insert all events into one array).
|
755 |
+
Mixing opus-tracks is only slightly harder, but it's common enough
|
756 |
+
that a dedicated function is useful.
|
757 |
+
'''
|
758 |
+
output_score = [1000, []]
|
759 |
+
for input_track in input_tracks: # 5.8
|
760 |
+
input_score = opus2score([1000, input_track])
|
761 |
+
for event in input_score[1]:
|
762 |
+
output_score[1].append(event)
|
763 |
+
output_score[1].sort(key=_ticks)
|
764 |
+
output_opus = score2opus(output_score)
|
765 |
+
return output_opus[1]
|
766 |
+
|
767 |
+
def mix_scores(scores):
|
768 |
+
r'''Mixes a list of scores into one one-track score.
|
769 |
+
A mixed score cannot be un-mixed. Hopefully the scores
|
770 |
+
have no undesirable channel-conflicts between them.
|
771 |
+
If the scores differ in their "ticks" parameter,
|
772 |
+
they will all get converted to millisecond-tick format.
|
773 |
+
'''
|
774 |
+
input_scores = _consistentise_ticks(scores) # 3.6
|
775 |
+
output_score = [1000, []]
|
776 |
+
for input_score in input_scores:
|
777 |
+
for input_track in input_score[1:]:
|
778 |
+
output_score[1].extend(input_track)
|
779 |
+
return output_score
|
780 |
+
|
781 |
+
def score2stats(opus_or_score=None):
|
782 |
+
r'''Returns a dict of some basic stats about the score, like
|
783 |
+
bank_select (list of tuples (msb,lsb)),
|
784 |
+
channels_by_track (list of lists), channels_total (set),
|
785 |
+
general_midi_mode (list),
|
786 |
+
ntracks, nticks, patch_changes_by_track (list of dicts),
|
787 |
+
num_notes_by_channel (list of numbers),
|
788 |
+
patch_changes_total (set),
|
789 |
+
percussion (dict histogram of channel 9 events),
|
790 |
+
pitches (dict histogram of pitches on channels other than 9),
|
791 |
+
pitch_range_by_track (list, by track, of two-member-tuples),
|
792 |
+
pitch_range_sum (sum over tracks of the pitch_ranges),
|
793 |
+
'''
|
794 |
+
bank_select_msb = -1
|
795 |
+
bank_select_lsb = -1
|
796 |
+
bank_select = []
|
797 |
+
channels_by_track = []
|
798 |
+
channels_total = set([])
|
799 |
+
general_midi_mode = []
|
800 |
+
num_notes_by_channel = dict([])
|
801 |
+
patches_used_by_track = []
|
802 |
+
patches_used_total = set([])
|
803 |
+
patch_changes_by_track = []
|
804 |
+
patch_changes_total = set([])
|
805 |
+
percussion = dict([]) # histogram of channel 9 "pitches"
|
806 |
+
pitches = dict([]) # histogram of pitch-occurrences channels 0-8,10-15
|
807 |
+
pitch_range_sum = 0 # u pitch-ranges of each track
|
808 |
+
pitch_range_by_track = []
|
809 |
+
is_a_score = True
|
810 |
+
if opus_or_score == None:
|
811 |
+
return {'bank_select':[], 'channels_by_track':[], 'channels_total':[],
|
812 |
+
'general_midi_mode':[], 'ntracks':0, 'nticks':0,
|
813 |
+
'num_notes_by_channel':dict([]),
|
814 |
+
'patch_changes_by_track':[], 'patch_changes_total':[],
|
815 |
+
'percussion':{}, 'pitches':{}, 'pitch_range_by_track':[],
|
816 |
+
'ticks_per_quarter':0, 'pitch_range_sum':0}
|
817 |
+
ticks_per_quarter = opus_or_score[0]
|
818 |
+
i = 1 # ignore first element, which is ticks
|
819 |
+
nticks = 0
|
820 |
+
while i < len(opus_or_score):
|
821 |
+
highest_pitch = 0
|
822 |
+
lowest_pitch = 128
|
823 |
+
channels_this_track = set([])
|
824 |
+
patch_changes_this_track = dict({})
|
825 |
+
for event in opus_or_score[i]:
|
826 |
+
if event[0] == 'note':
|
827 |
+
num_notes_by_channel[event[3]] = num_notes_by_channel.get(event[3],0) + 1
|
828 |
+
if event[3] == 9:
|
829 |
+
percussion[event[4]] = percussion.get(event[4],0) + 1
|
830 |
+
else:
|
831 |
+
pitches[event[4]] = pitches.get(event[4],0) + 1
|
832 |
+
if event[4] > highest_pitch:
|
833 |
+
highest_pitch = event[4]
|
834 |
+
if event[4] < lowest_pitch:
|
835 |
+
lowest_pitch = event[4]
|
836 |
+
channels_this_track.add(event[3])
|
837 |
+
channels_total.add(event[3])
|
838 |
+
finish_time = event[1] + event[2]
|
839 |
+
if finish_time > nticks:
|
840 |
+
nticks = finish_time
|
841 |
+
elif event[0] == 'note_off' or (event[0] == 'note_on' and event[4] == 0): # 4.8
|
842 |
+
finish_time = event[1]
|
843 |
+
if finish_time > nticks:
|
844 |
+
nticks = finish_time
|
845 |
+
elif event[0] == 'note_on':
|
846 |
+
is_a_score = False
|
847 |
+
num_notes_by_channel[event[2]] = num_notes_by_channel.get(event[2],0) + 1
|
848 |
+
if event[2] == 9:
|
849 |
+
percussion[event[3]] = percussion.get(event[3],0) + 1
|
850 |
+
else:
|
851 |
+
pitches[event[3]] = pitches.get(event[3],0) + 1
|
852 |
+
if event[3] > highest_pitch:
|
853 |
+
highest_pitch = event[3]
|
854 |
+
if event[3] < lowest_pitch:
|
855 |
+
lowest_pitch = event[3]
|
856 |
+
channels_this_track.add(event[2])
|
857 |
+
channels_total.add(event[2])
|
858 |
+
elif event[0] == 'patch_change':
|
859 |
+
patch_changes_this_track[event[2]] = event[3]
|
860 |
+
patch_changes_total.add(event[3])
|
861 |
+
elif event[0] == 'control_change':
|
862 |
+
if event[3] == 0: # bank select MSB
|
863 |
+
bank_select_msb = event[4]
|
864 |
+
elif event[3] == 32: # bank select LSB
|
865 |
+
bank_select_lsb = event[4]
|
866 |
+
if bank_select_msb >= 0 and bank_select_lsb >= 0:
|
867 |
+
bank_select.append((bank_select_msb,bank_select_lsb))
|
868 |
+
bank_select_msb = -1
|
869 |
+
bank_select_lsb = -1
|
870 |
+
elif event[0] == 'sysex_f0':
|
871 |
+
if _sysex2midimode.get(event[2], -1) >= 0:
|
872 |
+
general_midi_mode.append(_sysex2midimode.get(event[2]))
|
873 |
+
if is_a_score:
|
874 |
+
if event[1] > nticks:
|
875 |
+
nticks = event[1]
|
876 |
+
else:
|
877 |
+
nticks += event[1]
|
878 |
+
if lowest_pitch == 128:
|
879 |
+
lowest_pitch = 0
|
880 |
+
channels_by_track.append(channels_this_track)
|
881 |
+
patch_changes_by_track.append(patch_changes_this_track)
|
882 |
+
pitch_range_by_track.append((lowest_pitch,highest_pitch))
|
883 |
+
pitch_range_sum += (highest_pitch-lowest_pitch)
|
884 |
+
i += 1
|
885 |
+
|
886 |
+
return {'bank_select':bank_select,
|
887 |
+
'channels_by_track':channels_by_track,
|
888 |
+
'channels_total':channels_total,
|
889 |
+
'general_midi_mode':general_midi_mode,
|
890 |
+
'ntracks':len(opus_or_score)-1,
|
891 |
+
'nticks':nticks,
|
892 |
+
'num_notes_by_channel':num_notes_by_channel,
|
893 |
+
'patch_changes_by_track':patch_changes_by_track,
|
894 |
+
'patch_changes_total':patch_changes_total,
|
895 |
+
'percussion':percussion,
|
896 |
+
'pitches':pitches,
|
897 |
+
'pitch_range_by_track':pitch_range_by_track,
|
898 |
+
'pitch_range_sum':pitch_range_sum,
|
899 |
+
'ticks_per_quarter':ticks_per_quarter}
|
900 |
+
|
901 |
+
#----------------------------- Event stuff --------------------------
|
902 |
+
|
903 |
+
_sysex2midimode = {
|
904 |
+
"\x7E\x7F\x09\x01\xF7": 1,
|
905 |
+
"\x7E\x7F\x09\x02\xF7": 0,
|
906 |
+
"\x7E\x7F\x09\x03\xF7": 2,
|
907 |
+
}
|
908 |
+
|
909 |
+
# Some public-access tuples:
|
910 |
+
MIDI_events = tuple('''note_off note_on key_after_touch
|
911 |
+
control_change patch_change channel_after_touch
|
912 |
+
pitch_wheel_change'''.split())
|
913 |
+
|
914 |
+
Text_events = tuple('''text_event copyright_text_event
|
915 |
+
track_name instrument_name lyric marker cue_point text_event_08
|
916 |
+
text_event_09 text_event_0a text_event_0b text_event_0c
|
917 |
+
text_event_0d text_event_0e text_event_0f'''.split())
|
918 |
+
|
919 |
+
Nontext_meta_events = tuple('''end_track set_tempo
|
920 |
+
smpte_offset time_signature key_signature sequencer_specific
|
921 |
+
raw_meta_event sysex_f0 sysex_f7 song_position song_select
|
922 |
+
tune_request'''.split())
|
923 |
+
# unsupported: raw_data
|
924 |
+
|
925 |
+
# Actually, 'tune_request' is is F-series event, not strictly a meta-event...
|
926 |
+
Meta_events = Text_events + Nontext_meta_events
|
927 |
+
All_events = MIDI_events + Meta_events
|
928 |
+
|
929 |
+
# And three dictionaries:
|
930 |
+
Number2patch = { # General MIDI patch numbers:
|
931 |
+
0:'Acoustic Grand',
|
932 |
+
1:'Bright Acoustic',
|
933 |
+
2:'Electric Grand',
|
934 |
+
3:'Honky-Tonk',
|
935 |
+
4:'Electric Piano 1',
|
936 |
+
5:'Electric Piano 2',
|
937 |
+
6:'Harpsichord',
|
938 |
+
7:'Clav',
|
939 |
+
8:'Celesta',
|
940 |
+
9:'Glockenspiel',
|
941 |
+
10:'Music Box',
|
942 |
+
11:'Vibraphone',
|
943 |
+
12:'Marimba',
|
944 |
+
13:'Xylophone',
|
945 |
+
14:'Tubular Bells',
|
946 |
+
15:'Dulcimer',
|
947 |
+
16:'Drawbar Organ',
|
948 |
+
17:'Percussive Organ',
|
949 |
+
18:'Rock Organ',
|
950 |
+
19:'Church Organ',
|
951 |
+
20:'Reed Organ',
|
952 |
+
21:'Accordion',
|
953 |
+
22:'Harmonica',
|
954 |
+
23:'Tango Accordion',
|
955 |
+
24:'Acoustic Guitar(nylon)',
|
956 |
+
25:'Acoustic Guitar(steel)',
|
957 |
+
26:'Electric Guitar(jazz)',
|
958 |
+
27:'Electric Guitar(clean)',
|
959 |
+
28:'Electric Guitar(muted)',
|
960 |
+
29:'Overdriven Guitar',
|
961 |
+
30:'Distortion Guitar',
|
962 |
+
31:'Guitar Harmonics',
|
963 |
+
32:'Acoustic Bass',
|
964 |
+
33:'Electric Bass(finger)',
|
965 |
+
34:'Electric Bass(pick)',
|
966 |
+
35:'Fretless Bass',
|
967 |
+
36:'Slap Bass 1',
|
968 |
+
37:'Slap Bass 2',
|
969 |
+
38:'Synth Bass 1',
|
970 |
+
39:'Synth Bass 2',
|
971 |
+
40:'Violin',
|
972 |
+
41:'Viola',
|
973 |
+
42:'Cello',
|
974 |
+
43:'Contrabass',
|
975 |
+
44:'Tremolo Strings',
|
976 |
+
45:'Pizzicato Strings',
|
977 |
+
46:'Orchestral Harp',
|
978 |
+
47:'Timpani',
|
979 |
+
48:'String Ensemble 1',
|
980 |
+
49:'String Ensemble 2',
|
981 |
+
50:'SynthStrings 1',
|
982 |
+
51:'SynthStrings 2',
|
983 |
+
52:'Choir Aahs',
|
984 |
+
53:'Voice Oohs',
|
985 |
+
54:'Synth Voice',
|
986 |
+
55:'Orchestra Hit',
|
987 |
+
56:'Trumpet',
|
988 |
+
57:'Trombone',
|
989 |
+
58:'Tuba',
|
990 |
+
59:'Muted Trumpet',
|
991 |
+
60:'French Horn',
|
992 |
+
61:'Brass Section',
|
993 |
+
62:'SynthBrass 1',
|
994 |
+
63:'SynthBrass 2',
|
995 |
+
64:'Soprano Sax',
|
996 |
+
65:'Alto Sax',
|
997 |
+
66:'Tenor Sax',
|
998 |
+
67:'Baritone Sax',
|
999 |
+
68:'Oboe',
|
1000 |
+
69:'English Horn',
|
1001 |
+
70:'Bassoon',
|
1002 |
+
71:'Clarinet',
|
1003 |
+
72:'Piccolo',
|
1004 |
+
73:'Flute',
|
1005 |
+
74:'Recorder',
|
1006 |
+
75:'Pan Flute',
|
1007 |
+
76:'Blown Bottle',
|
1008 |
+
77:'Skakuhachi',
|
1009 |
+
78:'Whistle',
|
1010 |
+
79:'Ocarina',
|
1011 |
+
80:'Lead 1 (square)',
|
1012 |
+
81:'Lead 2 (sawtooth)',
|
1013 |
+
82:'Lead 3 (calliope)',
|
1014 |
+
83:'Lead 4 (chiff)',
|
1015 |
+
84:'Lead 5 (charang)',
|
1016 |
+
85:'Lead 6 (voice)',
|
1017 |
+
86:'Lead 7 (fifths)',
|
1018 |
+
87:'Lead 8 (bass+lead)',
|
1019 |
+
88:'Pad 1 (new age)',
|
1020 |
+
89:'Pad 2 (warm)',
|
1021 |
+
90:'Pad 3 (polysynth)',
|
1022 |
+
91:'Pad 4 (choir)',
|
1023 |
+
92:'Pad 5 (bowed)',
|
1024 |
+
93:'Pad 6 (metallic)',
|
1025 |
+
94:'Pad 7 (halo)',
|
1026 |
+
95:'Pad 8 (sweep)',
|
1027 |
+
96:'FX 1 (rain)',
|
1028 |
+
97:'FX 2 (soundtrack)',
|
1029 |
+
98:'FX 3 (crystal)',
|
1030 |
+
99:'FX 4 (atmosphere)',
|
1031 |
+
100:'FX 5 (brightness)',
|
1032 |
+
101:'FX 6 (goblins)',
|
1033 |
+
102:'FX 7 (echoes)',
|
1034 |
+
103:'FX 8 (sci-fi)',
|
1035 |
+
104:'Sitar',
|
1036 |
+
105:'Banjo',
|
1037 |
+
106:'Shamisen',
|
1038 |
+
107:'Koto',
|
1039 |
+
108:'Kalimba',
|
1040 |
+
109:'Bagpipe',
|
1041 |
+
110:'Fiddle',
|
1042 |
+
111:'Shanai',
|
1043 |
+
112:'Tinkle Bell',
|
1044 |
+
113:'Agogo',
|
1045 |
+
114:'Steel Drums',
|
1046 |
+
115:'Woodblock',
|
1047 |
+
116:'Taiko Drum',
|
1048 |
+
117:'Melodic Tom',
|
1049 |
+
118:'Synth Drum',
|
1050 |
+
119:'Reverse Cymbal',
|
1051 |
+
120:'Guitar Fret Noise',
|
1052 |
+
121:'Breath Noise',
|
1053 |
+
122:'Seashore',
|
1054 |
+
123:'Bird Tweet',
|
1055 |
+
124:'Telephone Ring',
|
1056 |
+
125:'Helicopter',
|
1057 |
+
126:'Applause',
|
1058 |
+
127:'Gunshot',
|
1059 |
+
}
|
1060 |
+
Notenum2percussion = { # General MIDI Percussion (on Channel 9):
|
1061 |
+
35:'Acoustic Bass Drum',
|
1062 |
+
36:'Bass Drum 1',
|
1063 |
+
37:'Side Stick',
|
1064 |
+
38:'Acoustic Snare',
|
1065 |
+
39:'Hand Clap',
|
1066 |
+
40:'Electric Snare',
|
1067 |
+
41:'Low Floor Tom',
|
1068 |
+
42:'Closed Hi-Hat',
|
1069 |
+
43:'High Floor Tom',
|
1070 |
+
44:'Pedal Hi-Hat',
|
1071 |
+
45:'Low Tom',
|
1072 |
+
46:'Open Hi-Hat',
|
1073 |
+
47:'Low-Mid Tom',
|
1074 |
+
48:'Hi-Mid Tom',
|
1075 |
+
49:'Crash Cymbal 1',
|
1076 |
+
50:'High Tom',
|
1077 |
+
51:'Ride Cymbal 1',
|
1078 |
+
52:'Chinese Cymbal',
|
1079 |
+
53:'Ride Bell',
|
1080 |
+
54:'Tambourine',
|
1081 |
+
55:'Splash Cymbal',
|
1082 |
+
56:'Cowbell',
|
1083 |
+
57:'Crash Cymbal 2',
|
1084 |
+
58:'Vibraslap',
|
1085 |
+
59:'Ride Cymbal 2',
|
1086 |
+
60:'Hi Bongo',
|
1087 |
+
61:'Low Bongo',
|
1088 |
+
62:'Mute Hi Conga',
|
1089 |
+
63:'Open Hi Conga',
|
1090 |
+
64:'Low Conga',
|
1091 |
+
65:'High Timbale',
|
1092 |
+
66:'Low Timbale',
|
1093 |
+
67:'High Agogo',
|
1094 |
+
68:'Low Agogo',
|
1095 |
+
69:'Cabasa',
|
1096 |
+
70:'Maracas',
|
1097 |
+
71:'Short Whistle',
|
1098 |
+
72:'Long Whistle',
|
1099 |
+
73:'Short Guiro',
|
1100 |
+
74:'Long Guiro',
|
1101 |
+
75:'Claves',
|
1102 |
+
76:'Hi Wood Block',
|
1103 |
+
77:'Low Wood Block',
|
1104 |
+
78:'Mute Cuica',
|
1105 |
+
79:'Open Cuica',
|
1106 |
+
80:'Mute Triangle',
|
1107 |
+
81:'Open Triangle',
|
1108 |
+
}
|
1109 |
+
|
1110 |
+
Event2channelindex = { 'note':3, 'note_off':2, 'note_on':2,
|
1111 |
+
'key_after_touch':2, 'control_change':2, 'patch_change':2,
|
1112 |
+
'channel_after_touch':2, 'pitch_wheel_change':2
|
1113 |
+
}
|
1114 |
+
|
1115 |
+
################################################################
|
1116 |
+
# The code below this line is full of frightening things, all to
|
1117 |
+
# do with the actual encoding and decoding of binary MIDI data.
|
1118 |
+
|
1119 |
+
def _twobytes2int(byte_a):
|
1120 |
+
r'''decode a 16 bit quantity from two bytes,'''
|
1121 |
+
return (byte_a[1] | (byte_a[0] << 8))
|
1122 |
+
|
1123 |
+
def _int2twobytes(int_16bit):
|
1124 |
+
r'''encode a 16 bit quantity into two bytes,'''
|
1125 |
+
return bytes([(int_16bit>>8) & 0xFF, int_16bit & 0xFF])
|
1126 |
+
|
1127 |
+
def _read_14_bit(byte_a):
|
1128 |
+
r'''decode a 14 bit quantity from two bytes,'''
|
1129 |
+
return (byte_a[0] | (byte_a[1] << 7))
|
1130 |
+
|
1131 |
+
def _write_14_bit(int_14bit):
|
1132 |
+
r'''encode a 14 bit quantity into two bytes,'''
|
1133 |
+
return bytes([int_14bit & 0x7F, (int_14bit>>7) & 0x7F])
|
1134 |
+
|
1135 |
+
def _ber_compressed_int(integer):
|
1136 |
+
r'''BER compressed integer (not an ASN.1 BER, see perlpacktut for
|
1137 |
+
details). Its bytes represent an unsigned integer in base 128,
|
1138 |
+
most significant digit first, with as few digits as possible.
|
1139 |
+
Bit eight (the high bit) is set on each byte except the last.
|
1140 |
+
'''
|
1141 |
+
ber = bytearray(b'')
|
1142 |
+
seven_bits = 0x7F & integer
|
1143 |
+
ber.insert(0, seven_bits) # XXX surely should convert to a char ?
|
1144 |
+
integer >>= 7
|
1145 |
+
while integer > 0:
|
1146 |
+
seven_bits = 0x7F & integer
|
1147 |
+
ber.insert(0, 0x80|seven_bits) # XXX surely should convert to a char ?
|
1148 |
+
integer >>= 7
|
1149 |
+
return ber
|
1150 |
+
|
1151 |
+
def _unshift_ber_int(ba):
|
1152 |
+
r'''Given a bytearray, returns a tuple of (the ber-integer at the
|
1153 |
+
start, and the remainder of the bytearray).
|
1154 |
+
'''
|
1155 |
+
if not len(ba): # 6.7
|
1156 |
+
_warn('_unshift_ber_int: no integer found')
|
1157 |
+
return ((0, b""))
|
1158 |
+
byte = ba.pop(0)
|
1159 |
+
integer = 0
|
1160 |
+
while True:
|
1161 |
+
integer += (byte & 0x7F)
|
1162 |
+
if not (byte & 0x80):
|
1163 |
+
return ((integer, ba))
|
1164 |
+
if not len(ba):
|
1165 |
+
_warn('_unshift_ber_int: no end-of-integer found')
|
1166 |
+
return ((0, ba))
|
1167 |
+
byte = ba.pop(0)
|
1168 |
+
integer <<= 7
|
1169 |
+
|
1170 |
+
def _clean_up_warnings(): # 5.4
|
1171 |
+
# Call this before returning from any publicly callable function
|
1172 |
+
# whenever there's a possibility that a warning might have been printed
|
1173 |
+
# by the function, or by any private functions it might have called.
|
1174 |
+
if _no_warning:
|
1175 |
+
return
|
1176 |
+
global _previous_times
|
1177 |
+
global _previous_warning
|
1178 |
+
if _previous_times > 1:
|
1179 |
+
# E:1176, 0: invalid syntax (<string>, line 1176) (syntax-error) ???
|
1180 |
+
# print(' previous message repeated '+str(_previous_times)+' times', file=sys.stderr)
|
1181 |
+
# 6.7
|
1182 |
+
sys.stderr.write(' previous message repeated {0} times\n'.format(_previous_times))
|
1183 |
+
elif _previous_times > 0:
|
1184 |
+
sys.stderr.write(' previous message repeated\n')
|
1185 |
+
_previous_times = 0
|
1186 |
+
_previous_warning = ''
|
1187 |
+
|
1188 |
+
def _warn(s=''):
|
1189 |
+
if _no_warning:
|
1190 |
+
return
|
1191 |
+
global _previous_times
|
1192 |
+
global _previous_warning
|
1193 |
+
if s == _previous_warning: # 5.4
|
1194 |
+
_previous_times = _previous_times + 1
|
1195 |
+
else:
|
1196 |
+
_clean_up_warnings()
|
1197 |
+
sys.stderr.write(str(s)+"\n")
|
1198 |
+
_previous_warning = s
|
1199 |
+
|
1200 |
+
def _some_text_event(which_kind=0x01, text=b'some_text'):
|
1201 |
+
if str(type(text)).find("'str'") >= 0: # 6.4 test for back-compatibility
|
1202 |
+
data = bytes(text, encoding='ISO-8859-1')
|
1203 |
+
else:
|
1204 |
+
data = bytes(text)
|
1205 |
+
return b'\xFF'+bytes((which_kind,))+_ber_compressed_int(len(data))+data
|
1206 |
+
|
1207 |
+
def _consistentise_ticks(scores): # 3.6
|
1208 |
+
# used by mix_scores, merge_scores, concatenate_scores
|
1209 |
+
if len(scores) == 1:
|
1210 |
+
return copy.deepcopy(scores)
|
1211 |
+
are_consistent = True
|
1212 |
+
ticks = scores[0][0]
|
1213 |
+
iscore = 1
|
1214 |
+
while iscore < len(scores):
|
1215 |
+
if scores[iscore][0] != ticks:
|
1216 |
+
are_consistent = False
|
1217 |
+
break
|
1218 |
+
iscore += 1
|
1219 |
+
if are_consistent:
|
1220 |
+
return copy.deepcopy(scores)
|
1221 |
+
new_scores = []
|
1222 |
+
iscore = 0
|
1223 |
+
while iscore < len(scores):
|
1224 |
+
score = scores[iscore]
|
1225 |
+
new_scores.append(opus2score(to_millisecs(score2opus(score))))
|
1226 |
+
iscore += 1
|
1227 |
+
return new_scores
|
1228 |
+
|
1229 |
+
|
1230 |
+
###########################################################################
|
1231 |
+
|
1232 |
+
def _decode(trackdata=b'', exclude=None, include=None,
|
1233 |
+
event_callback=None, exclusive_event_callback=None, no_eot_magic=False):
|
1234 |
+
r'''Decodes MIDI track data into an opus-style list of events.
|
1235 |
+
The options:
|
1236 |
+
'exclude' is a list of event types which will be ignored SHOULD BE A SET
|
1237 |
+
'include' (and no exclude), makes exclude a list
|
1238 |
+
of all possible events, /minus/ what include specifies
|
1239 |
+
'event_callback' is a coderef
|
1240 |
+
'exclusive_event_callback' is a coderef
|
1241 |
+
'''
|
1242 |
+
trackdata = bytearray(trackdata)
|
1243 |
+
if exclude == None:
|
1244 |
+
exclude = []
|
1245 |
+
if include == None:
|
1246 |
+
include = []
|
1247 |
+
if include and not exclude:
|
1248 |
+
exclude = All_events
|
1249 |
+
include = set(include)
|
1250 |
+
exclude = set(exclude)
|
1251 |
+
|
1252 |
+
# Pointer = 0; not used here; we eat through the bytearray instead.
|
1253 |
+
event_code = -1; # used for running status
|
1254 |
+
event_count = 0;
|
1255 |
+
events = []
|
1256 |
+
|
1257 |
+
while(len(trackdata)):
|
1258 |
+
# loop while there's anything to analyze ...
|
1259 |
+
eot = False # When True, the event registrar aborts this loop
|
1260 |
+
event_count += 1
|
1261 |
+
|
1262 |
+
E = []
|
1263 |
+
# E for events - we'll feed it to the event registrar at the end.
|
1264 |
+
|
1265 |
+
# Slice off the delta time code, and analyze it
|
1266 |
+
[time, remainder] = _unshift_ber_int(trackdata)
|
1267 |
+
|
1268 |
+
# Now let's see what we can make of the command
|
1269 |
+
first_byte = trackdata.pop(0) & 0xFF
|
1270 |
+
|
1271 |
+
if (first_byte < 0xF0): # It's a MIDI event
|
1272 |
+
if (first_byte & 0x80):
|
1273 |
+
event_code = first_byte
|
1274 |
+
else:
|
1275 |
+
# It wants running status; use last event_code value
|
1276 |
+
trackdata.insert(0, first_byte)
|
1277 |
+
if (event_code == -1):
|
1278 |
+
_warn("Running status not set; Aborting track.")
|
1279 |
+
return []
|
1280 |
+
|
1281 |
+
command = event_code & 0xF0
|
1282 |
+
channel = event_code & 0x0F
|
1283 |
+
|
1284 |
+
if (command == 0xF6): # 0-byte argument
|
1285 |
+
pass
|
1286 |
+
elif (command == 0xC0 or command == 0xD0): # 1-byte argument
|
1287 |
+
parameter = trackdata.pop(0) # could be B
|
1288 |
+
else: # 2-byte argument could be BB or 14-bit
|
1289 |
+
parameter = (trackdata.pop(0), trackdata.pop(0))
|
1290 |
+
|
1291 |
+
#################################################################
|
1292 |
+
# MIDI events
|
1293 |
+
|
1294 |
+
if (command == 0x80):
|
1295 |
+
if 'note_off' in exclude:
|
1296 |
+
continue
|
1297 |
+
E = ['note_off', time, channel, parameter[0], parameter[1]]
|
1298 |
+
elif (command == 0x90):
|
1299 |
+
if 'note_on' in exclude:
|
1300 |
+
continue
|
1301 |
+
E = ['note_on', time, channel, parameter[0], parameter[1]]
|
1302 |
+
elif (command == 0xA0):
|
1303 |
+
if 'key_after_touch' in exclude:
|
1304 |
+
continue
|
1305 |
+
E = ['key_after_touch',time,channel,parameter[0],parameter[1]]
|
1306 |
+
elif (command == 0xB0):
|
1307 |
+
if 'control_change' in exclude:
|
1308 |
+
continue
|
1309 |
+
E = ['control_change',time,channel,parameter[0],parameter[1]]
|
1310 |
+
elif (command == 0xC0):
|
1311 |
+
if 'patch_change' in exclude:
|
1312 |
+
continue
|
1313 |
+
E = ['patch_change', time, channel, parameter]
|
1314 |
+
elif (command == 0xD0):
|
1315 |
+
if 'channel_after_touch' in exclude:
|
1316 |
+
continue
|
1317 |
+
E = ['channel_after_touch', time, channel, parameter]
|
1318 |
+
elif (command == 0xE0):
|
1319 |
+
if 'pitch_wheel_change' in exclude:
|
1320 |
+
continue
|
1321 |
+
E = ['pitch_wheel_change', time, channel,
|
1322 |
+
_read_14_bit(parameter)-0x2000]
|
1323 |
+
else:
|
1324 |
+
_warn("Shouldn't get here; command="+hex(command))
|
1325 |
+
|
1326 |
+
elif (first_byte == 0xFF): # It's a Meta-Event! ##################
|
1327 |
+
#[command, length, remainder] =
|
1328 |
+
# unpack("xCwa*", substr(trackdata, $Pointer, 6));
|
1329 |
+
#Pointer += 6 - len(remainder);
|
1330 |
+
# # Move past JUST the length-encoded.
|
1331 |
+
command = trackdata.pop(0) & 0xFF
|
1332 |
+
[length, trackdata] = _unshift_ber_int(trackdata)
|
1333 |
+
if (command == 0x00):
|
1334 |
+
if (length == 2):
|
1335 |
+
E = ['set_sequence_number',time,_twobytes2int(trackdata)]
|
1336 |
+
else:
|
1337 |
+
_warn('set_sequence_number: length must be 2, not '+str(length))
|
1338 |
+
E = ['set_sequence_number', time, 0]
|
1339 |
+
|
1340 |
+
elif command >= 0x01 and command <= 0x0f: # Text events
|
1341 |
+
# 6.2 take it in bytes; let the user get the right encoding.
|
1342 |
+
# text_str = trackdata[0:length].decode('ascii','ignore')
|
1343 |
+
# text_str = trackdata[0:length].decode('ISO-8859-1')
|
1344 |
+
# 6.4 take it in bytes; let the user get the right encoding.
|
1345 |
+
text_data = bytes(trackdata[0:length]) # 6.4
|
1346 |
+
# Defined text events
|
1347 |
+
if (command == 0x01):
|
1348 |
+
E = ['text_event', time, text_data]
|
1349 |
+
elif (command == 0x02):
|
1350 |
+
E = ['copyright_text_event', time, text_data]
|
1351 |
+
elif (command == 0x03):
|
1352 |
+
E = ['track_name', time, text_data]
|
1353 |
+
elif (command == 0x04):
|
1354 |
+
E = ['instrument_name', time, text_data]
|
1355 |
+
elif (command == 0x05):
|
1356 |
+
E = ['lyric', time, text_data]
|
1357 |
+
elif (command == 0x06):
|
1358 |
+
E = ['marker', time, text_data]
|
1359 |
+
elif (command == 0x07):
|
1360 |
+
E = ['cue_point', time, text_data]
|
1361 |
+
# Reserved but apparently unassigned text events
|
1362 |
+
elif (command == 0x08):
|
1363 |
+
E = ['text_event_08', time, text_data]
|
1364 |
+
elif (command == 0x09):
|
1365 |
+
E = ['text_event_09', time, text_data]
|
1366 |
+
elif (command == 0x0a):
|
1367 |
+
E = ['text_event_0a', time, text_data]
|
1368 |
+
elif (command == 0x0b):
|
1369 |
+
E = ['text_event_0b', time, text_data]
|
1370 |
+
elif (command == 0x0c):
|
1371 |
+
E = ['text_event_0c', time, text_data]
|
1372 |
+
elif (command == 0x0d):
|
1373 |
+
E = ['text_event_0d', time, text_data]
|
1374 |
+
elif (command == 0x0e):
|
1375 |
+
E = ['text_event_0e', time, text_data]
|
1376 |
+
elif (command == 0x0f):
|
1377 |
+
E = ['text_event_0f', time, text_data]
|
1378 |
+
|
1379 |
+
# Now the sticky events -------------------------------------
|
1380 |
+
elif (command == 0x2F):
|
1381 |
+
E = ['end_track', time]
|
1382 |
+
# The code for handling this, oddly, comes LATER,
|
1383 |
+
# in the event registrar.
|
1384 |
+
elif (command == 0x51): # DTime, Microseconds/Crochet
|
1385 |
+
if length != 3:
|
1386 |
+
_warn('set_tempo event, but length='+str(length))
|
1387 |
+
E = ['set_tempo', time,
|
1388 |
+
struct.unpack(">I", b'\x00'+trackdata[0:3])[0]]
|
1389 |
+
elif (command == 0x54):
|
1390 |
+
if length != 5: # DTime, HR, MN, SE, FR, FF
|
1391 |
+
_warn('smpte_offset event, but length='+str(length))
|
1392 |
+
E = ['smpte_offset',time] + list(struct.unpack(">BBBBB",trackdata[0:5]))
|
1393 |
+
elif (command == 0x58):
|
1394 |
+
if length != 4: # DTime, NN, DD, CC, BB
|
1395 |
+
_warn('time_signature event, but length='+str(length))
|
1396 |
+
E = ['time_signature', time]+list(trackdata[0:4])
|
1397 |
+
elif (command == 0x59):
|
1398 |
+
if length != 2: # DTime, SF(signed), MI
|
1399 |
+
_warn('key_signature event, but length='+str(length))
|
1400 |
+
E = ['key_signature',time] + list(struct.unpack(">bB",trackdata[0:2]))
|
1401 |
+
elif (command == 0x7F): # 6.4
|
1402 |
+
E = ['sequencer_specific',time, bytes(trackdata[0:length])]
|
1403 |
+
else:
|
1404 |
+
E = ['raw_meta_event', time, command,
|
1405 |
+
bytes(trackdata[0:length])] # 6.0
|
1406 |
+
#"[uninterpretable meta-event command of length length]"
|
1407 |
+
# DTime, Command, Binary Data
|
1408 |
+
# It's uninterpretable; record it as raw_data.
|
1409 |
+
|
1410 |
+
# Pointer += length; # Now move Pointer
|
1411 |
+
trackdata = trackdata[length:]
|
1412 |
+
|
1413 |
+
######################################################################
|
1414 |
+
elif (first_byte == 0xF0 or first_byte == 0xF7):
|
1415 |
+
# Note that sysexes in MIDI /files/ are different than sysexes
|
1416 |
+
# in MIDI transmissions!! The vast majority of system exclusive
|
1417 |
+
# messages will just use the F0 format. For instance, the
|
1418 |
+
# transmitted message F0 43 12 00 07 F7 would be stored in a
|
1419 |
+
# MIDI file as F0 05 43 12 00 07 F7. As mentioned above, it is
|
1420 |
+
# required to include the F7 at the end so that the reader of the
|
1421 |
+
# MIDI file knows that it has read the entire message. (But the F7
|
1422 |
+
# is omitted if this is a non-final block in a multiblock sysex;
|
1423 |
+
# but the F7 (if there) is counted in the message's declared
|
1424 |
+
# length, so we don't have to think about it anyway.)
|
1425 |
+
#command = trackdata.pop(0)
|
1426 |
+
[length, trackdata] = _unshift_ber_int(trackdata)
|
1427 |
+
if first_byte == 0xF0:
|
1428 |
+
# 20091008 added ISO-8859-1 to get an 8-bit str
|
1429 |
+
# 6.4 return bytes instead
|
1430 |
+
E = ['sysex_f0', time, bytes(trackdata[0:length])]
|
1431 |
+
else:
|
1432 |
+
E = ['sysex_f7', time, bytes(trackdata[0:length])]
|
1433 |
+
trackdata = trackdata[length:]
|
1434 |
+
|
1435 |
+
######################################################################
|
1436 |
+
# Now, the MIDI file spec says:
|
1437 |
+
# <track data> = <MTrk event>+
|
1438 |
+
# <MTrk event> = <delta-time> <event>
|
1439 |
+
# <event> = <MIDI event> | <sysex event> | <meta-event>
|
1440 |
+
# I know that, on the wire, <MIDI event> can include note_on,
|
1441 |
+
# note_off, and all the other 8x to Ex events, AND Fx events
|
1442 |
+
# other than F0, F7, and FF -- namely, <song position msg>,
|
1443 |
+
# <song select msg>, and <tune request>.
|
1444 |
+
#
|
1445 |
+
# Whether these can occur in MIDI files is not clear specified
|
1446 |
+
# from the MIDI file spec. So, I'm going to assume that
|
1447 |
+
# they CAN, in practice, occur. I don't know whether it's
|
1448 |
+
# proper for you to actually emit these into a MIDI file.
|
1449 |
+
|
1450 |
+
elif (first_byte == 0xF2): # DTime, Beats
|
1451 |
+
# <song position msg> ::= F2 <data pair>
|
1452 |
+
E = ['song_position', time, _read_14_bit(trackdata[:2])]
|
1453 |
+
trackdata = trackdata[2:]
|
1454 |
+
|
1455 |
+
elif (first_byte == 0xF3): # <song select msg> ::= F3 <data singlet>
|
1456 |
+
# E = ['song_select', time, struct.unpack('>B',trackdata.pop(0))[0]]
|
1457 |
+
E = ['song_select', time, trackdata[0]]
|
1458 |
+
trackdata = trackdata[1:]
|
1459 |
+
# DTime, Thing (what?! song number? whatever ...)
|
1460 |
+
|
1461 |
+
elif (first_byte == 0xF6): # DTime
|
1462 |
+
E = ['tune_request', time]
|
1463 |
+
# What would a tune request be doing in a MIDI /file/?
|
1464 |
+
|
1465 |
+
#########################################################
|
1466 |
+
# ADD MORE META-EVENTS HERE. TODO:
|
1467 |
+
# f1 -- MTC Quarter Frame Message. One data byte follows
|
1468 |
+
# the Status; it's the time code value, from 0 to 127.
|
1469 |
+
# f8 -- MIDI clock. no data.
|
1470 |
+
# fa -- MIDI start. no data.
|
1471 |
+
# fb -- MIDI continue. no data.
|
1472 |
+
# fc -- MIDI stop. no data.
|
1473 |
+
# fe -- Active sense. no data.
|
1474 |
+
# f4 f5 f9 fd -- unallocated
|
1475 |
+
|
1476 |
+
r'''
|
1477 |
+
elif (first_byte > 0xF0) { # Some unknown kinda F-series event ####
|
1478 |
+
# Here we only produce a one-byte piece of raw data.
|
1479 |
+
# But the encoder for 'raw_data' accepts any length of it.
|
1480 |
+
E = [ 'raw_data',
|
1481 |
+
time, substr(trackdata,Pointer,1) ]
|
1482 |
+
# DTime and the Data (in this case, the one Event-byte)
|
1483 |
+
++Pointer; # itself
|
1484 |
+
|
1485 |
+
'''
|
1486 |
+
elif first_byte > 0xF0: # Some unknown F-series event
|
1487 |
+
# Here we only produce a one-byte piece of raw data.
|
1488 |
+
# E = ['raw_data', time, bytest(trackdata[0])] # 6.4
|
1489 |
+
E = ['raw_data', time, trackdata[0]] # 6.4 6.7
|
1490 |
+
trackdata = trackdata[1:]
|
1491 |
+
else: # Fallthru.
|
1492 |
+
_warn("Aborting track. Command-byte first_byte="+hex(first_byte))
|
1493 |
+
break
|
1494 |
+
# End of the big if-group
|
1495 |
+
|
1496 |
+
|
1497 |
+
######################################################################
|
1498 |
+
# THE EVENT REGISTRAR...
|
1499 |
+
if E and (E[0] == 'end_track'):
|
1500 |
+
# This is the code for exceptional handling of the EOT event.
|
1501 |
+
eot = True
|
1502 |
+
if not no_eot_magic:
|
1503 |
+
if E[1] > 0: # a null text-event to carry the delta-time
|
1504 |
+
E = ['text_event', E[1], '']
|
1505 |
+
else:
|
1506 |
+
E = [] # EOT with a delta-time of 0; ignore it.
|
1507 |
+
|
1508 |
+
if E and not (E[0] in exclude):
|
1509 |
+
#if ( $exclusive_event_callback ):
|
1510 |
+
# &{ $exclusive_event_callback }( @E );
|
1511 |
+
#else:
|
1512 |
+
# &{ $event_callback }( @E ) if $event_callback;
|
1513 |
+
events.append(E)
|
1514 |
+
if eot:
|
1515 |
+
break
|
1516 |
+
|
1517 |
+
# End of the big "Event" while-block
|
1518 |
+
|
1519 |
+
return events
|
1520 |
+
|
1521 |
+
|
1522 |
+
###########################################################################
|
1523 |
+
def _encode(events_lol, unknown_callback=None, never_add_eot=False,
|
1524 |
+
no_eot_magic=False, no_running_status=False):
|
1525 |
+
# encode an event structure, presumably for writing to a file
|
1526 |
+
# Calling format:
|
1527 |
+
# $data_r = MIDI::Event::encode( \@event_lol, { options } );
|
1528 |
+
# Takes a REFERENCE to an event structure (a LoL)
|
1529 |
+
# Returns an (unblessed) REFERENCE to track data.
|
1530 |
+
|
1531 |
+
# If you want to use this to encode a /single/ event,
|
1532 |
+
# you still have to do it as a reference to an event structure (a LoL)
|
1533 |
+
# that just happens to have just one event. I.e.,
|
1534 |
+
# encode( [ $event ] ) or encode( [ [ 'note_on', 100, 5, 42, 64] ] )
|
1535 |
+
# If you're doing this, consider the never_add_eot track option, as in
|
1536 |
+
# print MIDI ${ encode( [ $event], { 'never_add_eot' => 1} ) };
|
1537 |
+
|
1538 |
+
data = [] # what I'll store the chunks of byte-data in
|
1539 |
+
|
1540 |
+
# This is so my end_track magic won't corrupt the original
|
1541 |
+
events = copy.deepcopy(events_lol)
|
1542 |
+
|
1543 |
+
if not never_add_eot:
|
1544 |
+
# One way or another, tack on an 'end_track'
|
1545 |
+
if events:
|
1546 |
+
last = events[-1]
|
1547 |
+
if not (last[0] == 'end_track'): # no end_track already
|
1548 |
+
if (last[0] == 'text_event' and len(last[2]) == 0):
|
1549 |
+
# 0-length text event at track-end.
|
1550 |
+
if no_eot_magic:
|
1551 |
+
# Exceptional case: don't mess with track-final
|
1552 |
+
# 0-length text_events; just peg on an end_track
|
1553 |
+
events.append(['end_track', 0])
|
1554 |
+
else:
|
1555 |
+
# NORMAL CASE: replace with an end_track, leaving DTime
|
1556 |
+
last[0] = 'end_track'
|
1557 |
+
else:
|
1558 |
+
# last event was neither 0-length text_event nor end_track
|
1559 |
+
events.append(['end_track', 0])
|
1560 |
+
else: # an eventless track!
|
1561 |
+
events = [['end_track', 0],]
|
1562 |
+
|
1563 |
+
# maybe_running_status = not no_running_status # unused? 4.7
|
1564 |
+
last_status = -1
|
1565 |
+
|
1566 |
+
for event_r in (events):
|
1567 |
+
E = copy.deepcopy(event_r)
|
1568 |
+
# otherwise the shifting'd corrupt the original
|
1569 |
+
if not E:
|
1570 |
+
continue
|
1571 |
+
|
1572 |
+
event = E.pop(0)
|
1573 |
+
if not len(event):
|
1574 |
+
continue
|
1575 |
+
|
1576 |
+
dtime = int(E.pop(0))
|
1577 |
+
# print('event='+str(event)+' dtime='+str(dtime))
|
1578 |
+
|
1579 |
+
event_data = ''
|
1580 |
+
|
1581 |
+
if ( # MIDI events -- eligible for running status
|
1582 |
+
event == 'note_on'
|
1583 |
+
or event == 'note_off'
|
1584 |
+
or event == 'control_change'
|
1585 |
+
or event == 'key_after_touch'
|
1586 |
+
or event == 'patch_change'
|
1587 |
+
or event == 'channel_after_touch'
|
1588 |
+
or event == 'pitch_wheel_change' ):
|
1589 |
+
|
1590 |
+
# This block is where we spend most of the time. Gotta be tight.
|
1591 |
+
if (event == 'note_off'):
|
1592 |
+
status = 0x80 | (int(E[0]) & 0x0F)
|
1593 |
+
parameters = struct.pack('>BB', int(E[1])&0x7F, int(E[2])&0x7F)
|
1594 |
+
elif (event == 'note_on'):
|
1595 |
+
status = 0x90 | (int(E[0]) & 0x0F)
|
1596 |
+
parameters = struct.pack('>BB', int(E[1])&0x7F, int(E[2])&0x7F)
|
1597 |
+
elif (event == 'key_after_touch'):
|
1598 |
+
status = 0xA0 | (int(E[0]) & 0x0F)
|
1599 |
+
parameters = struct.pack('>BB', int(E[1])&0x7F, int(E[2])&0x7F)
|
1600 |
+
elif (event == 'control_change'):
|
1601 |
+
status = 0xB0 | (int(E[0]) & 0x0F)
|
1602 |
+
parameters = struct.pack('>BB', int(E[1])&0xFF, int(E[2])&0xFF)
|
1603 |
+
elif (event == 'patch_change'):
|
1604 |
+
status = 0xC0 | (int(E[0]) & 0x0F)
|
1605 |
+
parameters = struct.pack('>B', int(E[1]) & 0xFF)
|
1606 |
+
elif (event == 'channel_after_touch'):
|
1607 |
+
status = 0xD0 | (int(E[0]) & 0x0F)
|
1608 |
+
parameters = struct.pack('>B', int(E[1]) & 0xFF)
|
1609 |
+
elif (event == 'pitch_wheel_change'):
|
1610 |
+
status = 0xE0 | (int(E[0]) & 0x0F)
|
1611 |
+
parameters = _write_14_bit(int(E[1]) + 0x2000)
|
1612 |
+
else:
|
1613 |
+
_warn("BADASS FREAKOUT ERROR 31415!")
|
1614 |
+
|
1615 |
+
# And now the encoding
|
1616 |
+
# w = BER compressed integer (not ASN.1 BER, see perlpacktut for
|
1617 |
+
# details). Its bytes represent an unsigned integer in base 128,
|
1618 |
+
# most significant digit first, with as few digits as possible.
|
1619 |
+
# Bit eight (the high bit) is set on each byte except the last.
|
1620 |
+
|
1621 |
+
data.append(_ber_compressed_int(dtime))
|
1622 |
+
if (status != last_status) or no_running_status:
|
1623 |
+
data.append(struct.pack('>B', status))
|
1624 |
+
data.append(parameters)
|
1625 |
+
|
1626 |
+
last_status = status
|
1627 |
+
continue
|
1628 |
+
else:
|
1629 |
+
# Not a MIDI event.
|
1630 |
+
# All the code in this block could be more efficient,
|
1631 |
+
# but this is not where the code needs to be tight.
|
1632 |
+
# print "zaz $event\n";
|
1633 |
+
last_status = -1
|
1634 |
+
|
1635 |
+
if event == 'raw_meta_event':
|
1636 |
+
event_data = _some_text_event(int(E[0]), E[1])
|
1637 |
+
elif (event == 'set_sequence_number'): # 3.9
|
1638 |
+
event_data = b'\xFF\x00\x02'+_int2twobytes(E[0])
|
1639 |
+
|
1640 |
+
# Text meta-events...
|
1641 |
+
# a case for a dict, I think (pjb) ...
|
1642 |
+
elif (event == 'text_event'):
|
1643 |
+
event_data = _some_text_event(0x01, E[0])
|
1644 |
+
elif (event == 'copyright_text_event'):
|
1645 |
+
event_data = _some_text_event(0x02, E[0])
|
1646 |
+
elif (event == 'track_name'):
|
1647 |
+
event_data = _some_text_event(0x03, E[0])
|
1648 |
+
elif (event == 'instrument_name'):
|
1649 |
+
event_data = _some_text_event(0x04, E[0])
|
1650 |
+
elif (event == 'lyric'):
|
1651 |
+
event_data = _some_text_event(0x05, E[0])
|
1652 |
+
elif (event == 'marker'):
|
1653 |
+
event_data = _some_text_event(0x06, E[0])
|
1654 |
+
elif (event == 'cue_point'):
|
1655 |
+
event_data = _some_text_event(0x07, E[0])
|
1656 |
+
elif (event == 'text_event_08'):
|
1657 |
+
event_data = _some_text_event(0x08, E[0])
|
1658 |
+
elif (event == 'text_event_09'):
|
1659 |
+
event_data = _some_text_event(0x09, E[0])
|
1660 |
+
elif (event == 'text_event_0a'):
|
1661 |
+
event_data = _some_text_event(0x0A, E[0])
|
1662 |
+
elif (event == 'text_event_0b'):
|
1663 |
+
event_data = _some_text_event(0x0B, E[0])
|
1664 |
+
elif (event == 'text_event_0c'):
|
1665 |
+
event_data = _some_text_event(0x0C, E[0])
|
1666 |
+
elif (event == 'text_event_0d'):
|
1667 |
+
event_data = _some_text_event(0x0D, E[0])
|
1668 |
+
elif (event == 'text_event_0e'):
|
1669 |
+
event_data = _some_text_event(0x0E, E[0])
|
1670 |
+
elif (event == 'text_event_0f'):
|
1671 |
+
event_data = _some_text_event(0x0F, E[0])
|
1672 |
+
# End of text meta-events
|
1673 |
+
|
1674 |
+
elif (event == 'end_track'):
|
1675 |
+
event_data = b"\xFF\x2F\x00"
|
1676 |
+
|
1677 |
+
elif (event == 'set_tempo'):
|
1678 |
+
#event_data = struct.pack(">BBwa*", 0xFF, 0x51, 3,
|
1679 |
+
# substr( struct.pack('>I', E[0]), 1, 3))
|
1680 |
+
event_data = b'\xFF\x51\x03'+struct.pack('>I',E[0])[1:]
|
1681 |
+
elif (event == 'smpte_offset'):
|
1682 |
+
# event_data = struct.pack(">BBwBBBBB", 0xFF, 0x54, 5, E[0:5] )
|
1683 |
+
event_data = struct.pack(">BBBbBBBB", 0xFF,0x54,0x05,E[0],E[1],E[2],E[3],E[4])
|
1684 |
+
elif (event == 'time_signature'):
|
1685 |
+
# event_data = struct.pack(">BBwBBBB", 0xFF, 0x58, 4, E[0:4] )
|
1686 |
+
event_data = struct.pack(">BBBbBBB", 0xFF, 0x58, 0x04, E[0],E[1],E[2],E[3])
|
1687 |
+
elif (event == 'key_signature'):
|
1688 |
+
event_data = struct.pack(">BBBbB", 0xFF, 0x59, 0x02, E[0],E[1])
|
1689 |
+
elif (event == 'sequencer_specific'):
|
1690 |
+
# event_data = struct.pack(">BBwa*", 0xFF,0x7F, len(E[0]), E[0])
|
1691 |
+
event_data = _some_text_event(0x7F, E[0])
|
1692 |
+
# End of Meta-events
|
1693 |
+
|
1694 |
+
# Other Things...
|
1695 |
+
elif (event == 'sysex_f0'):
|
1696 |
+
#event_data = struct.pack(">Bwa*", 0xF0, len(E[0]), E[0])
|
1697 |
+
#B=bitstring w=BER-compressed-integer a=null-padded-ascii-str
|
1698 |
+
event_data = bytearray(b'\xF0')+_ber_compressed_int(len(E[0]))+bytearray(E[0])
|
1699 |
+
elif (event == 'sysex_f7'):
|
1700 |
+
#event_data = struct.pack(">Bwa*", 0xF7, len(E[0]), E[0])
|
1701 |
+
event_data = bytearray(b'\xF7')+_ber_compressed_int(len(E[0]))+bytearray(E[0])
|
1702 |
+
|
1703 |
+
elif (event == 'song_position'):
|
1704 |
+
event_data = b"\xF2" + _write_14_bit( E[0] )
|
1705 |
+
elif (event == 'song_select'):
|
1706 |
+
event_data = struct.pack('>BB', 0xF3, E[0] )
|
1707 |
+
elif (event == 'tune_request'):
|
1708 |
+
event_data = b"\xF6"
|
1709 |
+
elif (event == 'raw_data'):
|
1710 |
+
_warn("_encode: raw_data event not supported")
|
1711 |
+
# event_data = E[0]
|
1712 |
+
continue
|
1713 |
+
# End of Other Stuff
|
1714 |
+
|
1715 |
+
else:
|
1716 |
+
# The Big Fallthru
|
1717 |
+
if unknown_callback:
|
1718 |
+
# push(@data, &{ $unknown_callback }( @$event_r ))
|
1719 |
+
pass
|
1720 |
+
else:
|
1721 |
+
_warn("Unknown event: "+str(event))
|
1722 |
+
# To surpress complaint here, just set
|
1723 |
+
# 'unknown_callback' => sub { return () }
|
1724 |
+
continue
|
1725 |
+
|
1726 |
+
#print "Event $event encoded part 2\n"
|
1727 |
+
if str(type(event_data)).find("'str'") >= 0:
|
1728 |
+
event_data = bytearray(event_data.encode('Latin1', 'ignore'))
|
1729 |
+
if len(event_data): # how could $event_data be empty
|
1730 |
+
# data.append(struct.pack('>wa*', dtime, event_data))
|
1731 |
+
# print(' event_data='+str(event_data))
|
1732 |
+
data.append(_ber_compressed_int(dtime)+event_data)
|
1733 |
+
|
1734 |
+
return b''.join(data)
|
1735 |
+
|
README.md
CHANGED
@@ -1,13 +1,13 @@
|
|
1 |
-
---
|
2 |
-
title: Midi
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
-
sdk:
|
7 |
-
sdk_version: 4.
|
8 |
-
app_file: app.py
|
9 |
-
pinned:
|
10 |
-
license:
|
11 |
-
---
|
12 |
-
|
13 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
1 |
+
---
|
2 |
+
title: Midi Music Generator
|
3 |
+
emoji: 🎼🎶
|
4 |
+
colorFrom: red
|
5 |
+
colorTo: indigo
|
6 |
+
sdk: docker
|
7 |
+
sdk_version: 4.36.1
|
8 |
+
app_file: app.py
|
9 |
+
pinned: true
|
10 |
+
license: apache-2.0
|
11 |
+
---
|
12 |
+
|
13 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app.py
ADDED
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import argparse
|
2 |
+
import glob
|
3 |
+
import os.path
|
4 |
+
import uuid
|
5 |
+
|
6 |
+
import gradio as gr
|
7 |
+
import numpy as np
|
8 |
+
import onnxruntime as rt
|
9 |
+
import tqdm
|
10 |
+
import json
|
11 |
+
from huggingface_hub import hf_hub_download
|
12 |
+
|
13 |
+
import MIDI
|
14 |
+
from midi_synthesizer import synthesis
|
15 |
+
from midi_tokenizer import MIDITokenizer
|
16 |
+
|
17 |
+
in_space = os.getenv("SYSTEM") == "spaces"
|
18 |
+
|
19 |
+
|
20 |
+
def softmax(x, axis):
|
21 |
+
x_max = np.amax(x, axis=axis, keepdims=True)
|
22 |
+
exp_x_shifted = np.exp(x - x_max)
|
23 |
+
return exp_x_shifted / np.sum(exp_x_shifted, axis=axis, keepdims=True)
|
24 |
+
|
25 |
+
|
26 |
+
def sample_top_p_k(probs, p, k):
|
27 |
+
probs_idx = np.argsort(-probs, axis=-1)
|
28 |
+
probs_sort = np.take_along_axis(probs, probs_idx, -1)
|
29 |
+
probs_sum = np.cumsum(probs_sort, axis=-1)
|
30 |
+
mask = probs_sum - probs_sort > p
|
31 |
+
probs_sort[mask] = 0.0
|
32 |
+
mask = np.zeros(probs_sort.shape[-1])
|
33 |
+
mask[:k] = 1
|
34 |
+
probs_sort = probs_sort * mask
|
35 |
+
probs_sort /= np.sum(probs_sort, axis=-1, keepdims=True)
|
36 |
+
shape = probs_sort.shape
|
37 |
+
probs_sort_flat = probs_sort.reshape(-1, shape[-1])
|
38 |
+
probs_idx_flat = probs_idx.reshape(-1, shape[-1])
|
39 |
+
next_token = np.stack([np.random.choice(idxs, p=pvals) for pvals, idxs in zip(probs_sort_flat, probs_idx_flat)])
|
40 |
+
next_token = next_token.reshape(*shape[:-1])
|
41 |
+
return next_token
|
42 |
+
|
43 |
+
|
44 |
+
def generate(model, prompt=None, max_len=512, temp=1.0, top_p=0.98, top_k=20,
|
45 |
+
disable_patch_change=False, disable_control_change=False, disable_channels=None):
|
46 |
+
if disable_channels is not None:
|
47 |
+
disable_channels = [tokenizer.parameter_ids["channel"][c] for c in disable_channels]
|
48 |
+
else:
|
49 |
+
disable_channels = []
|
50 |
+
max_token_seq = tokenizer.max_token_seq
|
51 |
+
if prompt is None:
|
52 |
+
input_tensor = np.full((1, max_token_seq), tokenizer.pad_id, dtype=np.int64)
|
53 |
+
input_tensor[0, 0] = tokenizer.bos_id # bos
|
54 |
+
else:
|
55 |
+
prompt = prompt[:, :max_token_seq]
|
56 |
+
if prompt.shape[-1] < max_token_seq:
|
57 |
+
prompt = np.pad(prompt, ((0, 0), (0, max_token_seq - prompt.shape[-1])),
|
58 |
+
mode="constant", constant_values=tokenizer.pad_id)
|
59 |
+
input_tensor = prompt
|
60 |
+
input_tensor = input_tensor[None, :, :]
|
61 |
+
cur_len = input_tensor.shape[1]
|
62 |
+
bar = tqdm.tqdm(desc="generating", total=max_len - cur_len, disable=in_space)
|
63 |
+
with bar:
|
64 |
+
while cur_len < max_len:
|
65 |
+
end = False
|
66 |
+
hidden = model[0].run(None, {'x': input_tensor})[0][:, -1]
|
67 |
+
next_token_seq = np.empty((1, 0), dtype=np.int64)
|
68 |
+
event_name = ""
|
69 |
+
for i in range(max_token_seq):
|
70 |
+
mask = np.zeros(tokenizer.vocab_size, dtype=np.int64)
|
71 |
+
if i == 0:
|
72 |
+
mask_ids = list(tokenizer.event_ids.values()) + [tokenizer.eos_id]
|
73 |
+
if disable_patch_change:
|
74 |
+
mask_ids.remove(tokenizer.event_ids["patch_change"])
|
75 |
+
if disable_control_change:
|
76 |
+
mask_ids.remove(tokenizer.event_ids["control_change"])
|
77 |
+
mask[mask_ids] = 1
|
78 |
+
else:
|
79 |
+
param_name = tokenizer.events[event_name][i - 1]
|
80 |
+
mask_ids = tokenizer.parameter_ids[param_name]
|
81 |
+
if param_name == "channel":
|
82 |
+
mask_ids = [i for i in mask_ids if i not in disable_channels]
|
83 |
+
mask[mask_ids] = 1
|
84 |
+
logits = model[1].run(None, {'x': next_token_seq, "hidden": hidden})[0][:, -1:]
|
85 |
+
scores = softmax(logits / temp, -1) * mask
|
86 |
+
sample = sample_top_p_k(scores, top_p, top_k)
|
87 |
+
if i == 0:
|
88 |
+
next_token_seq = sample
|
89 |
+
eid = sample.item()
|
90 |
+
if eid == tokenizer.eos_id:
|
91 |
+
end = True
|
92 |
+
break
|
93 |
+
event_name = tokenizer.id_events[eid]
|
94 |
+
else:
|
95 |
+
next_token_seq = np.concatenate([next_token_seq, sample], axis=1)
|
96 |
+
if len(tokenizer.events[event_name]) == i:
|
97 |
+
break
|
98 |
+
if next_token_seq.shape[1] < max_token_seq:
|
99 |
+
next_token_seq = np.pad(next_token_seq, ((0, 0), (0, max_token_seq - next_token_seq.shape[-1])),
|
100 |
+
mode="constant", constant_values=tokenizer.pad_id)
|
101 |
+
next_token_seq = next_token_seq[None, :, :]
|
102 |
+
input_tensor = np.concatenate([input_tensor, next_token_seq], axis=1)
|
103 |
+
cur_len += 1
|
104 |
+
bar.update(1)
|
105 |
+
yield next_token_seq.reshape(-1)
|
106 |
+
if end:
|
107 |
+
break
|
108 |
+
|
109 |
+
|
110 |
+
def create_msg(name, data):
|
111 |
+
return {"name": name, "data": data, "uuid": uuid.uuid4().hex}
|
112 |
+
|
113 |
+
|
114 |
+
def send_msgs(msgs, msgs_history):
|
115 |
+
msgs_history.append(msgs)
|
116 |
+
if len(msgs_history) > 50:
|
117 |
+
msgs_history.pop(0)
|
118 |
+
return json.dumps(msgs_history)
|
119 |
+
|
120 |
+
|
121 |
+
def run(model_name, tab, instruments, drum_kit, mid, midi_events, gen_events, temp, top_p, top_k, allow_cc):
|
122 |
+
msgs_history = []
|
123 |
+
mid_seq = []
|
124 |
+
gen_events = int(gen_events)
|
125 |
+
max_len = gen_events
|
126 |
+
|
127 |
+
disable_patch_change = False
|
128 |
+
disable_channels = None
|
129 |
+
if tab == 0:
|
130 |
+
i = 0
|
131 |
+
mid = [[tokenizer.bos_id] + [tokenizer.pad_id] * (tokenizer.max_token_seq - 1)]
|
132 |
+
patches = {}
|
133 |
+
if instruments is None:
|
134 |
+
instruments = []
|
135 |
+
for instr in instruments:
|
136 |
+
patches[i] = patch2number[instr]
|
137 |
+
i = (i + 1) if i != 8 else 10
|
138 |
+
if drum_kit != "None":
|
139 |
+
patches[9] = drum_kits2number[drum_kit]
|
140 |
+
for i, (c, p) in enumerate(patches.items()):
|
141 |
+
mid.append(tokenizer.event2tokens(["patch_change", 0, 0, i, c, p]))
|
142 |
+
mid_seq = mid
|
143 |
+
mid = np.asarray(mid, dtype=np.int64)
|
144 |
+
if len(instruments) > 0:
|
145 |
+
disable_patch_change = True
|
146 |
+
disable_channels = [i for i in range(16) if i not in patches]
|
147 |
+
elif mid is not None:
|
148 |
+
mid = tokenizer.tokenize(MIDI.midi2score(mid))
|
149 |
+
mid = np.asarray(mid, dtype=np.int64)
|
150 |
+
mid = mid[:int(midi_events)]
|
151 |
+
max_len += len(mid)
|
152 |
+
for token_seq in mid:
|
153 |
+
mid_seq.append(token_seq.tolist())
|
154 |
+
init_msgs = [create_msg("visualizer_clear", None)]
|
155 |
+
for tokens in mid_seq:
|
156 |
+
init_msgs.append(create_msg("visualizer_append", tokenizer.tokens2event(tokens)))
|
157 |
+
yield mid_seq, None, None, send_msgs(init_msgs, msgs_history), msgs_history
|
158 |
+
model = models[model_name]
|
159 |
+
generator = generate(model, mid, max_len=max_len, temp=temp, top_p=top_p, top_k=top_k,
|
160 |
+
disable_patch_change=disable_patch_change, disable_control_change=not allow_cc,
|
161 |
+
disable_channels=disable_channels)
|
162 |
+
for i, token_seq in enumerate(generator):
|
163 |
+
token_seq = token_seq.tolist()
|
164 |
+
mid_seq.append(token_seq)
|
165 |
+
event = tokenizer.tokens2event(token_seq)
|
166 |
+
yield mid_seq, None, None, send_msgs([create_msg("visualizer_append", event), create_msg("progress", [i + 1, gen_events])], msgs_history), msgs_history
|
167 |
+
mid = tokenizer.detokenize(mid_seq)
|
168 |
+
with open(f"output.mid", 'wb') as f:
|
169 |
+
f.write(MIDI.score2midi(mid))
|
170 |
+
audio = synthesis(MIDI.score2opus(mid), soundfont_path)
|
171 |
+
yield mid_seq, "output.mid", (44100, audio), send_msgs([create_msg("visualizer_end", None)], msgs_history), msgs_history
|
172 |
+
|
173 |
+
|
174 |
+
def cancel_run(mid_seq, msgs_history):
|
175 |
+
if mid_seq is None:
|
176 |
+
return None, None, []
|
177 |
+
mid = tokenizer.detokenize(mid_seq)
|
178 |
+
with open(f"output.mid", 'wb') as f:
|
179 |
+
f.write(MIDI.score2midi(mid))
|
180 |
+
audio = synthesis(MIDI.score2opus(mid), soundfont_path)
|
181 |
+
return "output.mid", (44100, audio), send_msgs([create_msg("visualizer_end", None)], msgs_history)
|
182 |
+
|
183 |
+
|
184 |
+
def load_javascript(dir="javascript"):
|
185 |
+
scripts_list = glob.glob(f"{dir}/*.js")
|
186 |
+
javascript = ""
|
187 |
+
for path in scripts_list:
|
188 |
+
with open(path, "r", encoding="utf8") as jsfile:
|
189 |
+
javascript += f"\n<!-- {path} --><script>{jsfile.read()}</script>"
|
190 |
+
template_response_ori = gr.routes.templates.TemplateResponse
|
191 |
+
|
192 |
+
def template_response(*args, **kwargs):
|
193 |
+
res = template_response_ori(*args, **kwargs)
|
194 |
+
res.body = res.body.replace(
|
195 |
+
b'</head>', f'{javascript}</head>'.encode("utf8"))
|
196 |
+
res.init_headers()
|
197 |
+
return res
|
198 |
+
|
199 |
+
gr.routes.templates.TemplateResponse = template_response
|
200 |
+
|
201 |
+
|
202 |
+
number2drum_kits = {-1: "None", 0: "Standard", 8: "Room", 16: "Power", 24: "Electric", 25: "TR-808", 32: "Jazz",
|
203 |
+
40: "Blush", 48: "Orchestra"}
|
204 |
+
patch2number = {v: k for k, v in MIDI.Number2patch.items()}
|
205 |
+
drum_kits2number = {v: k for k, v in number2drum_kits.items()}
|
206 |
+
|
207 |
+
if __name__ == "__main__":
|
208 |
+
parser = argparse.ArgumentParser()
|
209 |
+
parser.add_argument("--share", action="store_true", default=False, help="share gradio app")
|
210 |
+
parser.add_argument("--port", type=int, default=7860, help="gradio server port")
|
211 |
+
parser.add_argument("--max-gen", type=int, default=1024, help="max")
|
212 |
+
opt = parser.parse_args()
|
213 |
+
soundfont_path = hf_hub_download(repo_id="skytnt/midi-model", filename="soundfont.sf2")
|
214 |
+
models_info = {"generic pretrain model": ["skytnt/midi-model", ""],
|
215 |
+
"j-pop finetune model": ["skytnt/midi-model-ft", "jpop/"],
|
216 |
+
"touhou finetune model": ["skytnt/midi-model-ft", "touhou/"],
|
217 |
+
}
|
218 |
+
models = {}
|
219 |
+
tokenizer = MIDITokenizer()
|
220 |
+
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
|
221 |
+
for name, (repo_id, path) in models_info.items():
|
222 |
+
model_base_path = hf_hub_download(repo_id=repo_id, filename=f"{path}onnx/model_base.onnx")
|
223 |
+
model_token_path = hf_hub_download(repo_id=repo_id, filename=f"{path}onnx/model_token.onnx")
|
224 |
+
model_base = rt.InferenceSession(model_base_path, providers=providers)
|
225 |
+
model_token = rt.InferenceSession(model_token_path, providers=providers)
|
226 |
+
models[name] = [model_base, model_token]
|
227 |
+
|
228 |
+
load_javascript()
|
229 |
+
app = gr.Blocks()
|
230 |
+
with app:
|
231 |
+
gr.Markdown("<h1 style='text-align: center; margin-bottom: 1rem'>Midi Composer</h1>")
|
232 |
+
gr.Markdown("![Visitors](https://api.visitorbadge.io/api/visitors?path=skytnt.midi-composer&style=flat)\n\n"
|
233 |
+
"Midi event transformer for music generation\n\n"
|
234 |
+
"Demo for [SkyTNT/midi-model](https://github.com/SkyTNT/midi-model)\n\n"
|
235 |
+
"[Open In Colab]"
|
236 |
+
"(https://colab.research.google.com/github/SkyTNT/midi-model/blob/main/demo.ipynb)"
|
237 |
+
" for faster running and longer generation"
|
238 |
+
)
|
239 |
+
js_msg_history_state = gr.State(value=[])
|
240 |
+
js_msg = gr.Textbox(elem_id="msg_receiver", visible=False)
|
241 |
+
js_msg.change(None, [js_msg], [], js="""
|
242 |
+
(msg_json) =>{
|
243 |
+
let msgs = JSON.parse(msg_json);
|
244 |
+
executeCallbacks(msgReceiveCallbacks, msgs);
|
245 |
+
return [];
|
246 |
+
}
|
247 |
+
""")
|
248 |
+
input_model = gr.Dropdown(label="select model", choices=list(models.keys()),
|
249 |
+
type="value", value=list(models.keys())[0])
|
250 |
+
tab_select = gr.State(value=0)
|
251 |
+
with gr.Tabs():
|
252 |
+
with gr.TabItem("instrument prompt") as tab1:
|
253 |
+
input_instruments = gr.Dropdown(label="instruments (auto if empty)", choices=list(patch2number.keys()),
|
254 |
+
multiselect=True, max_choices=15, type="value")
|
255 |
+
input_drum_kit = gr.Dropdown(label="drum kit", choices=list(drum_kits2number.keys()), type="value",
|
256 |
+
value="None")
|
257 |
+
example1 = gr.Examples([
|
258 |
+
[[], "None"],
|
259 |
+
[["Acoustic Grand"], "None"],
|
260 |
+
[["Acoustic Grand", "Violin", "Viola", "Cello", "Contrabass"], "Orchestra"],
|
261 |
+
[["Flute", "Cello", "Bassoon", "Tuba"], "None"],
|
262 |
+
[["Violin", "Viola", "Cello", "Contrabass", "Trumpet", "French Horn", "Brass Section",
|
263 |
+
"Flute", "Piccolo", "Tuba", "Trombone", "Timpani"], "Orchestra"],
|
264 |
+
[["Acoustic Guitar(nylon)", "Acoustic Guitar(steel)", "Electric Guitar(jazz)",
|
265 |
+
"Electric Guitar(clean)", "Electric Guitar(muted)", "Overdriven Guitar", "Distortion Guitar",
|
266 |
+
"Electric Bass(finger)"], "Standard"]
|
267 |
+
], [input_instruments, input_drum_kit])
|
268 |
+
with gr.TabItem("midi prompt") as tab2:
|
269 |
+
input_midi = gr.File(label="input midi", file_types=[".midi", ".mid"], type="binary")
|
270 |
+
input_midi_events = gr.Slider(label="use first n midi events as prompt", minimum=1, maximum=512,
|
271 |
+
step=1,
|
272 |
+
value=128)
|
273 |
+
example2 = gr.Examples([[file, 128] for file in glob.glob("example/*.mid")],
|
274 |
+
[input_midi, input_midi_events])
|
275 |
+
|
276 |
+
tab1.select(lambda: 0, None, tab_select, queue=False)
|
277 |
+
tab2.select(lambda: 1, None, tab_select, queue=False)
|
278 |
+
input_gen_events = gr.Slider(label="generate n midi events", minimum=1, maximum=opt.max_gen,
|
279 |
+
step=1, value=opt.max_gen // 2)
|
280 |
+
with gr.Accordion("options", open=False):
|
281 |
+
input_temp = gr.Slider(label="temperature", minimum=0.1, maximum=1.2, step=0.01, value=1)
|
282 |
+
input_top_p = gr.Slider(label="top p", minimum=0.1, maximum=1, step=0.01, value=0.98)
|
283 |
+
input_top_k = gr.Slider(label="top k", minimum=1, maximum=20, step=1, value=12)
|
284 |
+
input_allow_cc = gr.Checkbox(label="allow midi cc event", value=True)
|
285 |
+
example3 = gr.Examples([[1, 0.98, 12], [1.2, 0.95, 8]], [input_temp, input_top_p, input_top_k])
|
286 |
+
run_btn = gr.Button("generate", variant="primary")
|
287 |
+
stop_btn = gr.Button("stop and output")
|
288 |
+
output_midi_seq = gr.State()
|
289 |
+
output_midi_visualizer = gr.HTML(elem_id="midi_visualizer_container")
|
290 |
+
output_audio = gr.Audio(label="output audio", format="mp3", elem_id="midi_audio")
|
291 |
+
output_midi = gr.File(label="output midi", file_types=[".mid"])
|
292 |
+
run_event = run_btn.click(run, [input_model, tab_select, input_instruments, input_drum_kit, input_midi,
|
293 |
+
input_midi_events, input_gen_events, input_temp, input_top_p, input_top_k,
|
294 |
+
input_allow_cc],
|
295 |
+
[output_midi_seq, output_midi, output_audio, js_msg, js_msg_history_state],
|
296 |
+
concurrency_limit=3)
|
297 |
+
stop_btn.click(cancel_run, [output_midi_seq, js_msg_history_state], [output_midi, output_audio, js_msg], cancels=run_event, queue=False)
|
298 |
+
app.launch(server_port=opt.port, share=opt.share, inbrowser=True)
|
example/Bach--Fugue-in-D-Minor.mid
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:1398121eb86a33e73f90ec84be71dac6abc0ddf11372ea7cdd9e01586938a56b
|
3 |
+
size 7720
|
example/Beethoven--Symphony-No5-in-C-Minor-Fate-Opus-67.mid
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:28ff6fdcd644e781d36411bf40ab7a1f4849adddbcd1040eaec22751c5ca99d2
|
3 |
+
size 87090
|
example/Chopin--Nocturne No. 9 in B Major, Opus 32 No.1, Andante Sostenuto.mid
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:3a236e647ad9f5d0af680d3ca19d3b60f334c4bde6b4f86310f63405245c476e
|
3 |
+
size 13484
|
example/Mozart--Requiem, No.1..mid
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:aa49bf4633401e16777fe47f6f53a494c2166f5101af6dafc60114932a59b9bd
|
3 |
+
size 14695
|
example/castle_in_the_sky.mid
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:fa14aec6f1be15c4fddd0decc6d9152204f160d4e07e05d8d1dc9f209c309ff7
|
3 |
+
size 7957
|
example/eva-残酷な天使のテーゼ.mid
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:e513487543d7e27ec5dc30f027302d2a3b5a3aaf9af554def1e5cd6a7a8d355a
|
3 |
+
size 17671
|
javascript/app.js
ADDED
@@ -0,0 +1,449 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* 自动绕过 shadowRoot 的 querySelector
|
3 |
+
* @param {string} selector - 要查询的 CSS 选择器
|
4 |
+
* @returns {Element|null} - 匹配的元素或 null 如果未找到
|
5 |
+
*/
|
6 |
+
function deepQuerySelector(selector) {
|
7 |
+
/**
|
8 |
+
* 在指定的根元素或文档对象下深度查询元素
|
9 |
+
* @param {Element|Document} root - 要开始搜索的根元素或文档对象
|
10 |
+
* @param {string} selector - 要查询的 CSS 选择器
|
11 |
+
* @returns {Element|null} - 匹配的元素或 null 如果未找到
|
12 |
+
*/
|
13 |
+
function deepSearch(root, selector) {
|
14 |
+
// 在当前根元素下查找
|
15 |
+
let element = root.querySelector(selector);
|
16 |
+
if (element) {
|
17 |
+
return element;
|
18 |
+
}
|
19 |
+
|
20 |
+
// 如果未找到,递归检查 shadow DOM
|
21 |
+
const shadowHosts = root.querySelectorAll('*');
|
22 |
+
|
23 |
+
for (let i = 0; i < shadowHosts.length; i++) {
|
24 |
+
const host = shadowHosts[i];
|
25 |
+
|
26 |
+
// 检查当前元素是否有 shadowRoot
|
27 |
+
if (host.shadowRoot) {
|
28 |
+
element = deepSearch(host.shadowRoot, selector);
|
29 |
+
if (element) {
|
30 |
+
return element;
|
31 |
+
}
|
32 |
+
}
|
33 |
+
}
|
34 |
+
// 未找到元素
|
35 |
+
return null;
|
36 |
+
}
|
37 |
+
|
38 |
+
return deepSearch(this, selector);
|
39 |
+
}
|
40 |
+
|
41 |
+
Element.prototype.deepQuerySelector = deepQuerySelector;
|
42 |
+
Document.prototype.deepQuerySelector = deepQuerySelector;
|
43 |
+
|
44 |
+
function gradioApp() {
|
45 |
+
const elems = document.getElementsByTagName('gradio-app')
|
46 |
+
const gradioShadowRoot = elems.length == 0 ? null : elems[0].shadowRoot
|
47 |
+
return !!gradioShadowRoot ? gradioShadowRoot : document;
|
48 |
+
}
|
49 |
+
|
50 |
+
uiUpdateCallbacks = []
|
51 |
+
msgReceiveCallbacks = []
|
52 |
+
|
53 |
+
function onUiUpdate(callback){
|
54 |
+
uiUpdateCallbacks.push(callback)
|
55 |
+
}
|
56 |
+
|
57 |
+
function onMsgReceive(callback){
|
58 |
+
msgReceiveCallbacks.push(callback)
|
59 |
+
}
|
60 |
+
|
61 |
+
function runCallback(x, m){
|
62 |
+
try {
|
63 |
+
x(m)
|
64 |
+
} catch (e) {
|
65 |
+
(console.error || console.log).call(console, e.message, e);
|
66 |
+
}
|
67 |
+
}
|
68 |
+
function executeCallbacks(queue, m) {
|
69 |
+
queue.forEach(function(x){runCallback(x, m)})
|
70 |
+
}
|
71 |
+
|
72 |
+
document.addEventListener("DOMContentLoaded", function() {
|
73 |
+
var mutationObserver = new MutationObserver(function(m){
|
74 |
+
executeCallbacks(uiUpdateCallbacks, m);
|
75 |
+
});
|
76 |
+
mutationObserver.observe( gradioApp(), { childList:true, subtree:true })
|
77 |
+
});
|
78 |
+
|
79 |
+
function HSVtoRGB(h, s, v) {
|
80 |
+
let r, g, b, i, f, p, q, t;
|
81 |
+
i = Math.floor(h * 6);
|
82 |
+
f = h * 6 - i;
|
83 |
+
p = v * (1 - s);
|
84 |
+
q = v * (1 - f * s);
|
85 |
+
t = v * (1 - (1 - f) * s);
|
86 |
+
switch (i % 6) {
|
87 |
+
case 0: r = v; g = t; b = p; break;
|
88 |
+
case 1: r = q; g = v; b = p; break;
|
89 |
+
case 2: r = p; g = v; b = t; break;
|
90 |
+
case 3: r = p; g = q; b = v; break;
|
91 |
+
case 4: r = t; g = p; b = v; break;
|
92 |
+
case 5: r = v; g = p; b = q; break;
|
93 |
+
}
|
94 |
+
return {
|
95 |
+
r: Math.round(r * 255),
|
96 |
+
g: Math.round(g * 255),
|
97 |
+
b: Math.round(b * 255)
|
98 |
+
};
|
99 |
+
}
|
100 |
+
|
101 |
+
class MidiVisualizer extends HTMLElement{
|
102 |
+
constructor() {
|
103 |
+
super();
|
104 |
+
this.midiEvents = [];
|
105 |
+
this.activeNotes = [];
|
106 |
+
this.midiTimes = [];
|
107 |
+
this.wrapper = null;
|
108 |
+
this.svg = null;
|
109 |
+
this.timeLine = null;
|
110 |
+
this.config = {
|
111 |
+
noteHeight : 4,
|
112 |
+
beatWidth: 32
|
113 |
+
}
|
114 |
+
this.timePreBeat = 16
|
115 |
+
this.svgWidth = 0;
|
116 |
+
this.t1 = 0;
|
117 |
+
this.totalTimeMs = 0
|
118 |
+
this.playTime = 0
|
119 |
+
this.playTimeMs = 0
|
120 |
+
this.colorMap = new Map();
|
121 |
+
this.playing = false;
|
122 |
+
this.timer = null;
|
123 |
+
this.init();
|
124 |
+
}
|
125 |
+
|
126 |
+
init(){
|
127 |
+
this.innerHTML=''
|
128 |
+
const shadow = this.attachShadow({mode: 'open'});
|
129 |
+
const style = document.createElement("style");
|
130 |
+
const wrapper = document.createElement('div');
|
131 |
+
style.textContent = ".note.active {stroke: black;stroke-width: 0.75;stroke-opacity: 0.75;}";
|
132 |
+
wrapper.style.overflowX= "scroll"
|
133 |
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
134 |
+
svg.style.height = `${this.config.noteHeight*128}px`;
|
135 |
+
svg.style.width = `${this.svgWidth}px`;
|
136 |
+
const timeLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
137 |
+
timeLine.style.stroke = "green"
|
138 |
+
timeLine.style.strokeWidth = 2;
|
139 |
+
shadow.appendChild(style)
|
140 |
+
shadow.appendChild(wrapper);
|
141 |
+
wrapper.appendChild(svg);
|
142 |
+
svg.appendChild(timeLine)
|
143 |
+
this.wrapper = wrapper;
|
144 |
+
this.svg = svg;
|
145 |
+
this.timeLine= timeLine;
|
146 |
+
this.setPlayTime(0);
|
147 |
+
}
|
148 |
+
|
149 |
+
clearMidiEvents(){
|
150 |
+
this.pause()
|
151 |
+
this.midiEvents = [];
|
152 |
+
this.activeNotes = [];
|
153 |
+
this.midiTimes = [];
|
154 |
+
this.t1 = 0
|
155 |
+
this.colorMap.clear()
|
156 |
+
this.setPlayTime(0);
|
157 |
+
this.totalTimeMs = 0;
|
158 |
+
this.playTimeMs = 0
|
159 |
+
this.svgWidth = 0
|
160 |
+
this.svg.innerHTML = ''
|
161 |
+
this.svg.style.width = `${this.svgWidth}px`;
|
162 |
+
this.svg.appendChild(this.timeLine)
|
163 |
+
}
|
164 |
+
|
165 |
+
appendMidiEvent(midiEvent){
|
166 |
+
if(midiEvent instanceof Array && midiEvent.length > 0){
|
167 |
+
|
168 |
+
this.t1 += midiEvent[1]
|
169 |
+
let t = this.t1*this.timePreBeat + midiEvent[2]
|
170 |
+
midiEvent = [midiEvent[0], t].concat(midiEvent.slice(3))
|
171 |
+
if(midiEvent[0] === "note"){
|
172 |
+
let track = midiEvent[2]
|
173 |
+
let duration = midiEvent[3]
|
174 |
+
let channel = midiEvent[4]
|
175 |
+
let pitch = midiEvent[5]
|
176 |
+
let velocity = midiEvent[6]
|
177 |
+
let x = (t/this.timePreBeat)*this.config.beatWidth
|
178 |
+
let y = (127 - pitch)*this.config.noteHeight
|
179 |
+
let w = (duration/this.timePreBeat)*this.config.beatWidth
|
180 |
+
let h = this.config.noteHeight
|
181 |
+
this.svgWidth = Math.ceil(Math.max(x + w, this.svgWidth))
|
182 |
+
let color = this.getColor(track, channel)
|
183 |
+
let opacity = Math.min(1, velocity/127 + 0.1).toFixed(2)
|
184 |
+
let rect = this.drawNote(x,y,w,h, `rgba(${color.r}, ${color.g}, ${color.b}, ${opacity})`)
|
185 |
+
midiEvent.push(rect)
|
186 |
+
this.setPlayTime(t);
|
187 |
+
this.wrapper.scrollTo(this.svgWidth - this.wrapper.offsetWidth, 0)
|
188 |
+
}
|
189 |
+
this.midiEvents.push(midiEvent);
|
190 |
+
this.svg.style.width = `${this.svgWidth}px`;
|
191 |
+
}
|
192 |
+
|
193 |
+
}
|
194 |
+
|
195 |
+
getColor(track, channel){
|
196 |
+
let key = `${track},${channel}`;
|
197 |
+
let color = this.colorMap.get(key);
|
198 |
+
if(!!color){
|
199 |
+
return color;
|
200 |
+
}
|
201 |
+
color = HSVtoRGB(Math.random(),Math.random()*0.5 + 0.5,1);
|
202 |
+
this.colorMap.set(key, color);
|
203 |
+
return color;
|
204 |
+
}
|
205 |
+
|
206 |
+
drawNote(x, y, w, h, fill) {
|
207 |
+
if (!this.svg) {
|
208 |
+
return null;
|
209 |
+
}
|
210 |
+
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
211 |
+
rect.classList.add('note');
|
212 |
+
rect.setAttribute('fill', fill);
|
213 |
+
// Round values to the nearest integer to avoid partially filled pixels.
|
214 |
+
rect.setAttribute('x', `${Math.round(x)}`);
|
215 |
+
rect.setAttribute('y', `${Math.round(y)}`);
|
216 |
+
rect.setAttribute('width', `${Math.round(w)}`);
|
217 |
+
rect.setAttribute('height', `${Math.round(h)}`);
|
218 |
+
this.svg.appendChild(rect);
|
219 |
+
return rect
|
220 |
+
}
|
221 |
+
|
222 |
+
finishAppendMidiEvent(){
|
223 |
+
this.pause()
|
224 |
+
let midiEvents = this.midiEvents.sort((a, b)=>a[1]-b[1])
|
225 |
+
let tempo = (60 / 120) * 10 ** 3
|
226 |
+
let ms = 0
|
227 |
+
let lastT = 0
|
228 |
+
this.midiTimes.push({ms:ms, t: 0, tempo: tempo})
|
229 |
+
midiEvents.forEach((midiEvent)=>{
|
230 |
+
let t = midiEvent[1]
|
231 |
+
ms += ((t- lastT) / this.timePreBeat) * tempo
|
232 |
+
if(midiEvent[0]==="set_tempo"){
|
233 |
+
tempo = (60 / midiEvent[3]) * 10 ** 3
|
234 |
+
this.midiTimes.push({ms:ms, t: t, tempo: tempo})
|
235 |
+
}
|
236 |
+
if(midiEvent[0]==="note"){
|
237 |
+
this.totalTimeMs = Math.max(this.totalTimeMs, ms + (midiEvent[3]/ this.timePreBeat)*tempo)
|
238 |
+
}else{
|
239 |
+
this.totalTimeMs = Math.max(this.totalTimeMs, ms);
|
240 |
+
}
|
241 |
+
lastT = t;
|
242 |
+
})
|
243 |
+
}
|
244 |
+
|
245 |
+
setPlayTime(t){
|
246 |
+
this.playTime = t
|
247 |
+
let x = Math.round((t/this.timePreBeat)*this.config.beatWidth)
|
248 |
+
this.timeLine.setAttribute('x1', `${x}`);
|
249 |
+
this.timeLine.setAttribute('y1', '0');
|
250 |
+
this.timeLine.setAttribute('x2', `${x}`);
|
251 |
+
this.timeLine.setAttribute('y2', `${this.config.noteHeight*128}`);
|
252 |
+
|
253 |
+
this.wrapper.scrollTo(Math.max(0, x - this.wrapper.offsetWidth/2), 0)
|
254 |
+
|
255 |
+
if(this.playing){
|
256 |
+
let activeNotes = []
|
257 |
+
this.removeActiveNotes(this.activeNotes)
|
258 |
+
this.midiEvents.forEach((midiEvent)=>{
|
259 |
+
if(midiEvent[0] === "note"){
|
260 |
+
let time = midiEvent[1]
|
261 |
+
let duration = midiEvent[3]
|
262 |
+
let note = midiEvent[midiEvent.length - 1]
|
263 |
+
if(time <=this.playTime && time+duration>= this.playTime){
|
264 |
+
activeNotes.push(note)
|
265 |
+
}
|
266 |
+
}
|
267 |
+
})
|
268 |
+
this.addActiveNotes(activeNotes)
|
269 |
+
}
|
270 |
+
}
|
271 |
+
|
272 |
+
setPlayTimeMs(ms){
|
273 |
+
this.playTimeMs = ms
|
274 |
+
let playTime = 0
|
275 |
+
for(let i =0;i<this.midiTimes.length;i++){
|
276 |
+
let midiTime = this.midiTimes[i]
|
277 |
+
if(midiTime.ms>=ms){
|
278 |
+
break;
|
279 |
+
}
|
280 |
+
playTime = midiTime.t + (ms-midiTime.ms) * this.timePreBeat / midiTime.tempo
|
281 |
+
}
|
282 |
+
this.setPlayTime(playTime)
|
283 |
+
}
|
284 |
+
|
285 |
+
addActiveNotes(notes){
|
286 |
+
notes.forEach((note)=>{
|
287 |
+
this.activeNotes.push(note)
|
288 |
+
note.classList.add('active');
|
289 |
+
});
|
290 |
+
}
|
291 |
+
|
292 |
+
removeActiveNotes(notes){
|
293 |
+
notes.forEach((note)=>{
|
294 |
+
let idx = this.activeNotes.indexOf(note)
|
295 |
+
if(idx>-1)
|
296 |
+
this.activeNotes.splice(idx, 1);
|
297 |
+
note.classList.remove('active');
|
298 |
+
});
|
299 |
+
}
|
300 |
+
|
301 |
+
play(){
|
302 |
+
this.playing = true;
|
303 |
+
}
|
304 |
+
|
305 |
+
pause(){
|
306 |
+
this.removeActiveNotes(this.activeNotes)
|
307 |
+
this.playing = false;
|
308 |
+
}
|
309 |
+
|
310 |
+
|
311 |
+
bindAudioPlayer(audio){
|
312 |
+
this.pause()
|
313 |
+
audio.addEventListener("play", (event)=>{
|
314 |
+
this.play()
|
315 |
+
})
|
316 |
+
audio.addEventListener("pause", (event)=>{
|
317 |
+
this.pause()
|
318 |
+
})
|
319 |
+
audio.addEventListener("loadedmetadata", (event)=>{
|
320 |
+
//I don't know why the calculated totalTimeMs is different from audio.duration*10**3
|
321 |
+
this.totalTimeMs = audio.duration*10**3;
|
322 |
+
})
|
323 |
+
}
|
324 |
+
|
325 |
+
bindWaveformCursor(cursor){
|
326 |
+
let self = this;
|
327 |
+
const callback = function(mutationsList, observer) {
|
328 |
+
for(let mutation of mutationsList) {
|
329 |
+
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
|
330 |
+
let progress = parseFloat(mutation.target.style.left.slice(0,-1))*0.01;
|
331 |
+
if(!isNaN(progress)){
|
332 |
+
self.setPlayTimeMs(progress*self.totalTimeMs);
|
333 |
+
}
|
334 |
+
}
|
335 |
+
}
|
336 |
+
};
|
337 |
+
const observer = new MutationObserver(callback);
|
338 |
+
observer.observe(cursor, {
|
339 |
+
attributes: true,
|
340 |
+
attributeFilter: ['style']
|
341 |
+
});
|
342 |
+
}
|
343 |
+
}
|
344 |
+
|
345 |
+
customElements.define('midi-visualizer', MidiVisualizer);
|
346 |
+
|
347 |
+
(()=>{
|
348 |
+
let midi_visualizer_container_inited = null
|
349 |
+
let midi_audio_audio_inited = null;
|
350 |
+
let midi_audio_cursor_inited = null;
|
351 |
+
let midi_visualizer = document.createElement('midi-visualizer')
|
352 |
+
onUiUpdate((m)=>{
|
353 |
+
let app = gradioApp()
|
354 |
+
let midi_visualizer_container = app.querySelector("#midi_visualizer_container");
|
355 |
+
if(!!midi_visualizer_container && midi_visualizer_container_inited!== midi_visualizer_container){
|
356 |
+
midi_visualizer_container.appendChild(midi_visualizer)
|
357 |
+
midi_visualizer_container_inited = midi_visualizer_container;
|
358 |
+
}
|
359 |
+
let midi_audio = app.querySelector("#midi_audio");
|
360 |
+
if (!!midi_audio){
|
361 |
+
let midi_audio_cursor = midi_audio.deepQuerySelector(".cursor");
|
362 |
+
if(!!midi_audio_cursor && midi_audio_cursor_inited!==midi_audio_cursor){
|
363 |
+
midi_visualizer.bindWaveformCursor(midi_audio_cursor)
|
364 |
+
midi_audio_cursor_inited = midi_audio_cursor
|
365 |
+
}
|
366 |
+
let midi_audio_audio = midi_audio.deepQuerySelector("audio");
|
367 |
+
if(!!midi_audio_audio && midi_audio_audio_inited!==midi_audio_audio){
|
368 |
+
midi_visualizer.bindAudioPlayer(midi_audio_audio)
|
369 |
+
midi_audio_audio_inited = midi_audio_audio
|
370 |
+
}
|
371 |
+
}
|
372 |
+
})
|
373 |
+
|
374 |
+
function createProgressBar(progressbarContainer){
|
375 |
+
let parentProgressbar = progressbarContainer.parentNode;
|
376 |
+
let divProgress = document.createElement('div');
|
377 |
+
divProgress.className='progressDiv';
|
378 |
+
let rect = progressbarContainer.getBoundingClientRect();
|
379 |
+
divProgress.style.width = rect.width + "px";
|
380 |
+
divProgress.style.background = "#b4c0cc";
|
381 |
+
divProgress.style.borderRadius = "8px";
|
382 |
+
let divInner = document.createElement('div');
|
383 |
+
divInner.className='progress';
|
384 |
+
divInner.style.color = "white";
|
385 |
+
divInner.style.background = "#0060df";
|
386 |
+
divInner.style.textAlign = "right";
|
387 |
+
divInner.style.fontWeight = "bold";
|
388 |
+
divInner.style.borderRadius = "8px";
|
389 |
+
divInner.style.height = "20px";
|
390 |
+
divInner.style.lineHeight = "20px";
|
391 |
+
divInner.style.paddingRight = "8px"
|
392 |
+
divInner.style.width = "0%";
|
393 |
+
divProgress.appendChild(divInner);
|
394 |
+
parentProgressbar.insertBefore(divProgress, progressbarContainer);
|
395 |
+
}
|
396 |
+
|
397 |
+
function removeProgressBar(progressbarContainer){
|
398 |
+
let parentProgressbar = progressbarContainer.parentNode;
|
399 |
+
let divProgress = parentProgressbar.querySelector(".progressDiv");
|
400 |
+
parentProgressbar.removeChild(divProgress);
|
401 |
+
}
|
402 |
+
|
403 |
+
function setProgressBar(progressbarContainer, progress, total){
|
404 |
+
let parentProgressbar = progressbarContainer.parentNode;
|
405 |
+
let divProgress = parentProgressbar.querySelector(".progressDiv");
|
406 |
+
let divInner = parentProgressbar.querySelector(".progress");
|
407 |
+
if(total===0)
|
408 |
+
total = 1;
|
409 |
+
divInner.style.width = `${(progress/total)*100}%`;
|
410 |
+
divInner.textContent = `${progress}/${total}`;
|
411 |
+
}
|
412 |
+
|
413 |
+
onMsgReceive((msgs)=>{
|
414 |
+
for(let msg of msgs){
|
415 |
+
if(msg instanceof Array){
|
416 |
+
msg.forEach((o)=>{handleMsg(o)});
|
417 |
+
}else{
|
418 |
+
handleMsg(msg);
|
419 |
+
}
|
420 |
+
}
|
421 |
+
})
|
422 |
+
let handled_msgs = [];
|
423 |
+
function handleMsg(msg){
|
424 |
+
if(handled_msgs.indexOf(msg.uuid)!== -1)
|
425 |
+
return;
|
426 |
+
handled_msgs.push(msg.uuid);
|
427 |
+
switch (msg.name) {
|
428 |
+
case "visualizer_clear":
|
429 |
+
midi_visualizer.clearMidiEvents();
|
430 |
+
createProgressBar(midi_visualizer_container_inited)
|
431 |
+
break;
|
432 |
+
case "visualizer_append":
|
433 |
+
midi_visualizer.appendMidiEvent(msg.data);
|
434 |
+
break;
|
435 |
+
case "progress":
|
436 |
+
let progress = msg.data[0]
|
437 |
+
let total = msg.data[1]
|
438 |
+
setProgressBar(midi_visualizer_container_inited, progress, total)
|
439 |
+
break;
|
440 |
+
case "visualizer_end":
|
441 |
+
midi_visualizer.finishAppendMidiEvent()
|
442 |
+
midi_visualizer.setPlayTime(0);
|
443 |
+
removeProgressBar(midi_visualizer_container_inited);
|
444 |
+
handled_msgs = []
|
445 |
+
break;
|
446 |
+
default:
|
447 |
+
}
|
448 |
+
}
|
449 |
+
})();
|
midi_synthesizer.py
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import fluidsynth
|
2 |
+
import numpy as np
|
3 |
+
|
4 |
+
|
5 |
+
def synthesis(midi_opus, soundfont_path, sample_rate=44100):
|
6 |
+
ticks_per_beat = midi_opus[0]
|
7 |
+
event_list = []
|
8 |
+
for track_idx, track in enumerate(midi_opus[1:]):
|
9 |
+
abs_t = 0
|
10 |
+
for event in track:
|
11 |
+
abs_t += event[1]
|
12 |
+
event_new = [*event]
|
13 |
+
event_new[1] = abs_t
|
14 |
+
event_list.append(event_new)
|
15 |
+
event_list = sorted(event_list, key=lambda e: e[1])
|
16 |
+
|
17 |
+
tempo = int((60 / 120) * 10 ** 6) # default 120 bpm
|
18 |
+
ss = np.empty((0, 2), dtype=np.int16)
|
19 |
+
fl = fluidsynth.Synth(samplerate=float(sample_rate))
|
20 |
+
sfid = fl.sfload(soundfont_path)
|
21 |
+
last_t = 0
|
22 |
+
for c in range(16):
|
23 |
+
fl.program_select(c, sfid, 128 if c == 9 else 0, 0)
|
24 |
+
for event in event_list:
|
25 |
+
name = event[0]
|
26 |
+
sample_len = int(((event[1] / ticks_per_beat) * tempo / (10 ** 6)) * sample_rate)
|
27 |
+
sample_len -= int(((last_t / ticks_per_beat) * tempo / (10 ** 6)) * sample_rate)
|
28 |
+
last_t = event[1]
|
29 |
+
if sample_len > 0:
|
30 |
+
sample = fl.get_samples(sample_len).reshape(sample_len, 2)
|
31 |
+
ss = np.concatenate([ss, sample])
|
32 |
+
if name == "set_tempo":
|
33 |
+
tempo = event[2]
|
34 |
+
elif name == "patch_change":
|
35 |
+
c, p = event[2:4]
|
36 |
+
fl.program_select(c, sfid, 128 if c == 9 else 0, p)
|
37 |
+
elif name == "control_change":
|
38 |
+
c, cc, v = event[2:5]
|
39 |
+
fl.cc(c, cc, v)
|
40 |
+
elif name == "note_on" and event[3] > 0:
|
41 |
+
c, p, v = event[2:5]
|
42 |
+
fl.noteon(c, p, v)
|
43 |
+
elif name == "note_off" or (name == "note_on" and event[3] == 0):
|
44 |
+
c, p = event[2:4]
|
45 |
+
fl.noteoff(c, p)
|
46 |
+
|
47 |
+
fl.delete()
|
48 |
+
if ss.shape[0] > 0:
|
49 |
+
max_val = np.abs(ss).max()
|
50 |
+
if max_val != 0:
|
51 |
+
ss = (ss / max_val) * np.iinfo(np.int16).max
|
52 |
+
ss = ss.astype(np.int16)
|
53 |
+
return ss
|
midi_tokenizer.py
ADDED
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import random
|
2 |
+
|
3 |
+
import PIL
|
4 |
+
import numpy as np
|
5 |
+
|
6 |
+
|
7 |
+
class MIDITokenizer:
|
8 |
+
def __init__(self):
|
9 |
+
self.vocab_size = 0
|
10 |
+
|
11 |
+
def allocate_ids(size):
|
12 |
+
ids = [self.vocab_size + i for i in range(size)]
|
13 |
+
self.vocab_size += size
|
14 |
+
return ids
|
15 |
+
|
16 |
+
self.pad_id = allocate_ids(1)[0]
|
17 |
+
self.bos_id = allocate_ids(1)[0]
|
18 |
+
self.eos_id = allocate_ids(1)[0]
|
19 |
+
self.events = {
|
20 |
+
"note": ["time1", "time2", "track", "duration", "channel", "pitch", "velocity"],
|
21 |
+
"patch_change": ["time1", "time2", "track", "channel", "patch"],
|
22 |
+
"control_change": ["time1", "time2", "track", "channel", "controller", "value"],
|
23 |
+
"set_tempo": ["time1", "time2", "track", "bpm"],
|
24 |
+
}
|
25 |
+
self.event_parameters = {
|
26 |
+
"time1": 128, "time2": 16, "duration": 2048, "track": 128, "channel": 16, "pitch": 128, "velocity": 128,
|
27 |
+
"patch": 128, "controller": 128, "value": 128, "bpm": 256
|
28 |
+
}
|
29 |
+
self.event_ids = {e: allocate_ids(1)[0] for e in self.events.keys()}
|
30 |
+
self.id_events = {i: e for e, i in self.event_ids.items()}
|
31 |
+
self.parameter_ids = {p: allocate_ids(s) for p, s in self.event_parameters.items()}
|
32 |
+
self.max_token_seq = max([len(ps) for ps in self.events.values()]) + 1
|
33 |
+
|
34 |
+
def tempo2bpm(self, tempo):
|
35 |
+
tempo = tempo / 10 ** 6 # us to s
|
36 |
+
bpm = 60 / tempo
|
37 |
+
return bpm
|
38 |
+
|
39 |
+
def bpm2tempo(self, bpm):
|
40 |
+
if bpm == 0:
|
41 |
+
bpm = 1
|
42 |
+
tempo = int((60 / bpm) * 10 ** 6)
|
43 |
+
return tempo
|
44 |
+
|
45 |
+
def tokenize(self, midi_score, add_bos_eos=True):
|
46 |
+
ticks_per_beat = midi_score[0]
|
47 |
+
event_list = {}
|
48 |
+
for track_idx, track in enumerate(midi_score[1:129]):
|
49 |
+
last_notes = {}
|
50 |
+
for event in track:
|
51 |
+
t = round(16 * event[1] / ticks_per_beat) # quantization
|
52 |
+
new_event = [event[0], t // 16, t % 16, track_idx] + event[2:]
|
53 |
+
if event[0] == "note":
|
54 |
+
new_event[4] = max(1, round(16 * new_event[4] / ticks_per_beat))
|
55 |
+
elif event[0] == "set_tempo":
|
56 |
+
new_event[4] = int(self.tempo2bpm(new_event[4]))
|
57 |
+
if event[0] == "note":
|
58 |
+
key = tuple(new_event[:4] + new_event[5:-1])
|
59 |
+
else:
|
60 |
+
key = tuple(new_event[:-1])
|
61 |
+
if event[0] == "note": # to eliminate note overlap due to quantization
|
62 |
+
cp = tuple(new_event[5:7])
|
63 |
+
if cp in last_notes:
|
64 |
+
last_note_key, last_note = last_notes[cp]
|
65 |
+
last_t = last_note[1] * 16 + last_note[2]
|
66 |
+
last_note[4] = max(0, min(last_note[4], t - last_t))
|
67 |
+
if last_note[4] == 0:
|
68 |
+
event_list.pop(last_note_key)
|
69 |
+
last_notes[cp] = (key, new_event)
|
70 |
+
event_list[key] = new_event
|
71 |
+
event_list = list(event_list.values())
|
72 |
+
event_list = sorted(event_list, key=lambda e: e[1:4])
|
73 |
+
midi_seq = []
|
74 |
+
|
75 |
+
last_t1 = 0
|
76 |
+
for event in event_list:
|
77 |
+
name = event[0]
|
78 |
+
if name in self.event_ids:
|
79 |
+
params = event[1:]
|
80 |
+
cur_t1 = params[0]
|
81 |
+
params[0] = params[0] - last_t1
|
82 |
+
if not all([0 <= params[i] < self.event_parameters[p] for i, p in enumerate(self.events[name])]):
|
83 |
+
continue
|
84 |
+
tokens = [self.event_ids[name]] + [self.parameter_ids[p][params[i]]
|
85 |
+
for i, p in enumerate(self.events[name])]
|
86 |
+
tokens += [self.pad_id] * (self.max_token_seq - len(tokens))
|
87 |
+
midi_seq.append(tokens)
|
88 |
+
last_t1 = cur_t1
|
89 |
+
|
90 |
+
if add_bos_eos:
|
91 |
+
bos = [self.bos_id] + [self.pad_id] * (self.max_token_seq - 1)
|
92 |
+
eos = [self.eos_id] + [self.pad_id] * (self.max_token_seq - 1)
|
93 |
+
midi_seq = [bos] + midi_seq + [eos]
|
94 |
+
return midi_seq
|
95 |
+
|
96 |
+
def event2tokens(self, event):
|
97 |
+
name = event[0]
|
98 |
+
params = event[1:]
|
99 |
+
tokens = [self.event_ids[name]] + [self.parameter_ids[p][params[i]]
|
100 |
+
for i, p in enumerate(self.events[name])]
|
101 |
+
tokens += [self.pad_id] * (self.max_token_seq - len(tokens))
|
102 |
+
return tokens
|
103 |
+
|
104 |
+
def tokens2event(self, tokens):
|
105 |
+
if tokens[0] in self.id_events:
|
106 |
+
name = self.id_events[tokens[0]]
|
107 |
+
if len(tokens) <= len(self.events[name]):
|
108 |
+
return []
|
109 |
+
params = tokens[1:]
|
110 |
+
params = [params[i] - self.parameter_ids[p][0] for i, p in enumerate(self.events[name])]
|
111 |
+
if not all([0 <= params[i] < self.event_parameters[p] for i, p in enumerate(self.events[name])]):
|
112 |
+
return []
|
113 |
+
event = [name] + params
|
114 |
+
return event
|
115 |
+
return []
|
116 |
+
|
117 |
+
def detokenize(self, midi_seq):
|
118 |
+
ticks_per_beat = 480
|
119 |
+
tracks_dict = {}
|
120 |
+
t1 = 0
|
121 |
+
for tokens in midi_seq:
|
122 |
+
if tokens[0] in self.id_events:
|
123 |
+
name = self.id_events[tokens[0]]
|
124 |
+
if len(tokens) <= len(self.events[name]):
|
125 |
+
continue
|
126 |
+
params = tokens[1:]
|
127 |
+
params = [params[i] - self.parameter_ids[p][0] for i, p in enumerate(self.events[name])]
|
128 |
+
if not all([0 <= params[i] < self.event_parameters[p] for i, p in enumerate(self.events[name])]):
|
129 |
+
continue
|
130 |
+
event = [name] + params
|
131 |
+
if name == "set_tempo":
|
132 |
+
event[4] = self.bpm2tempo(event[4])
|
133 |
+
if event[0] == "note":
|
134 |
+
event[4] = int(event[4] * ticks_per_beat / 16)
|
135 |
+
t1 += event[1]
|
136 |
+
t = t1 * 16 + event[2]
|
137 |
+
t = int(t * ticks_per_beat / 16)
|
138 |
+
track_idx = event[3]
|
139 |
+
if track_idx not in tracks_dict:
|
140 |
+
tracks_dict[track_idx] = []
|
141 |
+
tracks_dict[track_idx].append([event[0], t] + event[4:])
|
142 |
+
tracks = list(tracks_dict.values())
|
143 |
+
|
144 |
+
for i in range(len(tracks)): # to eliminate note overlap
|
145 |
+
track = tracks[i]
|
146 |
+
track = sorted(track, key=lambda e: e[1])
|
147 |
+
last_note_t = {}
|
148 |
+
zero_len_notes = []
|
149 |
+
for e in reversed(track):
|
150 |
+
if e[0] == "note":
|
151 |
+
t, d, c, p = e[1:5]
|
152 |
+
key = (c, p)
|
153 |
+
if key in last_note_t:
|
154 |
+
d = min(d, max(last_note_t[key] - t, 0))
|
155 |
+
last_note_t[key] = t
|
156 |
+
e[2] = d
|
157 |
+
if d == 0:
|
158 |
+
zero_len_notes.append(e)
|
159 |
+
for e in zero_len_notes:
|
160 |
+
track.remove(e)
|
161 |
+
tracks[i] = track
|
162 |
+
return [ticks_per_beat, *tracks]
|
163 |
+
|
164 |
+
def midi2img(self, midi_score):
|
165 |
+
ticks_per_beat = midi_score[0]
|
166 |
+
notes = []
|
167 |
+
max_time = 1
|
168 |
+
track_num = len(midi_score[1:])
|
169 |
+
for track_idx, track in enumerate(midi_score[1:]):
|
170 |
+
for event in track:
|
171 |
+
t = round(16 * event[1] / ticks_per_beat)
|
172 |
+
if event[0] == "note":
|
173 |
+
d = max(1, round(16 * event[2] / ticks_per_beat))
|
174 |
+
c, p = event[3:5]
|
175 |
+
max_time = max(max_time, t + d + 1)
|
176 |
+
notes.append((track_idx, c, p, t, d))
|
177 |
+
img = np.zeros((128, max_time, 3), dtype=np.uint8)
|
178 |
+
colors = {(i, j): np.random.randint(50, 256, 3) for i in range(track_num) for j in range(16)}
|
179 |
+
for note in notes:
|
180 |
+
tr, c, p, t, d = note
|
181 |
+
img[p, t: t + d] = colors[(tr, c)]
|
182 |
+
img = PIL.Image.fromarray(np.flip(img, 0))
|
183 |
+
return img
|
184 |
+
|
185 |
+
def augment(self, midi_seq, max_pitch_shift=4, max_vel_shift=10, max_cc_val_shift=10, max_bpm_shift=10,
|
186 |
+
max_track_shift=128, max_channel_shift=16):
|
187 |
+
pitch_shift = random.randint(-max_pitch_shift, max_pitch_shift)
|
188 |
+
vel_shift = random.randint(-max_vel_shift, max_vel_shift)
|
189 |
+
cc_val_shift = random.randint(-max_cc_val_shift, max_cc_val_shift)
|
190 |
+
bpm_shift = random.randint(-max_bpm_shift, max_bpm_shift)
|
191 |
+
track_shift = random.randint(0, max_track_shift)
|
192 |
+
channel_shift = random.randint(0, max_channel_shift)
|
193 |
+
midi_seq_new = []
|
194 |
+
for tokens in midi_seq:
|
195 |
+
tokens_new = [*tokens]
|
196 |
+
if tokens[0] in self.id_events:
|
197 |
+
name = self.id_events[tokens[0]]
|
198 |
+
for i, pn in enumerate(self.events[name]):
|
199 |
+
if pn == "track":
|
200 |
+
tr = tokens[1 + i] - self.parameter_ids[pn][0]
|
201 |
+
tr += track_shift
|
202 |
+
tr = tr % self.event_parameters[pn]
|
203 |
+
tokens_new[1 + i] = self.parameter_ids[pn][tr]
|
204 |
+
elif pn == "channel":
|
205 |
+
c = tokens[1 + i] - self.parameter_ids[pn][0]
|
206 |
+
c0 = c
|
207 |
+
c += channel_shift
|
208 |
+
c = c % self.event_parameters[pn]
|
209 |
+
if c0 == 9:
|
210 |
+
c = 9
|
211 |
+
elif c == 9:
|
212 |
+
c = (9 + channel_shift) % self.event_parameters[pn]
|
213 |
+
tokens_new[1 + i] = self.parameter_ids[pn][c]
|
214 |
+
|
215 |
+
if name == "note":
|
216 |
+
c = tokens[5] - self.parameter_ids["channel"][0]
|
217 |
+
p = tokens[6] - self.parameter_ids["pitch"][0]
|
218 |
+
v = tokens[7] - self.parameter_ids["velocity"][0]
|
219 |
+
if c != 9: # no shift for drums
|
220 |
+
p += pitch_shift
|
221 |
+
if not 0 <= p < 128:
|
222 |
+
return midi_seq
|
223 |
+
v += vel_shift
|
224 |
+
v = max(1, min(127, v))
|
225 |
+
tokens_new[6] = self.parameter_ids["pitch"][p]
|
226 |
+
tokens_new[7] = self.parameter_ids["velocity"][v]
|
227 |
+
elif name == "control_change":
|
228 |
+
cc = tokens[5] - self.parameter_ids["controller"][0]
|
229 |
+
val = tokens[6] - self.parameter_ids["value"][0]
|
230 |
+
if cc in [1, 2, 7, 11]:
|
231 |
+
val += cc_val_shift
|
232 |
+
val = max(1, min(127, val))
|
233 |
+
tokens_new[6] = self.parameter_ids["value"][val]
|
234 |
+
elif name == "set_tempo":
|
235 |
+
bpm = tokens[4] - self.parameter_ids["bpm"][0]
|
236 |
+
bpm += bpm_shift
|
237 |
+
bpm = max(1, min(255, bpm))
|
238 |
+
tokens_new[4] = self.parameter_ids["bpm"][bpm]
|
239 |
+
midi_seq_new.append(tokens_new)
|
240 |
+
return midi_seq_new
|
241 |
+
|
242 |
+
def check_alignment(self, midi_seq, threshold=0.3):
|
243 |
+
total = 0
|
244 |
+
hist = [0] * 16
|
245 |
+
for tokens in midi_seq:
|
246 |
+
if tokens[0] in self.id_events and self.id_events[tokens[0]] == "note":
|
247 |
+
t2 = tokens[2] - self.parameter_ids["time2"][0]
|
248 |
+
total += 1
|
249 |
+
hist[t2] += 1
|
250 |
+
if total == 0:
|
251 |
+
return False
|
252 |
+
hist = sorted(hist, reverse=True)
|
253 |
+
p = sum(hist[:2]) / total
|
254 |
+
return p > threshold
|
packages.txt
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
fluidsynth
|
requirements.txt
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Pillow
|
2 |
+
numpy
|
3 |
+
onnxruntime-gpu
|
4 |
+
gradio==4.36.1
|
5 |
+
pyfluidsynth
|