Cleanup
ci / checks (push) Failing after 23m22s

This commit is contained in:
khodowany
2026-10-08 02:13:45 -05:00
parent 300097a37e
commit bc7baea7c4
88 changed files with 18844 additions and 1335 deletions
+5
View File
@@ -61,6 +61,11 @@ add_test(
# Complete deterministic phrase searches make this suite CPU-sensitive;
# leave headroom for concurrent asset generation on development machines.
set_tests_properties(product_vamp_generator PROPERTIES TIMEOUT 600)
add_test(
NAME product_vamp_assets
COMMAND ${VAMP_PYTHON_COMMAND} -m tests.asset_contract ${CMAKE_BINARY_DIR}/generated_assets
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
add_custom_target(check
COMMAND ${CMAKE_CTEST_COMMAND}
--test-dir ${CMAKE_BINARY_DIR}
+17
View File
@@ -0,0 +1,17 @@
"""Compare complete generated assets with the reviewed, pinned output baseline."""
import hashlib
import json
import pathlib
import sys
def verify(root):
expected = json.loads(pathlib.Path(__file__).with_name('fixtures').joinpath('assets_manifest.json').read_text())
actual = {path.relative_to(root).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest()
for path in sorted(root.rglob('*')) if path.is_file() and path.relative_to(root).parts[0] != 'analysis'}
changed = sorted(name for name in expected.keys() | actual.keys() if expected.get(name) != actual.get(name))
if changed:
raise AssertionError('Generated assets differ from the reviewed baseline:\n' + '\n'.join(changed))
print(f'All {len(expected)} generated assets match the reviewed baseline.')
if __name__ == '__main__':
verify(pathlib.Path(sys.argv[1]))
+411
View File
@@ -0,0 +1,411 @@
{
"backing/all_of_me_120.wav": "5104097be1d4ed0459dacedb61c2509effcfe0e26025b24574291a3467962f3d",
"backing/autumn_leaves_120.wav": "990e0218f9a09161a7a8a255bd1e64daf9fd01ac41935faa273228906775d016",
"catalog.db": "0891794877e06d201e1e149ea82f7bfdc880baf35c78a29b7b724f6c0ecce1e9",
"fonts/petaluma/OFL.txt": "5789e74d38031e93a4fbdb0a72392d1fd58cf15c29da3efa695c5099433ad0ce",
"fonts/petaluma/PetalumaScript.otf": "3b4be972cc9f857a7c1019ff49cd07cb083d5cc99058c0fb8563897fc75c32c8",
"pico/titles/42076b2eaa61a6fb408eeaec133b130967cf2c32981a9444dde6903ed7771fc8.mask": "42076b2eaa61a6fb408eeaec133b130967cf2c32981a9444dde6903ed7771fc8",
"pico/titles/8ed7df6668e9e4a24c148ba0fe9ac57209e504ca6a7e8a9b29179c097076ecae.mask": "8ed7df6668e9e4a24c148ba0fe9ac57209e504ca6a7e8a9b29179c097076ecae",
"print/all_of_me_acoustic.pdf": "aff0697fa82bde5283f705395028a3e4f6e87d4e0f1eab271dea9cffdb6c72e2",
"print/all_of_me_electric.pdf": "1185ece1e8f507d6fd7046e9d78fcb28b9e0a667c1027304abcb13c8e0519b97",
"print/all_of_me_vocal.pdf": "0ff78cb9d315dff93e7a88209610e959fa804c480cf63a8c4fb018a17c46ff66",
"print/autumn_leaves_acoustic.pdf": "badafe4d5484ab7e8a7c94633174d2233233436318fcf62bfcc043f624d097d6",
"print/autumn_leaves_electric.pdf": "a7416f9c4390072f74736024162172340d273de5108681bdf6543c9b8fa69d60",
"print/autumn_leaves_vocal.pdf": "78cc66dd27c7088e196f917eb752266989de13c51361692195b6bb9765210001",
"songs/all_of_me_acoustic.json": "b5f9f3781a79a351a59bbb54a333cbb1dac9e21c46a33c4296d6baef0ed8ff41",
"songs/all_of_me_acoustic.pages/0001.png": "165e827c3a05c1ee2aa1e5fd41c9dd2b273dac04e067df6d3e136f16fd2255a1",
"songs/all_of_me_acoustic.pages/0002.png": "b8738a70ad3d32911a8147ff18e62e4aa26651277562c348cb28f9180f045888",
"songs/all_of_me_acoustic.pages/0003.png": "af9631bdb1736de168527ac7a881de5c7b9fa1fe532aa10bffe4b4419cb67297",
"songs/all_of_me_acoustic.pages/0004.png": "8b2fdc7a064945bdfafb77b9a5f87313a86ef759a87daedb861cd3822dd2fdd2",
"songs/all_of_me_acoustic.pages/0005.png": "c11b85ac3affe9cee9184c126dd9cbffea5ef01d5f238440f831ce80b477f09b",
"songs/all_of_me_acoustic.pages/0006.png": "28bcdd62274aa79a6626935279dd88a9c85223f9ee3e4e7e914cd2ff56c704b6",
"songs/all_of_me_acoustic.pages/0007.png": "bfe462e693c5eb849b53463b2d64ba201e11196f64d5ec94e7d80a00f437d95f",
"songs/all_of_me_acoustic.pages/0008.png": "a7fa969178c12295a31429289b6d7ec273cc6e388343c174793d503856084c2c",
"songs/all_of_me_acoustic.pages/0009.png": "19a2fb51af8e4e09dafcba21b4e2277052125bb34222d2b1638a75582266bcea",
"songs/all_of_me_acoustic.pages/0010.png": "d423c4a0ae3c9d5e626bf8a633b202501ea5768c83ddbf68a6df8265d2a88315",
"songs/all_of_me_acoustic.pages/0011.png": "5e21353bfac521f66b49b6cd4e0dc00c4e85c27bac41d69809b8f88f3e9be310",
"songs/all_of_me_acoustic.pages/0012.png": "c36c7a82d1900f5394b1af3ab0bf663055e2bd7a1788fd60f416d7ec17902c7a",
"songs/all_of_me_acoustic.pages/0013.png": "063ba3b58613d9c383e379b0c6fa9b2d8b620cff5ff4def145fca918fa80f5e0",
"songs/all_of_me_acoustic.pages/0014.png": "ae3622cc934dbc126bff80f02bda6159edabd5044b5fc6c2448161652dc983ad",
"songs/all_of_me_acoustic.pages/0015.png": "99acae22eadaacd195fd986ae31f10c32800972e4d164f82cb5151ee5691c030",
"songs/all_of_me_acoustic.pages/0016.png": "c73a7111028d2e875ed441c54f9299b93947f53738e1815cb463d8a84fb7ef19",
"songs/all_of_me_acoustic.pages/0017.png": "6fa69caf6a91a38b3da2c57c146f4e79be712237b54910afe22d504cb7b46004",
"songs/all_of_me_acoustic.pages/0018.png": "f92e147f22c688b8936a7f2589b8ad6b7337e7b0aa9d9136641cff269623945b",
"songs/all_of_me_acoustic.pages/0019.png": "de5094eb5c2809146af4dc5470d58ab8ae70feb138d6fca83aa837aeb5bacc36",
"songs/all_of_me_acoustic.pages/0020.png": "d7c14a96a210c03918ad1c42240b34b27baf3412d03a443a0e51ffc631b27569",
"songs/all_of_me_acoustic.pages/0021.png": "81435e1c822309d48cd80a454a468e595f98684b8fc13e9aab61e7f9efbc8326",
"songs/all_of_me_acoustic.pages/0022.png": "78e7d1cc5b05ea1dd59b396d3554ee6b335b30d4adfc92d9da7deb80e420865c",
"songs/all_of_me_acoustic.pages/0023.png": "ed8a533012b886e46430d25b21d2a7e6ff026399f1f04a6561d8b8c465e7edf8",
"songs/all_of_me_acoustic.pages/0024.png": "d1ef3d847cc874f50fe1f7e84acba316431a56ef04101371c5eeefb38e03f54d",
"songs/all_of_me_acoustic.pages/0025.png": "6c22754c4af52c0bc1b986a9bb40d2611eaf19b891686d4fdd7082a7b37ae700",
"songs/all_of_me_acoustic.pages/0026.png": "f7433c965a235e41952d8d259c9aea0630792fe0470360208eba22d50ca2bc12",
"songs/all_of_me_acoustic.pages/0027.png": "2a7f5cb133d68b538f4ffb6e99a531a0416dcc62b8b35b9fdd002ada3277474a",
"songs/all_of_me_acoustic.pages/0028.png": "7c5a369f5dc4acef1bcc270543d6d8075a7066ceafa1cf2fabeca0fb78bcd9f7",
"songs/all_of_me_acoustic.pages/0029.png": "b58871c97069f13a3d5af60473af5aa5fb081e3373424b7761ca6f312f06e45b",
"songs/all_of_me_acoustic.pages/playback-0002.png": "89275aa531c7787a33b1450a8e84afbdb7d952a42f0b2105b58fca57243ed8d6",
"songs/all_of_me_acoustic.pages/playback-0003.png": "838b2d12000f748c0fd102de50eaea0ca3950cef07c051c6657d81f628169297",
"songs/all_of_me_acoustic.pages/playback-0004.png": "439e1856d32875c83d8d156bc9aa9f11bc493564bb342cf99044bf578980e25e",
"songs/all_of_me_acoustic.pages/playback-0005.png": "7789a952684a77309e19ca867a5500928fc96f67f5f82f371b346c9153778e96",
"songs/all_of_me_acoustic.pages/playback-0006.png": "ced45cec6c95fb0616552be40cf0543f9ab730ba0cf2414e72b156c12a74a9bc",
"songs/all_of_me_acoustic.pages/playback-0007.png": "2ed7cca6a962fead8ffcc95361aa3549e664038d99be1ec3d258ed51e917a53d",
"songs/all_of_me_acoustic.pages/playback-0008.png": "3efcd0671de4d386b189181ee7ebe03d4c50eddcfa9080c07a99c4642d4d58aa",
"songs/all_of_me_acoustic.pages/playback-0009.png": "cc4dc2ccfccda12074d69452e8e0427d40e6a0c9fb3b3da8308ab3b84240dbb2",
"songs/all_of_me_acoustic.pages/playback-0010.png": "fca310b66181635a06f6da5150a7e6ea13ce169aae94fe722139c371f5bbe1b6",
"songs/all_of_me_acoustic.pages/playback-0011.png": "dc92c989d193387eb5cc690b8274e57326eea652fa18688e37d405ca69cef051",
"songs/all_of_me_acoustic.pages/playback-0012.png": "7043e31820e325d716e7fa77992d4e416a7c9b8cf6391fc87e71ae6c685a75f8",
"songs/all_of_me_acoustic.pages/playback-0013.png": "67fbc90a1d4c3b2455bae0d0fb8f035430ad8cfed8f45a3a87988116cad5aeea",
"songs/all_of_me_acoustic.pages/playback-0014.png": "93561857be092664c798e2dd04cc6c15284c2a2e1759a056a978242f3b86e3ab",
"songs/all_of_me_acoustic.pages/playback-0015.png": "1ae5794742afae872c34f3bf1d21f3f6276d8ede4cca737ee5b58bc0a6c4b627",
"songs/all_of_me_acoustic.pages/playback-0016.png": "99274eea14c17d69f7fa940cc2dd14148e1406f4b3ed37fbabe72e1edcbc09b9",
"songs/all_of_me_acoustic.pages/playback-0017.png": "ac38a8f3c94e7a331516c90a46daa435f5c820a7db9527895dfccb2c4f2d4b3d",
"songs/all_of_me_acoustic.pages/playback-0018.png": "02c1308be295df3a8e1464e8809acbf9fb283d5e0bc400d30c99cba6f383f4d4",
"songs/all_of_me_acoustic.pages/playback-0019.png": "b009489f22aea8f68c8ad4ae0e09f7244c75e330245032ffe31bcf4812bf14b3",
"songs/all_of_me_acoustic.pages/playback-0020.png": "5a57c46ec799b8588ac42d7cf5bc98d5597302bde4d75745be343428cd4cbf74",
"songs/all_of_me_acoustic.pages/playback-0021.png": "184d0d6fbf9864dd2e3ab2a947eb21fa0ba6ef8ffc19f4ff4e05902b640c48d9",
"songs/all_of_me_acoustic.pages/playback-0022.png": "d4edcc5eb21d4b1fd97110a4e063cd1997f9f490012ea15f54f2a54ff71f0533",
"songs/all_of_me_acoustic.pages/playback-0023.png": "4fc5208032155c47172d7868529876b798596f73c9cca31bf68f4d7a63fa5660",
"songs/all_of_me_acoustic.pages/playback-0024.png": "5451f8652c7b7966d20db24fc07229dbc8839b63b04f318a7682c63e04216287",
"songs/all_of_me_acoustic.pages/playback-0025.png": "2b607bd202d85aef6d9b87d51a0cf16c6d15a14ab00ed4e894be7bdd9e5cfb77",
"songs/all_of_me_acoustic.pages/playback-0026.png": "a741f9177b792d6392eeaa1dc098df7f503e3a829fc862dabf0fc89a2334d6f1",
"songs/all_of_me_acoustic.pages/playback-0027.png": "2bf42f6fc15e0941c54f9f098f4983f2f45d9a7567eaa604a6b6319c0ce31c19",
"songs/all_of_me_acoustic.pages/playback-0028.png": "bead93f269e3ea6a91a0e71a03eb3e386c8a0f972034e7e3e23cf8807de2e589",
"songs/all_of_me_acoustic.pages/playback-0029.png": "196dd7c0f49190342cbcf4e88267462ea4fd353949f1a66fba0d129349579f85",
"songs/all_of_me_acoustic.pages/playback-0030.png": "339afb9ccf4e5fc0b18205aa3154ec0494d873494245f14512d1c77e7acdb3f2",
"songs/all_of_me_acoustic.pages/playback-0031.png": "f30c7d9824cf9eee91050879ee1b406e608d47461fa5de720639ce060ac10368",
"songs/all_of_me_acoustic.pages/playback-0032.png": "421170df4fdb0b746077ba2959ff33480c05edfaffc2505e7ebcf51a9aecf2ca",
"songs/all_of_me_acoustic.pages/playback-0033.png": "2af68de5667ca9b8d738703c562c0338e8c2fb5face892d07eb2894cb7b8e6c8",
"songs/all_of_me_acoustic.pages/playback-0034.png": "da78e34c05b7527c636124a349a38a95201173ca59d5778a88b7ad19fd458973",
"songs/all_of_me_acoustic.pages/playback-0035.png": "d3b699e968ef04d32f3330a4af99e4722f0bf436daaa818e16ea8076dd35968f",
"songs/all_of_me_acoustic.pages/playback-0036.png": "48c5688c1368946da513361c0e241ad13157fe7cf4dcaec216fe85031e63455f",
"songs/all_of_me_acoustic.pages/playback-0037.png": "e11a4272fe7596b73ac1ed2029c259268c0255bdf4805026fd28412c955a9b88",
"songs/all_of_me_acoustic.pages/playback-0038.png": "2ed4ea3113dfdf04f5416154904fc2bd409024e6beca62823bf8d49306f9dd32",
"songs/all_of_me_acoustic.pages/playback-0039.png": "506e04f4080141b20b51122756e1fb636b795b0c7a9cfea794787ef8f6d20b28",
"songs/all_of_me_acoustic.pages/playback-0040.png": "71b20e99fd27f4222435cb1349df375fc93f0749e6c07bc49946493044cfb088",
"songs/all_of_me_acoustic.pages/playback-0041.png": "01e099e1e9bb31eb89c0806ca3885793373243de5cb759b87782aba64548eca0",
"songs/all_of_me_acoustic.pages/playback-0042.png": "a26dc16d90c54856c8897fac364be9d4a5e67616afcd863582e4f36c14e689c2",
"songs/all_of_me_acoustic.pages/playback-0043.png": "1385cc3c40f04f897b69f277dc0f89a3c70fb0f4509722f13ab3f8a08d4a301b",
"songs/all_of_me_acoustic.pages/playback-0044.png": "c12d277f421d3120d318bf7ad14662153a1fa7af6941dd0362da460e967e0312",
"songs/all_of_me_acoustic.pages/playback-0045.png": "95c52e795b0aff090ce88d8f3048ccb1bfedb478a8ea0fc9611843e7d3233b33",
"songs/all_of_me_acoustic.pages/playback-0046.png": "f4fb50ff913ba429633c2e80c5dac60fa16dd3ea9a1c4873794f1e482c6c9c90",
"songs/all_of_me_acoustic.pages/playback-0047.png": "9ce7a7f624a654561fd132628400a16e95665cbc8bc33c437c7d21835de18adf",
"songs/all_of_me_acoustic.pages/playback-0048.png": "2faf0c360563387702a2244b26dba8885d5227c3ded4238d480fb81b607531f5",
"songs/all_of_me_acoustic.pages/playback-0049.png": "4e94b56e4962ad097bf6ad703be517215b11749e234efc86b7bf1e7ed845f0a2",
"songs/all_of_me_acoustic.pages/playback-0050.png": "aeeea727737f35d556abba012444551fde6f1202f99e0959b807d39840384801",
"songs/all_of_me_acoustic.pages/playback-0051.png": "7f918b1847cf774167f62437cc5524b2d7465355e7d3a34bc1778e0c5e01df7f",
"songs/all_of_me_acoustic.pages/playback-0052.png": "5da7caa11f7d223c7be0baf95d049ee226a28236c91a7fe3c95424fe509c5f63",
"songs/all_of_me_acoustic.pages/playback-0053.png": "6dae421ab2d5a85d253f0cc6a750d562a47f2a4c782fc70259c0d0dcdd5f6008",
"songs/all_of_me_acoustic.pages/playback-0054.png": "9a3cd474eaadf4c40b3df43ec805d47d1bc81110d0c56d1e17356d550b731cef",
"songs/all_of_me_acoustic.pages/playback-0055.png": "6489f62576b32c1c7071361fa9d3db372a47927d66f6865ee417bb99c9c04c15",
"songs/all_of_me_acoustic.pages/playback-0056.png": "f7ab14f136adcc844acec06935fdfec2b166972f6c7d6b3993883aaa0ca9e859",
"songs/all_of_me_acoustic.pages/playback-0057.png": "8fcc3723e92877b1005cedba8045a4488e5134e3f1f2185a465b8b6115823f4f",
"songs/all_of_me_acoustic.pages/playback-0058.png": "7d0a9ef281a2e38814506d04932276bcb5041c1c3947dd6aaa874ff066a038fd",
"songs/all_of_me_electric.json": "ae2bf27df59d93436bdcb8ae67821e635014e104e64ec8a4585326bc4bdc087a",
"songs/all_of_me_electric.pages/0001.png": "4080b675aeb49f1b745a18641fc284235b9c3f5e36d07bf258772300f6fed6b8",
"songs/all_of_me_electric.pages/0002.png": "b4d590883d16efdd43d8199324bb825ac23d84f1f5d4fc5c5131a82d504eb756",
"songs/all_of_me_electric.pages/0003.png": "6d16d6d083ddc2c3025c1b379cc145cb0e1de8074e7a51bfcaa817089c51e7a7",
"songs/all_of_me_electric.pages/0004.png": "106a0cd8cd598455a9694d62c3bc905c6e8f038910b40efabafdf43cf2836642",
"songs/all_of_me_electric.pages/0005.png": "5e69575380ed38032dc7ede57d4e815540e15eaa8fad7baeee3d60dd84b7d70b",
"songs/all_of_me_electric.pages/0006.png": "d21342d7e8dd3a63c246ae37dd69567914cb7c3e6c5171de04ed0ffbdc008717",
"songs/all_of_me_electric.pages/0007.png": "c418f413c20c48ce85fcb86ab20eb64d1b9635a45adaac8acfc54403ab2477b9",
"songs/all_of_me_electric.pages/0008.png": "166c390f64d654ac0adba802c45b2c5f3c47bcacf57a4180ddb0f36597f349f9",
"songs/all_of_me_electric.pages/0009.png": "c50e6e4582423176dd2c4e52933eb6263192350f63d16192bd13842d1bd901c8",
"songs/all_of_me_electric.pages/0010.png": "50b939ff62a4430102cd0d851758a326a7d9ee4478afc6471428c3b20692a154",
"songs/all_of_me_electric.pages/0011.png": "2a063242e638507b6978f03043505c441b392ab90943e8b8f39c9a5b6489105c",
"songs/all_of_me_electric.pages/0012.png": "843fd8cd70334df8b327e1316fd3df00efba5f2e7587abc96f14808f25d785ba",
"songs/all_of_me_electric.pages/0013.png": "d1648e1633ad74b790d1b21d3ac85946ea66e048ac8ebebc338dc58eba1453aa",
"songs/all_of_me_electric.pages/0014.png": "5859ff6cd737294f010ecfcf9202924530c037b56e14ffbb29ea7f3fc712a11f",
"songs/all_of_me_electric.pages/0015.png": "b38390c67e0e56a9218f1fd82d386583f8a5fb827255357d6d375ece7cc4e39a",
"songs/all_of_me_electric.pages/0016.png": "ce83a70d0c06b0fee259c1b537b889aa82e74f058a3d46edcbe2956a7cb96aa0",
"songs/all_of_me_electric.pages/0017.png": "97b5da09798a71b8ceae26748da222ea6a90dab14dd59ae4e0eddf55f42d706f",
"songs/all_of_me_electric.pages/0018.png": "124accb8cbb0b8becc086a3e0883052d1d44add715adc46c7fe1e4d1c65d0c62",
"songs/all_of_me_electric.pages/0019.png": "f3886454bbce1128ec0de1de71a2a1dba4dcec41b68fedf815a2c22280c83708",
"songs/all_of_me_electric.pages/0020.png": "6c78af0299bc6cc344800a18f3eeb310ef273f732588dd6d131a9315660c6c8b",
"songs/all_of_me_electric.pages/0021.png": "8d248495a0a6c711d1619475ffafb16ad6f831a6246bb0c32cb2500e69d0ad8b",
"songs/all_of_me_electric.pages/0022.png": "42e5cf25476df3a4ee8030a32d3522401f5de45c539504e544780ebb862bc3c2",
"songs/all_of_me_electric.pages/0023.png": "a7dfa4060c0b5fc26440ed299466db702eeb2cf80cd40c91f83e031f8be53b93",
"songs/all_of_me_electric.pages/playback-0002.png": "403f0cfd04a73ecc7d6277c1ad5d6f0638218386d71a18a960316ad75625de3a",
"songs/all_of_me_electric.pages/playback-0003.png": "0aaa246f780dcdce077fadf99f0bb2febb47eb70462c341545a01c351fa03a18",
"songs/all_of_me_electric.pages/playback-0004.png": "f272b7991e1e2721dea59fdc74836421ef501f7b937b5487ce82985d54feb65e",
"songs/all_of_me_electric.pages/playback-0005.png": "227c94ab1b8f2d28cda0cf2c55710796b1739f48e4e47ae8d88af5b3e7f3fa71",
"songs/all_of_me_electric.pages/playback-0006.png": "196910b9157c0fd142aed1555e294068a726ebf4a12fb077953ab4ffa078cc3b",
"songs/all_of_me_electric.pages/playback-0007.png": "65a2e7466bed99e53a52fa6135926a5d11309e2f5141b14dc1ecde85bb4a2974",
"songs/all_of_me_electric.pages/playback-0008.png": "d28dd164add8c1081408f6310bddad34f60c8fbbf2559d88f8d3024ff089ae18",
"songs/all_of_me_electric.pages/playback-0009.png": "7f9814e8de25ffa797cfcb723bf0146943d9dd0b3c7708e1b8eee34ccdcf65c9",
"songs/all_of_me_electric.pages/playback-0010.png": "0aeb5e7a51504708f1ac1a7f971c8bfd6164745ac334a44cdceb3ddc3cdc62d2",
"songs/all_of_me_electric.pages/playback-0011.png": "d3f67966eb3cc8a5198f8c5832ed9387607e3c657ba730361c06bee9bdeb8862",
"songs/all_of_me_electric.pages/playback-0012.png": "21e1f3c7978796f5b82aa3cf126ece81e409f6c48129681292f5703fb05dc371",
"songs/all_of_me_electric.pages/playback-0013.png": "6cbfc21a3ecdf4980b10636b8374cb918ff017abc5592dc9aef4edf55d27ddbd",
"songs/all_of_me_electric.pages/playback-0014.png": "dd2d3cf5ed13638c91a4b9f92462200494bc78001062f38d8be172d08804e5cf",
"songs/all_of_me_electric.pages/playback-0015.png": "e9dd230dd3410f8ccc6ba55c04155653a02a7cc54ba9903f07bd115894d57019",
"songs/all_of_me_electric.pages/playback-0016.png": "870884db997e94c9bafa6f8a173059dffa55f99f3a38e400e59658a251737b97",
"songs/all_of_me_electric.pages/playback-0017.png": "21f0489f8f948e4131a7c14585169f0d4e9795166a75459eeadc698a49e7c36a",
"songs/all_of_me_electric.pages/playback-0018.png": "e834520df8a74a2f762044d4d2a0bc220124227e348555b33bf73dca11d848a2",
"songs/all_of_me_electric.pages/playback-0019.png": "d046a861e398d0fa4e2eb0bd316f6101455e4c27cb9c73ece1c7a7e8e296032f",
"songs/all_of_me_electric.pages/playback-0020.png": "979701ac47e9cdcd4f21fd63af0dac2e0fede6567c9bb417c4a3133b99df66ee",
"songs/all_of_me_electric.pages/playback-0021.png": "62edac1a5171cd41a1e9eeaf67991ec7574941bb03a74e2bd742ea68840617f8",
"songs/all_of_me_electric.pages/playback-0022.png": "343e6c59f3f3b5718df93d3ef871ca6e041a0a674d719df122b6ab298dcd96ba",
"songs/all_of_me_electric.pages/playback-0023.png": "14735b44d4a7b65b957c1d733e7082c639a0e4acc9caffffde1b7bddf236eee7",
"songs/all_of_me_electric.pages/playback-0024.png": "cc21f078595a1fd138f00153b30cebcccecd0acc8c8b4a4a17e7908ad854e1a6",
"songs/all_of_me_electric.pages/playback-0025.png": "ce3df5c546abada13bf1cc0b409d30239df2bc9eff0f7d04857c75b4734bc642",
"songs/all_of_me_electric.pages/playback-0026.png": "367021a47fac7e2dc54e3288399b708ae46bd21a6055e8593eead39d1b195d58",
"songs/all_of_me_electric.pages/playback-0027.png": "8544e3d66736b42b4b4c6f4433b00945f14aabdf2fa02159e666e5005f66206e",
"songs/all_of_me_electric.pages/playback-0028.png": "96d88ee25fe1e2543fb0599a5e6f18f6fa20430146a3cbe2ec1cb4e164776943",
"songs/all_of_me_electric.pages/playback-0029.png": "689aac0d1696ce3940d05bceadad057c485bd36ffe6615aa679ee974c36b2cf1",
"songs/all_of_me_electric.pages/playback-0030.png": "c9961fb33a22d8cfe0ac23b708b491c8fe066c28565f946634aa35ecc50d2164",
"songs/all_of_me_electric.pages/playback-0031.png": "eb03bc4ff5d5037c5e068323060610f268678a4c8083d3f4f727c7171cc6b1cb",
"songs/all_of_me_electric.pages/playback-0032.png": "6ee11cbeb454dd51199b702178f54c8c17bb626542776419818477c77bb5c6e6",
"songs/all_of_me_electric.pages/playback-0033.png": "8b0bc5cd261d542f6aad5ba56d23aabba32892739b32e09e84f0c147beea7708",
"songs/all_of_me_electric.pages/playback-0034.png": "f413fb00b1231b1783a0578063212217043adc73a270c23cad8c8913bfd70e98",
"songs/all_of_me_electric.pages/playback-0035.png": "88f772174ce53af5d84ac4d804d827e4c3096ff64126b0ea2eb206589d75fe46",
"songs/all_of_me_electric.pages/playback-0036.png": "f0e09108d57afdc64791164cb8df197ff1794e5df3bd803325b1ab4ec904d761",
"songs/all_of_me_electric.pages/playback-0037.png": "b1a6b868a18f4a5b18dfb8b11d38ed5e5505c3a1736cf4d71172c6f75d9e6c22",
"songs/all_of_me_electric.pages/playback-0038.png": "aaef3ac9fc6d52703bb4bbfe588c51f630e6cef75a85fa9cbd3197c5c252bb23",
"songs/all_of_me_electric.pages/playback-0039.png": "43606ff740a8127560d60a384b9ca910a294cb96de13f3da2492a646097abe82",
"songs/all_of_me_electric.pages/playback-0040.png": "8f7809d7a0fcad64e9a7c5ab644b7de771460d7d48b554ef603d0c93a98a0124",
"songs/all_of_me_electric.pages/playback-0041.png": "7c0befdd861d8a98d170c04fa813a0c091568bf74bf94fcc14024ce7618d016f",
"songs/all_of_me_electric.pages/playback-0042.png": "349e59923c00ccd2675f61763dcb2bc81d9debe3efac409e0b25d8bcd1e3bd42",
"songs/all_of_me_electric.pages/playback-0043.png": "90c5d4f9149e380fcfcfdb18882381f535c8f619b475c480210dffe3570a3d8f",
"songs/all_of_me_electric.pages/playback-0044.png": "3548e497fe84e7a6f3344cdd5bfb7b4d44e89b6f2cc6778a8bf891fdb0d95ae3",
"songs/all_of_me_electric.pages/playback-0045.png": "1f21274695106a635e92551f7d12b739a82f1a5a17db8d911e60a00fca170e10",
"songs/all_of_me_vocal.json": "04c2712621766ee10529135463c0ceeffdd2def707134c81fb450e30a2d6cd67",
"songs/all_of_me_vocal.pages/0001.png": "5ff59ef10db0937c0dc132effc4f1ab761c6a4cdd9898aed1cbfc79c3ee126ad",
"songs/all_of_me_vocal.pages/0002.png": "8c737499c4e537b779160b86bf779de9b0a6771ece8f594af64ccd66e896f0d6",
"songs/all_of_me_vocal.pages/0003.png": "1ae152662e101117056704e7ccc92e7fadf546e4b0b68b11a463b8a778a2449e",
"songs/all_of_me_vocal.pages/0004.png": "d921899d08554455d72c71709d1110d808c5227de5cfc0c9aff68a168c7d4ecb",
"songs/all_of_me_vocal.pages/0005.png": "4c9fb6f70a887dceb7aca582122c7ba3d06a38431a37247b422e11a81f917e5e",
"songs/all_of_me_vocal.pages/0006.png": "a2792f1dc70d11d7f1b26f4c51686c8c6739844021e237c0d30f3604a2bc6306",
"songs/all_of_me_vocal.pages/0007.png": "ecdad2ac3c0a38bb0a553986b68fd64b6bce4dae7e2e654c5a152bc83abc61b9",
"songs/all_of_me_vocal.pages/0008.png": "0e619566374434cae278bd66344d94c80507e7477f678172da1f1cdf24bb65b9",
"songs/all_of_me_vocal.pages/0009.png": "8ef33bc47271fe389b3cc1c03be11e8689e8f5d1e3638a8af127adecbe90cde2",
"songs/all_of_me_vocal.pages/0010.png": "5f40610e4d3b143d3260b574c28c633b3a18e6e298fb3afe8adac95878bde3f7",
"songs/all_of_me_vocal.pages/0011.png": "32c857d8cdac61f34eb97031e27dce2f3b7d85f354e002c76c6947c07a49fb48",
"songs/all_of_me_vocal.pages/0012.png": "f6674afe2c6f4ead074491509ff12fc572656a73d894b170a7ef78ea0f83adce",
"songs/all_of_me_vocal.pages/0013.png": "886cee78fa1a4f5868a6adba5205d0843a6b9de0af42ea80e047c514b6ba0bc4",
"songs/all_of_me_vocal.pages/0014.png": "0e7d208b5da11cf6fd25b499ee138d865e63e85e2d9392ea9363623145b5677b",
"songs/all_of_me_vocal.pages/0015.png": "ba792e980325d694638ff90c7e9b87713de6b313c8ca2667e2889cd278581c42",
"songs/all_of_me_vocal.pages/playback-0002.png": "ad7fc6426e39e204109e3122bfa142d203abe60ba76a59b4df507e61745d923a",
"songs/all_of_me_vocal.pages/playback-0003.png": "4980352dee0600cad30f54ffba81577f4285248c7987df23aaa84f28544d02ed",
"songs/all_of_me_vocal.pages/playback-0004.png": "ad23b22d0fb931f098b88124fb6e48d2a1b03437f12944d8dbfbab9e5a6639e1",
"songs/all_of_me_vocal.pages/playback-0005.png": "065262ff6b758419107fd41f7b3956d996c325d6552a72f62281288c57ed29c1",
"songs/all_of_me_vocal.pages/playback-0006.png": "bd6cff39e52a41aab31473740a77357c47b2bb2b3dff42cfa794d3fe7888d7c1",
"songs/all_of_me_vocal.pages/playback-0007.png": "7cf0ebec216da49ab7385514a10e466656770d6668975840c8967f132414d2ce",
"songs/all_of_me_vocal.pages/playback-0008.png": "b4d59c19612d34d766cf2d76db71c6d6238d9afd6ce88f51dfebd117e6b1c5e1",
"songs/all_of_me_vocal.pages/playback-0009.png": "3d30c0180874fee2163821c4ef160cc5e134e5c5069f6e77ba542e40f5174bde",
"songs/all_of_me_vocal.pages/playback-0010.png": "5464485dc6c19973ac39dd3e573b9d7884cae22a6ef632f3c31e0e863635c7d6",
"songs/all_of_me_vocal.pages/playback-0011.png": "761aa1f8a8096046dabef1c1a30875205aec3309030cd60ae27938f51247f154",
"songs/all_of_me_vocal.pages/playback-0012.png": "0285ff0feffa0a61ec1a26b3eea54ca9156cba357847c74b1cfeac2084d454f4",
"songs/all_of_me_vocal.pages/playback-0013.png": "6177b132b398724081c814a65006ec205df52ea90403ed0c543e6ac9bdfdf2e8",
"songs/all_of_me_vocal.pages/playback-0014.png": "5fb997b60b54f808490b9fa0c6fedc396540f3b407c33b4562b54290daca4afe",
"songs/all_of_me_vocal.pages/playback-0015.png": "2a4a6345c63a9e6548519a3303d369bff76896eb1c4bfb40da42dc29fe08f710",
"songs/all_of_me_vocal.pages/playback-0016.png": "2ce1db668951fa36a075f119a423b1f50a1c4f346a29b1077cd55ecb31a5fa7b",
"songs/all_of_me_vocal.pages/playback-0017.png": "9f2677349e9d54f8de873e12399fb1f30a24d18e5f06c990aea2190cc41737e3",
"songs/all_of_me_vocal.pages/playback-0018.png": "25e7ab38426b3ce2c970c0c474aac7758f0f8c6a816fd0472a1a98ae4e37df9f",
"songs/all_of_me_vocal.pages/playback-0019.png": "f580eb1801c2ffad826c48a5a3c0b35c7513a8e4949723bba3f8f4522d03534e",
"songs/all_of_me_vocal.pages/playback-0020.png": "e6f23793d569cab0069560e88b1bc38e8fe44b1a11843d3a1a7639a33a6ab2fd",
"songs/all_of_me_vocal.pages/playback-0021.png": "a3cd07885f5ec333ba7ab3bf61807cb66263b7a4da4f1cc53e36717388a0cc13",
"songs/all_of_me_vocal.pages/playback-0022.png": "82529db7c06aa1ebe3bf5608287d2f4a6822337a5e1261d4687a6a9665478efc",
"songs/all_of_me_vocal.pages/playback-0023.png": "0b2c34c2524e5d437187d4bbf5cda395c55149081cc1478405a338d29af02ee3",
"songs/all_of_me_vocal.pages/playback-0024.png": "08d783cfb2d4bc09e83f3647a86455394f044dcce723ab9c5fa55063b222e844",
"songs/all_of_me_vocal.pages/playback-0025.png": "9c65d79d14f04bf05bee9de69c6c37a871e50aab635de797a91eb62fd81aac31",
"songs/all_of_me_vocal.pages/playback-0026.png": "79e87e2185d99c32aa753ac54068fabb698f81b16d6017ee7285f3b7c4d20054",
"songs/all_of_me_vocal.pages/playback-0027.png": "015cd105d6e5d2130393ced1268413b4929ff77231c30501b3a2748be6783b45",
"songs/all_of_me_vocal.pages/playback-0028.png": "7f3f16ced98698ef1034101976c05a1fb33b7c513057066b160b42eefacdbe64",
"songs/all_of_me_vocal.pages/playback-0029.png": "dfe0f2ea02945997653b0fac1b6d1355cd1c9a2cb5307f38ee59d51a54d4ec53",
"songs/all_of_me_vocal.pages/playback-0030.png": "98e2a6862eff212591ffc51d55c48ab65267e670bdb56d6c7ca6fb00ad750086",
"songs/autumn_leaves_acoustic.json": "e07a7e4fec89c811e6c26c1bba64af690743662074faa6b8f0fc81288097b53f",
"songs/autumn_leaves_acoustic.pages/0001.png": "54cbffc07ff6a31d8c340a4acaa14e6818b50b08d60041d7aa409fed01c2864c",
"songs/autumn_leaves_acoustic.pages/0002.png": "563ffa15c52a986603a17596e3ae9177bfc5222910a5c5bf31cbf61cdd1845fe",
"songs/autumn_leaves_acoustic.pages/0003.png": "fad7549f767ab0b261e58266ef9d6f227f5fe8d90f5c1ebcb8ecf1b0fee790a2",
"songs/autumn_leaves_acoustic.pages/0004.png": "fb5b442534e05f2fa9729965d0a05acc9c2d2878b9af297b6d646d7f625dd1e2",
"songs/autumn_leaves_acoustic.pages/0005.png": "a32a12e59624f65cf30685c5f4439514575d54fff42243506278b68b4961199c",
"songs/autumn_leaves_acoustic.pages/0006.png": "749df9fa3bf3bed1033b7a9950755f64e19e9793da91f35fa95e44da85d7dd4d",
"songs/autumn_leaves_acoustic.pages/0007.png": "e90ab72d11b381e70b11c987532ab25d84518d272fb7b03517d25a1f952aba6a",
"songs/autumn_leaves_acoustic.pages/0008.png": "846f529b487a4df90f8041d0c893225e8b3cf71feb1bd8ee903fd3a31126b8ef",
"songs/autumn_leaves_acoustic.pages/0009.png": "e8ac60d749ee6e1be2bd40457be800927eb8a321d11b2b98e663ef3835d2e8bd",
"songs/autumn_leaves_acoustic.pages/0010.png": "694c524e7cfc5b23e0fd8a301c230f7871bb783ad5243b7d45f34101a2ea22ae",
"songs/autumn_leaves_acoustic.pages/0011.png": "25e7b830b5fe8b9c9aaab2d31ab713a1b8da720f2ed116c547162ad77b99c10c",
"songs/autumn_leaves_acoustic.pages/0012.png": "afb17b0fae1be9504395a497776033736d7eccb8b040061333bdfe456035acb5",
"songs/autumn_leaves_acoustic.pages/0013.png": "76bd3e7c8a6c3a95a98f8c96e521509bc3ff4563fd5446032939e7c2c577f21d",
"songs/autumn_leaves_acoustic.pages/0014.png": "9701a0f3d5f5a3f32fb18ff1c11c5cc525961a9a9416c98b83818ff4607afb37",
"songs/autumn_leaves_acoustic.pages/0015.png": "235584fcf484de70f3beb463d1eb3783ed20d6c296eacd1fb8a5b9b3127eccab",
"songs/autumn_leaves_acoustic.pages/0016.png": "fe7ce9c311c2f57e9b6cb10fb5ca01f7f103d0d0f29e4da806e96b6cc6e005a9",
"songs/autumn_leaves_acoustic.pages/0017.png": "639450b174f68900a71cbbd3ab8a66b621f239e07922673e9482e49f30a892a0",
"songs/autumn_leaves_acoustic.pages/0018.png": "c15b4d2cfa76bebc9866dd022527999b0e7a09726738a7da6ba2285c7c4d9420",
"songs/autumn_leaves_acoustic.pages/0019.png": "84f637a60432cc35981a895899993b9b1b9f35a6ea6d211bb4aa95ccaeb42d8c",
"songs/autumn_leaves_acoustic.pages/0020.png": "e20b7a45b003e68b0f890b263f06adbcda947c3ecada541c2f0feb9dc9424741",
"songs/autumn_leaves_acoustic.pages/0021.png": "1651902b453243d5bd7fbc203417d232398063d2161eae37dfaba9327a3d35ad",
"songs/autumn_leaves_acoustic.pages/0022.png": "3051d9af506ddc661b259f2ca5c9f2d80076b1e9d6e303f38970091958f48bf0",
"songs/autumn_leaves_acoustic.pages/0023.png": "7c554926f10ee5090bde0da5f182f965cf8650889f8a3d6227334006ba65ec8c",
"songs/autumn_leaves_acoustic.pages/0024.png": "ead80368cefbb664988df29571cc3ac68a4ec599d70f0c53d628715e9795fc1b",
"songs/autumn_leaves_acoustic.pages/0025.png": "953fabb18457d14a5797ad5a6fd5ce0f3af099bb3877f08fd2813fdeca7e2481",
"songs/autumn_leaves_acoustic.pages/0026.png": "13da788d7bcce194552854ebdbb1601d5322bed6ac0bbae09e3cc07c34610379",
"songs/autumn_leaves_acoustic.pages/0027.png": "bbc81588269392772e6ae2671289716e1f1b51cfe8ebb790e887e866e465a7a7",
"songs/autumn_leaves_acoustic.pages/0028.png": "109adbfb7abf9059d198ce6abe1612457f6f0674636219ab33ba109e89fb8ef5",
"songs/autumn_leaves_acoustic.pages/0029.png": "1052f1b8e1e7746fc846c6893a02e587b0b204f32d8307800d7fd74ac06383ca",
"songs/autumn_leaves_acoustic.pages/playback-0002.png": "e82e130512e34e2801d39a730d4acac3ca689e0736ab100770d49404a598d034",
"songs/autumn_leaves_acoustic.pages/playback-0003.png": "5ba697f7f059b627dd4467589c4f4891413cbf7ad85932800a7fc00c7c70255a",
"songs/autumn_leaves_acoustic.pages/playback-0004.png": "f1bb58fce2cdcffa5523ac60099b6e52c356c6696fc1a4ff60a5cccd2dd3ee1e",
"songs/autumn_leaves_acoustic.pages/playback-0005.png": "15645d74bb6ab75901e87485dbcfc2c797e8a79b03973c1e1708889be2fc68c8",
"songs/autumn_leaves_acoustic.pages/playback-0006.png": "724eaab2251f9b799a439fb0fab1cb200ca471ee8a7e13718167e0de7481e20c",
"songs/autumn_leaves_acoustic.pages/playback-0007.png": "b1043e1876883e3d380cf8e3a59b2f6850d48054e9d0e570ca1eb988f3b78cf1",
"songs/autumn_leaves_acoustic.pages/playback-0008.png": "e5529398605d81e4265239afc5b6117bf0e89743fa2662d9edc4c8a1a71dba3b",
"songs/autumn_leaves_acoustic.pages/playback-0009.png": "690e52975aee8c6e21231e3d2e8be84fcb15fd42134bbb69c8bdccc3e8c8be7a",
"songs/autumn_leaves_acoustic.pages/playback-0010.png": "fb950d14561f570f1f645903cd4bce289bac97d182a237b1d26d3ca4c1e40357",
"songs/autumn_leaves_acoustic.pages/playback-0011.png": "3c6d7cb9269f25fd89feaac66eb14c4e1c5f40b1c1e5b4a11cd9993e90d70180",
"songs/autumn_leaves_acoustic.pages/playback-0012.png": "b317e5306a4f8ab59bfd21688640aa02df31382dcadbaaeac016e8682d4a95fd",
"songs/autumn_leaves_acoustic.pages/playback-0013.png": "b0949d15a05cb2458fd2fa5f6f842855591ddcea8caa260ce3006d849a7b3498",
"songs/autumn_leaves_acoustic.pages/playback-0014.png": "676df949c9074b9f096f1ec8535ff78a8852020a23ba50055bf95e4a03515771",
"songs/autumn_leaves_acoustic.pages/playback-0015.png": "1da00065ad926e8e46ff475f1db3a21d1beb1b7d021fe1e899e1ceb330dd6a6e",
"songs/autumn_leaves_acoustic.pages/playback-0016.png": "62504e382dee30dbb52e999c70bb4cd7619412cdd4304deb619527f5e81e03db",
"songs/autumn_leaves_acoustic.pages/playback-0017.png": "4f30f876ad7bec3f1a7962838b9431b640a1cd663421068e8d31400c4123b1c5",
"songs/autumn_leaves_acoustic.pages/playback-0018.png": "af9ddd4d84fddf1356ae21ca5f54f74011bbc7860b6ca34094daba6f0374ae8f",
"songs/autumn_leaves_acoustic.pages/playback-0019.png": "b46f18b2e4dcd8272f6b97a4599787e16a94afdd78adcb3c697cbc415573d861",
"songs/autumn_leaves_acoustic.pages/playback-0020.png": "6c84621d515f3fa2b6b5ddedb4c7d7a28e1f7916ab86241a27aed3e5e0036c97",
"songs/autumn_leaves_acoustic.pages/playback-0021.png": "9a95f5db7d7350cd5dd1b81408c65395826329eb0f2e95d0f12d93f281e7cf69",
"songs/autumn_leaves_acoustic.pages/playback-0022.png": "9c41ee3793c44018873ffbc0847e001a2d2a7273ae487b1d8f47a7ec10b1c51e",
"songs/autumn_leaves_acoustic.pages/playback-0023.png": "213ee4b1ed54411d16c880907b0d4d6c04ba9f97bed9eec4e4c5a09067040987",
"songs/autumn_leaves_acoustic.pages/playback-0024.png": "b37549829a19c82c525d4b2710b6ad0db21f7ca9735b71817e6e06de826796c9",
"songs/autumn_leaves_acoustic.pages/playback-0025.png": "950f311dad81dcaff3b6d73128c2869ab6d7277b7bac4358599d9bad3fe07147",
"songs/autumn_leaves_acoustic.pages/playback-0026.png": "6495623d373cf5d464c01e8a3c7f6e95984af75fc8ba6370fe72bba2a9c5a1d2",
"songs/autumn_leaves_acoustic.pages/playback-0027.png": "36e1a9c2df257d706af5f319c4616dfc9bcd6aab587f97bedcb4a1e8594a524d",
"songs/autumn_leaves_acoustic.pages/playback-0028.png": "e732e810da00f84bf74fe2f26d57b2fae01ea4691c39bf17cfc89b7707672716",
"songs/autumn_leaves_acoustic.pages/playback-0029.png": "01da01ecae8bcdaad33240fe686b2f44604a40ed5979cd5bb0aa4481035c9f11",
"songs/autumn_leaves_acoustic.pages/playback-0030.png": "52d1acf4d3cc5bc531ca879e9d47281d72dafb8079dca349bd12965528ecba52",
"songs/autumn_leaves_acoustic.pages/playback-0031.png": "e0c220f7f8522679bf628b03ce472b03340b8951f4be5010952822fcad8f6bd2",
"songs/autumn_leaves_acoustic.pages/playback-0032.png": "42801ea8c17bc1f49689a6ca7379928dfb1e569ee7354a014451ded48112bc5c",
"songs/autumn_leaves_acoustic.pages/playback-0033.png": "e447393a28aad3360e02dd4fdd6886eff73a46503f94511056938f68e629e13e",
"songs/autumn_leaves_acoustic.pages/playback-0034.png": "0b490c24ccba2e1d43649e64d21e48b1dea2f34f91d2e459d850bb02a4fd5e17",
"songs/autumn_leaves_acoustic.pages/playback-0035.png": "079872d5fb0bde62bc41909ca74246ed9804c6d46e8bef78185fffd6f41fd33a",
"songs/autumn_leaves_acoustic.pages/playback-0036.png": "6b90cb2c9cf07e7e5896cb57961cba6de2c5cb226772e0ad67ca75caab34dc3d",
"songs/autumn_leaves_acoustic.pages/playback-0037.png": "d5d2d73bfac0ad110ac55c54e87bd63a26799d14af76fe20378100e72b715782",
"songs/autumn_leaves_acoustic.pages/playback-0038.png": "7ee879807b0e1371326ea4cfd38ba59218c649397ef1448589f0a4b0557b9b85",
"songs/autumn_leaves_acoustic.pages/playback-0039.png": "9566293d49f978823df090545d7239172ce836b54d0a184c2f50672509f5095a",
"songs/autumn_leaves_acoustic.pages/playback-0040.png": "f4a68c14292b2c8cf1b870f0f5ea74603439a20317c931435b94bf4d69b6c7ba",
"songs/autumn_leaves_acoustic.pages/playback-0041.png": "d899bd39cc713bbda7c521537d81c775b609e02df199d4b740eace1a948cb27e",
"songs/autumn_leaves_acoustic.pages/playback-0042.png": "e6f07ad76a91f5a0a1c563a238e5f859fd0d8799d18e429ebc6967c1c391ae0e",
"songs/autumn_leaves_acoustic.pages/playback-0043.png": "9b0cd3c1bce012b04afe478cbeba630643de02d695324c42f2062c515efb7321",
"songs/autumn_leaves_acoustic.pages/playback-0044.png": "1eca5856dbd1f7c58297b9e47766675efa44fabbbbf5668d07dede8d47c2bdea",
"songs/autumn_leaves_acoustic.pages/playback-0045.png": "d9eaf533b05d3a889bba6005f1cef1c1b62466db18aa7bac819f5ba2b25d27e7",
"songs/autumn_leaves_acoustic.pages/playback-0046.png": "8019f806b1782995ad79a2f06463e0241c3442195ba9cbc3f6a0c2719c3afa85",
"songs/autumn_leaves_acoustic.pages/playback-0047.png": "b0384920846c4089363ef8cfa4a5ce70fc2562788592a06138282b4c019da225",
"songs/autumn_leaves_acoustic.pages/playback-0048.png": "b7a7489c6ef5744c5daa995600896dd2694eeab383f878a02dc7057e530d657e",
"songs/autumn_leaves_acoustic.pages/playback-0049.png": "c76ff27923d2908a30e1eea6d880064de7e7c941814608452c3b8dd594daf850",
"songs/autumn_leaves_acoustic.pages/playback-0050.png": "ff9d2e94684c7b0342f6b9833c8e7e3c5611be9a436e8fd403d809c8540f8ed5",
"songs/autumn_leaves_acoustic.pages/playback-0051.png": "f376a335fe15961cee9781f8ea53307b3f6578a06b194f3304e4704278e5defb",
"songs/autumn_leaves_acoustic.pages/playback-0052.png": "8be1c27434d95f231ddad913981f140a3df9beb4914ca6e4d1e4a5dc42a65798",
"songs/autumn_leaves_acoustic.pages/playback-0053.png": "7261e3d2465bf7d4de7b7710aff229d64b61cf3861870cc6aeda931d88d602b3",
"songs/autumn_leaves_acoustic.pages/playback-0054.png": "51375ba98654c3ddf198dd4da24c72b688b37f6c3670fd0eb77ffa5fdabb3965",
"songs/autumn_leaves_acoustic.pages/playback-0055.png": "c2e8d36d522a39131eff7af3cfabc8139baa90ca55d1e523aa6cb2590be933c6",
"songs/autumn_leaves_acoustic.pages/playback-0056.png": "2d4c1e0611eb4c4d2e653e25f4719f4da8ae93b4d481e22bc401f615cd04093e",
"songs/autumn_leaves_acoustic.pages/playback-0057.png": "d15d0441b80e087fc3d8f01dd7086a6fcad5ec6c66df3ee410920ddf8e8172fc",
"songs/autumn_leaves_electric.json": "90250a7776c295c4886c1cad6aaa0ee5f42430e9309289fdc35b8a99ec0e1de3",
"songs/autumn_leaves_electric.pages/0001.png": "6a10d99239f2a2f7cb86f10a371410191e8eab0d7335ac96f382efb7ebc8be73",
"songs/autumn_leaves_electric.pages/0002.png": "6ab880d8341c5ee92fece84d0756f942bd12e4f80cef6648f3ed779d386a93b5",
"songs/autumn_leaves_electric.pages/0003.png": "26e73ccda608f5baebec6e9c1beceb5586b13b5d138f0d29d4c13f58a6924715",
"songs/autumn_leaves_electric.pages/0004.png": "d7d47739c2c32a676198d091d546e027e218530dbe76147a71ad7156dad96f8c",
"songs/autumn_leaves_electric.pages/0005.png": "3c190110e3b59e42185dedcd4351e36f90d6e79788732f91b55fc92e059053ab",
"songs/autumn_leaves_electric.pages/0006.png": "39ce5045c8bb217a3b8820d4a1af14896faf313ca91ebe63ff848eba6c76e0fc",
"songs/autumn_leaves_electric.pages/0007.png": "1f5f36692863b4ccc4f08cf3a8b9c2c8edbd9f5b72d8ee76aae78dee8fb5220f",
"songs/autumn_leaves_electric.pages/0008.png": "52e12ff6e4983451ad7e59c45f3c5cd049171e00f2c19ce40e10de994764cbb7",
"songs/autumn_leaves_electric.pages/0009.png": "eb18d7f17fd0a101ae02df700409f7064cdc1ed585f315dcc25220490a3a3ca4",
"songs/autumn_leaves_electric.pages/0010.png": "8b923a580500ec4dddfba53ac57029806acc34f4bc51045296d46ba9db9fe0b8",
"songs/autumn_leaves_electric.pages/0011.png": "ceafe100023db75a8d34ea08e8c66155ba76aa12e8a24c9b83294d2a01779ca9",
"songs/autumn_leaves_electric.pages/0012.png": "813635f070252ce2cfa76de28fdb7badca2534783edfd7d791983d8b22d05b20",
"songs/autumn_leaves_electric.pages/0013.png": "e828faeb75fa104b6e15991e0489db7a43652bbde47ebcf818348a54614d5452",
"songs/autumn_leaves_electric.pages/0014.png": "6bcff78f5fcd2b214795e1463cda7f817785f2cd80c9727147ea12fe169760cf",
"songs/autumn_leaves_electric.pages/0015.png": "19bde11ca45dd5927797ee23a8cdcbfc891e7ff29bc9a01c2ca945b84e07e5e8",
"songs/autumn_leaves_electric.pages/0016.png": "071a61fa83944bea0b04aa221b19d124876c2a5f1b1e8347b8edaed4ed1aae92",
"songs/autumn_leaves_electric.pages/0017.png": "1d1ecbdf8e123aa2d003e352e85070eaa8b20979dbe25ea04be61f9fb8efccd0",
"songs/autumn_leaves_electric.pages/0018.png": "d551633f1c99dbe7b74e2fb2c2c7ef490eceee0d812cf697702de2077c2b74b7",
"songs/autumn_leaves_electric.pages/0019.png": "71672fc8613539914aa4069a4668751363e0be3d2e9822cf078755fa6b9116f3",
"songs/autumn_leaves_electric.pages/0020.png": "86747ada8e9ff0b1c05824d1a1ec9328f01cf33a65c3ae13c64a1e075c0a52ba",
"songs/autumn_leaves_electric.pages/0021.png": "c981fe628c1621c6f5add8635e33aaacae112fe954b2b43f15b3f02604c6c9e9",
"songs/autumn_leaves_electric.pages/playback-0002.png": "53f34213f307f1c7ab471c3c28db34a28e551936989b1a450282a325b533b71d",
"songs/autumn_leaves_electric.pages/playback-0003.png": "33f686464316f82a30d2b71bbafb6f8d2a74c44461f9b6b2c4754632671c4381",
"songs/autumn_leaves_electric.pages/playback-0004.png": "d99dcc2f8f15f120c2bf3cc6eba91b794aa57cd0078961d930ffc8dae843c791",
"songs/autumn_leaves_electric.pages/playback-0005.png": "568f242912b71a2a0e6b438fabaeaf043a62087d1b3018d61838093e6812c392",
"songs/autumn_leaves_electric.pages/playback-0006.png": "dc5b754b3d11d136a89c82b3160b3cb631572065505b2770f529c312c8beb0cc",
"songs/autumn_leaves_electric.pages/playback-0007.png": "13d877cc54210a860f0e08006c5e386a6a3e5c339e2d334df11ad37afd8b8fec",
"songs/autumn_leaves_electric.pages/playback-0008.png": "90c1df01c76f8c919cfd7276f80a899259851e090975ea1d1cabdcc613d02d72",
"songs/autumn_leaves_electric.pages/playback-0009.png": "dc2be361fa7c0c2c56fe02c24a97bcb628a1eff960e8b1db78739679f562759e",
"songs/autumn_leaves_electric.pages/playback-0010.png": "07d5c2b2ae31ebd88d4fb4708caefac36aca5d436048c1ffad55feffa6d87925",
"songs/autumn_leaves_electric.pages/playback-0011.png": "8d22eb7cc86906c36967434e31811a4542b1e44f6590eabff23f9a940916d207",
"songs/autumn_leaves_electric.pages/playback-0012.png": "c964863cbd956968210fc939b75ef6681d6f797f50860b75d8b7d0736ca12305",
"songs/autumn_leaves_electric.pages/playback-0013.png": "7d9c22406e24aff928e56cbebbb4749d1e8880542975fd53447afaca19b44584",
"songs/autumn_leaves_electric.pages/playback-0014.png": "2fce4a9c3624e90ebf964965a11d37e951db7e18824a2268c487abe2dd414168",
"songs/autumn_leaves_electric.pages/playback-0015.png": "edda9e50c852a5da934c21cd2d8a70a19e8c54b9c7020feb0d542ed0b257c2b7",
"songs/autumn_leaves_electric.pages/playback-0016.png": "50cba6043f888330d3a01ecd695de8f3ed7f3e51ae94da54b3faadb1088e74de",
"songs/autumn_leaves_electric.pages/playback-0017.png": "7c8bcaccb8b7879665b50a1185a90cc3ee31e6bcd21ecc2fed2dcd723d31e114",
"songs/autumn_leaves_electric.pages/playback-0018.png": "084a522a8e146a7fe533b3498eb842a586cf743c6a034b79f15b067a6ce52a4e",
"songs/autumn_leaves_electric.pages/playback-0019.png": "9f47a55406500ef63d7936065fd53685cef4d72b7b2190a14e6daf28e4d8bbbd",
"songs/autumn_leaves_electric.pages/playback-0020.png": "95a7d3e90b7a7cf435ce87c1d83366822f38f53857d993cb9c242afce3f1ac2a",
"songs/autumn_leaves_electric.pages/playback-0021.png": "f983e3bccc183efa8fbbe9f83a7629b056820d15129bc84108f02934a4b52ae3",
"songs/autumn_leaves_electric.pages/playback-0022.png": "614a51979fa8c9229503018645533118f05f496ffee4be4003e18a2281015844",
"songs/autumn_leaves_electric.pages/playback-0023.png": "223fcf58efd3e678fd0e5a761a2f504eea569a1cb4f6b27f85baa8d425bdb499",
"songs/autumn_leaves_electric.pages/playback-0024.png": "8c3ef22114aa00a42f603daac996f923d3d4aef9ee1cfdd2daa437a8760a9c85",
"songs/autumn_leaves_electric.pages/playback-0025.png": "ba7d51c2f2eeeefa52099d2401d695ccdf3091a49599c206eaa1daa39c68eccf",
"songs/autumn_leaves_electric.pages/playback-0026.png": "9c09778168a6b0b44d3beedb49c915a9e126fe47e8ec0afd4e72061ebadae533",
"songs/autumn_leaves_electric.pages/playback-0027.png": "53f851a9583415aa36f60aa4655f04d92e38cc3a679d9205bd93fbc971798e41",
"songs/autumn_leaves_electric.pages/playback-0028.png": "591e475f21cf481cb35278ee065cd808fa141162320d54c1ad589d8ff692143e",
"songs/autumn_leaves_electric.pages/playback-0029.png": "01fe5d38ca9867a442696e3332fb1a5c11ffadcd1e1182ed6164f6caae1c7950",
"songs/autumn_leaves_electric.pages/playback-0030.png": "6cc22a97e81be03b6eef0ef11b0cf8d088336eb19da26a8bb4e366c3615145db",
"songs/autumn_leaves_electric.pages/playback-0031.png": "93a7835d071c07c17d9b3fc50e1e2ff3b19d0b3087f21ef19c18bc622519ded6",
"songs/autumn_leaves_electric.pages/playback-0032.png": "ffdd2864d336f12375e963cb0fd4647992b790d92899f809a17f0aa2941b9324",
"songs/autumn_leaves_electric.pages/playback-0033.png": "8c2097ce48fb79c218aeeda3513025d3c6c3488aaf4682c672cf703f05366b74",
"songs/autumn_leaves_electric.pages/playback-0034.png": "07192aa9420386cb8165b1f43be384b83a7b588b383e57d3905b235acf52d77b",
"songs/autumn_leaves_electric.pages/playback-0035.png": "786e44c54bdb27b095dff2f903df5319e794355f986a2ed86882de78e299b589",
"songs/autumn_leaves_electric.pages/playback-0036.png": "f4d8b82bda3c599dd803b66a5ef1d7ef77f57d5757dafcda168caba6fb8a0399",
"songs/autumn_leaves_electric.pages/playback-0037.png": "d3e999968b05cb703fba44bcb1f3b8cc1b0934ca7513f9e952db41a27d9dbccd",
"songs/autumn_leaves_electric.pages/playback-0038.png": "43773db6f940cdd5457053e9d1cebb1f715d7a9d069fef8f29dd280bc664383f",
"songs/autumn_leaves_electric.pages/playback-0039.png": "da683c3e3d6198a49464d6e9463f9bed2ad9595c6c69ac09adaeeeeddb626003",
"songs/autumn_leaves_electric.pages/playback-0040.png": "1e1fe40450357ed40ab4a688906e62c69306dc706ec53e4c23de94d7228dffc3",
"songs/autumn_leaves_electric.pages/playback-0041.png": "f7c3ab577b12be529b9c73b44521852db930681e6dba1410aae569e526fc4a29",
"songs/autumn_leaves_electric.pages/playback-0042.png": "a827c32cf60d5bafb3b042d36f14883bee389e677091bfc5033771356c164254",
"songs/autumn_leaves_vocal.json": "412981cfd4ea208bbda8de8bbfc2d751164c379340bc02a61233692ef19f1032",
"songs/autumn_leaves_vocal.pages/0001.png": "4b9402563b8ae88a34f25482d5db3d5279643bed60731c7baac69a030b3ce2d9",
"songs/autumn_leaves_vocal.pages/0002.png": "b961fb05e5539ce4335b561df9fc9558b81574872a35956da0beb192a8db258e",
"songs/autumn_leaves_vocal.pages/0003.png": "a3e5423f083d8159e52dd4f1fa1532b982a4290dc3db75921a120fa3befb5c7d",
"songs/autumn_leaves_vocal.pages/0004.png": "9aecc0d1c84bdebdf214c99469ef2fbc7a8e625471b8b06e26c3267dea1dfc42",
"songs/autumn_leaves_vocal.pages/0005.png": "80f3956a196ef56b01647c7daa0d58989c12f04b2ddd8c166cedaf846c1d0f18",
"songs/autumn_leaves_vocal.pages/0006.png": "56e0fcb5faf8c5bd514efd6568284d6548708b4fed7d470c138d9478db34eb77",
"songs/autumn_leaves_vocal.pages/0007.png": "81b80d0a70883a69e10b8f06fd2c2f904670dc3a1cae13b980d239ae4a882e46",
"songs/autumn_leaves_vocal.pages/0008.png": "d55633751f78440bd94f1dbbed846c151cf79bebc5fa4b0f338add0c4b6d2e99",
"songs/autumn_leaves_vocal.pages/0009.png": "2da18b1db70f1cff719f697c39ecda329dbc60d3fe0d605351fcae8583e6901a",
"songs/autumn_leaves_vocal.pages/0010.png": "321d18c54e138248b31a00df6e3a95b9af62588a227a83e56db7475acb923d7a",
"songs/autumn_leaves_vocal.pages/0011.png": "c02c41a2ff2b14ec5bde44f22bec03fd71edf2d9e272d2ef0a8434b843a26b6b",
"songs/autumn_leaves_vocal.pages/0012.png": "ac4bf6573e196d5afbb0049bcc54836a7acac6a1e9e8cce48dedad203f4b43e0",
"songs/autumn_leaves_vocal.pages/0013.png": "6c22d2dde54e4e7d6cd4cef815ff4e4a9738cd48707f344e90e75737939f68ec",
"songs/autumn_leaves_vocal.pages/0014.png": "3b602afdbb2705281dc2dc5120427105649c5df1692155540c9ad9adae3621c8",
"songs/autumn_leaves_vocal.pages/0015.png": "59339718670a3c8d97b6efb7acc3e7234cb26b11e135da9f502689c77824be8a",
"songs/autumn_leaves_vocal.pages/playback-0002.png": "c95138bff07d8dae52535a96bb33cc3a31c6509d7da49c60c35a55f2a1072c4c",
"songs/autumn_leaves_vocal.pages/playback-0003.png": "fb030bc5b0aa831b534223b63b07cbadd7ab76ce9376c3bde8965f09e73d41f8",
"songs/autumn_leaves_vocal.pages/playback-0004.png": "250262570aac9adaff71ddde21a61c65550afb46849016dcd5162cfcedb522da",
"songs/autumn_leaves_vocal.pages/playback-0005.png": "4832b81482f73003ec7e2990f1558ac8d4cc62883ad1c01d8e78837a37a6de6c",
"songs/autumn_leaves_vocal.pages/playback-0006.png": "0b753c25706437aa8b716f9a254e5dfc03c0c441dd2dfc4f038c3fba7528dd19",
"songs/autumn_leaves_vocal.pages/playback-0007.png": "9f9cfa20a8dfa15cd5ac9e0c47f5d6c0632177702229d5f5c7566c315aa99459",
"songs/autumn_leaves_vocal.pages/playback-0008.png": "d525a0af7688927b7cb5f14302ab7151b9e2d0dd83d04d4b53ac7414f8c928e4",
"songs/autumn_leaves_vocal.pages/playback-0009.png": "f6dbfeca5e20dcf475f8da0285632b317759e1d2d4d69a54a4232d853db68004",
"songs/autumn_leaves_vocal.pages/playback-0010.png": "b54d909532c99f359dded5fc2cb11f93309bd8a158b500ee3be524338e471f48",
"songs/autumn_leaves_vocal.pages/playback-0011.png": "619b4621c5ade46fd29e5e70f08c8d421e1916e0578fa2979bcb4a8204dd99d7",
"songs/autumn_leaves_vocal.pages/playback-0012.png": "4472606d66ca6d1a6d490e89f127327e121331eba14c89b1f20e108ed63cb2af",
"songs/autumn_leaves_vocal.pages/playback-0013.png": "525be592c83a6a119c1b2bc8f0eba24de0bac7ac66c56899f10921e959ae7290",
"songs/autumn_leaves_vocal.pages/playback-0014.png": "8f792dd41bad096867edf9e51fcbe5e6773db1256e849cc32b9402aed59ca991",
"songs/autumn_leaves_vocal.pages/playback-0015.png": "2767f2d9979040258f3a619274e6f11fc8e750b7dc3f0d34adbf373455c08200",
"songs/autumn_leaves_vocal.pages/playback-0016.png": "8a61095e966c67c35df99da5fe32d0bb710c9e0443f135585593100a3b76a8bd",
"songs/autumn_leaves_vocal.pages/playback-0017.png": "234f9045d2cc31fc73f1c11bed69641acc5887ac794d051760ba041df5279b8f",
"songs/autumn_leaves_vocal.pages/playback-0018.png": "6d3156bc58fbcd88abc9c5b18e41ab62a7fece8996655c6723a52501869a60a3",
"songs/autumn_leaves_vocal.pages/playback-0019.png": "9851262495f7b0916982a8f46421948068d4208af9ec1d7ff22fd38a80860d24",
"songs/autumn_leaves_vocal.pages/playback-0020.png": "03026a3ab967ac2c963336c475f9ecb3033c799ae28d9e832a2e9d3ac5367c99",
"songs/autumn_leaves_vocal.pages/playback-0021.png": "d72cd7ed7c58cd4fd44372f6dfcef7e91b84b3c74f747bf1fc84a2c81c44da36",
"songs/autumn_leaves_vocal.pages/playback-0022.png": "46ff7fd7202c1378a9598982379d4d7322f5de934280a78eb5cd76a14f6fad35",
"songs/autumn_leaves_vocal.pages/playback-0023.png": "406830c4e11ecc52774bf67351a2d6c3410f2606df206c43cafa44c5beed42cf",
"songs/autumn_leaves_vocal.pages/playback-0024.png": "072dda94b8a251c775663f81b4b264edac9bdb64c67d115fd87ae469eb7b812f",
"songs/autumn_leaves_vocal.pages/playback-0025.png": "b98a8b5879620dfe0bccd063f8d6122fc6826b86b11d07da3aa7abb8a07f265d",
"songs/autumn_leaves_vocal.pages/playback-0026.png": "476255d5814bba2f838f9addc852f080408c24fa3d1c2b549c93da3d59bc11bb",
"songs/autumn_leaves_vocal.pages/playback-0027.png": "0678a2cf026519a06e62c8ef3599a429290092968785e71431584634bba26375",
"songs/autumn_leaves_vocal.pages/playback-0028.png": "d89db0cd8a2435a978a2e11b070c7aaacb3f791542aed8da4b3e17af888ceee5",
"songs/autumn_leaves_vocal.pages/playback-0029.png": "b8ae37ad031080e1a70f8113123e6706455798e51e8dae87913381f846661e08",
"songs/autumn_leaves_vocal.pages/playback-0030.png": "c04d59f0f791b9d813ba659fdb4bd616b649afca3a79b9ed437781f2301f04d8",
"sounds/beat1.wav": "634e73055b4ae05857de54c0cff0bee2ac4476dcde874b474b192512ba113a39",
"sounds/beat2.wav": "9bc510822228479fe4d4fd85be0d3b2f9eac009449f7fd6f916dd54661124c34"
}
+16725
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -11,7 +11,6 @@ import generator.harmonic_time
import generator.solo.types
import generator.solo.placement
import generator.solo.profiles
import generator.solo.decoder
import generator.solo.learned
import generator.solo.harmonic
import dataclasses
@@ -45,6 +44,6 @@ def install(test):
test.enterContext(unittest.mock.patch('generator.production.generate_tree', side_effect=serial))
test.enterContext(unittest.mock.patch('generator.vocabulary.load', return_value=resources))
test.enterContext(unittest.mock.patch('generator.solo.authored.lookup', return_value=None))
test.enterContext(unittest.mock.patch('generator.production._solo_model', return_value=predictor))
test.enterContext(unittest.mock.patch('generator.production._resource_key', return_value={'model': 'synthetic infrastructure fixture'}))
test.enterContext(unittest.mock.patch('generator.composition._solo_model', return_value=predictor))
test.enterContext(unittest.mock.patch('generator.composition._resource_key', return_value={'model': 'synthetic infrastructure fixture'}))
test.enterContext(unittest.mock.patch('generator.solo.composer.compose', side_effect=compose))
+2 -2
View File
@@ -1,6 +1,6 @@
import copy, fractions
import generator.model, generator.parts, generator.harmony
import generator.solo.analysis, generator.solo.composer, generator.solo.types
import generator.source, generator.solo.composer, generator.solo.types
import tests.test_musicxml
import generator.harmonic_time
@@ -20,7 +20,7 @@ def song(meter=4, count=6, chords=('Dm7', 'G7', 'CM7')):
return generator.parts.prepare_document(document)
def compose(document, part='acoustic', cutoff=None):
analysis = generator.solo.analysis.analyze(document)
analysis = generator.source.analyze(document)
measures = document['staves'][0]['measures']
end = sum(generator.model.fraction(m['duration']) for m in measures) if cutoff is None else cutoff
return generator.solo.composer.compose(analysis, measures, part, end)
+5 -5
View File
@@ -1,10 +1,10 @@
import generator.validation
import copy
import fractions
import pathlib
import unittest
import unittest.mock
import generator.accompaniment
import generator.acoustic
import generator.arrange.generate
import generator.engraving.fonts
@@ -41,19 +41,19 @@ def acoustic_document(bars):
for note in notes:
note.update(voice=lane, stem='down' if lane == 'bass' else 'up')
measure['voices'].append({'id': lane, 'notes': notes})
generator.model.validate_document(document)
generator.validation.validate_document(document)
return document
class AcousticTests(unittest.TestCase):
def combine(self, document):
generator.acoustic.prepare(document)
generator.model.validate_document(document)
generator.validation.validate_document(document)
prepared = copy.deepcopy(document)
generator.acoustic.prepare(document)
self.assertEqual(document, prepared)
before = sounding(document)
generator.acoustic.combine(document)
generator.model.validate_document(document)
generator.validation.validate_document(document)
self.assertEqual(sounding(document), before)
self.assertTrue(all(len(m['voices']) == 1 for m in document['staves'][0]['measures']))
for measure in document['staves'][0]['measures']:
@@ -130,7 +130,7 @@ class AcousticTests(unittest.TestCase):
separate = builder(source, 'acoustic')
combined = builder(source, 'acoustic')
self.assertEqual(sounding(separate), sounding(combined))
generator.model.validate_document(combined)
generator.validation.validate_document(combined)
for measure in combined['staves'][0]['measures']:
self.assertIn(len(measure['voices']), (1, 2))
if len(measure['voices']) == 2:
+2 -1
View File
@@ -1,4 +1,5 @@
"""Acoustic attack assignment across musical, rather than storage, boundaries."""
import generator.validation
import copy
import dataclasses
import fractions
@@ -108,7 +109,7 @@ class AcousticPairs(unittest.TestCase):
source['staves'][0]['measures'] = rendered
source['staves'][0].update(arrangement_part_type='acoustic', tablature=True)
generator.acoustic.combine(source)
generator.model.validate_document(source)
generator.validation.validate_document(source)
generator.fretboard.validate_document(source)
generator.acoustic.validate_comping(source)
attacks = [n for n in generator.score.measure_notes(rendered[2]) if n['pitches']]
+3 -4
View File
@@ -1,3 +1,4 @@
import generator.validation
import copy
import fractions
import itertools
@@ -7,11 +8,9 @@ import unittest
import generator.accompaniment
import generator.acoustic
import generator.acoustic_voicing
import generator.arrange.generate
import generator.fretboard
import generator.harmony
import generator.model
import generator.musicxml
import tests.test_acoustic
import tests.test_accompaniment
@@ -79,7 +78,7 @@ class AcousticVoicingTests(unittest.TestCase):
self.assertIn(strings, ([5], [6]))
self.assertFalse(any(k.startswith('_') for k in note))
self.assertTrue(all('tie_from' not in p and 'tie_to' not in p for p in note['pitches']))
generator.model.validate_document(document)
generator.validation.validate_document(document)
return spans
def test_independent_attacks_keep_one_complete_voicing(self):
document = planned_document([[(0, 'FM7')]])
@@ -89,7 +88,7 @@ class AcousticVoicingTests(unittest.TestCase):
before = tests.test_acoustic.sounding(document)
generator.acoustic.combine(document)
self.assertEqual(tests.test_acoustic.sounding(document), before)
generator.model.validate_document(document)
generator.validation.validate_document(document)
def test_four_strings_allow_all_defined_bass_inversions(self):
for symbol in ('CM', 'Dm', 'Caug', 'Cdim', 'C7', 'Cdim7', 'C7no5', 'C7b9', 'C7no1', 'C6', 'C13', 'Csus', 'Csus2'):
with self.subTest(chord=symbol):
+7 -5
View File
@@ -1,11 +1,14 @@
"""The held recording tail has exact score time and no vocal attacks."""
import generator.arrange.form
import generator.composition
import generator.source
import generator.validation
import copy
import fractions
import pathlib
import unittest
import generator.acoustic
import generator.arrange.ending
import generator.arrange.generate
import generator.fretboard
import generator.harmony
@@ -13,7 +16,6 @@ import generator.model
import generator.musicxml
import generator.parts
import generator.performance
import generator.production
import generator.score
import tests.solo_fixtures
@@ -45,7 +47,7 @@ class ArrangementEnding(unittest.TestCase):
self.assertEqual(generator.model.fraction(timing['sustain_start']), 672)
self.assertEqual(generator.model.fraction(timing['duration']), 684)
self.assertEqual(len(timing['clicks']), 672)
generator.model.validate_document(result)
generator.validation.validate_document(result)
generator.fretboard.validate_document(result)
self.assertEqual(source, before)
def test_ending_uses_final_meter_and_does_not_extend_solo_windows(self):
@@ -54,10 +56,10 @@ class ArrangementEnding(unittest.TestCase):
prepared = generator.parts.prepare_document(source)
for part in ('acoustic', 'electric'):
role = part + '_accompaniment'
result = generator.production.generate_role(generator.production.SourceAnalysis.read(prepared), role, pathlib.Path('data'))
result = generator.composition.generate_role(generator.source.SourceAnalysis.read(prepared), role, pathlib.Path('data'))
tail = result['document']['staves'][0]['measures'][-3:]
self.assertEqual([generator.model.fraction(m['duration']) for m in tail], [meter] * 3)
windows = generator.production.windows(prepared, part)
windows = generator.arrange.form.windows(prepared, part)
self.assertEqual(windows[-1][2], role)
self.assertEqual(windows[-1][1] - windows[-1][0], 3 * meter)
self.assertEqual(sum(b - a for a, b, r in windows if r == part + '_solo'), 2 * meter)
+6 -6
View File
@@ -1,9 +1,9 @@
import generator.validation
import copy
import fractions
import pathlib
import unittest
import generator.arrange.form
import generator.arrange.generate
import generator.arrange.pickup
import generator.model
@@ -29,7 +29,7 @@ class ArrangementPickupTests(unittest.TestCase):
for part in generator.parts.PART_TYPES:
with self.subTest(song=path.stem, part=part):
result = generator.arrange.generate.arrange_document(document, part)
generator.model.validate_document(result)
generator.validation.validate_document(result)
measures = result['staves'][0]['measures']
self.assertEqual(len(measures), 171)
self.assertEqual([m['number'] for m in measures], [str(i) for i in range(1, 172)])
@@ -57,7 +57,7 @@ class ArrangementPickupTests(unittest.TestCase):
for part, boundaries in (('vocal', (1, 5)), ('electric', (2,))):
with self.subTest(part=part):
result = generator.arrange.generate._arrange_document(generator.parts.prepare_document(document), part, owned={'melody', 'vocal'})
generator.model.validate_document(result)
generator.validation.validate_document(result)
measures = result['staves'][0]['measures']
self.assertEqual(len(measures), 9)
for boundary in boundaries:
@@ -84,13 +84,13 @@ class ArrangementPickupTests(unittest.TestCase):
self.assertEqual(notes[1]['lyrics'][0]['syllables'][0]['text'], 'doo')
self.assertEqual([generator.model.fraction(n['onset']) for n in notes], [0, 1])
document['staves'][0]['measures'] = [destination]
generator.model.validate_document(document)
generator.validation.validate_document(document)
def test_pickup_harmonies_are_planned_for_every_part_at_the_same_offset(self):
document = source(tests.test_parts.xml_harmony('F') + tests.test_musicxml.xml_note('F', 1, 'quarter'))
prepared = generator.parts.prepare_document(document)
for part in generator.parts.PART_TYPES:
result = generator.arrange.generate.arrange_document(document, part)
generator.model.validate_document(result)
generator.validation.validate_document(result)
for index in (0, 1, 4):
chords = result['staves'][0]['measures'][index]['harmonies']
self.assertEqual([(generator.model.fraction(h['onset']), h['root']['letter']) for h in chords], [(0, 'c'), (3, 'f')])
@@ -102,7 +102,7 @@ class ArrangementPickupTests(unittest.TestCase):
document = source(notes, divisions=3)
for part in ('vocal', 'electric'):
result = generator.arrange.generate.arrange_document(document, part)
generator.model.validate_document(result)
generator.validation.validate_document(result)
spans = result['staves'][0]['tuplets']
self.assertEqual(len(spans), 2 if part == 'vocal' else 1)
self.assertEqual(len({s['id'] for s in spans}), len(spans))
+2 -2
View File
@@ -1,10 +1,10 @@
import generator.production
import tests.production_fixtures
import json
import pathlib
import tempfile
import unittest
import generator.arrange.generate
import tests.solo_fixtures
import tests.test_musicxml
@@ -16,7 +16,7 @@ class CatalogTests(unittest.TestCase):
source, output = pathlib.Path(temporary) / 'songs', pathlib.Path(temporary) / 'output'
source.mkdir()
(source / 'song.xml').write_text(tests.test_musicxml.xml_score('<harmony><root><root-step>E</root-step><root-alter>-1</root-alter></root><kind>major</kind></harmony>' + tests.test_musicxml.xml_note('E', alter=-1), '<key><fifths>-3</fifths></key>'))
self.assertEqual(generator.arrange.generate.generate_performance_scores(source, output), 3)
self.assertEqual(generator.production.generate_tree(source, output), 3)
self.assertEqual({p.name for p in output.iterdir()}, {'song_acoustic.json', 'song_electric.json', 'song_vocal.json'})
for path in output.iterdir():
metadata = json.loads(path.read_text())['metadata']
+5 -6
View File
@@ -1,18 +1,17 @@
"""General chord families and one-voice acoustic rhythm, not song overrides."""
import generator.composition
import generator.validation
import copy
import fractions
import unittest
import generator.acoustic
import generator.accompaniment
import generator.chord_families
import generator.comping
import generator.comping_rhythm
import generator.fretboard
import generator.guitar
import generator.harmony
import generator.model
import generator.production
import generator.vocabulary
import tests.solo_fixtures
import tests.test_acoustic
@@ -89,8 +88,8 @@ class ChordFamilies(unittest.TestCase):
changed = copy.deepcopy(resources)
changed['accompaniment']['instruments']['acoustic']['patterns'][0]['events'][0]['duration'] = '1/2'
for role in ('electric_accompaniment', 'electric_melody', 'electric_solo', 'vocal_melody'):
self.assertEqual(generator.production.dependencies(role, resources), generator.production.dependencies(role, changed))
self.assertNotEqual(generator.production.dependencies('acoustic_accompaniment', resources), generator.production.dependencies('acoustic_accompaniment', changed))
self.assertEqual(generator.composition.dependencies(role, resources), generator.composition.dependencies(role, changed))
self.assertNotEqual(generator.composition.dependencies('acoustic_accompaniment', resources), generator.composition.dependencies('acoustic_accompaniment', changed))
class CombinedAcoustic(unittest.TestCase):
def document(self, bars):
@@ -107,7 +106,7 @@ class CombinedAcoustic(unittest.TestCase):
def convert(self, document):
before = tests.test_acoustic.sounding(document)
generator.acoustic.combine(document)
generator.model.validate_document(document)
generator.validation.validate_document(document)
generator.fretboard.validate_document(document)
self.assertEqual(tests.test_acoustic.sounding(document), before)
self.assertTrue(all(len(m['voices']) == 1 for m in document['staves'][0]['measures']))
+13 -9
View File
@@ -1,4 +1,8 @@
"""Recurring source-position grooves, fills and chord-first bass behavior."""
import generator.composition_types
import generator.composition
import generator.source
import generator.validation
import concurrent.futures
import copy
import dataclasses
@@ -213,7 +217,7 @@ class RecurringGrooves(unittest.TestCase):
source['staves'][0].update(arrangement_part_type='acoustic', tablature=True)
generator.acoustic.validate_comping(source)
generator.acoustic.combine(source)
generator.model.validate_document(source)
generator.validation.validate_document(source)
generator.fretboard.validate_document(source)
generator.acoustic.validate_comping(source)
first = source['staves'][0]['measures'][0]
@@ -239,26 +243,26 @@ class RecurringGrooves(unittest.TestCase):
r = generator.comping_rhythm.realize(pattern, inputs, 'acoustic', generator.comping_rhythm.melody_contexts(source))
self.assertTrue(all(e.lane == 'combined' for e in r.events))
def test_clean_cached_parallel_and_other_role_settings_keep_groove(self):
source = generator.production.SourceAnalysis.read(tests.solo_fixtures.song(count=10, chords=('C7', 'FM7')))
source = generator.source.SourceAnalysis.read(tests.solo_fixtures.song(count=10, chords=('C7', 'FM7')))
root = pathlib.Path('data').resolve()
roles = ('acoustic_accompaniment', 'electric_accompaniment')
clean = {r: generator.production.generate_role(source, r, root) for r in roles}
clean = {r: generator.composition.generate_role(source, r, root) for r in roles}
with tempfile.TemporaryDirectory() as directory:
for r in roles:
cold = generator.production.generate_role(source, r, root, cache_dir=directory)
warm = generator.production.generate_role(source, r, root, cache_dir=directory)
cold = generator.composition.generate_role(source, r, root, cache_dir=directory)
warm = generator.composition.generate_role(source, r, root, cache_dir=directory)
self.assertTrue(warm['cache_hit'])
self.assertEqual(clean[r]['document'], cold['document'])
self.assertEqual(clean[r]['document'], warm['document'])
generator.production.generate_role(source, roles[1], root, {'seed': 'different'}, directory)
self.assertEqual(clean[roles[0]]['document'], generator.production.generate_role(source, roles[0], root, cache_dir=directory)['document'])
generator.composition.generate_role(source, roles[1], root, {'seed': 'different'}, directory)
self.assertEqual(clean[roles[0]]['document'], generator.composition.generate_role(source, roles[0], root, cache_dir=directory)['document'])
with concurrent.futures.ProcessPoolExecutor(max_workers=2, mp_context=multiprocessing.get_context('spawn')) as pool:
futures = {r: pool.submit(generator.production._job, (source, r, root, {}, None)) for r in reversed(roles)}
futures = {r: pool.submit(generator.production._job, generator.composition_types.RoleRequest(source, r, root)) for r in reversed(roles)}
for r, future in futures.items():
self.assertEqual(clean[r]['document'], future.result()['document'])
def test_reuse_audit_checks_rests_and_all_six_roles(self):
source = tests.solo_fixtures.song(count=2, chords=('C7',))
for role in generator.production.ROLES:
for role in generator.composition_types.ROLES:
part = role.split('_')[0]
wanted = 'slash' if role.endswith('_solo') else 'vocal' if part == 'vocal' else 'melody' if role.endswith('_melody') else 'accompaniment'
section = generator.arrange.form.ArrangementSection('Repeated role', 0, 2, 1, wanted)
+11 -8
View File
@@ -1,4 +1,7 @@
"""Meter-specific vocabulary and source-derived quintuple grouping."""
import generator.composition
import generator.harmony
import generator.source
import concurrent.futures
import copy
import fractions
@@ -159,15 +162,15 @@ class CompingMeters(unittest.TestCase):
def test_unused_meter_and_other_role_data_do_not_reseed(self):
resources = generator.vocabulary.load()
measures = self.source(4)['staves'][0]['measures']
baseline = generator.production.dependencies('acoustic_accompaniment', resources, measures)
baseline = generator.composition.dependencies('acoustic_accompaniment', resources, measures)
changed = copy.deepcopy(resources)
for role, policy in changed['accompaniment']['instruments'].items():
for p in policy['patterns']:
if role == 'electric' or p['meter'] in ('3/4', '5/4'):
p['events'] = []
self.assertEqual(baseline, generator.production.dependencies('acoustic_accompaniment', changed, measures))
self.assertEqual(baseline, generator.composition.dependencies('acoustic_accompaniment', changed, measures))
changed['accompaniment']['instruments']['acoustic']['patterns'][0]['events'] = []
self.assertNotEqual(baseline, generator.production.dependencies('acoustic_accompaniment', changed, measures))
self.assertNotEqual(baseline, generator.composition.dependencies('acoustic_accompaniment', changed, measures))
def test_chord_changes_anticipations_and_cross_bar_ties(self):
for beats, grouping in ((4, ()), (3, ()), (5, (3, 2)), (5, (2, 3))):
source = self.source(beats, 2, grouping)
@@ -188,18 +191,18 @@ class CompingMeters(unittest.TestCase):
self.assertTrue(anticipated)
def test_roles_are_deterministic_alone_cached_and_parallel(self):
for meter in (3, 5):
source = generator.production.SourceAnalysis.read(self.source(meter, 9))
source = generator.source.SourceAnalysis.read(self.source(meter, 9))
roles = ('acoustic_accompaniment', 'electric_accompaniment')
with tempfile.TemporaryDirectory() as cache:
clean = {r: generator.production.generate_role(source, r, pathlib.Path('data')) for r in roles}
clean = {r: generator.composition.generate_role(source, r, pathlib.Path('data')) for r in roles}
for role in reversed(roles):
cold = generator.production.generate_role(source, role, pathlib.Path('data'), cache_dir=cache)
warm = generator.production.generate_role(source, role, pathlib.Path('data'), cache_dir=cache)
cold = generator.composition.generate_role(source, role, pathlib.Path('data'), cache_dir=cache)
warm = generator.composition.generate_role(source, role, pathlib.Path('data'), cache_dir=cache)
self.assertEqual(clean[role]['document'], cold['document'])
self.assertEqual(cold['document'], warm['document'])
self.assertTrue(warm['cache_hit'])
with concurrent.futures.ProcessPoolExecutor(max_workers=2, mp_context=multiprocessing.get_context('spawn')) as pool:
jobs = {r: pool.submit(generator.production._job, (source, r, pathlib.Path('data'), {}, None)) for r in reversed(roles)}
jobs = {r: pool.submit(generator.production._job, generator.composition_types.RoleRequest(source, r, pathlib.Path('data'))) for r in reversed(roles)}
for role, job in jobs.items():
self.assertEqual(clean[role]['document'], job.result()['document'])
+13 -10
View File
@@ -1,4 +1,8 @@
"""Independent rhythm contracts and their complete sounding durations."""
import generator.composition_types
import generator.arrange.assembly
import generator.composition
import generator.validation
import copy
import dataclasses
import fractions
@@ -14,9 +18,8 @@ import generator.fretboard
import generator.harmonic_time
import generator.harmony
import generator.model
import generator.production
import generator.score
import generator.solo.analysis
import generator.source
import generator.solo.decoder
import generator.solo.harmonic
import generator.solo.learned
@@ -187,17 +190,17 @@ class LayeredComping(unittest.TestCase):
changed = copy.deepcopy(self.source)
changed['metadata']['tempo'] = 240
changed['staves'][0]['measures'][0]['tempos'] = [{'onset': generator.model.rational(0), 'bpm': generator.model.rational(240), 'written': {'type': 'quarter', 'dots': 0}}]
a, b = map(generator.production.SourceAnalysis.read, (self.source, changed))
a, b = map(generator.source.SourceAnalysis.read, (self.source, changed))
self.assertEqual(a.accompaniment_key(), b.accompaniment_key())
for role in ('acoustic_accompaniment', 'electric_accompaniment'):
first = generator.production.generate_role(a, role, pathlib.Path('data'))
second = generator.production.generate_role(b, role, pathlib.Path('data'))
first = generator.composition.generate_role(a, role, pathlib.Path('data'))
second = generator.composition.generate_role(b, role, pathlib.Path('data'))
self.assertEqual(sounding(first['document']), sounding(second['document']))
self.assertEqual(first['details'], second['details'])
self.assertNotEqual(first['trace']['cache_key'], second['trace']['cache_key'])
def test_assembly_keeps_releases_and_canonical_reuse(self):
source = generator.production.SourceAnalysis.read(self.source)
results = {role: generator.production.generate_role(source, role, pathlib.Path('data')) for role in generator.production.ROLES if not role.endswith('_solo')}
source = generator.source.SourceAnalysis.read(self.source)
results = {role: generator.composition.generate_role(source, role, pathlib.Path('data')) for role in generator.composition_types.ROLES if not role.endswith('_solo')}
# Silence in source-defined solo slots is sufficient to test union;
# no generated solo participates in these accompaniment decisions.
for part in ('acoustic', 'electric'):
@@ -207,12 +210,12 @@ class LayeredComping(unittest.TestCase):
for n in v['notes']:
n.update(kind='rest', pitches=[])
results[part + '_solo'] = empty
assembled = generator.production.assemble(source, results)
assembled = generator.arrange.assembly.assemble(source, results)
self.assertTrue(all(len(m['voices']) == 1 for m in assembled['acoustic']['staves'][0]['measures']))
audit = generator.arrange.accompaniment.audit(self.source, assembled)
self.assertGreater(audit['acoustic']['reused_bars'], 0)
for doc in assembled.values():
generator.model.validate_document(doc)
generator.validation.validate_document(doc)
generator.fretboard.validate_document(doc)
def test_combined_voices_reject_conflicting_strings_and_fret_spans(self):
for string, fret in ((5, 3), (2, 10)):
@@ -266,7 +269,7 @@ class SoloRhythmConditioning(unittest.TestCase):
if held:
m = source['staves'][0]['measures'][0]
m['voices'][0]['notes'] = [generator.model.make_event('held', fractions.Fraction(), fractions.Fraction(4), 'note', [{'letter': 'c', 'alter': 0, 'octave': 4}])]
analysis = generator.solo.analysis.analyze(source)
analysis = generator.source.analyze(source)
harmony = generator.solo.harmonic.Harmony(generator.harmonic_time.read(source['staves'][0]))
examples.append({'analysis': analysis, 'harmony': harmony, 'events': [(fractions.Fraction(), fractions.Fraction(duration), 64), (fractions.Fraction(duration), fractions.Fraction(4), 67)]})
model = generator.solo.learned.Model(generator.solo.learned.fit(examples, {'purpose': 'independently authored conditional-rhythm test'}))
+17 -16
View File
@@ -1,4 +1,6 @@
"""Accompaniment geometry and whole-form movement, using authored small cases."""
import generator.composition
import generator.source
import copy
import fractions
import itertools
@@ -14,7 +16,6 @@ import generator.fretboard
import generator.guitar
import generator.harmony
import generator.model
import generator.production
import generator.score
import generator.vocabulary
import tests.solo_fixtures
@@ -43,13 +44,13 @@ class CompingVoicings(unittest.TestCase):
path, _ = generator.comping.select_grips(((low, high),), (costs,), 'electric', 'lower', left=high, right=high)
self.assertEqual(path, (high,))
def test_electric_voicing_policy_does_not_reseed_rhythm(self):
source = generator.production.SourceAnalysis.read(tests.solo_fixtures.song(count=4, chords=('C7', 'FM7', 'Dm7', 'G7')))
source = generator.source.SourceAnalysis.read(tests.solo_fixtures.song(count=4, chords=('C7', 'FM7', 'Dm7', 'G7')))
root = pathlib.Path('data')
current = generator.production.generate_role(source, 'electric_accompaniment', root)
current = generator.composition.generate_role(source, 'electric_accompaniment', root)
with tempfile.TemporaryDirectory() as cache:
settings = {'voicing': {'register_weight': 0, 'high_position_weight': 0}}
other = generator.production.generate_role(source, 'electric_accompaniment', root, settings, cache)
warm = generator.production.generate_role(source, 'electric_accompaniment', root, settings, cache)
other = generator.composition.generate_role(source, 'electric_accompaniment', root, settings, cache)
warm = generator.composition.generate_role(source, 'electric_accompaniment', root, settings, cache)
self.assertTrue(warm['cache_hit'])
self.assertEqual(other['document'], warm['document'])
self.assertNotEqual(current['trace']['cache_key'], other['trace']['cache_key'])
@@ -99,7 +100,7 @@ class CompingVoicings(unittest.TestCase):
with generator.vocabulary.using(resources, 'acoustic_accompaniment', {'voicing': {'register_weight': -1}}), self.assertRaises(ValueError):
generator.comping.local_cost('acoustic', key, high, 53)
def test_production_voicing_policy_changes_cache_not_rhythm_or_electric(self):
source = generator.production.SourceAnalysis.read(tests.solo_fixtures.song(count=6, chords=('Em7b5', 'A7b9', 'Dm7', 'G7', 'Cm7', 'F7')))
source = generator.source.SourceAnalysis.read(tests.solo_fixtures.song(count=6, chords=('Em7b5', 'A7b9', 'Dm7', 'G7', 'Cm7', 'F7')))
def rhythm(result):
return [[(n['onset'], n['duration'], n['written'], n['kind'], len(n['pitches']), tuple((bool(p.get('tie_from')), bool(p.get('tie_to'))) for p in n['pitches'])) for n in generator.score.measure_notes(m)] for m in result['document']['staves'][0]['measures']]
with tempfile.TemporaryDirectory() as directory:
@@ -109,14 +110,14 @@ class CompingVoicings(unittest.TestCase):
data = json.loads(role_path.read_text())
policy = data['roles']['acoustic_accompaniment'].pop('voicing')
role_path.write_text(json.dumps(data))
legacy = generator.production.generate_role(source, 'acoustic_accompaniment', root)
electric = generator.production.generate_role(source, 'electric_accompaniment', root)
legacy = generator.composition.generate_role(source, 'acoustic_accompaniment', root)
electric = generator.composition.generate_role(source, 'electric_accompaniment', root)
data['roles']['acoustic_accompaniment']['voicing'] = policy
role_path.write_text(json.dumps(data))
current = generator.production.generate_role(source, 'acoustic_accompaniment', root)
current = generator.composition.generate_role(source, 'acoustic_accompaniment', root)
settings = {'voicing': {'register_weight': 0, 'high_position_weight': 0, 'comfortable_max_fret': 9}}
old = generator.production.generate_role(source, 'acoustic_accompaniment', root, settings, pathlib.Path(directory) / 'cache')
warm = generator.production.generate_role(source, 'acoustic_accompaniment', root, settings, pathlib.Path(directory) / 'cache')
old = generator.composition.generate_role(source, 'acoustic_accompaniment', root, settings, pathlib.Path(directory) / 'cache')
warm = generator.composition.generate_role(source, 'acoustic_accompaniment', root, settings, pathlib.Path(directory) / 'cache')
self.assertTrue(warm['cache_hit'])
self.assertEqual(old['document'], warm['document'])
self.assertNotEqual(current['document'], old['document'])
@@ -125,7 +126,7 @@ class CompingVoicings(unittest.TestCase):
self.assertEqual(legacy['document'], current['document'])
self.assertEqual(rhythm(current), rhythm(old))
self.assertEqual(current['details']['accompaniment_rhythm'], old['details']['accompaniment_rhythm'])
self.assertEqual(electric['document'], generator.production.generate_role(source, 'electric_accompaniment', root)['document'])
self.assertEqual(electric['document'], generator.composition.generate_role(source, 'electric_accompaniment', root)['document'])
def test_report_balanced_cost_matches_selected_path(self):
canonical = generator.comping.Canonical(tests.solo_fixtures.song(count=4, chords=('Em7b5', 'A7b9', 'Dm7', 'G7')), 'acoustic', 'report')
rows = [r for r in canonical.report if r['tones']]
@@ -242,13 +243,13 @@ class CompingVoicings(unittest.TestCase):
high = generator.comping.Canonical(source, part, 'fixed-rhythm')
self.assertEqual(rhythm(low), rhythm(high))
def test_accompaniment_cache_matches_clean_generation(self):
source = generator.production.SourceAnalysis.read(tests.solo_fixtures.song(count=3))
source = generator.source.SourceAnalysis.read(tests.solo_fixtures.song(count=3))
for part in ('acoustic', 'electric'):
with tempfile.TemporaryDirectory() as directory:
role = part + '_accompaniment'
cold = generator.production.generate_role(source, role, pathlib.Path('data'), cache_dir=directory)
warm = generator.production.generate_role(source, role, pathlib.Path('data'), cache_dir=directory)
clean = generator.production.generate_role(source, role, pathlib.Path('data'))
cold = generator.composition.generate_role(source, role, pathlib.Path('data'), cache_dir=directory)
warm = generator.composition.generate_role(source, role, pathlib.Path('data'), cache_dir=directory)
clean = generator.composition.generate_role(source, role, pathlib.Path('data'))
self.assertTrue(warm['cache_hit'])
self.assertEqual(cold['document'], warm['document'])
self.assertEqual(cold['document'], clean['document'])
+2 -2
View File
@@ -1,10 +1,10 @@
import generator.validation
import copy
import fractions
import pathlib
import unittest
import unittest.mock
import generator.arrange.form
import generator.arrange.generate
import generator.electric
import generator.fretboard
@@ -110,6 +110,6 @@ class ElectricVoicingTests(unittest.TestCase):
before = copy.deepcopy(source)
with unittest.mock.patch('generator.electric.reuse_voicings', side_effect=AssertionError('cross-part grip dependency')) as reuse:
result = generator.arrange.generate.arrange_document(source, 'electric')
generator.model.validate_document(result)
generator.validation.validate_document(result)
reuse.assert_not_called()
self.assertEqual(source, before)
+5 -4
View File
@@ -1,3 +1,4 @@
import generator.score_io
import tests.production_fixtures
import argparse
import contextlib
@@ -19,11 +20,11 @@ import generator.engraving.cache
import generator.engraving.fonts
import generator.engraving.layout
import generator.engraving.pipeline
import generator.engraving.worker
import generator.engraving.raster
import generator.engraving.types
import generator.engraving.validation
import generator.pico_artwork
import generator.score
import tests.solo_fixtures
import tests.test_musicxml
@@ -173,8 +174,8 @@ class EngravingCacheTests(unittest.TestCase):
style = generator.engraving.types.Style()
documents = [self.document, tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('D')))]
pages = [generator.engraving.layout.layout_score(document, style, family).pages[0] for document in documents]
result = generator.engraving.pipeline.GroupResult()
bands = generator.engraving.pipeline.BodyBands(self.root / 'spool', None, result, limit=1)
result = generator.engraving.worker.GroupResult()
bands = generator.engraving.worker.BodyBands(self.root / 'spool', None, result, limit=1)
self.addCleanup(bands.close)
original = None
for index in (0, 1, 0):
@@ -338,7 +339,7 @@ class EngravingCacheTests(unittest.TestCase):
reads.append(path)
return read_text(path, *args, **kwargs)
with unittest.mock.patch.object(pathlib.Path, 'read_text', read):
generator.score.validate_generated_tree(root)
generator.score_io.validate_generated_tree(root)
self.assertCountEqual(reads, sorted(root.rglob('*.json')))
def test_invalid_worker_count_and_overlapping_cache_are_rejected(self):
root = self.root / 'assets'
+10 -11
View File
@@ -1,3 +1,5 @@
import generator.score_io
import generator.validation
import copy
import fractions
import pathlib
@@ -14,9 +16,6 @@ import generator.fretboard
import generator.guitar
import generator.harmony
import generator.model
import generator.score
import generator.solo.analysis
import generator.solo.placement
import tests.solo_fixtures
import tests.test_acoustic_voicing
import tests.test_musicxml
@@ -97,7 +96,7 @@ class FretPolicyTests(unittest.TestCase):
self.assertFalse(generator.fretboard.span_penalty((position(4, 5), position(3, 6))))
before = tests.score_events.timeline(source)
generator.guitar.place_guitar(source)
generator.model.validate_document(source)
generator.validation.validate_document(source)
generator.fretboard.validate_document(source)
self.assertEqual(tests.score_events.timeline(source), before)
self.assertEqual([p['tablature'] for p in measure['voices'][0]['notes'][0]['pitches']], [{'string': 4, 'fret': 5}, {'string': 2, 'fret': 2}])
@@ -114,7 +113,7 @@ class FretPolicyTests(unittest.TestCase):
states = generator.guitar.advance_positions([n], fractions.Fraction(2 * i), tones, states, True)
self.assertEqual(min(s.cost for s in states.values()).extended_grips, 1)
generator.guitar.place_guitar(source)
generator.model.validate_document(source)
generator.validation.validate_document(source)
generator.fretboard.validate_document(source)
self.assertEqual([p['tablature'] for p in pieces[0]['pitches']], [p['tablature'] for p in pieces[1]['pitches']])
# Removing the low anchor early still leaves an illegal high single tail.
@@ -151,7 +150,7 @@ class FretPolicyTests(unittest.TestCase):
source = document(tuple(position(s, f) for s, f in zip((2, 1), frets)))
if valid:
generator.guitar.place_guitar(source)
generator.model.validate_document(source)
generator.validation.validate_document(source)
generator.fretboard.validate_document(source)
else:
with self.assertRaises(ValueError):
@@ -162,7 +161,7 @@ class FretPolicyTests(unittest.TestCase):
source = document((position(2, 12), position(1, 14)))
anchor = tests.test_musicxml.notes(source)[0]['pitches'][0]
anchor.update(duration=generator.model.rational(2), written=generator.model.written_for(fractions.Fraction(2)))
generator.model.validate_document(source)
generator.validation.validate_document(source)
with self.assertRaisesRegex(ValueError, 'held/tied tails'):
generator.fretboard.validate_document(source)
with self.assertRaisesRegex(ValueError, 'no guitar placement'):
@@ -175,12 +174,12 @@ class FretPolicyTests(unittest.TestCase):
a['tie_to'], b['tie_from'] = b['id'], a['id']
source['staves'][0]['measures'][0]['voices'][0]['notes'] = split
generator.guitar.place_guitar(source)
generator.model.validate_document(source)
generator.validation.validate_document(source)
generator.fretboard.validate_document(source)
self.assertEqual([p['tablature'] for p in split[0]['pitches']], [p['tablature'] for p in split[1]['pitches']])
split[0]['pitches'][0].pop('tie_to')
split[1]['pitches'].pop(0)
generator.model.validate_document(source)
generator.validation.validate_document(source)
with self.assertRaisesRegex(ValueError, 'held/tied tails'):
generator.fretboard.validate_document(source)
def test_another_voice_cannot_supply_a_single_notes_anchor(self):
@@ -221,9 +220,9 @@ class FretPolicyTests(unittest.TestCase):
target = document(tuple(position(s, 12) for s in (3, 2, 1)))
reference, note = tests.test_musicxml.notes(source)[0], tests.test_musicxml.notes(target)[0]
generator.electric.reuse_voicings([generator.electric.VoicingLink(note, (reference,))])
generator.model.validate_document(target)
generator.validation.validate_document(target)
with tempfile.TemporaryDirectory() as directory:
output = pathlib.Path(directory) / 'score.json'
with self.assertRaisesRegex(ValueError, 'each sounding chord requires an anchor'):
generator.score.write_json(output, target)
generator.score_io.write_json(output, target)
self.assertFalse(output.exists())
+64
View File
@@ -0,0 +1,64 @@
"""Dependency direction, cache coverage and explicit composition inputs."""
import ast
import json
import pathlib
import subprocess
import sys
import unittest
import unittest.mock
import generator.cache
import generator.composition
import generator.role_cache
import generator.source
import generator.vocabulary
import tests.solo_fixtures
ROOT = pathlib.Path(__file__).resolve().parents[1]
class GeneratorBoundaries(unittest.TestCase):
def test_generator_imports_are_acyclic(self):
files = {'.'.join(path.relative_to(ROOT).with_suffix('').parts): path for path in (ROOT / 'generator').rglob('*.py')}
graph = {name: set(generator.cache.imports(path.read_bytes())) & files.keys() for name, path in files.items()}
visited, active = set(), []
def visit(name):
self.assertNotIn(name, active, ' -> '.join([*active, name]))
if name in visited:
return
active.append(name)
for dependency in graph[name]:
visit(dependency)
active.pop()
visited.add(name)
for name in graph:
visit(name)
def test_composition_import_does_not_load_rendering_or_publication(self):
code = "import generator.composition, sys, json; print(json.dumps(sorted(sys.modules)))"
result = subprocess.run([sys.executable, '-c', code], cwd=ROOT, capture_output=True, text=True, check=True)
forbidden = ('PIL', 'fontTools', 'generator.engraving', 'generator.production', 'generator.score_io')
self.assertFalse([name for name in json.loads(result.stdout) if name.startswith(forbidden)])
def test_all_previously_missing_role_dependencies_change_the_fingerprint(self):
baseline = generator.role_cache.code_key()
read = pathlib.Path.read_bytes
for name in ('solo/policy.py', 'source.py', 'accompaniment.py'):
changed = ROOT / 'generator' / name
with self.subTest(dependency=name):
def contents(path):
return read(path) + (b'\n# changed implementation\n' if path == changed else b'')
with unittest.mock.patch.object(pathlib.Path, 'read_bytes', contents):
self.assertNotEqual(generator.role_cache.code_key(), baseline)
def test_context_owns_immutable_resources_and_survives_caller_mutation(self):
resources = generator.vocabulary.load()
context = generator.vocabulary.CompositionContext.create(resources, 'electric_accompaniment')
expected = context.role()['center']
resources['roles']['roles']['electric_accompaniment']['center'] = 999
self.assertEqual(context.role()['center'], expected)
with self.assertRaises(TypeError):
context.data('roles')['roles']['electric_accompaniment']['center'] = 999
self.assertNotEqual(generator.vocabulary.load()['roles']['roles']['electric_accompaniment']['center'], 999)
def test_role_generation_does_not_consult_ambient_policy(self):
source = generator.source.SourceAnalysis.read(tests.solo_fixtures.song(count=1, chords=('CM7',)))
with unittest.mock.patch('generator.vocabulary.current', side_effect=AssertionError('implicit policy dependency')):
for role in generator.composition_types.ROLES:
with self.subTest(role=role):
generator.composition.generate_role(source, role, ROOT / 'data', {'predict': True})
+9 -7
View File
@@ -1,3 +1,5 @@
import generator.production
import generator.validation
import tests.production_fixtures
import argparse
import copy
@@ -74,7 +76,7 @@ class GuitarTests(unittest.TestCase):
self.assertEqual(document, before)
document['staves'][0]['tablature'] = True
with self.assertRaisesRegex(ValueError, 'invalid generated tablature'):
generator.model.validate_document(document)
generator.validation.validate_document(document)
def test_theory_controls_omissions_and_bass(self):
for symbol in ('CM', 'G7b9', 'FM7#11/A', 'CmM7', 'Cdim7', 'C13', 'Csus', 'Cmadd9'):
with self.subTest(symbol=symbol):
@@ -135,7 +137,7 @@ class GuitarTests(unittest.TestCase):
before = copy.deepcopy(source)
electric = tests.arrangement_fixtures.section_document(source, 'electric')
vocal = tests.arrangement_fixtures.section_document(source, 'vocal')
generator.model.validate_document(electric)
generator.validation.validate_document(electric)
self.assertEqual(source, before)
self.assertEqual([(e['onset'], e['duration'], generator.model.fraction(e['pitch']) - 12) for e in tests.score_events.timeline(electric)['events']], [(e['onset'], e['duration'], generator.model.fraction(e['pitch'])) for e in tests.score_events.timeline(vocal)['events']])
self.assertEqual([(e['pitch'], e['onset'], e['duration']) for e in tests.score_events.timeline(electric)['events']], [(e['pitch'], e['onset'], e['duration']) for e in tests.score_events.timeline(source)['events']])
@@ -156,7 +158,7 @@ class GuitarTests(unittest.TestCase):
self.assertEqual(generator.model.pitch_number(tone), 12 * (octave + 1) + alter)
self.assertEqual(tone['tablature'], tab)
self.assertEqual(generator.model.fraction(tests.score_events.timeline(document)['events'][0]['pitch']), expected)
generator.model.validate_document(document)
generator.validation.validate_document(document)
def test_guitar_normalization_is_idempotent_and_respects_timed_transposition(self):
change = '<attributes><transpose><chromatic>0</chromatic></transpose></attributes>'
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(duration=2, kind='half') + change + tests.test_musicxml.xml_note(duration=2, kind='half')))
@@ -173,7 +175,7 @@ class GuitarTests(unittest.TestCase):
for part in generator.parts.PART_TYPES:
with self.subTest(generator=generate.__name__, part=part):
document = generate(source, part)
generator.model.validate_document(document)
generator.validation.validate_document(document)
written = {p['id']: generator.model.pitch_number(p) for m in document['staves'][0]['measures'] for v in m['voices'] for n in v['notes'] for p in n['pitches']}
events = tests.score_events.timeline(document)['events']
self.assertTrue(events)
@@ -231,7 +233,7 @@ class GuitarTests(unittest.TestCase):
chord['pitches'] = []
chord['_harmony'] = generator.harmony.parse_symbol('CM')
generator.guitar.place_guitar(document)
generator.model.validate_document(document)
generator.validation.validate_document(document)
self.assertEqual(melody['pitches'][0]['tablature'], {'string': 1, 'fret': 12})
self.assertNotIn(1, [p['tablature']['string'] for p in chord['pitches']])
self.assertEqual(len(tests.score_events.timeline(document)['events']), 3)
@@ -256,9 +258,9 @@ class GuitarTests(unittest.TestCase):
source, output = pathlib.Path(temporary) / 'source', pathlib.Path(temporary) / 'output'
source.mkdir()
(source / '0000.xml').write_text(tests.test_musicxml.xml_score('<harmony><root><root-step>E</root-step><root-alter>-1</root-alter></root><kind>major</kind></harmony>' + tests.test_musicxml.xml_note('E', alter=-1), '<key><fifths>-3</fifths></key>'))
self.assertEqual(generator.arrange.generate.generate_performance_scores(source, output), 3)
self.assertEqual(generator.production.generate_tree(source, output), 3)
first = {p.name: p.read_bytes() for p in output.rglob('*.json')}
generator.arrange.generate.generate_performance_scores(source, output)
generator.production.generate_tree(source, output)
self.assertEqual(first, {p.name: p.read_bytes() for p in output.rglob('*.json')})
def test_unplayable_generation_preserves_previous_assets(self):
with tempfile.TemporaryDirectory() as temporary:
+5 -6
View File
@@ -1,15 +1,14 @@
import generator.score
import generator.validation
import copy
import fractions
import unittest
import generator.accompaniment
import generator.acoustic_voicing
import generator.arrange.generate
import generator.fretboard
import generator.guitar
import generator.harmony
import generator.model
import generator.parts
import tests.score_events
import tests.arrangement_fixtures
import tests.test_accompaniment
@@ -20,7 +19,7 @@ class GuitarPartTests(unittest.TestCase):
def section(self, content, part='electric', divisions=1):
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(content, divisions=divisions))
result = tests.arrangement_fixtures.section_document(source, part)
generator.model.validate_document(result)
generator.validation.validate_document(result)
return result
def test_profile_shapes_cover_defining_chord_tones(self):
for symbol in ('CM', 'Cm', 'C7', 'CmM7', 'Csus', 'Cdim7', 'Caug7', 'C7b9', 'C13', 'CM/E'):
@@ -50,7 +49,7 @@ class GuitarPartTests(unittest.TestCase):
self.assertTrue(all({p.midi % 12 for p in c} == {0, 7} for c in candidates))
def test_acoustic_uses_bass_and_three_upper_notes(self):
result = self.section(tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note(), 'acoustic')
generator.model.validate_document(result)
generator.validation.validate_document(result)
generator.fretboard.validate_document(result)
sounding = [n for m in result['staves'][0]['measures'] for n in generator.score.measure_notes(m) if n['pitches']]
self.assertTrue(sounding)
@@ -99,7 +98,7 @@ class GuitarPartTests(unittest.TestCase):
second = tests.test_parts.xml_harmony(root) + tests.test_musicxml.xml_note('E', octave=4, extra='<tie type="stop"/>')
source = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([first, second]))
result = tests.arrangement_fixtures.section_document(source, 'electric')
generator.model.validate_document(result)
generator.validation.validate_document(result)
notes = [n for m in result['staves'][0]['measures'] for v in m['voices'] for n in v['notes']]
self.assertTrue(all(len(n['pitches']) in (3, 4) for n in notes))
self.assertEqual(bool(notes[0]['pitches'][0].get('tie_to')), root == 'C')
+2 -4
View File
@@ -7,15 +7,13 @@ import unittest
import generator.train
import generator.training_io
import generator.vocabulary
import generator.harmonic_time
import generator.harmony
import generator.solo.harmonic
import generator.solo.types
import generator.solo.decoder
import generator.solo.learned
import generator.solo.analysis
import generator.solo.expressions
import generator.source
import tests.solo_fixtures
import generator.musicxml
import generator.parts
@@ -128,7 +126,7 @@ class ContextualHarmony(unittest.TestCase):
class PredictionReachability(unittest.TestCase):
def engine(self, chords):
doc = tests.solo_fixtures.song(count=2, chords=chords)
return generator.solo.decoder.Decoder(generator.solo.analysis.analyze(doc), generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(chords)), generator.solo.learned.Model.load(ROOT / 'data/models/solo.json'), 'acoustic', fractions.Fraction(), fractions.Fraction(8))
return generator.solo.decoder.Decoder(generator.source.analyze(doc), generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(chords)), generator.solo.learned.Model.load(ROOT / 'data/models/solo.json'), 'acoustic', fractions.Fraction(), fractions.Fraction(8))
def test_joint_resolved_color_reaches_the_real_action_builder(self):
d = self.engine(('C7', 'FM7'))
n = d.note(fractions.Fraction(), fractions.Fraction(3, 2), 66)
-1
View File
@@ -6,7 +6,6 @@ import generator.engraving.fonts
import generator.engraving.layout
import generator.engraving.types
import generator.harmony
import generator.model
import generator.musicxml
import generator.musicxml_writer
import generator.pitch
+27 -27
View File
@@ -1,4 +1,9 @@
"""Dependency and music contracts use synthetic sources, never audition scores."""
import generator.composition_types
import generator.arrange.assembly
import generator.composition
import generator.source
import generator.validation
import concurrent.futures
import copy
import contextlib
@@ -13,15 +18,10 @@ import unittest.mock
import generator.production
import generator.model
import generator.vocabulary
import generator.solo.expressions
import generator.solo.types
import generator.solo.harmonic
import generator.solo.placement
import generator.solo.notation
import generator.solo.composer
import tests.solo_fixtures
import tests.solo_fixtures
import tests.solo_fixtures
def music(result):
@@ -30,23 +30,23 @@ def music(result):
class IndependentRoles(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.source = generator.production.SourceAnalysis.read(tests.solo_fixtures.song(count=1, chords=('CM7',)))
cls.source = generator.source.SourceAnalysis.read(tests.solo_fixtures.song(count=1, chords=('CM7',)))
cls.root = pathlib.Path('data').resolve()
def test_alone_order_parallel_and_assembly(self):
serial = {role: generator.production.generate_role(self.source, role, self.root) for role in generator.production.ROLES}
serial = {role: generator.composition.generate_role(self.source, role, self.root) for role in generator.composition_types.ROLES}
with concurrent.futures.ProcessPoolExecutor(max_workers=2, mp_context=multiprocessing.get_context('spawn')) as pool:
jobs = {role: pool.submit(generator.production._job, (self.source, role, self.root, {}, None)) for role in reversed(generator.production.ROLES)}
jobs = {role: pool.submit(generator.production._job, generator.composition_types.RoleRequest(self.source, role, self.root)) for role in reversed(generator.composition_types.ROLES)}
parallel = {role: future.result() for role, future in jobs.items()}
for role in generator.production.ROLES:
for role in generator.composition_types.ROLES:
self.assertEqual(music(serial[role]), music(parallel[role]), role)
for doc in generator.production.assemble(self.source, serial).values():
generator.model.validate_document(doc)
for doc in generator.arrange.assembly.assemble(self.source, serial).values():
generator.validation.validate_document(doc)
def test_role_setting_and_cache_are_independent(self):
with tempfile.TemporaryDirectory() as directory:
a = generator.production.generate_role(self.source, 'acoustic_solo', self.root, cache_dir=directory)
b = generator.production.generate_role(self.source, 'electric_solo', self.root, {'seed': 'changed'}, directory)
again = generator.production.generate_role(self.source, 'acoustic_solo', self.root, cache_dir=directory)
clean = generator.production.generate_role(self.source, 'acoustic_solo', self.root)
a = generator.composition.generate_role(self.source, 'acoustic_solo', self.root, cache_dir=directory)
b = generator.composition.generate_role(self.source, 'electric_solo', self.root, {'seed': 'changed'}, directory)
again = generator.composition.generate_role(self.source, 'acoustic_solo', self.root, cache_dir=directory)
clean = generator.composition.generate_role(self.source, 'acoustic_solo', self.root)
self.assertTrue(again['cache_hit'])
self.assertEqual(music(a), music(again))
self.assertEqual(music(a), music(clean))
@@ -60,7 +60,7 @@ class IndependentRoles(unittest.TestCase):
self.assertNotIn('generated_assets', str(path))
return original(path, *args, **kwargs)
with unittest.mock.patch.object(pathlib.Path, 'open', guarded):
result = generator.production.generate_role(self.source, 'acoustic_solo', self.root)
result = generator.composition.generate_role(self.source, 'acoustic_solo', self.root)
self.assertNotIn('reference', json.dumps(result['trace']))
def test_titles_and_export_ids_do_not_supply_musical_identity(self):
document = tests.solo_fixtures.song(count=1, chords=('CM7',))
@@ -69,7 +69,7 @@ class IndependentRoles(unittest.TestCase):
for m in other['staves'][0]['measures']:
m['id'] = 'exporter_123'
m['number'] = '99'
self.assertEqual(generator.production.SourceAnalysis.read(document), generator.production.SourceAnalysis.read(other))
self.assertEqual(generator.source.SourceAnalysis.read(document), generator.source.SourceAnalysis.read(other))
def test_reference_presence_and_changes_do_not_change_clean_music(self):
# No repository reference is a fixture or prerequisite. These deliberately
# invalid optional files test the absence of a discovery/fallback branch.
@@ -78,16 +78,16 @@ class IndependentRoles(unittest.TestCase):
reference_dir.mkdir()
references = [reference_dir / (source_name + '-solo-' + str(index) + suffix + '.xml')
for source_name in ('all-of-me', 'autumn-leaves') for index in (1, 2) for suffix in ('', '(4)')]
before = generator.production.generate_role(self.source, 'electric_solo', self.root)
before = generator.composition.generate_role(self.source, 'electric_solo', self.root)
for reference in references:
reference.write_text('<not-a-production-source/>')
present = generator.production.generate_role(self.source, 'electric_solo', self.root)
present = generator.composition.generate_role(self.source, 'electric_solo', self.root)
for reference in references:
reference.write_text('changed optional target, still not MusicXML')
changed = generator.production.generate_role(self.source, 'electric_solo', self.root)
changed = generator.composition.generate_role(self.source, 'electric_solo', self.root)
for reference in references:
reference.unlink()
absent = generator.production.generate_role(self.source, 'electric_solo', self.root)
absent = generator.composition.generate_role(self.source, 'electric_solo', self.root)
self.assertTrue(all(music(before) == music(v) for v in (present, changed, absent)))
def test_original_melody_changes_the_theme_with_unchanged_harmony(self):
source = tests.solo_fixtures.song(count=4, chords=('CM7',))
@@ -97,11 +97,11 @@ class IndependentRoles(unittest.TestCase):
for n in v['notes']:
for p in n['pitches']:
p['letter'] = {'c': 'e', 'd': 'f', 'e': 'g', 'f': 'a'}[p['letter']]
a = generator.production.generate_role(generator.production.SourceAnalysis.read(source), 'acoustic_solo', self.root)
b = generator.production.generate_role(generator.production.SourceAnalysis.read(changed), 'acoustic_solo', self.root)
a = generator.composition.generate_role(generator.source.SourceAnalysis.read(source), 'acoustic_solo', self.root)
b = generator.composition.generate_role(generator.source.SourceAnalysis.read(changed), 'acoustic_solo', self.root)
self.assertNotEqual(a['details']['phrases'][0]['notes'], b['details']['phrases'][0]['notes'])
def test_source_tuplet_lanes_survive_assembly(self):
serial = {r: generator.production.generate_role(self.source, r, self.root) for r in generator.production.ROLES}
serial = {r: generator.composition.generate_role(self.source, r, self.root) for r in generator.composition_types.ROLES}
# Exercise a different original lane name, as in the imported Autumn head.
doc = serial['electric_melody']['document']
staff = doc['staves'][0]
@@ -117,10 +117,10 @@ class IndependentRoles(unittest.TestCase):
n['voice'] = 'voice_source'
for t in staff['tuplets']:
t['voice'] = 'voice_source'
generator.model.validate_document(doc)
generator.model.validate_document(generator.production.assemble(self.source, serial)['electric'])
generator.validation.validate_document(doc)
generator.validation.validate_document(generator.arrange.assembly.assemble(self.source, serial)['electric'])
def test_sparse_parts_reuse_the_canonical_form(self):
result = generator.production.generate_role(generator.production.SourceAnalysis.read(tests.solo_fixtures.song(count=4, chords=('Dm7', 'G7', 'CM7'))), 'acoustic_accompaniment', self.root)
result = generator.composition.generate_role(generator.source.SourceAnalysis.read(tests.solo_fixtures.song(count=4, chords=('Dm7', 'G7', 'CM7'))), 'acoustic_accompaniment', self.root)
measures = result['document']['staves'][0]['measures']
def signature(m):
return [(n['onset'], n['duration'], [(generator.model.pitch_number(p), p['tablature']) for p in n['pitches']]) for v in m['voices'] for n in v['notes']]
+2 -1
View File
@@ -1,3 +1,4 @@
import generator.validation
import copy
import fractions
import pathlib
@@ -43,7 +44,7 @@ class KeyCancellationTests(unittest.TestCase):
source = key_change(old, new, local)
before = copy.deepcopy(source)
result = instrument_score(source, *({'A': (1, 2), 'Ab': (-1, -2), 'G': (1, 2), 'F': (-1, -2)}[target]))
generator.model.validate_document(result)
generator.validation.validate_document(result)
measures = result['staves'][0]['measures']
self.assertEqual(measures[0]['key'], generator.model.key_from_fifths(expected_old))
change = measures[0]['attributes'][0] if local else measures[1]
+12 -12
View File
@@ -1,4 +1,5 @@
"""Learned inference and explicit authored reuse have different contracts."""
import generator.composition
import copy
import dataclasses
import json
@@ -8,7 +9,6 @@ import tempfile
import unittest
import unittest.mock
import generator.production
import generator.parts
import generator.musicxml
import generator.model
@@ -21,7 +21,7 @@ import generator.solo.learned
import generator.solo.composer
import generator.solo.decoder
import generator.solo.harmonic
import generator.solo.analysis
import generator.source
import tests.score_events
import tests.solo_fixtures
import tests.test_musicxml
@@ -53,9 +53,9 @@ class TrainingAndRepertoire(unittest.TestCase):
with self.subTest(pair=entry['id']):
part = 'acoustic' if entry['slot'] == 1 else 'electric'
doc = generator.parts.prepare_document(generator.musicxml.load_musicxml(ROOT / 'data/training' / entry['source']))
source = generator.production.SourceAnalysis.read(doc)
source = generator.source.SourceAnalysis.read(doc)
with unittest.mock.patch('generator.solo.composer.compose', side_effect=AssertionError('authored pair must not be predicted')):
result = generator.production.generate_role(source, part + '_solo', ROOT / 'data')
result = generator.composition.generate_role(source, part + '_solo', ROOT / 'data')
measures = result['document']['staves'][0]['measures']
first = next(i for i, m in enumerate(measures) if m.get('section_label') == part.capitalize() + ' Solo')
last = next((i for i in range(first + 1, len(measures)) if measures[i].get('section_label')), len(measures))
@@ -82,13 +82,13 @@ class TrainingAndRepertoire(unittest.TestCase):
def test_lookup_uses_source_music_and_slot_not_title_or_path(self):
entry = json.loads((ROOT / 'data/training/manifest.json').read_text())['examples'][0]
doc = generator.parts.prepare_document(generator.musicxml.load_musicxml(ROOT / 'data/training' / entry['source']))
source = generator.production.SourceAnalysis.read(doc)
source = generator.source.SourceAnalysis.read(doc)
part = 'acoustic' if entry['slot'] == 1 else 'electric'
self.assertIsNotNone(generator.solo.authored.lookup(source, part, ROOT / 'data'))
doc['metadata'].update(title='Renamed standard', composer='Display only')
self.assertEqual(source, generator.production.SourceAnalysis.read(doc))
self.assertEqual(source, generator.source.SourceAnalysis.read(doc))
doc['staves'][0]['measures'][0]['voices'][0]['notes'][0]['pitches'][0]['octave'] += 1
self.assertIsNone(generator.solo.authored.lookup(generator.production.SourceAnalysis.read(doc), part, ROOT / 'data'))
self.assertIsNone(generator.solo.authored.lookup(generator.source.SourceAnalysis.read(doc), part, ROOT / 'data'))
def test_adding_a_new_pair_requires_no_code_or_existing_song_names(self):
with tempfile.TemporaryDirectory() as directory:
root = pathlib.Path(directory)
@@ -102,7 +102,7 @@ class TrainingAndRepertoire(unittest.TestCase):
examples, _ = generator.train.load_examples(manifest)
self.assertEqual(len(examples), 1)
doc = generator.parts.prepare_document(generator.musicxml.load_musicxml(root / 'data/songs' / source.name))
context = generator.production.SourceAnalysis.read(doc)
context = generator.source.SourceAnalysis.read(doc)
self.assertIsNotNone(generator.solo.authored.lookup(context, 'acoustic', root / 'data'))
self.assertIsNone(generator.solo.authored.lookup(context, 'electric', root / 'data'))
self.assertEqual(solo.read_bytes(), (manifest.parent / solo.name).read_bytes())
@@ -130,7 +130,7 @@ class LearnedInference(unittest.TestCase):
generator.solo.learned.Model.load('/missing-model-file')
def test_decoder_runs_without_training_files_and_is_deterministic(self):
source = tests.solo_fixtures.song(count=1, chords=('C7',))
a = generator.solo.analysis.analyze(source)
a = generator.source.analyze(source)
predictor = generator.solo.learned.Model.load(ROOT / 'data/models/solo.json')
with generator.vocabulary.using(generator.vocabulary.load(), 'acoustic_solo', {'attempts': 2}):
original = pathlib.Path.open
@@ -146,7 +146,7 @@ class LearnedInference(unittest.TestCase):
self.assertEqual(first.diagnostics['construction'], 'learned_joint_events')
def test_event_score_does_not_depend_on_action_grouping(self):
source = tests.solo_fixtures.song(count=1, chords=('C7',))
a = generator.solo.analysis.analyze(source)
a = generator.source.analyze(source)
h = generator.solo.harmonic.Harmony(generator.harmonic_time.read(source['staves'][0]))
predictor = generator.solo.learned.Model.load(ROOT / 'data/models/solo.json')
engine = generator.solo.decoder.Decoder(a, h, predictor, 'acoustic', fractions.Fraction(), fractions.Fraction(4))
@@ -157,7 +157,7 @@ class LearnedInference(unittest.TestCase):
self.assertEqual(combined, split)
def test_source_features_include_actual_melodic_intervals(self):
source = tests.solo_fixtures.song(count=1, chords=('C7',))
a = generator.solo.analysis.analyze(source)
a = generator.source.analyze(source)
changed = dataclasses.replace(a, melody=tuple(dataclasses.replace(n, pitch=dataclasses.replace(n.pitch, octave=n.pitch.octave + (i == 1))) for i, n in enumerate(a.melody)))
self.assertNotEqual(generator.solo.learned.source_feature(a, fractions.Fraction(1)), generator.solo.learned.source_feature(changed, fractions.Fraction(1)))
def test_generated_long_nonchord_notes_still_need_support(self):
@@ -167,7 +167,7 @@ class LearnedInference(unittest.TestCase):
self.assertTrue(h.evaluate((tests.solo_fixtures.note(0, fractions.Fraction(1, 2), 'g'), tests.solo_fixtures.note(fractions.Fraction(1, 2), 1, 'f'), tests.solo_fixtures.note(1, 2, 'e'))).valid)
def test_held_chord_member_can_become_a_supported_color(self):
source = tests.solo_fixtures.song(count=2, chords=('C', 'Dm'))
a = generator.solo.analysis.analyze(source)
a = generator.source.analyze(source)
h = generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(('C', 'Dm')))
engine = generator.solo.decoder.Decoder(a, h, generator.solo.learned.Model.load(ROOT / 'data/models/solo.json'), 'acoustic', fractions.Fraction(), fractions.Fraction(8))
n = engine.note(fractions.Fraction(3), fractions.Fraction(6), 64) # E: C's third becomes D minor's ninth.
+7 -6
View File
@@ -1,3 +1,4 @@
import generator.validation
import tests.production_fixtures
import argparse
import fractions
@@ -73,16 +74,16 @@ class MusicModelTests(unittest.TestCase):
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note()))
tests.test_musicxml.notes(document)[0]['pitches'][0]['tie_to'] = 'missing'
with self.assertRaisesRegex(ValueError, 'dangling tie'):
generator.model.validate_document(document)
generator.validation.validate_document(document)
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note()))
tests.test_musicxml.notes(document)[0]['duration'] = {'numerator': 8, 'denominator': 2}
with self.assertRaisesRegex(ValueError, 'reduced'):
generator.model.validate_document(document)
generator.validation.validate_document(document)
def test_optional_turnaround_and_form(self):
source = generator.musicxml.load_musicxml(pathlib.Path('data/songs/all-of-me.xml'))
for part in ('acoustic', 'electric', 'vocal'):
result = generator.arrange.generate.arrange_document(source, part)
generator.model.validate_document(result)
generator.validation.validate_document(result)
measures = result['staves'][0]['measures']
self.assertEqual([m['section_label'] for m in measures if m.get('section_label')], ['Rhythm Lead-in', 'Vocal Melody', 'Guitar Melody', 'Acoustic Solo', 'Electric Solo', 'Vocal Melody', 'Ending'])
self.assertFalse(measures[-1]['harmonies'])
@@ -96,12 +97,12 @@ class MusicModelTests(unittest.TestCase):
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note()))
source['staves'][0]['measures'][0][field] = {}
with self.assertRaisesRegex(ValueError, 'repeat notation'):
generator.model.validate_document(source)
generator.validation.validate_document(source)
for field in ('sound_ties', 'printed_ties'):
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note()))
tests.test_musicxml.notes(source)[0]['pitches'][0][field] = []
with self.assertRaisesRegex(ValueError, 'pass-specific tie'):
generator.model.validate_document(source)
generator.validation.validate_document(source)
def test_failure_preserves_generated_assets(self):
with tempfile.TemporaryDirectory() as temp:
root = pathlib.Path(temp)
@@ -149,7 +150,7 @@ class MusicModelTests(unittest.TestCase):
source = tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(duration=1, kind='quarter', extra='<tie type="start"/>'), implicit=True)
source = source.replace('</measure></part>', '</measure><measure number="2">' + tests.test_musicxml.xml_note(extra='<tie type="stop"/>') + '</measure></part>')
result = generator.arrange.generate.arrange_document(tests.test_musicxml.parse(source), 'vocal')
generator.model.validate_document(result)
generator.validation.validate_document(result)
measures = result['staves'][0]['measures']
start = measures[0]['voices'][0]['notes'][-1]['pitches'][0]
end = measures[1]['voices'][0]['notes'][0]['pitches'][0]
+20
View File
@@ -0,0 +1,20 @@
"""Exact role scores protect musical choices, notation, IDs and tablature."""
import generator.composition_types
import generator.composition
import generator.source
import json
import pathlib
import unittest
import tests.solo_fixtures
class OutputContract(unittest.TestCase):
def test_complete_roles_including_learned_fallback_match_reviewed_output(self):
root = pathlib.Path(__file__).resolve().parents[1]
expected = json.loads((root / 'tests/fixtures/role_outputs.json').read_text())
source = generator.source.SourceAnalysis.read(tests.solo_fixtures.song(count=2, chords=('Dm7', 'G7')))
for role in generator.composition_types.ROLES:
with self.subTest(role=role):
settings = {'predict': True} if role.endswith('_solo') else {}
actual = generator.composition.generate_role(source, role, root / 'data', settings)['document']
self.assertEqual(actual, expected[role])
+9 -8
View File
@@ -1,3 +1,6 @@
import generator.production
import generator.score
import generator.validation
import tests.production_fixtures
import argparse
import copy
@@ -10,11 +13,9 @@ import unittest.mock
import xml.etree.ElementTree
import generator.__main__
import generator.accompaniment
import generator.arrange.generate
import generator.model
import generator.musicxml
import generator.parts
import generator.pitch
import tests.score_events
import tests.arrangement_fixtures
@@ -37,7 +38,7 @@ class PartGenerationTests(unittest.TestCase):
def test_dense_melody_keeps_rhythmic_accompaniment_without_sustaining_across_changes(self):
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(xml_harmony() + xml_harmony('F', offset=3) + ''.join(tests.test_musicxml.xml_note(duration=1, kind='eighth') for _ in range(8)), divisions=2))
for result in (tests.arrangement_fixtures.section_document(source, 'acoustic'), generator.arrange.generate.arrange_document(source, 'electric')):
generator.model.validate_document(result)
generator.validation.validate_document(result)
for measure in result['staves'][0]['measures']:
events = measure['voices'][0]['notes']
if measure.get('ending_sustain'):
@@ -72,13 +73,13 @@ class PartGenerationTests(unittest.TestCase):
self.assertTrue(all(generator.model.pitch_number(p) % 12 in {0, 5, 9} for n in carried for p in n['pitches']))
for index in (2, 3):
self.assertTrue(all(n['kind'] == 'rest' and not n['pitches'] for n in tests.test_musicxml.notes(result, index)))
generator.model.validate_document(result)
generator.validation.validate_document(result)
def test_sparse_accompaniment_preserves_form_and_intentional_space(self):
bar = xml_harmony() + ''.join(tests.test_musicxml.xml_note(duration=1, kind='quarter') for _ in range(4))
xml = tests.test_musicxml.xml_score(bar).replace('</part>', ''.join(f'<measure number="{i}">{bar}</measure>' for i in range(2, 13)) + '</part>')
result = tests.arrangement_fixtures.section_document(tests.test_musicxml.parse(xml), 'acoustic')
self.assertEqual(len(result['staves'][0]['measures']), 12)
generator.model.validate_document(result)
generator.validation.validate_document(result)
self.assertTrue(tests.score_events.timeline(result)['events'])
self.assertTrue(any(n['kind'] == 'rest' for m in result['staves'][0]['measures'] for n in generator.score.measure_notes(m)))
def test_melody_written_pitch_rhythm_rests_and_cross_bar_ties_are_preserved(self):
@@ -97,7 +98,7 @@ class PartGenerationTests(unittest.TestCase):
for part in ('electric', 'vocal'):
with self.subTest(builder=builder.__module__, part=part):
result = builder(source, part)
generator.model.validate_document(result)
generator.validation.validate_document(result)
measures = result['staves'][0]['measures']
starts = [0] if builder is tests.arrangement_fixtures.section_document else [i for i, m in enumerate(measures) if m.get('section_label') == ('Guitar Melody' if part == 'electric' else 'Vocal Melody')]
self.assertTrue(starts)
@@ -131,7 +132,7 @@ class PartGenerationTests(unittest.TestCase):
for name in names:
(source / (name + '.xml')).write_text(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note()))
with self.assertRaisesRegex(ValueError, 'conflicting output path') as raised:
generator.arrange.generate.generate_performance_scores(source, output)
generator.production.generate_tree(source, output)
for name in names:
self.assertIn(str(source / (name + '.xml')), str(raised.exception))
self.assertEqual([p.name for p in output.iterdir()], ['sentinel'])
@@ -148,7 +149,7 @@ class PartGenerationTests(unittest.TestCase):
unittest.mock.patch('generator.engraving.pipeline.engrave_tree'),
unittest.mock.patch('generator.pico_artwork.generate_titles'),
unittest.mock.patch('generator.catalog.build_catalog'),
unittest.mock.patch('generator.performance.copy_audio', return_value=[]),
unittest.mock.patch('generator.audio_assets.copy_audio', return_value=[]),
):
generator.__main__.generate_assets(args)
# Role catalog loading is replaced by this infrastructure fixture. The
+7 -6
View File
@@ -1,3 +1,4 @@
import generator.audio_assets
import argparse
import copy
import hashlib
@@ -41,7 +42,7 @@ def fixture(root):
with wave.open(str(path), 'wb') as output:
output.setparams((2, 2, 48000, 0, 'NONE', ''))
output.writeframes(value.to_bytes(2, 'little', signed=True) * 2 * frames)
entries.append(generator.performance.describe_wav(path, root, song_id=song_id, bpm=bpm))
entries.append(generator.audio_assets.describe_wav(path, root, song_id=song_id, bpm=bpm))
(root / 'manifest.audio').write_text(json.dumps(entries))
class PerformanceTests(unittest.TestCase):
@@ -49,7 +50,7 @@ class PerformanceTests(unittest.TestCase):
source = pathlib.Path('package/assets')
with tempfile.TemporaryDirectory() as temporary:
root = pathlib.Path(temporary)
entries = generator.performance.copy_audio(source, root, {'all_of_me', 'autumn_leaves'})
entries = generator.audio_assets.copy_audio(source, root, {'all_of_me', 'autumn_leaves'})
self.assertEqual(len(entries), 4)
for entry in entries:
self.assertEqual((source / entry['path']).read_bytes(), (root / entry['path']).read_bytes())
@@ -61,10 +62,10 @@ class PerformanceTests(unittest.TestCase):
path = root / 'backing/fixture_120.wav'
path.write_bytes(path.read_bytes()[:-2])
with self.assertRaisesRegex(ValueError, 'truncated'):
generator.performance.describe_wav(path, root)
generator.audio_assets.describe_wav(path, root)
path.rename(root / 'backing/fixture_115.wav')
with self.assertRaisesRegex(ValueError, 'unsupported'):
generator.performance.copy_audio(root, root / 'out', {'fixture'})
generator.audio_assets.copy_audio(root, root / 'out', {'fixture'})
def test_short_backing_cannot_publish(self):
with tempfile.TemporaryDirectory() as temporary:
root = pathlib.Path(temporary)
@@ -73,10 +74,10 @@ class PerformanceTests(unittest.TestCase):
bundle = json.loads((root / 'score.json').read_text())
(root / 'songs/fixture.json').write_text(json.dumps(bundle['music']))
entries = json.loads((root / 'manifest.audio').read_text())
generator.performance.validate_recording_lengths(root, entries)
generator.audio_assets.validate_recording_lengths(root, entries)
entries[-1]['frames'] = 1
with self.assertRaisesRegex(ValueError, 'shorter'):
generator.performance.validate_recording_lengths(root, entries)
generator.audio_assets.validate_recording_lengths(root, entries)
def test_page_coverage_and_measure_accents(self):
with tempfile.TemporaryDirectory() as temporary:
root = pathlib.Path(temporary)
+3 -4
View File
@@ -1,4 +1,5 @@
"""Build-only print layout, PDF contents and reproducible cache behavior."""
import generator.score_io
import copy
import dataclasses
import io
@@ -14,11 +15,9 @@ import PIL.PdfParser
import generator.arrange.generate
import generator.engraving.audit
import generator.engraving.fonts
import generator.engraving.types
import generator.model
import generator.musicxml
import generator.printing
import generator.score
import tests.solo_fixtures
import tests.test_musicxml
@@ -35,7 +34,7 @@ class PrintableParts(unittest.TestCase):
def tree(self, name, document=None):
root = self.root / name
(root / 'songs').mkdir(parents=True)
generator.score.write_json(root / 'songs/part.json', document or self.source)
generator.score_io.write_json(root / 'songs/part.json', document or self.source)
return root
def test_letter_pagination_preserves_complete_order_without_transition_duplicates(self):
source = tests.solo_fixtures.song(count=32, chords=('C7',))
@@ -137,7 +136,7 @@ class PrintableParts(unittest.TestCase):
generator.printing.generate_tree(root, self.font_path, cache_dir=cache)
changed = copy.deepcopy(self.source)
changed['metadata']['title'] = 'Another title'
generator.score.write_json(root / 'songs/part.json', changed)
generator.score_io.write_json(root / 'songs/part.json', changed)
with unittest.mock.patch('generator.printing.write_pdf', wraps=generator.printing.write_pdf) as render:
generator.printing.generate_tree(root, self.font_path, cache_dir=cache)
render.assert_called_once()
+7 -5
View File
@@ -4,7 +4,9 @@ import unittest
import unittest.mock
import modulus.artifacts
import modulus.cli
import modulus.build_context
import modulus.packaging
import modulus.process
import modulus.config
import modulus.python
@@ -13,17 +15,17 @@ class PackagePublishingTests(unittest.TestCase):
root = pathlib.Path(__file__).resolve().parents[1]
with tempfile.TemporaryDirectory() as temporary:
directory = pathlib.Path(temporary)
context = modulus.cli.build_context(directory, modulus.config.load_repo_config(root))
context = modulus.build_context.build_context(directory, modulus.config.load_repo_config(root))
def fake_build(context, command, **kwargs):
if command[:2] == ['cmake', '--install']:
stage = pathlib.Path(command[2]).parent / 'stage'
(stage / 'manifest.json').write_text('{}\n')
return 0
with unittest.mock.patch.object(modulus.cli, 'in_docker', return_value=True), \
unittest.mock.patch.object(modulus.cli, 'run', side_effect=fake_build), \
with unittest.mock.patch.object(modulus.process, 'in_docker', return_value=True), \
unittest.mock.patch.object(modulus.process, 'run', side_effect=fake_build), \
unittest.mock.patch.object(modulus.python, 'interpreter', return_value=pathlib.Path('/usr/bin/python3')), \
unittest.mock.patch.object(modulus.artifacts, 'main') as publish:
self.assertEqual(modulus.cli.package_artifacts(context, ['--no-remote']), 0)
self.assertEqual(modulus.packaging.package_artifacts(context, ['--no-remote']), 0)
publish.assert_not_called()
artifacts = modulus.artifacts.discover_artifacts(directory / 'build/packages', context.package['name'], context.artifact['targets'])
self.assertEqual([p.name for p in artifacts], ['product_vamp_arm_64_gcc12_release.tar.gz', 'product_vamp_arm_64_gcc12_release.tar.gz.sha256'])
+15 -15
View File
@@ -1,3 +1,5 @@
import generator.score_io
import generator.validation
import tests.production_fixtures
import argparse
import copy
@@ -14,7 +16,6 @@ import xml.etree.ElementTree
import PIL.Image
import PIL.ImageChops
import generator.__main__
import generator.arrange.generate
import generator.engraving.fonts
import generator.engraving.layout
@@ -30,7 +31,6 @@ import generator.performance
import generator.pitch
import tests.score_events
import tests.arrangement_fixtures
import generator.score
import generator.theme
import tests.musicxml_roundtrip
import tests.test_musicxml
@@ -48,7 +48,7 @@ class ScoreBundleTests(unittest.TestCase):
self.assertEqual(set(document), {'metadata', 'staves'})
for malformed in (None, [], {}, {'metadata': {}, 'staves': []}, {**document, 'staves': None}, {**document, 'staves': [{}]}):
with self.subTest(document=malformed), self.assertRaises(ValueError):
generator.model.validate_document(malformed)
generator.validation.validate_document(malformed)
def test_multiple_voices_keep_independent_ties_and_transformations(self):
def half(action):
return tests.test_musicxml.xml_note(duration=2, kind='half', extra=f'<voice>1</voice><tie type="{action}"/>')
@@ -59,9 +59,9 @@ class ScoreBundleTests(unittest.TestCase):
self.assertEqual([v['id'] for v in measure['voices']], ['1', '2'])
self.assertNotIn('tie_from', measure['voices'][1]['notes'][0]['pitches'][0])
for document in (source, generator.pitch.concert_document(source), generator.arrange.generate.arrange_document(source, 'vocal')):
generator.model.validate_document(document)
generator.validation.validate_document(document)
restored = self.round_trip(document)
generator.model.validate_document(restored)
generator.validation.validate_document(restored)
self.assertTrue(any(len(m['voices']) == 2 for m in restored['staves'][0]['measures']))
def test_contiguous_ties_continue_across_systems_without_repeat_state(self):
text = tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(extra='<tie type="start"/>'))
@@ -139,7 +139,7 @@ class ScoreBundleTests(unittest.TestCase):
tests.musicxml_roundtrip.audit_roundtrip(before, after)
source['staves'][0]['instruments'] = []
with self.assertRaisesRegex(ValueError, 'unknown score instrument'):
generator.model.validate_document(source)
generator.validation.validate_document(source)
def test_catalog_roundtrip_preserves_written_pitch_and_rhythm(self):
for path in sorted(pathlib.Path('data/songs').glob('*.xml')):
with self.subTest(path=path):
@@ -220,34 +220,34 @@ class ScoreBundleTests(unittest.TestCase):
bundle['performance'] = generator.performance.timing(document)
path = assets / 'score.json'
path.write_text(json.dumps(bundle))
self.assertEqual(generator.score.load_json(path), document)
self.assertEqual(generator.score_io.load_json(path), document)
page.write_bytes(data + b'corrupt')
with self.assertRaisesRegex(ValueError, 'checksum'):
generator.score.load_json(path)
generator.score_io.load_json(path)
page.unlink()
outside = root / 'outside.png'
outside.write_bytes(data)
page.symlink_to(outside)
with self.assertRaisesRegex(ValueError, 'escapes'):
generator.score.load_json(path)
generator.score_io.load_json(path)
def test_final_validation_reuses_content_checks_and_rechecks_files(self):
with tempfile.TemporaryDirectory() as temporary:
root = pathlib.Path(temporary)
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note()))
for index in range(3):
generator.score.write_json(root / 'songs' / f'{index}.json', document)
generator.score_io.write_json(root / 'songs' / f'{index}.json', document)
trusted = generator.engraving.pipeline.engrave_tree(root, pathlib.Path('package/assets/fonts/petaluma'))
generator.pico_artwork.generate_titles(root, pathlib.Path('package/assets/fonts/petaluma'))
with unittest.mock.patch('generator.engraving.validation.check_content', side_effect=AssertionError('already validated content')):
generator.score.validate_generated_tree(root, trusted)
generator.score_io.validate_generated_tree(root, trusted)
page = root / 'songs/2.pages/0001.png'
original = page.read_bytes()
page.write_bytes(original + b'changed')
with self.assertRaisesRegex(ValueError, 'checksum'):
generator.score.validate_generated_tree(root, trusted)
generator.score_io.validate_generated_tree(root, trusted)
page.write_bytes(original)
with unittest.mock.patch('generator.engraving.validation.check_content', wraps=generator.engraving.validation.check_content) as decode:
generator.score.validate_generated_tree(root)
generator.score_io.validate_generated_tree(root)
decode.assert_called_once()
PIL.Image.new('RGB', (976, 484), generator.theme.rgb('background')).save(page)
path = root / 'songs/2.json'
@@ -255,14 +255,14 @@ class ScoreBundleTests(unittest.TestCase):
bundle['presentation']['pages'][0]['sha256'] = hashlib.sha256(page.read_bytes()).hexdigest()
path.write_text(json.dumps(bundle))
with self.assertRaisesRegex(ValueError, 'blank page'):
generator.score.validate_generated_tree(root, trusted)
generator.score_io.validate_generated_tree(root, trusted)
def test_python_engraver_visual_golden_and_determinism(self):
document = generator.musicxml.load_musicxml(pathlib.Path('tests/fixtures/engraving.xml'))
with tempfile.TemporaryDirectory() as temporary:
root = pathlib.Path(temporary)
before = None
for _ in range(2):
generator.score.write_json(root / 'fixture.json', document)
generator.score_io.write_json(root / 'fixture.json', document)
generator.engraving.pipeline.engrave_tree(root, pathlib.Path('package/assets/fonts/petaluma'))
bundle = json.loads((root / 'fixture.json').read_text())
self.assertEqual(bundle['music'], document)
+33
View File
@@ -14,6 +14,7 @@
#include <boost/asio/redirect_error.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/asio/thread_pool.hpp>
#include <vamp_zero/errors.hpp>
#include <vamp_zero/score_loader.hpp>
@@ -93,6 +94,38 @@ BOOST_AUTO_TEST_CASE(score_loader_starts_asynchronously_from_executor_handler) {
BOOST_CHECK_EQUAL(*fixture.completions, 1U);
}
BOOST_AUTO_TEST_CASE(score_loader_worker_keeps_control_executor_responsive_and_returns_completion) {
auto io = boost::asio::io_context{};
auto worker = boost::asio::thread_pool{1};
auto entered = std::promise<void>{};
auto release = std::promise<void>{};
auto resume = release.get_future().share();
auto completed = false;
auto control_thread = std::this_thread::get_id();
auto loader = vamp_zero::score_loader{io.get_executor(),
[&entered, resume](vamp_zero::score_reference reference) -> boost::asio::awaitable<std::expected<vamp_zero::score_document, modulus::error>> {
entered.set_value();
resume.wait();
co_return vamp_zero::score_document{.id = reference.id};
},
[&] { completed = true; BOOST_CHECK(std::this_thread::get_id() == control_thread); }, worker.get_executor()};
loader.select(std::make_shared<vamp_zero::catalog>(), performance("a"));
auto started = entered.get_future().wait_for(std::chrono::seconds{2}) == std::future_status::ready;
auto serviced = false;
boost::asio::post(io, [&] { serviced = true; });
io.poll();
BOOST_CHECK(serviced);
BOOST_CHECK(!completed);
release.set_value();
worker.join();
io.restart();
io.run();
BOOST_REQUIRE(started);
BOOST_CHECK(completed);
BOOST_REQUIRE(loader.snapshot().document);
BOOST_CHECK_EQUAL(loader.snapshot().document->id, "a");
}
BOOST_AUTO_TEST_CASE(score_loader_coalesces_pending_and_discards_stale_results) {
auto fixture = loader_fixture{};
fixture.loader.select(fixture.catalog, performance("a"));
+2 -9
View File
@@ -3,18 +3,11 @@ import dataclasses
import fractions
import unittest
import generator.arrange.generate
import generator.arrange.form
import generator.model
import generator.parts
import generator.solo.arrangement
import generator.solo.analysis
import generator.source
import generator.solo.profiles
import generator.solo.placement
import generator.solo.types
import generator.solo.harmonic
import tests.solo_fixtures
import tests.solo_fixtures
import tests.solo_fixtures
def events(measure):
@@ -39,7 +32,7 @@ class SoloInstrumentAndIdentity(unittest.TestCase):
self.assertTrue(any(p.string_number == 1 for p in generator.solo.placement.positions(65, electric)))
self.assertTrue(any(p.string_number == 1 for p in generator.solo.placement.positions(65, acoustic)))
def test_soft_overlapping_registers_do_not_inherit_a_mandatory_octave(self):
source = generator.solo.analysis.analyze(generator.parts.prepare_document(tests.solo_fixtures.song(count=4, chords=('CM7',))))
source = generator.source.analyze(generator.parts.prepare_document(tests.solo_fixtures.song(count=4, chords=('CM7',))))
acoustic = generator.solo.profiles.for_part('acoustic', source)
electric = generator.solo.profiles.for_part('electric', source)
self.assertIsNone(electric.reference)
+2 -1
View File
@@ -1,3 +1,4 @@
import generator.validation
import copy
import pathlib
import unittest
@@ -209,7 +210,7 @@ class VoicingTests(unittest.TestCase):
source = generator.musicxml.load_musicxml(pathlib.Path('data/songs/autumn_leaves.xml'))
before = copy.deepcopy(source)
result = generator.arrange.generate.arrange_document(source, 'electric')
generator.model.validate_document(result)
generator.validation.validate_document(result)
section, checked = '', set()
source_index = generator.score.body_start_measure_index(source['staves'][0]['measures'])
for measure in result['staves'][0]['measures']: