@@ -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}
|
||||
|
||||
@@ -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]))
|
||||
Vendored
+411
@@ -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"
|
||||
}
|
||||
Vendored
+16725
File diff suppressed because it is too large
Load Diff
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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']]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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']))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'])
|
||||
|
||||
|
||||
@@ -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'}))
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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())
|
||||
|
||||
@@ -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})
|
||||
@@ -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:
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']]
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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']:
|
||||
|
||||
Reference in New Issue
Block a user